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

use Exception;
use Piwik\Common;
use Piwik\Config\GeneralConfig;
use Piwik\Container\StaticContainer;
use Piwik\DataTable\Renderer\Json;
use Piwik\Date;
use Piwik\Filesystem;
use Piwik\Log;
use Piwik\Nonce;
use Piwik\Notification;
use Piwik\Piwik;
use Piwik\Plugin;
use Piwik\Plugins\CorePluginsAdmin\Controller as PluginsController;
use Piwik\Plugins\CorePluginsAdmin\CorePluginsAdmin;
use Piwik\Plugins\CorePluginsAdmin\PluginInstaller;
use Piwik\Plugins\Login\PasswordVerifier;
use Piwik\Plugins\Marketplace\Input\PluginName;
use Piwik\Plugins\Marketplace\Input\PurchaseType;
use Piwik\Plugins\Marketplace\Input\Sort;
use Piwik\Plugins\Marketplace\PluginTrial\Service as PluginTrialService;
use Piwik\ProxyHttp;
use Piwik\Request;
use Piwik\SettingsPiwik;
use Piwik\SettingsServer;
use Piwik\Url;
use Piwik\View;
class Controller extends \Piwik\Plugin\ControllerAdmin
{
    public const UPDATE_NONCE = 'Marketplace.updatePlugin';
    public const INSTALL_NONCE = 'Marketplace.installPlugin';
    public const DOWNLOAD_NONCE_PREFIX = 'Marketplace.downloadPlugin.';
    /**
     * @var LicenseKey
     */
    private $licenseKey;
    /**
     * @var Plugins
     */
    private $plugins;
    /**
     * @var Api\Client
     */
    private $marketplaceApi;
    /**
     * @var Consumer
     */
    private $consumer;
    /**
     * @var PluginInstaller
     */
    private $pluginInstaller;
    /**
     * @var Plugin\Manager
     */
    private $pluginManager;
    /**
     * @var Environment
     */
    private $environment;
    /**
     * @var PasswordVerifier
     */
    private $passwordVerify;
    /**
     * @var array<int, array<string, mixed>>|null
     */
    private $paidPlugins;
    public function __construct(\Piwik\Plugins\Marketplace\LicenseKey $licenseKey, \Piwik\Plugins\Marketplace\Plugins $plugins, \Piwik\Plugins\Marketplace\Api\Client $marketplaceApi, \Piwik\Plugins\Marketplace\Consumer $consumer, PluginInstaller $pluginInstaller, \Piwik\Plugins\Marketplace\Environment $environment, PasswordVerifier $passwordVerify)
    {
        $this->licenseKey = $licenseKey;
        $this->plugins = $plugins;
        $this->marketplaceApi = $marketplaceApi;
        $this->consumer = $consumer;
        $this->pluginInstaller = $pluginInstaller;
        $this->pluginManager = Plugin\Manager::getInstance();
        $this->environment = $environment;
        $this->passwordVerify = $passwordVerify;
        $this->paidPlugins = null;
        parent::__construct();
    }
    public function subscriptionOverview()
    {
        Piwik::checkUserHasSuperUserAccess();
        // we want to make sure to fetch the latest results, eg in case user has purchased a subscription meanwhile
        // this is also like a self-repair to clear the caches :)
        $this->marketplaceApi->clearAllCacheEntries();
        $this->consumer->clearCache();
        // invalidate cache for plugin/manager
        Plugin\Manager::getLicenseCache()->flushAll();
        $hasLicenseKey = $this->licenseKey->has();
        $consumer = $this->consumer->getConsumer();
        $subscriptions = array();
        $loginUrl = '';
        if (!empty($consumer['loginUrl'])) {
            $loginUrl = $consumer['loginUrl'];
        }
        if (!empty($consumer['licenses'])) {
            foreach ($consumer['licenses'] as $subscription) {
                $subscription['start'] = $this->getPrettyLongDate($subscription['startDate']);
                $subscription['end'] = $this->getPrettyLongDate($subscription['endDate']);
                $subscription['nextPayment'] = $this->getPrettyLongDate($subscription['nextPaymentDate']);
                $subscriptions[] = $subscription;
            }
        }
        return $this->renderTemplate('@Marketplace/subscription-overview', array('hasLicenseKey' => $hasLicenseKey, 'subscriptions' => $subscriptions, 'loginUrl' => $loginUrl, 'numUsers' => $this->environment->getNumUsers()));
    }
    public function manageLicenseKey()
    {
        Piwik::checkUserHasSuperUserAccess();
        return $this->renderTemplate('@Marketplace/manageLicenseKey', array('hasValidLicenseKey' => $this->licenseKey->has() && $this->consumer->isValidConsumer()));
    }
    public function getPaidPluginsToInstallAtOnceParams()
    {
        Piwik::checkUserHasSuperUserAccess();
        Json::sendHeaderJSON();
        if (!$this->isInstallAllPaidPluginsVisible()) {
            return json_encode([]);
        }
        return json_encode(['paidPluginsToInstallAtOnce' => $this->getAllPaidPluginsToInstallAtOnce(), 'installAllPluginsNonce' => Nonce::getNonce(self::INSTALL_NONCE)], \JSON_HEX_APOS);
    }
    private function getPrettyLongDate($date)
    {
        if (empty($date)) {
            return '';
        }
        return Date::factory($date)->getLocalized(Date::DATE_FORMAT_LONG);
    }
    public function pluginDetails()
    {
        $view = $this->configureViewAndCheckPermission('@Marketplace/plugin-details');
        $pluginName = new PluginName();
        $pluginName = $pluginName->getPluginName();
        $activeTab = Common::getRequestVar('activeTab', '', 'string');
        if ('changelog' !== $activeTab) {
            $activeTab = '';
        }
        try {
            $plugin = $this->plugins->getPluginInfo($pluginName);
            if (empty($plugin['name'])) {
                throw new Exception('Plugin does not exist');
            }
        } catch (Exception $e) {
            $plugin = null;
            $view->errorMessage = $e->getMessage();
        }
        $view->plugin = $plugin;
        $view->isSuperUser = Piwik::hasUserSuperUserAccess();
        $view->installNonce = Nonce::getNonce(static::INSTALL_NONCE);
        $view->updateNonce = Nonce::getNonce(static::UPDATE_NONCE);
        $view->activeTab = $activeTab;
        $view->isAutoUpdatePossible = SettingsPiwik::isAutoUpdatePossible();
        $view->isAutoUpdateEnabled = SettingsPiwik::isAutoUpdateEnabled();
        return $view->render();
    }
    public function download()
    {
        Piwik::checkUserHasSuperUserAccess();
        $this->dieIfPluginsAdminIsDisabled();
        $pluginName = new PluginName();
        $pluginName = $pluginName->getPluginName();
        Nonce::checkNonce(static::DOWNLOAD_NONCE_PREFIX . $pluginName);
        $filename = $pluginName . '.zip';
        try {
            $pathToPlugin = $this->marketplaceApi->download($pluginName);
            ProxyHttp::serverStaticFile($pathToPlugin, 'application/zip', $expire = 0, $start = \false, $end = \false, $filename);
        } catch (Exception $e) {
            Common::sendResponseCode(500);
            Log::warning('Could not download file . ' . $e->getMessage());
        }
        if (!empty($pathToPlugin)) {
            Filesystem::deleteFileIfExists($pathToPlugin);
        }
    }
    private function getPaidPluginsToInstallAtOnceData(array $paidPlugins) : array
    {
        $paidPluginsToInstallAtOnce = [];
        if (SettingsPiwik::isAutoUpdatePossible()) {
            foreach ($paidPlugins as $paidPlugin) {
                if ($this->canPluginBeInstalled($paidPlugin) || $this->pluginManager->isPluginInstalled($paidPlugin['name'], \true) && !$this->pluginManager->isPluginActivated($paidPlugin['name'])) {
                    $paidPluginsToInstallAtOnce[] = $paidPlugin['displayName'];
                }
            }
        }
        return $paidPluginsToInstallAtOnce;
    }
    public function overview()
    {
        $view = $this->configureViewAndCheckPermission('@Marketplace/overview');
        $view->paidPluginsToInstallAtOnce = $this->getAllPaidPluginsToInstallAtOnce();
        $view->isValidConsumer = $this->consumer->isValidConsumer();
        $view->pluginTypeOptions = array('plugins' => Piwik::translate('General_Plugins'), 'premium' => Piwik::translate('Marketplace_PaidPlugins'), 'themes' => Piwik::translate('CorePluginsAdmin_Themes'));
        $view->pluginSortOptions = array(Sort::METHOD_LAST_UPDATED => Piwik::translate('Marketplace_SortByLastUpdated'), Sort::METHOD_POPULAR => Piwik::translate('Marketplace_SortByPopular'), Sort::METHOD_NEWEST => Piwik::translate('Marketplace_SortByNewest'), Sort::METHOD_ALPHA => Piwik::translate('Marketplace_SortByAlpha'));
        $view->defaultSort = Sort::DEFAULT_SORT;
        $view->installNonce = Nonce::getNonce(static::INSTALL_NONCE);
        $view->updateNonce = Nonce::getNonce(static::UPDATE_NONCE);
        $view->deactivateNonce = Nonce::getNonce(PluginsController::DEACTIVATE_NONCE);
        $view->activateNonce = Nonce::getNonce(PluginsController::ACTIVATE_NONCE);
        $view->currentUserEmail = Piwik::getCurrentUserEmail();
        $view->isSuperUser = Piwik::hasUserSuperUserAccess();
        $view->isPluginsAdminEnabled = CorePluginsAdmin::isPluginsAdminEnabled();
        $view->isAutoUpdatePossible = SettingsPiwik::isAutoUpdatePossible();
        $view->isAutoUpdateEnabled = SettingsPiwik::isAutoUpdateEnabled();
        $view->isPluginUploadEnabled = CorePluginsAdmin::isPluginUploadEnabled();
        $view->uploadLimit = SettingsServer::getPostMaxUploadSize();
        $view->inReportingMenu = (bool) Common::getRequestVar('embed', 0, 'int');
        $view->numUsers = $this->environment->getNumUsers();
        return $view->render();
    }
    public function updateOverview() : string
    {
        Piwik::checkUserIsNotAnonymous();
        $paidPlugins = $this->getPaidPlugins();
        $updateData = ['isValidConsumer' => $this->consumer->isValidConsumer()];
        Json::sendHeaderJSON();
        return json_encode($updateData);
    }
    public function searchPlugins() : string
    {
        Piwik::checkUserIsNotAnonymous();
        $request = Request::fromRequest();
        $query = $request->getStringParameter('query', '');
        $themesOnly = $request->getBoolParameter('themesOnly', \false);
        $purchaseType = $request->getStringParameter('purchaseType', '');
        $sort = $request->getStringParameter('sort', '');
        $purchaseType = (new PurchaseType())->getPurchaseType($purchaseType);
        $sort = (new Sort())->getSort($sort);
        $plugins = $this->plugins->searchPlugins($query, $sort, $themesOnly, $purchaseType);
        foreach ($plugins as &$plugin) {
            if ($plugin['isDownloadable']) {
                $plugin['downloadNonce'] = Nonce::getNonce(static::DOWNLOAD_NONCE_PREFIX . $plugin['name']);
            }
            $plugin['isTrialRequested'] = \false;
            $plugin['canTrialBeRequested'] = \false;
            if ($plugin['isEligibleForFreeTrial']) {
                $plugin['isTrialRequested'] = StaticContainer::get(PluginTrialService::class)->wasRequested($plugin['name']);
                $plugin['canTrialBeRequested'] = (int) GeneralConfig::getConfigValue('plugin_trial_request_expiration_in_days') !== -1;
            }
        }
        Json::sendHeaderJSON();
        return json_encode($plugins);
    }
    public function installAllPaidPlugins()
    {
        Piwik::checkUserHasSuperUserAccess();
        $this->dieIfPluginsAdminIsDisabled();
        Plugin\ControllerAdmin::displayWarningIfConfigFileNotWritable();
        $params = array('module' => 'Marketplace', 'action' => 'installAllPaidPlugins', 'nonce' => Common::getRequestVar('nonce'));
        if ($this->passwordVerify->requirePasswordVerifiedRecently($params)) {
            Nonce::checkNonce(static::INSTALL_NONCE);
            $paidPlugins = $this->getPaidPlugins();
            $hasErrors = \false;
            foreach ($paidPlugins as $paidPlugin) {
                if (!$this->canPluginBeInstalled($paidPlugin)) {
                    continue;
                }
                $pluginName = $paidPlugin['name'];
                try {
                    $this->pluginInstaller->installOrUpdatePluginFromMarketplace($pluginName);
                } catch (\Exception $e) {
                    $notification = new Notification($e->getMessage());
                    $notification->context = Notification::CONTEXT_ERROR;
                    if (method_exists($e, 'isHtmlMessage') && $e->isHtmlMessage()) {
                        $notification->raw = \true;
                    }
                    Notification\Manager::notify('Marketplace_Install' . $pluginName, $notification);
                    $hasErrors = \true;
                }
            }
            if ($hasErrors) {
                Url::redirectToReferrer();
                return;
            }
            $dependency = new Plugin\Dependency();
            for ($i = 0; $i <= 10; $i++) {
                foreach ($paidPlugins as $index => $paidPlugin) {
                    if (empty($paidPlugin)) {
                        continue;
                    }
                    $pluginName = $paidPlugin['name'];
                    if ($this->pluginManager->isPluginActivated($pluginName)) {
                        // we do not use unset since it might skip a plugin afterwards when removing index
                        $paidPlugins[$index] = null;
                        continue;
                    }
                    if (!$this->pluginManager->isPluginInFilesystem($pluginName)) {
                        $paidPlugins[$index] = null;
                        continue;
                    }
                    if (empty($paidPlugin['require']) || !$dependency->hasDependencyToDisabledPlugin($paidPlugin['require'])) {
                        $paidPlugins[$index] = null;
                        try {
                            $this->pluginManager->activatePlugin($pluginName);
                        } catch (Exception $e) {
                            $hasErrors = \true;
                            $notification = new Notification($e->getMessage());
                            $notification->context = Notification::CONTEXT_ERROR;
                            Notification\Manager::notify('Marketplace_Install' . $pluginName, $notification);
                        }
                    }
                }
                $paidPlugins = array_filter($paidPlugins);
            }
            if ($hasErrors) {
                $notification = new Notification(Piwik::translate('Marketplace_OnlySomePaidPluginsInstalledAndActivated'));
                $notification->context = Notification::CONTEXT_INFO;
            } else {
                $notification = new Notification(Piwik::translate('Marketplace_AllPaidPluginsInstalledAndActivated'));
                $notification->context = Notification::CONTEXT_SUCCESS;
            }
            Notification\Manager::notify('Marketplace_InstallAll', $notification);
            Url::redirectToUrl(Url::getCurrentUrlWithoutQueryString() . Url::getCurrentQueryStringWithParametersModified(['action' => 'overview', 'nonce' => null]));
        }
    }
    public function updatePlugin()
    {
        $view = $this->createUpdateOrInstallView('updatePlugin', static::UPDATE_NONCE);
        return $view->render();
    }
    public function installPlugin()
    {
        $params = array('module' => 'Marketplace', 'action' => 'installPlugin', 'mode' => 'admin', 'pluginName' => Common::getRequestVar('pluginName'), 'nonce' => Common::getRequestVar('nonce'));
        if ($this->passwordVerify->requirePasswordVerifiedRecently($params)) {
            $view = $this->createUpdateOrInstallView('installPlugin', static::INSTALL_NONCE);
            $view->nonce = Nonce::getNonce(PluginsController::ACTIVATE_NONCE);
            return $view->render();
        }
    }
    private function createUpdateOrInstallView($template, $nonceName)
    {
        Piwik::checkUserHasSuperUserAccess();
        $this->dieIfPluginsAdminIsDisabled();
        $this->displayWarningIfConfigFileNotWritable();
        $plugins = $this->getPluginNameIfNonceValid($nonceName);
        $view = new View('@Marketplace/' . $template);
        $this->setBasicVariablesView($view);
        $view->errorMessage = '';
        $pluginInfos = [];
        foreach ($plugins as $pluginName) {
            $currentPluginInfo = $this->plugins->getPluginInfo($pluginName);
            $pluginInfos[] = $currentPluginInfo;
            try {
                $this->pluginInstaller->installOrUpdatePluginFromMarketplace($pluginName);
            } catch (\Exception $e) {
                $message = $e->getMessage();
                $isRaw = \false;
                if (stripos($message, 'PCLZIP_ERR_BAD_FORMAT') !== \false) {
                    $faqLink = Url::addCampaignParametersToMatomoLink('https://matomo.org/faq/plugins/faq_21/');
                    if (!empty($currentPluginInfo['isPaid'])) {
                        $downloadLink = Url::addCampaignParametersToMatomoLink('https://shop.matomo.org/my-account/downloads');
                        $translateKey = 'Marketplace_PluginDownloadLinkMissingPremium';
                    } else {
                        $downloadLink = Url::addCampaignParametersToMatomoLink('https://plugins.matomo.org/' . $pluginName);
                        $translateKey = 'Marketplace_PluginDownloadLinkMissingFree';
                    }
                    $message = Piwik::translate($translateKey, [$pluginName, Url::getExternalLinkTag($downloadLink), '</a>', Url::getExternalLinkTag($faqLink), '</a>']);
                    $isRaw = \true;
                }
                $notification = new Notification($message);
                $notification->context = Notification::CONTEXT_ERROR;
                $notification->type = Notification::TYPE_PERSISTENT;
                $notification->flags = Notification::FLAG_CLEAR;
                if (method_exists($e, 'isHtmlMessage') && $e->isHtmlMessage() || $isRaw) {
                    $notification->raw = \true;
                }
                Notification\Manager::notify('CorePluginsAdmin_InstallPlugin', $notification);
                Url::redirectToReferrer();
                return;
            }
        }
        $view->plugins = $pluginInfos;
        return $view;
    }
    private function getPluginNameIfNonceValid($nonceName)
    {
        Nonce::checkNonce($nonceName);
        $pluginName = Common::getRequestVar('pluginName', null, 'string');
        $plugins = explode(',', $pluginName);
        $plugins = array_map('trim', $plugins);
        foreach ($plugins as $name) {
            if (!$this->pluginManager->isValidPluginName($name)) {
                throw new Exception('Invalid plugin name: ' . $name);
            }
        }
        return $plugins;
    }
    private function dieIfPluginsAdminIsDisabled()
    {
        if (!CorePluginsAdmin::isPluginsAdminEnabled()) {
            throw new \Exception('Enabling, disabling and uninstalling plugins has been disabled by Matomo admins.
            Please contact your Matomo admins with your request so they can assist you.');
        }
    }
    private function canPluginBeInstalled($plugin)
    {
        if (empty($plugin['isDownloadable'])) {
            return \false;
        }
        $pluginName = $plugin['name'];
        $isAlreadyInstalled = $this->pluginManager->isPluginInstalled($pluginName, \true) || $this->pluginManager->isPluginLoaded($pluginName) || $this->pluginManager->isPluginActivated($pluginName);
        return !$isAlreadyInstalled && $plugin['hasDownloadLink'];
    }
    protected function configureViewAndCheckPermission($template)
    {
        Piwik::checkUserIsNotAnonymous();
        $view = new View($template);
        $this->setBasicVariablesView($view);
        $this->displayWarningIfConfigFileNotWritable();
        $this->securityPolicy->addPolicy('img-src', '*.matomo.org');
        $this->securityPolicy->addPolicy('default-src', '*.matomo.org');
        $view->errorMessage = '';
        return $view;
    }
    private function isInstallAllPaidPluginsVisible() : bool
    {
        return $this->consumer->isValidConsumer() && Piwik::hasUserSuperUserAccess() && SettingsPiwik::isAutoUpdatePossible() && CorePluginsAdmin::isPluginsAdminEnabled() && count($this->getAllPaidPluginsToInstallAtOnce()) > 0;
    }
    private function getAllPaidPluginsToInstallAtOnce()
    {
        $paidPlugins = $this->getPaidPlugins();
        return $this->getPaidPluginsToInstallAtOnceData($paidPlugins);
    }
    /**
     * @return array<int, array<string, mixed>>
     */
    private function getPaidPlugins() : array
    {
        if ($this->paidPlugins === null) {
            $this->paidPlugins = $this->plugins->getAllPaidPlugins();
        }
        return $this->paidPlugins;
    }
}
