'Installation_Welcome', 'systemCheck' => 'Installation_SystemCheck', 'databaseSetup' => 'Installation_DatabaseSetup', 'tablesCreation' => 'Installation_Tables', 'setupSuperUser' => 'Installation_SuperUser', 'firstWebsiteSetup' => 'Installation_SetupWebsite', 'trackingCode' => 'General_JsTrackingTag', 'finished' => 'Installation_Congratulations', ); /** * Get installation steps * * @return array installation steps */ public function getInstallationSteps() { return $this->steps; } /** * Get default action (first installation step) * * @return string function name */ public function getDefaultAction() { $steps = array_keys($this->steps); return $steps[0]; } /** * Installation Step 1: Welcome * * Can also display an error message when there is a failure early (eg. DB connection failed) * * @param string $possibleErrorMessage Possible error message which may be set in the frontcontroller when event. Config.badConfigurationFile was triggered */ public function welcome($possibleErrorMessage = null) { // Delete merged js/css files to force regenerations based on updated activated plugin list Filesystem::deleteAllCacheOnUpdate(); $this->checkPiwikIsNotInstalled($possibleErrorMessage); $this->checkInstallationIsNotExpired(); $view = new View( '@Installation/welcome', $this->getInstallationSteps(), __FUNCTION__ ); $view->showNextStep = true; return $view->render(); } /** * Installation Step 2: System Check */ public function systemCheck() { $this->checkPiwikIsNotInstalled(); $this->deleteConfigFileIfNeeded(); $this->checkInstallationIsNotExpired(); $view = new View( '@Installation/systemCheck', $this->getInstallationSteps(), __FUNCTION__ ); // Do not use dependency injection because this service requires a lot of sub-services across plugins /** @var DiagnosticService $diagnosticService */ $diagnosticService = StaticContainer::get('Piwik\Plugins\Diagnostics\DiagnosticService'); $view->diagnosticReport = $diagnosticService->runDiagnostics(); $view->isInstallation = true; $view->systemCheckInfo = $this->getSystemCheckTextareaValue($view->diagnosticReport); $view->showNextStep = !$view->diagnosticReport->hasErrors(); // On the system check page, if all is green, display Next link at the top $view->showNextStepAtTop = $view->showNextStep && !$view->diagnosticReport->hasWarnings(); return $view->render(); } /** * Installation Step 3: Database Set-up * @throws Exception|Zend_Db_Adapter_Exception */ public function databaseSetup() { $this->checkPiwikIsNotInstalled(); $this->checkInstallationIsNotExpired(); $view = new View( '@Installation/databaseSetup', $this->getInstallationSteps(), __FUNCTION__ ); $view->showNextStep = false; $form = new FormDatabaseSetup(); if ($form->validate()) { try { $dbInfos = $form->createDatabaseObject(); $this->createConfigFile($dbInfos); DbHelper::checkDatabaseVersion(); Db::get()->checkClientVersion(); $this->redirectToNextStep(__FUNCTION__); } catch (Exception $e) { $view->errorMessage = Common::sanitizeInputValue($e->getMessage()); } } $view->addForm($form); return $view->render(); } /** * Installation Step 4: Table Creation */ public function tablesCreation() { $this->checkPiwikIsNotInstalled(); $this->checkInstallationIsNotExpired(); $view = new View( '@Installation/tablesCreation', $this->getInstallationSteps(), __FUNCTION__ ); if ($this->getParam('deleteTables')) { Manager::getInstance()->clearPluginsInstalledConfig(); Db::dropAllTables(); $view->existingTablesDeleted = true; } $tablesInstalled = DbHelper::getTablesInstalled(); $view->tablesInstalled = ''; if (count($tablesInstalled) > 0) { // we have existing tables $view->tablesInstalled = implode(', ', $tablesInstalled); $view->someTablesInstalled = true; $self = $this; Access::doAsSuperUser(function () use ($self, $tablesInstalled, $view) { Access::getInstance(); if ( $self->hasEnoughTablesToReuseDb($tablesInstalled) && count(APISitesManager::getInstance()->getAllSitesId()) > 0 && count(APIUsersManager::getInstance()->getUsers()) > 0 ) { $view->showReuseExistingTables = true; } }); } else { DbHelper::createTables(); DbHelper::createAnonymousUser(); DbHelper::recordInstallVersion(); $this->updateComponents(); Updater::recordComponentSuccessfullyUpdated('core', Version::VERSION); $view->tablesCreated = true; $view->showNextStep = true; } return $view->render(); } public function reuseTables() { $this->checkPiwikIsNotInstalled(); $this->checkInstallationIsNotExpired(); $steps = $this->getInstallationSteps(); $steps['tablesCreation'] = 'Installation_ReusingTables'; $view = new View( '@Installation/reuseTables', $steps, 'tablesCreation' ); $oldVersion = Option::get('version_core'); $result = $this->updateComponents(); if ($result === false) { $this->redirectToNextStep('tablesCreation'); } $view->coreError = $result['coreError']; $view->warningMessages = $result['warnings']; $view->errorMessages = $result['errors']; $view->deactivatedPlugins = $result['deactivatedPlugins']; $view->currentVersion = Version::VERSION; $view->oldVersion = $oldVersion; $view->showNextStep = true; return $view->render(); } /** * Installation Step 5: General Set-up (superuser login/password/email and subscriptions) */ public function setupSuperUser() { $this->checkPiwikIsNotInstalled(); $this->checkInstallationIsNotExpired(); $superUserAlreadyExists = Access::doAsSuperUser(function () { return count(APIUsersManager::getInstance()->getUsersHavingSuperUserAccess()) > 0; }); if ($superUserAlreadyExists) { $this->redirectToNextStep('setupSuperUser'); } $view = new View( '@Installation/setupSuperUser', $this->getInstallationSteps(), __FUNCTION__ ); $form = new FormSuperUser(); if ($form->validate()) { try { $loginName = $form->getSubmitValue('login'); $email = $form->getSubmitValue('email'); $this->createSuperUser( $loginName, $form->getSubmitValue('password'), $email ); $newsletterPiwikORG = $form->getSubmitValue('subscribe_newsletter_piwikorg'); $newsletterProfessionalServices = $form->getSubmitValue('subscribe_newsletter_professionalservices'); NewsletterSignup::signupForNewsletter( $loginName, $email, $newsletterPiwikORG, $newsletterProfessionalServices ); Onboarding::sendSysAdminMail($email); $this->redirectToNextStep(__FUNCTION__); } catch (Exception $e) { $view->errorMessage = $e->getMessage(); } } $view->addForm($form); return $view->render(); } /** * Installation Step 6: Configure first web-site */ public function firstWebsiteSetup() { $this->checkPiwikIsNotInstalled(); $this->checkInstallationIsNotExpired(); ServerFilesGenerator::createFilesForSecurity(); $siteIdsCount = Access::doAsSuperUser(function () { return count(APISitesManager::getInstance()->getAllSitesId()); }); if ($siteIdsCount > 0) { // if there is a already a website, skip this step and trackingCode step $this->redirectToNextStep('trackingCode'); } $view = new View( '@Installation/firstWebsiteSetup', $this->getInstallationSteps(), __FUNCTION__ ); $form = new FormFirstWebsiteSetup(); if ($form->validate()) { $name = Common::sanitizeInputValue($form->getSubmitValue('siteName')); $url = Common::unsanitizeInputValue($form->getSubmitValue('url')); $ecommerce = (int)$form->getSubmitValue('ecommerce'); try { $result = Access::doAsSuperUser(function () use ($name, $url, $ecommerce) { return APISitesManager::getInstance()->addSite($name, $url, $ecommerce); }); $params = array( 'site_idSite' => $result, 'site_name' => urlencode($name), ); $this->redirectToNextStep(__FUNCTION__, $params); } catch (Exception $e) { $view->errorMessage = $e->getMessage(); } } // Display previous step success message, when current step form was not submitted yet if (count($form->getErrorMessages()) == 0) { $view->displayGeneralSetupSuccess = true; } $view->addForm($form); return $view->render(); } /** * Installation Step 7: Display JavaScript tracking code */ public function trackingCode() { $this->checkPiwikIsNotInstalled(); $this->checkInstallationIsNotExpired(); $view = new View( '@Installation/trackingCode', $this->getInstallationSteps(), __FUNCTION__ ); $siteName = Common::unsanitizeInputValue($this->getParam('site_name')); $idSite = $this->getParam('site_idSite'); $javascriptGenerator = new TrackerCodeGenerator(); $jsTag = $javascriptGenerator->generate($idSite, Url::getCurrentUrlWithoutFileName()); // Needs to be generated as super user, as API requests would otherwise fail $emailBody = Access::doAsSuperUser( function () use ($idSite) { return SitesManager::renderTrackingCodeEmail($idSite); } ); // Load the Tracking code and help text from the SitesManager $viewTrackingHelp = new \Piwik\View('@SitesManager/_displayJavascriptCode'); $viewTrackingHelp->displaySiteName = $siteName; $viewTrackingHelp->jsTag = $jsTag; $viewTrackingHelp->emailBody = $emailBody; $viewTrackingHelp->idSite = $idSite; $viewTrackingHelp->piwikUrl = Url::getCurrentUrlWithoutFileName(); $viewTrackingHelp->isInstall = true; $view->trackingHelp = $viewTrackingHelp->render(); $view->displaySiteName = $siteName; $view->displayfirstWebsiteSetupSuccess = true; $view->showNextStep = true; return $view->render(); } /** * Installation Step 8: Finished! */ public function finished() { $this->checkPiwikIsNotInstalled(); $this->checkInstallationIsNotExpired(); $view = new View( '@Installation/finished', $this->getInstallationSteps(), __FUNCTION__ ); $form = new FormDefaultSettings(); /** * Triggered on initialization of the form to customize default Matomo settings (at the end of the installation process). * * @param \Piwik\Plugins\Installation\FormDefaultSettings $form */ Piwik::postEvent('Installation.defaultSettingsForm.init', array($form)); $form->addElement('submit', 'submit', array('value' => Piwik::translate('General_ContinueToPiwik') . ' ยป', 'class' => 'btn')); if ($form->validate()) { try { /** * Triggered on submission of the form to customize default Matomo settings (at the end of the installation process). * * @param \Piwik\Plugins\Installation\FormDefaultSettings $form */ Piwik::postEvent('Installation.defaultSettingsForm.submit', array($form)); $this->markInstallationAsCompleted(); Url::redirectToUrl('index.php'); } catch (Exception $e) { $view->errorMessage = $e->getMessage(); } } $view->addForm($form); $view->showNextStep = false; $output = $view->render(); return $output; } /** * System check will call this page which should load quickly, * in order to look at Response headers (eg. to detect if pagespeed is running) * * @return string */ public function getEmptyPageForSystemCheck() { return 'Hello, world!'; } /** * This controller action renders an admin tab that runs the installation * system check, so people can see if there are any issues w/ their running * Matomo installation. * * This admin tab is only viewable by the Super User. */ public function systemCheckPage() { Piwik::checkUserHasSuperUserAccess(); $view = new View( '@Installation/systemCheckPage', $this->getInstallationSteps(), __FUNCTION__ ); $this->setBasicVariablesView($view); /** @var DiagnosticService $diagnosticService */ $diagnosticService = StaticContainer::get('Piwik\Plugins\Diagnostics\DiagnosticService'); $view->diagnosticReport = $diagnosticService->runDiagnostics(); $view->systemCheckInfo = $this->getSystemCheckTextareaValue($view->diagnosticReport); return $view->render(); } /** * Save language selection in session-store */ public function saveLanguage() { if (DbHelper::isInstalled()) { $this->checkTokenInUrl(); } $language = $this->getParam('language'); LanguagesManager::setLanguageForSession($language); Url::redirectToReferrer(); } /** * Return the base.less compiled to css * * @return string */ public function getInstallationCss() { Common::sendHeader('Content-Type: text/css'); Common::sendHeader('Cache-Control: max-age=' . (60 * 60)); $files = array( 'plugins/Morpheus/stylesheets/base/bootstrap.css', 'plugins/Morpheus/stylesheets/base/icons.css', "node_modules/jquery-ui-dist/jquery-ui.theme.min.css", 'node_modules/@materializecss/materialize/dist/css/materialize.min.css', 'plugins/Morpheus/stylesheets/base.less', 'plugins/Morpheus/stylesheets/general/_forms.less', 'plugins/Installation/stylesheets/installation.css', ); return AssetManager::compileCustomStylesheets($files); } /** * Return the compiled JavaScript for the installation screen. * * @return string */ public function getInstallationJs() { Common::sendHeader('Content-Type: application/javascript; charset=UTF-8'); Common::sendHeader('Cache-Control: max-age=' . (60 * 60)); $files = array( "node_modules/jquery/dist/jquery.min.js", "node_modules/jquery-ui-dist/jquery-ui.min.js", 'node_modules/@materializecss/materialize/dist/js/materialize.min.js', "plugins/CoreHome/javascripts/materialize-bc.js", 'plugins/Installation/javascripts/installation.js', 'plugins/Morpheus/javascripts/piwikHelper.js', "plugins/CoreHome/javascripts/broadcast.js", ); CoreVue::addJsFilesTo($files); $files[] = AssetManager\UIAssetFetcher\PluginUmdAssetFetcher::getUmdFileToUseForPlugin('CoreHome'); $files[] = AssetManager\UIAssetFetcher\PluginUmdAssetFetcher::getUmdFileToUseForPlugin('Installation'); if ( defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE && file_exists(PIWIK_DOCUMENT_ROOT . '/tests/resources/screenshot-override/override.js') ) { $files[] = 'tests/resources/screenshot-override/override.js'; } return AssetManager::compileCustomJs($files); } private function getParam($name) { return Common::getRequestVar($name, false, 'string'); } /** * Write configuration file from session-store */ private function createConfigFile($dbInfos) { $config = Config::getInstance(); // make sure DB sessions are used if the filesystem is NFS if (count($headers = ProxyHeaders::getProxyClientHeaders()) > 0) { $config->General['proxy_client_headers'] = $headers; } if (count($headers = ProxyHeaders::getProxyHostHeaders()) > 0) { $config->General['proxy_host_headers'] = $headers; } if (count($headers = ProxyHeaders::getProxySchemeHeaders()) > 0) { $config->General['proxy_scheme_headers'] = $headers; } if (Common::getRequestVar('clientProtocol', 'http', 'string') == 'https') { $protocol = 'https'; } else { $protocol = ProxyHeaders::getProtocolInformation(); } if ( !empty($protocol) && !\Piwik\ProxyHttp::isHttps() ) { $config->General['assume_secure_protocol'] = '1'; } $config->General['salt'] = Common::generateUniqId(); $config->General['installation_in_progress'] = 1; $this->setTrustedHost($config); $config->database = $dbInfos; $config->database['charset'] = DbHelper::getDefaultCharset(); $config->database['collation'] = DbHelper::getDefaultCollationForCharset($config->database['charset']); $config->forceSave(); // re-save the currently viewed language (since we saved the config file, there is now a salt which makes the // existing session cookie invalid) $this->resetLanguageCookie(); } private function resetLanguageCookie() { /** @var Translator $translator */ $translator = StaticContainer::get('Piwik\Translation\Translator'); LanguagesManager::setLanguageForSession($translator->getCurrentLanguage()); } private function checkPiwikIsNotInstalled($possibleErrorMessage = null) { if (!SettingsPiwik::isMatomoInstalled()) { return; } $possibleErrorMessage = $possibleErrorMessage ? sprintf('

Original error was "%s".
', $possibleErrorMessage) : ''; \Piwik\Plugins\Login\Controller::clearSession(); $message = Piwik::translate( 'Installation_InvalidStateError', array($possibleErrorMessage . '
', // piwik-is-already-installed is checked against in checkPiwikServerWorking '', '') ); Piwik::exitWithErrorMessage($message); } /** * Write configuration file from session-store */ private function markInstallationAsCompleted() { $config = Config::getInstance(); unset($config->General['installation_in_progress']); unset($config->General['installation_first_accessed']); $config->forceSave(); } /** * Redirect to next step * * @param string $currentStep Current step * @return void */ private function redirectToNextStep($currentStep, $parameters = array()) { $steps = array_keys($this->steps); $nextStep = $steps[1 + array_search($currentStep, $steps)]; Piwik::redirectToModule('Installation', $nextStep, $parameters); } /** * Extract host from URL * * @param string $url URL * * @return string|false */ private function extractHostAndPort($url) { $host = parse_url($url, PHP_URL_HOST) ?? false; if (empty($host)) { return false; } $port = (int) parse_url($url, PHP_URL_PORT) ?? 0; if (!empty($port) && $port !== 80 && $port !== 443) { return $host . ':' . $port; } return $host; } /** * Sets trusted hosts in config */ private function setTrustedHost(Config $config): void { $host = Url::getHost(false); // check hostname in server variables is correctly parsable if ($host === $this->extractHostAndPort('http://' . $host)) { $config->General['trusted_hosts'] = [$host]; } } private function createSuperUser( $login, #[\SensitiveParameter] $password, $email ) { Access::doAsSuperUser(function () use ($login, $password, $email) { $api = APIUsersManager::getInstance(); $api->addUser($login, $password, $email); $userUpdater = new UserUpdater(); $userUpdater->setSuperUserAccessWithoutCurrentPassword($login, true); }); } // should be private but there's a bug in php 5.3.6 public function hasEnoughTablesToReuseDb($tablesInstalled) { if (empty($tablesInstalled) || !is_array($tablesInstalled)) { return false; } $archiveTables = ArchiveTableCreator::getTablesArchivesInstalled(); $baseTablesInstalled = count($tablesInstalled) - count($archiveTables); $minimumCountPiwikTables = 12; return $baseTablesInstalled >= $minimumCountPiwikTables; } private function deleteConfigFileIfNeeded() { $config = Config::getInstance(); if ($config->existsLocalConfig()) { $firstInstallationAccess = $config->General['installation_first_accessed']; $settingsProvider = StaticContainer::get(GlobalSettingsProvider::class); $config->deleteLocalConfig(); $settingsProvider->reload(); $this->setUpInstallationExpiration($config, $firstInstallationAccess); // deleting the config file removes the salt, which in turns invalidates existing cookies (including the // one for selected language), so we re-save that cookie now $this->resetLanguageCookie(); } } /** * @return array|bool */ protected function updateComponents() { Access::getInstance(); return Access::doAsSuperUser(function () { $updater = new Updater(); $componentsWithUpdateFile = $updater->getComponentUpdates(); if (empty($componentsWithUpdateFile)) { return false; } $result = $updater->updateComponents($componentsWithUpdateFile); return $result; }); } private function getSystemCheckTextareaValue(DiagnosticReport $diagnosticReport) { $view = new \Piwik\View('@Installation/_systemCheckSection'); $view->diagnosticReport = $diagnosticReport; return $view->render(); } private function checkInstallationIsNotExpired(): void { $config = Config::getInstance(); if ( empty($config->General['installation_first_accessed']) || !is_numeric($config->General['installation_first_accessed']) ) { $this->setUpInstallationExpiration($config, Date::getNowTimestamp()); return; } $firstAccess = (int) $config->General['installation_first_accessed']; $threeDaysAgo = Date::getNowTimestamp() - (3 * 24 * 60 * 60); if ($firstAccess < $threeDaysAgo) { Piwik::exitWithErrorMessage( Piwik::translate('Installation_ErrorExpired1') . "\n
" . Piwik::translate('Installation_ErrorExpired2') . "\n" . Piwik::translate('Installation_ErrorExpired6') . "\n
" . Piwik::translate('Installation_ErrorExpired7', [ Url::getExternalLinkTag('https://matomo.org/faq/how-to-install/manage-secure-access-to-the-matomo-installer/'), '', ]) ); } } private function setUpInstallationExpiration(Config $config, int $timestamp): void { if ( !empty($config->General['installation_first_accessed']) && is_numeric($config->General['installation_first_accessed']) ) { return; } $config->General['installation_first_accessed'] = $timestamp; $config->forceSave(); } }