PageRenderTime 45ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/core/FrontController.php

https://github.com/CodeYellowBV/piwik
PHP | 542 lines | 265 code | 63 blank | 214 comment | 47 complexity | 91f478bb446bc41181091abcfefb71c4 MD5 | raw file
Possible License(s): LGPL-3.0, JSON, MIT, GPL-3.0, LGPL-2.1, GPL-2.0, AGPL-1.0, BSD-2-Clause, BSD-3-Clause
  1. <?php
  2. /**
  3. * Piwik - free/libre analytics platform
  4. *
  5. * @link http://piwik.org
  6. * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
  7. *
  8. */
  9. namespace Piwik;
  10. use Exception;
  11. use Piwik\API\Request;
  12. use Piwik\API\ResponseBuilder;
  13. use Piwik\Plugin\Controller;
  14. use Piwik\Session;
  15. /**
  16. * This singleton dispatches requests to the appropriate plugin Controller.
  17. *
  18. * Piwik uses this class for all requests that go through **index.php**. Plugins can
  19. * use it to call controller actions of other plugins.
  20. *
  21. * ### Examples
  22. *
  23. * **Forwarding controller requests**
  24. *
  25. * public function myConfiguredRealtimeMap()
  26. * {
  27. * $_GET['changeVisitAlpha'] = false;
  28. * $_GET['removeOldVisits'] = false;
  29. * $_GET['showFooterMessage'] = false;
  30. * return FrontController::getInstance()->dispatch('UserCountryMap', 'realtimeMap');
  31. * }
  32. *
  33. * **Using other plugin controller actions**
  34. *
  35. * public function myPopupWithRealtimeMap()
  36. * {
  37. * $_GET['changeVisitAlpha'] = false;
  38. * $_GET['removeOldVisits'] = false;
  39. * $_GET['showFooterMessage'] = false;
  40. * $realtimeMap = FrontController::getInstance()->fetchDispatch('UserCountryMap', 'realtimeMap');
  41. *
  42. * $view = new View('@MyPlugin/myPopupWithRealtimeMap.twig');
  43. * $view->realtimeMap = $realtimeMap;
  44. * return $realtimeMap->render();
  45. * }
  46. *
  47. * For a detailed explanation, see the documentation [here](http://piwik.org/docs/plugins/framework-overview).
  48. *
  49. * @method static \Piwik\FrontController getInstance()
  50. */
  51. class FrontController extends Singleton
  52. {
  53. const DEFAULT_MODULE = 'CoreHome';
  54. /**
  55. * Set to false and the Front Controller will not dispatch the request
  56. *
  57. * @var bool
  58. */
  59. public static $enableDispatch = true;
  60. /**
  61. * Executes the requested plugin controller method.
  62. *
  63. * @throws Exception|\Piwik\PluginDeactivatedException in case the plugin doesn't exist, the action doesn't exist,
  64. * there is not enough permission, etc.
  65. *
  66. * @param string $module The name of the plugin whose controller to execute, eg, `'UserCountryMap'`.
  67. * @param string $action The controller method name, eg, `'realtimeMap'`.
  68. * @param array $parameters Array of parameters to pass to the controller method.
  69. * @return void|mixed The returned value of the call. This is the output of the controller method.
  70. * @api
  71. */
  72. public function dispatch($module = null, $action = null, $parameters = null)
  73. {
  74. if (self::$enableDispatch === false) {
  75. return;
  76. }
  77. try {
  78. $result = $this->doDispatch($module, $action, $parameters);
  79. return $result;
  80. } catch (NoAccessException $exception) {
  81. /**
  82. * Triggered when a user with insufficient access permissions tries to view some resource.
  83. *
  84. * This event can be used to customize the error that occurs when a user is denied access
  85. * (for example, displaying an error message, redirecting to a page other than login, etc.).
  86. *
  87. * @param \Piwik\NoAccessException $exception The exception that was caught.
  88. */
  89. Piwik::postEvent('User.isNotAuthorized', array($exception), $pending = true);
  90. } catch (Exception $e) {
  91. $debugTrace = $e->getTraceAsString();
  92. $message = Common::sanitizeInputValue($e->getMessage());
  93. Piwik_ExitWithMessage($message, $debugTrace, true, true);
  94. }
  95. }
  96. protected function makeController($module, $action)
  97. {
  98. $controllerClassName = $this->getClassNameController($module);
  99. // FrontController's autoloader
  100. if (!class_exists($controllerClassName, false)) {
  101. $moduleController = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/Controller.php';
  102. if (!is_readable($moduleController)) {
  103. throw new Exception("Module controller $moduleController not found!");
  104. }
  105. require_once $moduleController; // prefixed by PIWIK_INCLUDE_PATH
  106. }
  107. $class = $this->getClassNameController($module);
  108. /** @var $controller Controller */
  109. $controller = new $class;
  110. if ($action === false) {
  111. $action = $controller->getDefaultAction();
  112. }
  113. if (!is_callable(array($controller, $action))) {
  114. throw new Exception("Action '$action' not found in the controller '$controllerClassName'.");
  115. }
  116. return array($controller, $action);
  117. }
  118. protected function getClassNameController($module)
  119. {
  120. return "\\Piwik\\Plugins\\$module\\Controller";
  121. }
  122. /**
  123. * Executes the requested plugin controller method and returns the data, capturing anything the
  124. * method `echo`s.
  125. *
  126. * _Note: If the plugin controller returns something, the return value is returned instead
  127. * of whatever is in the output buffer._
  128. *
  129. * @param string $module The name of the plugin whose controller to execute, eg, `'UserCountryMap'`.
  130. * @param string $action The controller action name, eg, `'realtimeMap'`.
  131. * @param array $parameters Array of parameters to pass to the controller action method.
  132. * @return string The `echo`'d data or the return value of the controller action.
  133. * @deprecated
  134. */
  135. public function fetchDispatch($module = null, $actionName = null, $parameters = null)
  136. {
  137. ob_start();
  138. $output = $this->dispatch($module, $actionName, $parameters);
  139. // if nothing returned we try to load something that was printed on the screen
  140. if (empty($output)) {
  141. $output = ob_get_contents();
  142. } else {
  143. // if something was returned, flush output buffer as it is meant to be written to the screen
  144. ob_flush();
  145. }
  146. ob_end_clean();
  147. return $output;
  148. }
  149. /**
  150. * Called at the end of the page generation
  151. */
  152. public function __destruct()
  153. {
  154. try {
  155. if (class_exists('Piwik\\Profiler')
  156. && !SettingsServer::isTrackerApiRequest()) {
  157. // in tracker mode Piwik\Tracker\Db\Pdo\Mysql does currently not implement profiling
  158. Profiler::displayDbProfileReport();
  159. Profiler::printQueryCount();
  160. Log::debug(Registry::get('timer'));
  161. }
  162. } catch (Exception $e) {
  163. }
  164. }
  165. // Should we show exceptions messages directly rather than display an html error page?
  166. public static function shouldRethrowException()
  167. {
  168. // If we are in no dispatch mode, eg. a script reusing Piwik libs,
  169. // then we should return the exception directly, rather than trigger the event "bad config file"
  170. // which load the HTML page of the installer with the error.
  171. return (defined('PIWIK_ENABLE_DISPATCH') && !PIWIK_ENABLE_DISPATCH)
  172. || Common::isPhpCliMode()
  173. || SettingsServer::isArchivePhpTriggered();
  174. }
  175. static public function setUpSafeMode()
  176. {
  177. register_shutdown_function(array('\\Piwik\\FrontController','triggerSafeModeWhenError'));
  178. }
  179. static public function triggerSafeModeWhenError()
  180. {
  181. $lastError = error_get_last();
  182. if (!empty($lastError) && $lastError['type'] == E_ERROR) {
  183. $controller = FrontController::getInstance();
  184. $controller->init();
  185. $message = $controller->dispatch('CorePluginsAdmin', 'safemode', array($lastError));
  186. echo $message;
  187. }
  188. }
  189. /**
  190. * Loads the config file and assign to the global registry
  191. * This is overridden in tests to ensure test config file is used
  192. *
  193. * @return Exception
  194. */
  195. static public function createConfigObject()
  196. {
  197. $exceptionToThrow = false;
  198. try {
  199. Config::getInstance()->database; // access property to check if the local file exists
  200. } catch (Exception $exception) {
  201. /**
  202. * Triggered when the configuration file cannot be found or read, which usually
  203. * means Piwik is not installed yet.
  204. *
  205. * This event can be used to start the installation process or to display a custom error message.
  206. *
  207. * @param Exception $exception The exception that was thrown by `Config::getInstance()`.
  208. */
  209. Piwik::postEvent('Config.NoConfigurationFile', array($exception), $pending = true);
  210. $exceptionToThrow = $exception;
  211. }
  212. return $exceptionToThrow;
  213. }
  214. /**
  215. * Must be called before dispatch()
  216. * - checks that directories are writable,
  217. * - loads the configuration file,
  218. * - loads the plugin,
  219. * - inits the DB connection,
  220. * - etc.
  221. *
  222. * @throws Exception
  223. * @return void
  224. */
  225. public function init()
  226. {
  227. static $initialized = false;
  228. if ($initialized) {
  229. return;
  230. }
  231. $initialized = true;
  232. try {
  233. Registry::set('timer', new Timer);
  234. $directoriesToCheck = array(
  235. '/tmp/',
  236. '/tmp/assets/',
  237. '/tmp/cache/',
  238. '/tmp/logs/',
  239. '/tmp/tcpdf/',
  240. '/tmp/templates_c/',
  241. );
  242. Filechecks::dieIfDirectoriesNotWritable($directoriesToCheck);
  243. Translate::loadEnglishTranslation();
  244. $exceptionToThrow = self::createConfigObject();
  245. $this->handleMaintenanceMode();
  246. $this->handleProfiler();
  247. $this->handleSSLRedirection();
  248. Plugin\Manager::getInstance()->loadActivatedPlugins();
  249. if ($exceptionToThrow) {
  250. throw $exceptionToThrow;
  251. }
  252. try {
  253. Db::createDatabaseObject();
  254. Option::get('TestingIfDatabaseConnectionWorked');
  255. } catch (Exception $exception) {
  256. if (self::shouldRethrowException()) {
  257. throw $exception;
  258. }
  259. /**
  260. * Triggered if the INI config file has the incorrect format or if certain required configuration
  261. * options are absent.
  262. *
  263. * This event can be used to start the installation process or to display a custom error message.
  264. *
  265. * @param Exception $exception The exception thrown from creating and testing the database
  266. * connection.
  267. */
  268. Piwik::postEvent('Config.badConfigurationFile', array($exception), $pending = true);
  269. throw $exception;
  270. }
  271. // Init the Access object, so that eg. core/Updates/* can enforce Super User and use some APIs
  272. Access::getInstance();
  273. /**
  274. * Triggered just after the platform is initialized and plugins are loaded.
  275. *
  276. * This event can be used to do early initialization.
  277. *
  278. * _Note: At this point the user is not authenticated yet._
  279. */
  280. Piwik::postEvent('Request.dispatchCoreAndPluginUpdatesScreen');
  281. \Piwik\Plugin\Manager::getInstance()->installLoadedPlugins();
  282. // ensure the current Piwik URL is known for later use
  283. if (method_exists('Piwik\SettingsPiwik', 'getPiwikUrl')) {
  284. $host = SettingsPiwik::getPiwikUrl();
  285. }
  286. /**
  287. * Triggered before the user is authenticated, when the global authentication object
  288. * should be created.
  289. *
  290. * Plugins that provide their own authentication implementation should use this event
  291. * to set the global authentication object (which must derive from {@link Piwik\Auth}).
  292. *
  293. * **Example**
  294. *
  295. * Piwik::addAction('Request.initAuthenticationObject', function() {
  296. * Piwik\Registry::set('auth', new MyAuthImplementation());
  297. * });
  298. */
  299. Piwik::postEvent('Request.initAuthenticationObject');
  300. try {
  301. $authAdapter = Registry::get('auth');
  302. } catch (Exception $e) {
  303. throw new Exception("Authentication object cannot be found in the Registry. Maybe the Login plugin is not activated?
  304. <br />You can activate the plugin by adding:<br />
  305. <code>Plugins[] = Login</code><br />
  306. under the <code>[Plugins]</code> section in your config/config.ini.php");
  307. }
  308. Access::getInstance()->reloadAccess($authAdapter);
  309. // Force the auth to use the token_auth if specified, so that embed dashboard
  310. // and all other non widgetized controller methods works fine
  311. if (($token_auth = Common::getRequestVar('token_auth', false, 'string')) !== false) {
  312. Request::reloadAuthUsingTokenAuth();
  313. }
  314. SettingsServer::raiseMemoryLimitIfNecessary();
  315. Translate::reloadLanguage();
  316. \Piwik\Plugin\Manager::getInstance()->postLoadPlugins();
  317. /**
  318. * Triggered after the platform is initialized and after the user has been authenticated, but
  319. * before the platform has handled the request.
  320. *
  321. * Piwik uses this event to check for updates to Piwik.
  322. */
  323. Piwik::postEvent('Platform.initialized');
  324. } catch (Exception $e) {
  325. if (self::shouldRethrowException()) {
  326. throw $e;
  327. }
  328. $debugTrace = $e->getTraceAsString();
  329. Piwik_ExitWithMessage($e->getMessage(), $debugTrace, true);
  330. }
  331. }
  332. protected function prepareDispatch($module, $action, $parameters)
  333. {
  334. if (is_null($module)) {
  335. $module = Common::getRequestVar('module', self::DEFAULT_MODULE, 'string');
  336. }
  337. if (is_null($action)) {
  338. $action = Common::getRequestVar('action', false);
  339. }
  340. if (SettingsPiwik::isPiwikInstalled()
  341. && ($module !== 'API' || ($action && $action !== 'index'))
  342. ) {
  343. Session::start();
  344. }
  345. if (is_null($parameters)) {
  346. $parameters = array();
  347. }
  348. if (!ctype_alnum($module)) {
  349. throw new Exception("Invalid module name '$module'");
  350. }
  351. $module = Request::renameModule($module);
  352. if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated($module)) {
  353. throw new PluginDeactivatedException($module);
  354. }
  355. return array($module, $action, $parameters);
  356. }
  357. protected function handleMaintenanceMode()
  358. {
  359. if (Config::getInstance()->General['maintenance_mode'] == 1
  360. && !Common::isPhpCliMode()
  361. ) {
  362. $format = Common::getRequestVar('format', '');
  363. $message = "Piwik is in scheduled maintenance. Please come back later."
  364. . " The administrator can disable maintenance by editing the file piwik/config/config.ini.php and removing the following: "
  365. . " maintenance_mode=1 ";
  366. if (Config::getInstance()->Tracker['record_statistics'] == 0) {
  367. $message .= ' and record_statistics=0';
  368. }
  369. $exception = new Exception($message);
  370. // extend explain how to re-enable
  371. // show error message when record stats = 0
  372. if (empty($format)) {
  373. throw $exception;
  374. }
  375. $response = new ResponseBuilder($format);
  376. echo $response->getResponseException($exception);
  377. exit;
  378. }
  379. }
  380. protected function handleSSLRedirection()
  381. {
  382. // Specifically disable for the opt out iframe
  383. if(Piwik::getModule() == 'CoreAdminHome' && Piwik::getAction() == 'optOut') {
  384. return;
  385. }
  386. // Disable Https for VisitorGenerator
  387. if(Piwik::getModule() == 'VisitorGenerator') {
  388. return;
  389. }
  390. if(Common::isPhpCliMode()) {
  391. return;
  392. }
  393. // Only enable this feature after Piwik is already installed
  394. if(!SettingsPiwik::isPiwikInstalled()) {
  395. return;
  396. }
  397. // proceed only when force_ssl = 1
  398. if(!SettingsPiwik::isHttpsForced()) {
  399. return;
  400. }
  401. Url::redirectToHttps();
  402. }
  403. private function handleProfiler()
  404. {
  405. if (!empty($_GET['xhprof'])) {
  406. $mainRun = $_GET['xhprof'] == 1; // core:archive command sets xhprof=2
  407. Profiler::setupProfilerXHProf($mainRun);
  408. }
  409. }
  410. /**
  411. * @param $module
  412. * @param $action
  413. * @param $parameters
  414. * @return mixed
  415. */
  416. private function doDispatch($module, $action, $parameters)
  417. {
  418. list($module, $action, $parameters) = $this->prepareDispatch($module, $action, $parameters);
  419. /**
  420. * Triggered directly before controller actions are dispatched.
  421. *
  422. * This event can be used to modify the parameters passed to one or more controller actions
  423. * and can be used to change the controller action being dispatched to.
  424. *
  425. * @param string &$module The name of the plugin being dispatched to.
  426. * @param string &$action The name of the controller method being dispatched to.
  427. * @param array &$parameters The arguments passed to the controller action.
  428. */
  429. Piwik::postEvent('Request.dispatch', array(&$module, &$action, &$parameters));
  430. list($controller, $action) = $this->makeController($module, $action);
  431. /**
  432. * Triggered directly before controller actions are dispatched.
  433. *
  434. * This event exists for convenience and is triggered directly after the {@hook Request.dispatch}
  435. * event is triggered.
  436. *
  437. * It can be used to do the same things as the {@hook Request.dispatch} event, but for one controller
  438. * action only. Using this event will result in a little less code than {@hook Request.dispatch}.
  439. *
  440. * @param array &$parameters The arguments passed to the controller action.
  441. */
  442. Piwik::postEvent(sprintf('Controller.%s.%s', $module, $action), array(&$parameters));
  443. $result = call_user_func_array(array($controller, $action), $parameters);
  444. /**
  445. * Triggered after a controller action is successfully called.
  446. *
  447. * This event exists for convenience and is triggered immediately before the {@hook Request.dispatch.end}
  448. * event is triggered.
  449. *
  450. * It can be used to do the same things as the {@hook Request.dispatch.end} event, but for one
  451. * controller action only. Using this event will result in a little less code than
  452. * {@hook Request.dispatch.end}.
  453. *
  454. * @param mixed &$result The result of the controller action.
  455. * @param array $parameters The arguments passed to the controller action.
  456. */
  457. Piwik::postEvent(sprintf('Controller.%s.%s.end', $module, $action), array(&$result, $parameters));
  458. /**
  459. * Triggered after a controller action is successfully called.
  460. *
  461. * This event can be used to modify controller action output (if any) before the output is returned.
  462. *
  463. * @param mixed &$result The controller action result.
  464. * @param array $parameters The arguments passed to the controller action.
  465. */
  466. Piwik::postEvent('Request.dispatch.end', array(&$result, $module, $action, $parameters));
  467. return $result;
  468. }
  469. }
  470. /**
  471. * Exception thrown when the requested plugin is not activated in the config file
  472. */
  473. class PluginDeactivatedException extends Exception
  474. {
  475. public function __construct($module)
  476. {
  477. parent::__construct("The plugin $module is not enabled. You can activate the plugin on Settings > Plugins page in Piwik.");
  478. }
  479. }