<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */
namespace Piwik\Plugins\Installation;

use Exception;
use Piwik\Access;
use Piwik\Application\Kernel\GlobalSettingsProvider;
use Piwik\AssetManager;
use Piwik\Common;
use Piwik\Config;
use Piwik\Container\StaticContainer;
use Piwik\DataAccess\ArchiveTableCreator;
use Piwik\Date;
use Piwik\Db;
use Piwik\DbHelper;
use Piwik\Filesystem;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Plugin\ControllerAdmin;
use Piwik\Plugin\Manager;
use Piwik\Plugins\CoreVue\CoreVue;
use Piwik\Plugins\Diagnostics\DiagnosticReport;
use Piwik\Plugins\Diagnostics\DiagnosticService;
use Piwik\Plugins\LanguagesManager\LanguagesManager;
use Piwik\Plugins\SitesManager\API as APISitesManager;
use Piwik\Plugins\SitesManager\SitesManager;
use Piwik\Plugins\UsersManager\API as APIUsersManager;
use Piwik\Plugins\UsersManager\NewsletterSignup;
use Piwik\Plugins\UsersManager\UserUpdater;
use Piwik\ProxyHeaders;
use Piwik\SettingsPiwik;
use Piwik\Tracker\TrackerCodeGenerator;
use Piwik\Translation\Translator;
use Piwik\Updater;
use Piwik\Url;
use Piwik\Version;
use Zend_Db_Adapter_Exception;
/**
 * Installation controller
 */
class Controller extends ControllerAdmin
{
    public $steps = array('welcome' => '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 \Piwik\Plugins\Installation\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 \Piwik\Plugins\Installation\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 \Piwik\Plugins\Installation\View('@Installation/databaseSetup', $this->getInstallationSteps(), __FUNCTION__);
        $view->showNextStep = \false;
        $form = new \Piwik\Plugins\Installation\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 \Piwik\Plugins\Installation\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 \Piwik\Plugins\Installation\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 \Piwik\Plugins\Installation\View('@Installation/setupSuperUser', $this->getInstallationSteps(), __FUNCTION__);
        $form = new \Piwik\Plugins\Installation\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);
                \Piwik\Plugins\Installation\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();
        \Piwik\Plugins\Installation\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 \Piwik\Plugins\Installation\View('@Installation/firstWebsiteSetup', $this->getInstallationSteps(), __FUNCTION__);
        $form = new \Piwik\Plugins\Installation\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 \Piwik\Plugins\Installation\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 \Piwik\Plugins\Installation\View('@Installation/finished', $this->getInstallationSteps(), __FUNCTION__);
        $form = new \Piwik\Plugins\Installation\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 \Piwik\Plugins\Installation\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 base.less compiled to css
     *
     * @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('<br/><br/>Original error was "%s".<br/>', $possibleErrorMessage) : '';
        \Piwik\Plugins\Login\Controller::clearSession();
        $message = Piwik::translate('Installation_InvalidStateError', array(
            $possibleErrorMessage . '<br /><strong>',
            // piwik-is-already-installed is checked against in checkPiwikServerWorking
            '</strong><a id="piwik-is-already-installed" href=\'' . Common::sanitizeInputValue(Url::getCurrentUrlWithoutFileName()) . '\'>',
            '</a>',
        ));
        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<br/>" . Piwik::translate('Installation_ErrorExpired2') . "\n<ul>" . "\n<li>" . Piwik::translate('Installation_ErrorExpired3', ['<strong>', '</strong>']) . '</li>' . "\n<li>" . Piwik::translate('Installation_ErrorExpired4', ['<strong>', '</strong>']) . '</li>' . "\n<li>" . Piwik::translate('Installation_ErrorExpired5') . '</li>' . "\n</ul>" . Piwik::translate('Installation_ErrorExpired6') . "\n<br/>" . Piwik::translate('Installation_ErrorExpired7', [Url::getExternalLinkTag('https://matomo.org/faq/how-to-install/manage-secure-access-to-the-matomo-installer/'), '</a>']));
        }
    }
    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();
    }
}
