Teknik is a suite of services with attractive and functional interfaces. https://www.teknik.io/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

FrontController.php 22KB

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