<?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\CoreUpdater;

use Exception;
use Piwik\AssetManager;
use Piwik\Common;
use Piwik\Config;
use Piwik\DataTable\Renderer\Json;
use Piwik\DbHelper;
use Piwik\Development;
use Piwik\Filechecks;
use Piwik\FileIntegrity;
use Piwik\Filesystem;
use Piwik\Nonce;
use Piwik\Option;
use Piwik\Piwik;
use Piwik\Plugin\Manager as PluginManager;
use Piwik\Plugins\CoreVue\CoreVue;
use Piwik\Plugins\Marketplace\Plugins;
use Piwik\Request;
use Piwik\SettingsPiwik;
use Piwik\SettingsServer;
use Piwik\Updater as DbUpdater;
use Piwik\Updater\Migration\Db as DbMigration;
use Piwik\Url;
use Piwik\Version;
use Piwik\View;
use Piwik\View\OneClickDone;
class Controller extends \Piwik\Plugin\Controller
{
    private $coreError = \false;
    private $warningMessages = array();
    private $errorMessages = array();
    private $deactivatedPlugins = array();
    /**
     * @var Updater
     */
    private $updater;
    /**
     * @var Plugins
     */
    private $marketplacePlugins;
    public function __construct(\Piwik\Plugins\CoreUpdater\Updater $updater, ?Plugins $marketplacePlugins = null)
    {
        $this->updater = $updater;
        $this->marketplacePlugins = $marketplacePlugins;
        parent::__construct();
    }
    /**
     * Return the base.less compiled to css
     *
     * @return string
     */
    public function getUpdaterCss()
    {
        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/jquery-ui-dist/jquery-ui.structure.min.css", 'node_modules/@materializecss/materialize/dist/css/materialize.min.css', 'plugins/Morpheus/stylesheets/base.less', 'plugins/Morpheus/stylesheets/general/_forms.less', 'plugins/Morpheus/stylesheets/simple_structure.css', 'plugins/CoreHome/stylesheets/jquery.ui.autocomplete.css', 'plugins/Dashboard/stylesheets/dashboard.less', 'plugins/CoreUpdater/stylesheets/updateLayout.css');
        return AssetManager::compileCustomStylesheets($files);
    }
    /**
     * Return the base.less compiled to css
     *
     * @return string
     */
    public function getUpdaterJs()
    {
        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/Morpheus/javascripts/piwikHelper.js', "plugins/CoreHome/javascripts/broadcast.js", 'plugins/CoreUpdater/javascripts/updateLayout.js', 'plugins/Installation/javascripts/installation.js');
        CoreVue::addJsFilesTo($files);
        $coreHomeUmd = Development::isEnabled() ? 'CoreHome.umd.js' : 'CoreHome.umd.min.js';
        $files[] = "plugins/CoreHome/vue/dist/{$coreHomeUmd}";
        return AssetManager::compileCustomJs($files);
    }
    public function newVersionAvailable()
    {
        Piwik::checkUserHasSuperUserAccess();
        if (!SettingsPiwik::isAutoUpdateEnabled()) {
            throw new Exception('Auto updater is disabled');
        }
        $this->checkNewVersionIsAvailableOrDie();
        $newVersion = $this->updater->getLatestVersion();
        $view = new View('@CoreUpdater/newVersionAvailable');
        $this->addCustomLogoInfo($view);
        $this->setBasicVariablesView($view);
        $view->piwik_version = Version::VERSION;
        $view->piwik_new_version = $newVersion;
        $incompatiblePlugins = $this->getIncompatiblePlugins($newVersion);
        $marketplacePlugins = array();
        try {
            if (!empty($incompatiblePlugins) && $this->marketplacePlugins) {
                $marketplacePlugins = $this->marketplacePlugins->getAllAvailablePluginNames();
            }
        } catch (\Exception $e) {
        }
        $view->marketplacePlugins = $marketplacePlugins;
        $view->incompatiblePlugins = $incompatiblePlugins;
        $view->piwik_latest_version_url = $this->updater->getArchiveUrl($newVersion);
        $view->can_auto_update = Filechecks::canAutoUpdate();
        $view->makeWritableCommands = Filechecks::getAutoUpdateMakeWritableMessage();
        $view->nonce = Nonce::getNonce('oneClickUpdate');
        return $view->render();
    }
    public function oneClickUpdate()
    {
        Piwik::checkUserHasSuperUserAccess();
        if (!SettingsPiwik::isAutoUpdateEnabled()) {
            throw new Exception('Auto updater is disabled');
        }
        Nonce::checkNonce('oneClickUpdate');
        $view = new OneClickDone(Piwik::getCurrentUserTokenAuth());
        $useHttps = Common::getRequestVar('https', 1, 'int');
        try {
            $messages = $this->updater->updatePiwik($useHttps);
        } catch (\Piwik\Plugins\CoreUpdater\ArchiveDownloadException $e) {
            $view->httpsFail = $useHttps;
            $view->error = $e->getMessage();
            $messages = $e->getUpdateLogMessages();
        } catch (\Piwik\Plugins\CoreUpdater\UpdaterException $e) {
            $view->error = $e->getMessage();
            $messages = $e->getUpdateLogMessages();
        }
        $view->feedbackMessages = $messages;
        $result = $view->render();
        return $result;
    }
    public function oneClickUpdatePartTwo()
    {
        if (!SettingsPiwik::isAutoUpdateEnabled()) {
            throw new Exception('Auto updater is disabled');
        }
        Json::sendHeaderJSON();
        $task = "Couldn't update Marketplace plugins.";
        $nonce = Common::getRequestVar('nonce', '', 'string');
        if (empty($nonce)) {
            return json_encode(['No token. ' . $task]);
        }
        $value = Option::get('NonceOneClickUpdatePartTwo');
        if (empty($value)) {
            return json_encode(['Invalid token. ' . $task]);
        }
        $value = json_decode($value, \true);
        if (empty($value['nonce']) || empty($value['ttl']) || time() > (int) $value['ttl'] || $nonce !== $value['nonce']) {
            return json_encode(['Invalid nonce or nonce expired. ' . $task]);
        }
        try {
            $messages = $this->updater->oneClickUpdatePartTwo();
        } catch (\Piwik\Plugins\CoreUpdater\UpdaterException $e) {
            $messages = $e->getUpdateLogMessages();
            $messages[] = $e->getMessage();
        } catch (Exception $e) {
            $messages = [$e->getMessage()];
        }
        return json_encode($messages);
    }
    public function oneClickResults()
    {
        if (!SettingsPiwik::isAutoUpdateEnabled()) {
            throw new Exception('Auto updater is disabled');
        }
        $httpsFail = (bool) Common::getRequestVar('httpsFail', 0, 'int', $_POST);
        $error = Common::getRequestVar('error', '', 'string', $_POST);
        if ($httpsFail) {
            $view = new View('@CoreUpdater/updateHttpsError');
            $view->nonce = Nonce::getNonce('oneClickUpdate');
            $view->error = $error;
        } elseif ($error) {
            $view = new View('@CoreUpdater/updateHttpError');
            $view->error = $error;
        } else {
            $updateDetailsToken = $this->createUpdateDetailsTokenIfMissing();
            $runUpdaterUrl = Url::getCurrentUrlWithoutQueryString();
            if ($updateDetailsToken !== null) {
                $runUpdaterUrl .= '?module=CoreUpdater&updateDetailsToken=' . urlencode($updateDetailsToken);
            }
            $view = new View('@CoreUpdater/updateSuccess');
            $view->runUpdaterUrl = $runUpdaterUrl;
        }
        $messages = safe_unserialize(Request::fromPost()->getStringParameter('messages', ''));
        if (!is_array($messages)) {
            $messages = array();
        }
        $view->feedbackMessages = $messages;
        $this->addCustomLogoInfo($view);
        $this->setBasicVariablesView($view);
        return $view->render();
    }
    protected function redirectToDashboardWhenNoError(DbUpdater $updater)
    {
        if (count($updater->getSqlQueriesToExecute()) == 1 && !$this->coreError && empty($this->warningMessages) && empty($this->errorMessages) && empty($this->deactivatedPlugins)) {
            Piwik::redirectToModule('CoreHome');
        }
    }
    private function checkNewVersionIsAvailableOrDie()
    {
        if (!$this->updater->isNewVersionAvailable()) {
            throw new Exception(Piwik::translate('CoreUpdater_ExceptionAlreadyLatestVersion', Version::VERSION));
        }
    }
    public function index()
    {
        try {
            return $this->runUpdaterAndExit();
        } catch (\Piwik\Plugins\CoreUpdater\NoUpdatesFoundException $e) {
            Piwik::redirectToModule('CoreHome');
        }
    }
    public function runUpdaterAndExit($doDryRun = null)
    {
        if (!SettingsPiwik::isAutoUpdateEnabled()) {
            throw new Exception('Auto updater is disabled');
        }
        $updater = new DbUpdater();
        $componentsWithUpdateFile = $updater->getComponentUpdates();
        if (empty($componentsWithUpdateFile)) {
            // remove token if updater screen is not displayed
            $this->removeUpdateDetailsToken();
            throw new \Piwik\Plugins\CoreUpdater\NoUpdatesFoundException("Everything is already up to date.");
        }
        SettingsServer::setMaxExecutionTime(0);
        $welcomeTemplate = '@CoreUpdater/runUpdaterAndExit_welcome';
        $doneTemplate = '@CoreUpdater/runUpdaterAndExit_done';
        $viewWelcome = new View($welcomeTemplate);
        $this->addCustomLogoInfo($viewWelcome);
        $this->setBasicVariablesView($viewWelcome);
        $viewDone = new View($doneTemplate);
        $this->addCustomLogoInfo($viewDone);
        $this->setBasicVariablesView($viewDone);
        $doExecuteUpdates = Request::fromRequest()->getBoolParameter('updateCorePlugins', \false);
        if (is_null($doDryRun)) {
            $doDryRun = !$doExecuteUpdates;
        }
        if ($doDryRun) {
            $migrations = $updater->getSqlQueriesToExecute();
            $queryCount = count($migrations);
            $migrations = $this->groupMigrations($migrations);
            $viewWelcome->migrations = $migrations;
            $viewWelcome->queryCount = $queryCount;
            $viewWelcome->isMajor = $updater->hasMajorDbUpdate();
            $viewWelcome->showUpdateDetails = $this->checkUpdateDetailsToken();
            $this->doWelcomeUpdates($viewWelcome, $componentsWithUpdateFile);
            return $viewWelcome->render();
        }
        // Web
        if ($doExecuteUpdates) {
            $this->warningMessages = [];
            $this->doExecuteUpdates($viewDone, $updater, $componentsWithUpdateFile);
            $this->removeUpdateDetailsToken();
            $this->redirectToDashboardWhenNoError($updater);
            return $viewDone->render();
        }
        exit;
    }
    private function groupMigrations($migrations)
    {
        $result = [];
        $group = null;
        foreach ($migrations as $migration) {
            $type = $migration instanceof DbMigration ? 'sql' : 'command';
            if ($group === null || $type != $group['type']) {
                $group = ['type' => $type, 'migrations' => []];
                $result[] = $group;
            }
            $result[count($result) - 1]['migrations'][] = $migration;
        }
        return $result;
    }
    private function doWelcomeUpdates($view, $componentsWithUpdateFile)
    {
        $view->new_piwik_version = Version::VERSION;
        $view->commandUpgradePiwik = "php " . Filesystem::getPathToPiwikRoot() . "/console core:update";
        $instanceId = SettingsPiwik::getPiwikInstanceId();
        if ($instanceId) {
            $view->commandUpgradePiwik .= ' --matomo-domain="' . $instanceId . '"';
        }
        $pluginNamesToUpdate = array();
        $dimensionsToUpdate = array();
        $coreToUpdate = \false;
        // handle case of existing database with no tables
        if (!DbHelper::isInstalled()) {
            $this->errorMessages[] = Piwik::translate('CoreUpdater_EmptyDatabaseError', Config::getInstance()->database['dbname']);
            $this->coreError = \true;
            $currentVersion = 'N/A';
        } else {
            $this->errorMessages = array();
            try {
                $currentVersion = Option::get('version_core');
            } catch (Exception $e) {
                $currentVersion = '<= 0.2.9';
            }
            foreach ($componentsWithUpdateFile as $name => $filenames) {
                if ($name == 'core') {
                    $coreToUpdate = \true;
                } elseif (0 === strpos($name, 'log_')) {
                    $dimensionsToUpdate[] = $name;
                } else {
                    $pluginNamesToUpdate[] = $name;
                }
            }
        }
        // check file integrity
        [$success, $messages] = FileIntegrity::getFileIntegrityInformation();
        if (!$success) {
            $this->warningMessages[] = Piwik::translate('General_FileIntegrityWarning');
        }
        if (count($messages) > 0) {
            $this->warningMessages = array_merge($this->warningMessages, $messages);
        }
        Filesystem::deleteAllCacheOnUpdate();
        sort($dimensionsToUpdate);
        $view->coreError = $this->coreError;
        $view->warningMessages = $this->warningMessages;
        $view->errorMessages = $this->errorMessages;
        $view->current_piwik_version = $currentVersion;
        $view->pluginNamesToUpdate = $pluginNamesToUpdate;
        $view->dimensionsToUpdate = $dimensionsToUpdate;
        $view->coreToUpdate = $coreToUpdate;
    }
    private function doExecuteUpdates($view, DbUpdater $updater, $componentsWithUpdateFile)
    {
        $result = $updater->updateComponents($componentsWithUpdateFile);
        $this->coreError = $result['coreError'];
        $this->warningMessages = $result['warnings'];
        $this->errorMessages = $result['errors'];
        $this->deactivatedPlugins = $result['deactivatedPlugins'];
        $view->coreError = $this->coreError;
        $view->warningMessages = $this->warningMessages;
        $view->errorMessages = $this->errorMessages;
        $view->deactivatedPlugins = $this->deactivatedPlugins;
    }
    private function getIncompatiblePlugins($piwikVersion)
    {
        return PluginManager::getInstance()->getIncompatiblePlugins($piwikVersion);
    }
    private function checkUpdateDetailsToken() : bool
    {
        $config = Config::getInstance();
        if (empty($config->General['update_details_token'])) {
            return \false;
        }
        return $config->General['update_details_token'] === Request::fromRequest()->getStringParameter('updateDetailsToken', '');
    }
    private function createUpdateDetailsTokenIfMissing() : ?string
    {
        $config = Config::getInstance();
        if (!empty($config->General['update_details_token'])) {
            return null;
        }
        $token = Common::generateUniqId();
        $config->General['update_details_token'] = $token;
        $config->forceSave();
        return $token;
    }
    private function removeUpdateDetailsToken() : void
    {
        $config = Config::getInstance();
        if (empty($config->General['update_details_token'])) {
            return;
        }
        unset($config->General['update_details_token']);
        $config->forceSave();
    }
}
