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