1<?php
2/**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9namespace Piwik\Plugins\CorePluginsAdmin;
10
11use Exception;
12use Piwik\Access;
13use Piwik\API\Request;
14use Piwik\Common;
15use Piwik\Container\StaticContainer;
16use Piwik\ErrorHandler;
17use Piwik\Exception\MissingFilePermissionException;
18use Piwik\Filechecks;
19use Piwik\Filesystem;
20use Piwik\Nonce;
21use Piwik\Notification;
22use Piwik\Piwik;
23use Piwik\Plugin;
24use Piwik\Plugins\CorePluginsAdmin\Model\TagManagerTeaser;
25use Piwik\Plugins\Login\PasswordVerifier;
26use Piwik\Plugins\Marketplace\Marketplace;
27use Piwik\Plugins\Marketplace\Controller as MarketplaceController;
28use Piwik\Plugins\Marketplace\Plugins;
29use Piwik\Settings\Storage\Backend\PluginSettingsTable;
30use Piwik\SettingsPiwik;
31use Piwik\SettingsServer;
32use Piwik\Translation\Translator;
33use Piwik\Url;
34use Piwik\Version;
35use Piwik\View;
36
37class Controller extends Plugin\ControllerAdmin
38{
39    const ACTIVATE_NONCE = 'CorePluginsAdmin.activatePlugin';
40    const DEACTIVATE_NONCE = 'CorePluginsAdmin.deactivatePlugin';
41    const UNINSTALL_NONCE = 'CorePluginsAdmin.uninstallPlugin';
42
43    /**
44     * @var Translator
45     */
46    private $translator;
47
48    /**
49     * @var Plugin\SettingsProvider
50     */
51    private $settingsProvider;
52
53    /**
54     * @var PluginInstaller
55     */
56    private $pluginInstaller;
57    /**
58     * @var Plugin\Manager
59     */
60    private $pluginManager;
61
62    /**
63     * @var Plugins
64     */
65    private $marketplacePlugins;
66
67    /**
68     * @var PasswordVerifier
69     */
70    private $passwordVerify;
71
72    /**
73     * Controller constructor.
74     * @param Translator $translator
75     * @param Plugin\SettingsProvider $settingsProvider
76     * @param PluginInstaller $pluginInstaller
77     * @param Plugins $marketplacePlugins
78     * @param PasswordVerifier $passwordVerify
79     */
80    public function __construct(Translator $translator,
81                                Plugin\SettingsProvider $settingsProvider,
82                                PluginInstaller $pluginInstaller,
83                                PasswordVerifier $passwordVerify,
84                                $marketplacePlugins = null
85    ) {
86        $this->translator = $translator;
87        $this->settingsProvider = $settingsProvider;
88        $this->pluginInstaller = $pluginInstaller;
89        $this->pluginManager = Plugin\Manager::getInstance();
90        $this->passwordVerify = $passwordVerify;
91
92        if (!empty($marketplacePlugins)) {
93            $this->marketplacePlugins = $marketplacePlugins;
94        } elseif (Marketplace::isMarketplaceEnabled()) {
95            // we load it manually as marketplace might not be loaded
96            $this->marketplacePlugins = StaticContainer::get('Piwik\Plugins\Marketplace\Plugins');
97        }
98
99        parent::__construct();
100    }
101
102    public function uploadPlugin()
103    {
104        static::dieIfPluginsAdminIsDisabled();
105        Piwik::checkUserHasSuperUserAccess();
106
107        if (!CorePluginsAdmin::isPluginUploadEnabled()) {
108            throw new \Exception('Plugin upload disabled by config');
109        }
110
111        $nonce = Common::getRequestVar('nonce', null, 'string');
112
113        if (!Nonce::verifyNonce(MarketplaceController::INSTALL_NONCE, $nonce)) {
114            throw new \Exception($this->translator->translate('General_ExceptionNonceMismatch'));
115        }
116
117        Nonce::discardNonce(MarketplaceController::INSTALL_NONCE);
118
119        if (!$this->passwordVerify->isPasswordCorrect(
120            Piwik::getCurrentUserLogin(),
121            Common::getRequestVar('confirmPassword', null, 'string')
122        )) {
123            throw new \Exception($this->translator->translate('Login_LoginPasswordNotCorrect'));
124        }
125
126        if (empty($_FILES['pluginZip'])) {
127            throw new \Exception('You did not specify a ZIP file.');
128        }
129
130        if (!empty($_FILES['pluginZip']['error'])) {
131            throw new \Exception('Something went wrong during the plugin file upload. Please try again.');
132        }
133
134        $file = $_FILES['pluginZip']['tmp_name'];
135        if (!file_exists($file)) {
136            throw new \Exception('Something went wrong during the plugin file upload. Please try again.');
137        }
138
139        $view = $this->configureView('@CorePluginsAdmin/uploadPlugin');
140
141        $pluginMetadata = $this->pluginInstaller->installOrUpdatePluginFromFile($file);
142
143        $view->nonce = Nonce::getNonce(static::ACTIVATE_NONCE);
144        $view->plugin = array(
145            'name'        => $pluginMetadata->name,
146            'version'     => $pluginMetadata->version,
147            'isTheme'     => !empty($pluginMetadata->theme),
148            'isActivated' => $this->pluginManager->isPluginActivated($pluginMetadata->name)
149        );
150
151        return $view->render();
152    }
153
154    public function tagManagerTeaser()
155    {
156        $this->dieIfPluginsAdminIsDisabled();
157        Piwik::checkUserHasSomeAdminAccess();
158
159        $tagManagerTeaser = new TagManagerTeaser(Piwik::getCurrentUserLogin());
160
161        if (!$tagManagerTeaser->shouldShowTeaser()) {
162            $this->redirectToIndex('CoreHome', 'index');
163            return;
164        }
165
166        $nonce = '';
167        if (Piwik::hasUserSuperUserAccess()) {
168            $nonce = Nonce::getNonce(static::ACTIVATE_NONCE);
169        }
170
171        $view = new View('@CorePluginsAdmin/tagManagerTeaser');
172        $this->setGeneralVariablesView($view);
173        $view->contactEmail = implode(',', Piwik::getContactEmailAddresses());
174        $view->nonce = $nonce;
175        return $view->render();
176    }
177
178    public function disableActivateTagManagerPage()
179    {
180        $this->dieIfPluginsAdminIsDisabled();
181        Piwik::checkUserHasSomeAdminAccess();
182
183        $tagManagerTeaser = new TagManagerTeaser(Piwik::getCurrentUserLogin());
184
185        if (Piwik::hasUserSuperUserAccess()) {
186            $tagManagerTeaser->disableGlobally();
187        } else {
188            $tagManagerTeaser->disableForUser();
189        }
190
191        $date = Common::getRequestVar('date', false);
192        $this->redirectToIndex('CoreHome', 'index', $websiteId = null, $defaultPeriod = null, $date);
193    }
194
195    private function dieIfPluginsAdminIsDisabled()
196    {
197        Piwik::checkUserIsNotAnonymous();
198        if (!CorePluginsAdmin::isPluginsAdminEnabled()) {
199            throw new \Exception('Enabling, disabling and uninstalling plugins has been disabled by Piwik admins.
200            Please contact your Piwik admins with your request so they can assist you.');
201        }
202    }
203
204    private function createPluginsOrThemesView($template, $themesOnly)
205    {
206        Piwik::checkUserHasSuperUserAccess();
207
208        $view = $this->configureView('@CorePluginsAdmin/' . $template);
209
210        $view->updateNonce = Nonce::getNonce(MarketplaceController::UPDATE_NONCE);
211        $view->activateNonce = Nonce::getNonce(static::ACTIVATE_NONCE);
212        $view->uninstallNonce = Nonce::getNonce(static::UNINSTALL_NONCE);
213        $view->deactivateNonce = Nonce::getNonce(static::DEACTIVATE_NONCE);
214        $view->pluginsInfo = $this->getPluginsInfo($themesOnly);
215
216        $users = Request::processRequest('UsersManager.getUsers', array('filter_limit' => '-1'));
217        $view->otherUsersCount = count($users) - 1;
218        $view->themeEnabled = $this->pluginManager->getThemeEnabled()->getPluginName();
219
220        $view->pluginNamesHavingSettings = array_keys($this->settingsProvider->getAllSystemSettings());
221        $view->isMarketplaceEnabled = Marketplace::isMarketplaceEnabled();
222        $view->isPluginsAdminEnabled = CorePluginsAdmin::isPluginsAdminEnabled();
223
224        $view->pluginsHavingUpdate    = array();
225        $view->marketplacePluginNames = array();
226
227        if (Marketplace::isMarketplaceEnabled() && $this->marketplacePlugins) {
228            try {
229                $view->marketplacePluginNames = $this->marketplacePlugins->getAvailablePluginNames($themesOnly);
230                $view->pluginsHavingUpdate    = $this->marketplacePlugins->getPluginsHavingUpdate();
231            } catch(Exception $e) {
232                // curl exec connection error (ie. server not connected to internet)
233            }
234        }
235
236        $view->isPluginUploadEnabled = CorePluginsAdmin::isPluginUploadEnabled();
237        $view->uploadLimit = SettingsServer::getPostMaxUploadSize();
238        $view->installNonce = Nonce::getNonce(MarketplaceController::INSTALL_NONCE);
239
240        return $view;
241    }
242
243    public function plugins()
244    {
245        $view = $this->createPluginsOrThemesView('plugins', $themesOnly = false);
246        return $view->render();
247    }
248
249    public function themes()
250    {
251        $view = $this->createPluginsOrThemesView('themes', $themesOnly = true);
252        return $view->render();
253    }
254
255    protected function configureView($template)
256    {
257        Piwik::checkUserIsNotAnonymous();
258
259        $view = new View($template);
260        $this->setBasicVariablesView($view);
261
262        // If user can manage plugins+themes, display a warning if config not writable
263        if (CorePluginsAdmin::isPluginsAdminEnabled()) {
264            $this->displayWarningIfConfigFileNotWritable();
265        }
266
267        $view->errorMessage = '';
268
269        return $view;
270    }
271
272    protected function getPluginsInfo($themesOnly = false)
273    {
274        $plugins = $this->pluginManager->loadAllPluginsAndGetTheirInfo();
275
276        foreach ($plugins as $pluginName => &$plugin) {
277
278            $plugin['isCorePlugin'] = $this->pluginManager->isPluginBundledWithCore($pluginName);
279            $plugin['isOfficialPlugin'] = false;
280
281            if (isset($plugin['info']) && isset($plugin['info']['authors'])) {
282                foreach ($plugin['info']['authors'] as $author) {
283                    if (in_array(strtolower($author['name']), array('piwik', 'innocraft', 'matomo', 'matomo-org'))) {
284                        $plugin['isOfficialPlugin'] = true;
285                        break;
286                    }
287                }
288            }
289
290            if (!empty($plugin['info']['description'])) {
291                $plugin['info']['description'] = $this->translator->translate($plugin['info']['description']);
292            }
293
294            if (!isset($plugin['info'])) {
295
296                $suffix = $this->translator->translate('CorePluginsAdmin_PluginNotWorkingAlternative');
297                // If the plugin has been renamed, we do not show message to ask user to update plugin
298                list($pluginNameRenamed, $methodName) = Request::getRenamedModuleAndAction($pluginName, 'index');
299                if ($pluginName != $pluginNameRenamed) {
300                    $suffix = "You may uninstall the plugin or manually delete the files in /path/to/matomo/plugins/$pluginName/";
301                }
302
303                if ($this->pluginManager->isPluginInFilesystem($pluginName)) {
304                    $description = '<strong>'
305                        . $this->translator->translate('CorePluginsAdmin_PluginNotCompatibleWith',
306                            array($pluginName, self::getPiwikVersion()))
307                        . '</strong><br/>'
308                        . $suffix;
309                } else {
310                    $description = $this->translator->translate('CorePluginsAdmin_PluginNotFound',
311                            array($pluginName))
312                        . "\n"
313                        . $this->translator->translate('CorePluginsAdmin_PluginNotFoundAlternative');
314                }
315                $plugin['info'] = array(
316                    'description' => $description,
317                    'version'     => $this->translator->translate('General_Unknown'),
318                    'theme'       => false,
319                );
320            }
321        }
322
323        $pluginsFiltered = $this->keepPluginsOrThemes($themesOnly, $plugins);
324        return $pluginsFiltered;
325    }
326
327    protected function keepPluginsOrThemes($themesOnly, $plugins)
328    {
329        $pluginsFiltered = array();
330        foreach ($plugins as $name => $thisPlugin) {
331
332            $isTheme = false;
333            if (!empty($thisPlugin['info']['theme'])) {
334                $isTheme = (bool)$thisPlugin['info']['theme'];
335            }
336            if (($themesOnly && $isTheme)
337                || (!$themesOnly && !$isTheme)
338            ) {
339                $pluginsFiltered[$name] = $thisPlugin;
340            }
341        }
342        return $pluginsFiltered;
343    }
344
345    public function safemode($lastError = array())
346    {
347        if (ob_get_length()) {
348            ob_clean();
349        }
350
351        $this->tryToRepairPiwik();
352
353        if (empty($lastError) && defined('PIWIK_TEST_MODE') && PIWIK_TEST_MODE) {
354            $lastError = array(
355                'message' => Common::getRequestVar('error_message', null, 'string'),
356                'file'    => Common::getRequestVar('error_file', null, 'string'),
357                'line'    => Common::getRequestVar('error_line', null, 'integer')
358            );
359        } elseif (empty($lastError)) {
360            throw new Exception('Safemode not available');
361        }
362
363        $outputFormat = Common::getRequestVar('format', 'html', 'string');
364        $outputFormat = strtolower($outputFormat);
365
366        if (!empty($outputFormat) && 'html' !== $outputFormat) {
367
368            $errorMessage = $lastError['message'];
369
370            if (!empty($lastError['backtrace'])
371                && \Piwik_ShouldPrintBackTraceWithMessage()
372            ) {
373                $errorMessage .= $lastError['backtrace'];
374            }
375
376            if (Piwik::isUserIsAnonymous()) {
377                $errorMessage = 'A fatal error occurred.';
378            }
379
380            $response = new \Piwik\API\ResponseBuilder($outputFormat, [], false); // don't print the exception backtrace since it will be useless
381            $message  = $response->getResponseException(new Exception($errorMessage));
382
383            return $message;
384        }
385
386        if (Common::isPhpCliMode()) {
387            throw new Exception("Error: " . var_export($lastError, true));
388        }
389
390        if (!\Piwik_ShouldPrintBackTraceWithMessage()) {
391            unset($lastError['backtrace']);
392        }
393
394        $view = new View('@CorePluginsAdmin/safemode');
395        $view->lastError   = $lastError;
396        $view->isAllowedToTroubleshootAsSuperUser = $this->isAllowedToTroubleshootAsSuperUser();
397        $view->isSuperUser = Piwik::hasUserSuperUserAccess();
398        $view->isAnonymousUser = Piwik::isUserIsAnonymous();
399        $view->plugins         = $this->pluginManager->loadAllPluginsAndGetTheirInfo();
400        $view->deactivateNonce = Nonce::getNonce(static::DEACTIVATE_NONCE);
401        $view->deactivateIAmSuperUserSalt = Common::getRequestVar('i_am_super_user', '', 'string');
402        $view->uninstallNonce  = Nonce::getNonce(static::UNINSTALL_NONCE);
403        $view->contactEmail  = implode(',', Piwik::getContactEmailAddresses());
404        $view->piwikVersion    = Version::VERSION;
405        $view->showVersion     = !Common::getRequestVar('tests_hide_piwik_version', 0);
406        $view->pluginCausesIssue = '';
407
408        // When the CSS merger in StylesheetUIAssetMerger throws an exception, safe mode is displayed.
409        // This flag prevents an infinite loop where safemode would try to re-generate the cache buster which requires CSS merger..
410        $view->disableCacheBuster();
411
412        if (!empty($lastError['file'])) {
413            preg_match('/piwik\/plugins\/(.*)\//', $lastError['file'], $matches);
414
415            if (!empty($matches[1])) {
416                $view->pluginCausesIssue = $matches[1];
417            }
418        }
419
420        return $view->render();
421    }
422
423    public function activate($redirectAfter = true)
424    {
425        $this->dieIfPluginsAdminIsDisabled();
426
427        $params = [
428            'module' => 'CorePluginsAdmin',
429            'action' => 'activate',
430            'pluginName' => Common::getRequestVar('pluginName'),
431            'nonce' => Common::getRequestVar('nonce'),
432            'redirectTo' => Common::getRequestVar('redirectTo', '', 'string'),
433            'referrer' => urlencode(Url::getReferrer()),
434        ];
435
436        if (!$this->passwordVerify->requirePasswordVerifiedRecently($params)) {
437            return;
438        }
439
440        $pluginName = $this->initPluginModification(static::ACTIVATE_NONCE);
441
442        $this->pluginManager->activatePlugin($pluginName);
443
444        if ($redirectAfter) {
445            $message = $this->translator->translate('CorePluginsAdmin_SuccessfullyActicated', array($pluginName));
446
447            if ($this->settingsProvider->getSystemSettings($pluginName)) {
448                $target   = sprintf('<a href="index.php%s#%s">',
449                    Url::getCurrentQueryStringWithParametersModified(array('module' => 'CoreAdminHome', 'action' => 'generalSettings')),
450                    $pluginName);
451                $message .= ' ' . $this->translator->translate('CorePluginsAdmin_ChangeSettingsPossible', array($target, '</a>'));
452            }
453
454            $notification = new Notification($message);
455            $notification->raw     = true;
456            $notification->title   = $this->translator->translate('General_WellDone');
457            $notification->context = Notification::CONTEXT_SUCCESS;
458            Notification\Manager::notify('CorePluginsAdmin_PluginActivated', $notification);
459
460            $redirectTo = Common::getRequestVar('redirectTo', '', 'string');
461            if (!empty($redirectTo) && $redirectTo === 'marketplace') {
462                $this->redirectToIndex('Marketplace', 'overview');
463            } elseif (!empty($redirectTo) && $redirectTo === 'tagmanager') {
464                $this->redirectToIndex('TagManager', 'gettingStarted');
465            } elseif (!empty($redirectTo) && $redirectTo === 'referrer') {
466                $this->redirectAfterModification($redirectAfter);
467            } else {
468                $plugin = $this->pluginManager->loadPlugin($pluginName);
469
470                $actionToRedirect = 'plugins';
471                if ($plugin->isTheme()) {
472                    $actionToRedirect = 'themes';
473                }
474
475                $this->redirectToIndex('CorePluginsAdmin', $actionToRedirect);
476            }
477
478        }
479    }
480
481    public function deactivate($redirectAfter = true)
482    {
483        $params = [
484            'module' => 'CorePluginsAdmin',
485            'action' => 'deactivate',
486            'pluginName' => Common::getRequestVar('pluginName'),
487            'nonce' => Common::getRequestVar('nonce'),
488            'redirectTo' => Common::getRequestVar('redirectTo'),
489            'referrer' => urlencode(Url::getReferrer()),
490        ];
491        if (!$this->passwordVerify->requirePasswordVerifiedRecently($params)) {
492            return;
493        }
494
495        if($this->isAllowedToTroubleshootAsSuperUser()) {
496            Access::doAsSuperUser(function() use ($redirectAfter) {
497                $this->doDeactivatePlugin($redirectAfter);
498            });
499        } else {
500            $this->doDeactivatePlugin($redirectAfter);
501        }
502    }
503
504    public function uninstall($redirectAfter = true)
505    {
506        $this->dieIfPluginsAdminIsDisabled();
507
508        $params = [
509            'module' => 'CorePluginsAdmin',
510            'action' => 'uninstall',
511            'pluginName' => Common::getRequestVar('pluginName'),
512            'nonce' => Common::getRequestVar('nonce'),
513            'referrer' => urlencode(Url::getReferrer()),
514        ];
515        if (!$this->passwordVerify->requirePasswordVerifiedRecently($params)) {
516            return;
517        }
518
519        $pluginName = $this->initPluginModification(static::UNINSTALL_NONCE);
520
521        $uninstalled = $this->pluginManager->uninstallPlugin($pluginName);
522
523        if (!$uninstalled) {
524            $path = Plugin\Manager::getPluginDirectory($pluginName) . '/';
525
526            $messagePermissions = Filechecks::getErrorMessageMissingPermissions($path);
527
528            $messageIntro = $this->translator->translate("Warning: \"%s\" could not be uninstalled. Piwik did not have enough permission to delete the files in $path. ",
529                $pluginName);
530            $exitMessage  = $messageIntro . "<br/><br/>" . $messagePermissions;
531            $exitMessage .= "<br> Or manually delete this directory (using FTP or SSH access)";
532
533            $ex = new MissingFilePermissionException($exitMessage);
534            $ex->setIsHtmlMessage();
535
536            throw $ex;
537        }
538
539        $this->redirectAfterModification($redirectAfter);
540    }
541
542    public function showLicense()
543    {
544        Piwik::checkUserHasSomeViewAccess();
545
546        $pluginName = Common::getRequestVar('pluginName', null, 'string');
547
548        if (!Plugin\Manager::getInstance()->isPluginInFilesystem($pluginName)) {
549            throw new Exception('Invalid plugin');
550        }
551
552        $metadata = new Plugin\MetadataLoader($pluginName);
553        $license_file = $metadata->getPathToLicenseFile();
554
555        $license = 'No license file found for this plugin.';
556        if(!empty($license_file)) {
557            $license = file_get_contents($license_file);
558            $license = nl2br($license);
559        }
560
561        $view = $this->configureView('@CorePluginsAdmin/license');
562        $view->pluginName = $pluginName;
563        $view->license = $license;
564        return $view->render();
565    }
566
567    protected function initPluginModification($nonceName)
568    {
569        Piwik::checkUserHasSuperUserAccess();
570
571        $nonce = Common::getRequestVar('nonce', null, 'string');
572
573        if (!Nonce::verifyNonce($nonceName, $nonce)) {
574            throw new \Exception($this->translator->translate('General_ExceptionNonceMismatch'));
575        }
576
577        Nonce::discardNonce($nonceName);
578
579        $pluginName = Common::getRequestVar('pluginName', null, 'string');
580
581        if (!$this->pluginManager->isValidPluginName($pluginName)) {
582            throw new Exception('Invalid plugin name');
583        }
584
585        return $pluginName;
586    }
587
588    protected function redirectAfterModification($redirectAfter)
589    {
590        if (!$redirectAfter) {
591            return;
592        }
593
594        $referrer = Common::getRequestVar('referrer', false);
595        $referrer = Common::unsanitizeInputValue($referrer);
596        if (!empty($referrer)
597            && Url::isLocalUrl($referrer)
598        ) {
599            Url::redirectToUrl($referrer);
600        } else {
601            Url::redirectToReferrer();
602        }
603    }
604
605    private function tryToRepairPiwik()
606    {
607        // in case any opcaches etc were not cleared after an update for instance. Might prevent from getting the
608        // error again
609        try {
610            Filesystem::deleteAllCacheOnUpdate();
611        } catch (Exception $e) {}
612    }
613
614    /**
615     * Let Super User troubleshoot in safe mode, even when Login is broken, with this special trick
616     *
617     * @return bool
618     * @throws Exception
619     */
620    protected function isAllowedToTroubleshootAsSuperUser()
621    {
622        $isAllowedToTroubleshootAsSuperUser = false;
623        $salt = SettingsPiwik::getSalt();
624        if (!empty($salt)) {
625            $saltFromRequest = Common::getRequestVar('i_am_super_user', '', 'string');
626            $isAllowedToTroubleshootAsSuperUser = ($salt == $saltFromRequest);
627        }
628        return $isAllowedToTroubleshootAsSuperUser;
629    }
630
631    /**
632     * @param $redirectAfter
633     * @throws Exception
634     */
635    protected function doDeactivatePlugin($redirectAfter)
636    {
637        $pluginName = $this->initPluginModification(static::DEACTIVATE_NONCE);
638        $this->dieIfPluginsAdminIsDisabled();
639
640        $this->pluginManager->deactivatePlugin($pluginName);
641        $this->redirectAfterModification($redirectAfter);
642    }
643
644}
645