/core/FrontController.php
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
- <?php
- /**
- * Piwik - free/libre analytics platform
- *
- * @link http://piwik.org
- * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
- *
- */
- namespace Piwik;
- use Exception;
- use Piwik\API\Request;
- use Piwik\API\ResponseBuilder;
- use Piwik\Plugin\Controller;
- use Piwik\Session;
- /**
- * This singleton dispatches requests to the appropriate plugin Controller.
- *
- * Piwik uses this class for all requests that go through **index.php**. Plugins can
- * use it to call controller actions of other plugins.
- *
- * ### Examples
- *
- * **Forwarding controller requests**
- *
- * public function myConfiguredRealtimeMap()
- * {
- * $_GET['changeVisitAlpha'] = false;
- * $_GET['removeOldVisits'] = false;
- * $_GET['showFooterMessage'] = false;
- * return FrontController::getInstance()->dispatch('UserCountryMap', 'realtimeMap');
- * }
- *
- * **Using other plugin controller actions**
- *
- * public function myPopupWithRealtimeMap()
- * {
- * $_GET['changeVisitAlpha'] = false;
- * $_GET['removeOldVisits'] = false;
- * $_GET['showFooterMessage'] = false;
- * $realtimeMap = FrontController::getInstance()->fetchDispatch('UserCountryMap', 'realtimeMap');
- *
- * $view = new View('@MyPlugin/myPopupWithRealtimeMap.twig');
- * $view->realtimeMap = $realtimeMap;
- * return $realtimeMap->render();
- * }
- *
- * For a detailed explanation, see the documentation [here](http://piwik.org/docs/plugins/framework-overview).
- *
- * @method static \Piwik\FrontController getInstance()
- */
- class FrontController extends Singleton
- {
- const DEFAULT_MODULE = 'CoreHome';
- /**
- * Set to false and the Front Controller will not dispatch the request
- *
- * @var bool
- */
- public static $enableDispatch = true;
- /**
- * Executes the requested plugin controller method.
- *
- * @throws Exception|\Piwik\PluginDeactivatedException in case the plugin doesn't exist, the action doesn't exist,
- * there is not enough permission, etc.
- *
- * @param string $module The name of the plugin whose controller to execute, eg, `'UserCountryMap'`.
- * @param string $action The controller method name, eg, `'realtimeMap'`.
- * @param array $parameters Array of parameters to pass to the controller method.
- * @return void|mixed The returned value of the call. This is the output of the controller method.
- * @api
- */
- public function dispatch($module = null, $action = null, $parameters = null)
- {
- if (self::$enableDispatch === false) {
- return;
- }
- try {
- $result = $this->doDispatch($module, $action, $parameters);
- return $result;
- } catch (NoAccessException $exception) {
- /**
- * Triggered when a user with insufficient access permissions tries to view some resource.
- *
- * This event can be used to customize the error that occurs when a user is denied access
- * (for example, displaying an error message, redirecting to a page other than login, etc.).
- *
- * @param \Piwik\NoAccessException $exception The exception that was caught.
- */
- Piwik::postEvent('User.isNotAuthorized', array($exception), $pending = true);
- } catch (Exception $e) {
- $debugTrace = $e->getTraceAsString();
- $message = Common::sanitizeInputValue($e->getMessage());
- Piwik_ExitWithMessage($message, $debugTrace, true, true);
- }
- }
- protected function makeController($module, $action)
- {
- $controllerClassName = $this->getClassNameController($module);
- // FrontController's autoloader
- if (!class_exists($controllerClassName, false)) {
- $moduleController = PIWIK_INCLUDE_PATH . '/plugins/' . $module . '/Controller.php';
- if (!is_readable($moduleController)) {
- throw new Exception("Module controller $moduleController not found!");
- }
- require_once $moduleController; // prefixed by PIWIK_INCLUDE_PATH
- }
- $class = $this->getClassNameController($module);
- /** @var $controller Controller */
- $controller = new $class;
- if ($action === false) {
- $action = $controller->getDefaultAction();
- }
- if (!is_callable(array($controller, $action))) {
- throw new Exception("Action '$action' not found in the controller '$controllerClassName'.");
- }
- return array($controller, $action);
- }
- protected function getClassNameController($module)
- {
- return "\\Piwik\\Plugins\\$module\\Controller";
- }
- /**
- * Executes the requested plugin controller method and returns the data, capturing anything the
- * method `echo`s.
- *
- * _Note: If the plugin controller returns something, the return value is returned instead
- * of whatever is in the output buffer._
- *
- * @param string $module The name of the plugin whose controller to execute, eg, `'UserCountryMap'`.
- * @param string $action The controller action name, eg, `'realtimeMap'`.
- * @param array $parameters Array of parameters to pass to the controller action method.
- * @return string The `echo`'d data or the return value of the controller action.
- * @deprecated
- */
- public function fetchDispatch($module = null, $actionName = null, $parameters = null)
- {
- ob_start();
- $output = $this->dispatch($module, $actionName, $parameters);
- // if nothing returned we try to load something that was printed on the screen
- if (empty($output)) {
- $output = ob_get_contents();
- } else {
- // if something was returned, flush output buffer as it is meant to be written to the screen
- ob_flush();
- }
- ob_end_clean();
- return $output;
- }
- /**
- * Called at the end of the page generation
- */
- public function __destruct()
- {
- try {
- if (class_exists('Piwik\\Profiler')
- && !SettingsServer::isTrackerApiRequest()) {
- // in tracker mode Piwik\Tracker\Db\Pdo\Mysql does currently not implement profiling
- Profiler::displayDbProfileReport();
- Profiler::printQueryCount();
- Log::debug(Registry::get('timer'));
- }
- } catch (Exception $e) {
- }
- }
- // Should we show exceptions messages directly rather than display an html error page?
- public static function shouldRethrowException()
- {
- // If we are in no dispatch mode, eg. a script reusing Piwik libs,
- // then we should return the exception directly, rather than trigger the event "bad config file"
- // which load the HTML page of the installer with the error.
- return (defined('PIWIK_ENABLE_DISPATCH') && !PIWIK_ENABLE_DISPATCH)
- || Common::isPhpCliMode()
- || SettingsServer::isArchivePhpTriggered();
- }
- static public function setUpSafeMode()
- {
- register_shutdown_function(array('\\Piwik\\FrontController','triggerSafeModeWhenError'));
- }
- static public function triggerSafeModeWhenError()
- {
- $lastError = error_get_last();
- if (!empty($lastError) && $lastError['type'] == E_ERROR) {
- $controller = FrontController::getInstance();
- $controller->init();
- $message = $controller->dispatch('CorePluginsAdmin', 'safemode', array($lastError));
- echo $message;
- }
- }
- /**
- * Loads the config file and assign to the global registry
- * This is overridden in tests to ensure test config file is used
- *
- * @return Exception
- */
- static public function createConfigObject()
- {
- $exceptionToThrow = false;
- try {
- Config::getInstance()->database; // access property to check if the local file exists
- } catch (Exception $exception) {
- /**
- * Triggered when the configuration file cannot be found or read, which usually
- * means Piwik is not installed yet.
- *
- * This event can be used to start the installation process or to display a custom error message.
- *
- * @param Exception $exception The exception that was thrown by `Config::getInstance()`.
- */
- Piwik::postEvent('Config.NoConfigurationFile', array($exception), $pending = true);
- $exceptionToThrow = $exception;
- }
- return $exceptionToThrow;
- }
- /**
- * Must be called before dispatch()
- * - checks that directories are writable,
- * - loads the configuration file,
- * - loads the plugin,
- * - inits the DB connection,
- * - etc.
- *
- * @throws Exception
- * @return void
- */
- public function init()
- {
- static $initialized = false;
- if ($initialized) {
- return;
- }
- $initialized = true;
- try {
- Registry::set('timer', new Timer);
- $directoriesToCheck = array(
- '/tmp/',
- '/tmp/assets/',
- '/tmp/cache/',
- '/tmp/logs/',
- '/tmp/tcpdf/',
- '/tmp/templates_c/',
- );
- Filechecks::dieIfDirectoriesNotWritable($directoriesToCheck);
- Translate::loadEnglishTranslation();
- $exceptionToThrow = self::createConfigObject();
- $this->handleMaintenanceMode();
- $this->handleProfiler();
- $this->handleSSLRedirection();
- Plugin\Manager::getInstance()->loadActivatedPlugins();
- if ($exceptionToThrow) {
- throw $exceptionToThrow;
- }
- try {
- Db::createDatabaseObject();
- Option::get('TestingIfDatabaseConnectionWorked');
- } catch (Exception $exception) {
- if (self::shouldRethrowException()) {
- throw $exception;
- }
- /**
- * Triggered if the INI config file has the incorrect format or if certain required configuration
- * options are absent.
- *
- * This event can be used to start the installation process or to display a custom error message.
- *
- * @param Exception $exception The exception thrown from creating and testing the database
- * connection.
- */
- Piwik::postEvent('Config.badConfigurationFile', array($exception), $pending = true);
- throw $exception;
- }
- // Init the Access object, so that eg. core/Updates/* can enforce Super User and use some APIs
- Access::getInstance();
- /**
- * Triggered just after the platform is initialized and plugins are loaded.
- *
- * This event can be used to do early initialization.
- *
- * _Note: At this point the user is not authenticated yet._
- */
- Piwik::postEvent('Request.dispatchCoreAndPluginUpdatesScreen');
- \Piwik\Plugin\Manager::getInstance()->installLoadedPlugins();
- // ensure the current Piwik URL is known for later use
- if (method_exists('Piwik\SettingsPiwik', 'getPiwikUrl')) {
- $host = SettingsPiwik::getPiwikUrl();
- }
- /**
- * Triggered before the user is authenticated, when the global authentication object
- * should be created.
- *
- * Plugins that provide their own authentication implementation should use this event
- * to set the global authentication object (which must derive from {@link Piwik\Auth}).
- *
- * **Example**
- *
- * Piwik::addAction('Request.initAuthenticationObject', function() {
- * Piwik\Registry::set('auth', new MyAuthImplementation());
- * });
- */
- Piwik::postEvent('Request.initAuthenticationObject');
- try {
- $authAdapter = Registry::get('auth');
- } catch (Exception $e) {
- throw new Exception("Authentication object cannot be found in the Registry. Maybe the Login plugin is not activated?
- <br />You can activate the plugin by adding:<br />
- <code>Plugins[] = Login</code><br />
- under the <code>[Plugins]</code> section in your config/config.ini.php");
- }
- Access::getInstance()->reloadAccess($authAdapter);
- // Force the auth to use the token_auth if specified, so that embed dashboard
- // and all other non widgetized controller methods works fine
- if (($token_auth = Common::getRequestVar('token_auth', false, 'string')) !== false) {
- Request::reloadAuthUsingTokenAuth();
- }
- SettingsServer::raiseMemoryLimitIfNecessary();
- Translate::reloadLanguage();
- \Piwik\Plugin\Manager::getInstance()->postLoadPlugins();
- /**
- * Triggered after the platform is initialized and after the user has been authenticated, but
- * before the platform has handled the request.
- *
- * Piwik uses this event to check for updates to Piwik.
- */
- Piwik::postEvent('Platform.initialized');
- } catch (Exception $e) {
- if (self::shouldRethrowException()) {
- throw $e;
- }
- $debugTrace = $e->getTraceAsString();
- Piwik_ExitWithMessage($e->getMessage(), $debugTrace, true);
- }
- }
- protected function prepareDispatch($module, $action, $parameters)
- {
- if (is_null($module)) {
- $module = Common::getRequestVar('module', self::DEFAULT_MODULE, 'string');
- }
- if (is_null($action)) {
- $action = Common::getRequestVar('action', false);
- }
- if (SettingsPiwik::isPiwikInstalled()
- && ($module !== 'API' || ($action && $action !== 'index'))
- ) {
- Session::start();
- }
- if (is_null($parameters)) {
- $parameters = array();
- }
- if (!ctype_alnum($module)) {
- throw new Exception("Invalid module name '$module'");
- }
- $module = Request::renameModule($module);
- if (!\Piwik\Plugin\Manager::getInstance()->isPluginActivated($module)) {
- throw new PluginDeactivatedException($module);
- }
- return array($module, $action, $parameters);
- }
- protected function handleMaintenanceMode()
- {
- if (Config::getInstance()->General['maintenance_mode'] == 1
- && !Common::isPhpCliMode()
- ) {
- $format = Common::getRequestVar('format', '');
- $message = "Piwik is in scheduled maintenance. Please come back later."
- . " The administrator can disable maintenance by editing the file piwik/config/config.ini.php and removing the following: "
- . " maintenance_mode=1 ";
- if (Config::getInstance()->Tracker['record_statistics'] == 0) {
- $message .= ' and record_statistics=0';
- }
- $exception = new Exception($message);
- // extend explain how to re-enable
- // show error message when record stats = 0
- if (empty($format)) {
- throw $exception;
- }
- $response = new ResponseBuilder($format);
- echo $response->getResponseException($exception);
- exit;
- }
- }
- protected function handleSSLRedirection()
- {
- // Specifically disable for the opt out iframe
- if(Piwik::getModule() == 'CoreAdminHome' && Piwik::getAction() == 'optOut') {
- return;
- }
- // Disable Https for VisitorGenerator
- if(Piwik::getModule() == 'VisitorGenerator') {
- return;
- }
- if(Common::isPhpCliMode()) {
- return;
- }
- // Only enable this feature after Piwik is already installed
- if(!SettingsPiwik::isPiwikInstalled()) {
- return;
- }
- // proceed only when force_ssl = 1
- if(!SettingsPiwik::isHttpsForced()) {
- return;
- }
- Url::redirectToHttps();
- }
- private function handleProfiler()
- {
- if (!empty($_GET['xhprof'])) {
- $mainRun = $_GET['xhprof'] == 1; // core:archive command sets xhprof=2
- Profiler::setupProfilerXHProf($mainRun);
- }
- }
- /**
- * @param $module
- * @param $action
- * @param $parameters
- * @return mixed
- */
- private function doDispatch($module, $action, $parameters)
- {
- list($module, $action, $parameters) = $this->prepareDispatch($module, $action, $parameters);
- /**
- * Triggered directly before controller actions are dispatched.
- *
- * This event can be used to modify the parameters passed to one or more controller actions
- * and can be used to change the controller action being dispatched to.
- *
- * @param string &$module The name of the plugin being dispatched to.
- * @param string &$action The name of the controller method being dispatched to.
- * @param array &$parameters The arguments passed to the controller action.
- */
- Piwik::postEvent('Request.dispatch', array(&$module, &$action, &$parameters));
- list($controller, $action) = $this->makeController($module, $action);
- /**
- * Triggered directly before controller actions are dispatched.
- *
- * This event exists for convenience and is triggered directly after the {@hook Request.dispatch}
- * event is triggered.
- *
- * It can be used to do the same things as the {@hook Request.dispatch} event, but for one controller
- * action only. Using this event will result in a little less code than {@hook Request.dispatch}.
- *
- * @param array &$parameters The arguments passed to the controller action.
- */
- Piwik::postEvent(sprintf('Controller.%s.%s', $module, $action), array(&$parameters));
- $result = call_user_func_array(array($controller, $action), $parameters);
- /**
- * Triggered after a controller action is successfully called.
- *
- * This event exists for convenience and is triggered immediately before the {@hook Request.dispatch.end}
- * event is triggered.
- *
- * It can be used to do the same things as the {@hook Request.dispatch.end} event, but for one
- * controller action only. Using this event will result in a little less code than
- * {@hook Request.dispatch.end}.
- *
- * @param mixed &$result The result of the controller action.
- * @param array $parameters The arguments passed to the controller action.
- */
- Piwik::postEvent(sprintf('Controller.%s.%s.end', $module, $action), array(&$result, $parameters));
- /**
- * Triggered after a controller action is successfully called.
- *
- * This event can be used to modify controller action output (if any) before the output is returned.
- *
- * @param mixed &$result The controller action result.
- * @param array $parameters The arguments passed to the controller action.
- */
- Piwik::postEvent('Request.dispatch.end', array(&$result, $module, $action, $parameters));
- return $result;
- }
- }
- /**
- * Exception thrown when the requested plugin is not activated in the config file
- */
- class PluginDeactivatedException extends Exception
- {
- public function __construct($module)
- {
- parent::__construct("The plugin $module is not enabled. You can activate the plugin on Settings > Plugins page in Piwik.");
- }
- }