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 */ 9 10namespace Piwik\Plugin; 11 12use Piwik\Application\Kernel\PluginList; 13use Piwik\Cache; 14use Piwik\Columns\Dimension; 15use Piwik\Common; 16use Piwik\Config; 17use Piwik\Config as PiwikConfig; 18use Piwik\Container\StaticContainer; 19use Piwik\Date; 20use Piwik\Development; 21use Piwik\EventDispatcher; 22use Piwik\Exception\PluginDeactivatedException; 23use Piwik\Filesystem; 24use Piwik\Log; 25use Piwik\Notification; 26use Piwik\Option; 27use Piwik\Piwik; 28use Piwik\Plugin; 29use Piwik\Plugin\Dimension\ActionDimension; 30use Piwik\Plugin\Dimension\ConversionDimension; 31use Piwik\Plugin\Dimension\VisitDimension; 32use Piwik\Settings\Storage as SettingsStorage; 33use Piwik\SettingsPiwik; 34use Piwik\SettingsServer; 35use Piwik\Theme; 36use Piwik\Translation\Translator; 37use Piwik\Updater; 38 39/** 40 * The singleton that manages plugin loading/unloading and installation/uninstallation. 41 */ 42class Manager 43{ 44 const LAST_PLUGIN_ACTIVATION_TIME_OPTION_PREFIX = 'LastPluginActivation.'; 45 const LAST_PLUGIN_DEACTIVATION_TIME_OPTION_PREFIX = 'LastPluginDeactivation.'; 46 47 /** 48 * @return self 49 */ 50 public static function getInstance() 51 { 52 return StaticContainer::get('Piwik\Plugin\Manager'); 53 } 54 55 protected $pluginsToLoad = array(); 56 57 protected $doLoadPlugins = true; 58 59 protected static $pluginsToPathCache = array(); 60 protected static $pluginsToWebRootDirCache = array(); 61 62 private $pluginsLoadedAndActivated; 63 64 /** 65 * @var Plugin[] 66 */ 67 protected $loadedPlugins = array(); 68 /** 69 * Default theme used in Piwik. 70 */ 71 const DEFAULT_THEME = "Morpheus"; 72 73 protected $doLoadAlwaysActivatedPlugins = true; 74 75 // These are always activated and cannot be deactivated 76 protected $pluginToAlwaysActivate = array( 77 'BulkTracking', 78 'CoreVue', 79 'CoreHome', 80 'CoreUpdater', 81 'CoreAdminHome', 82 'CoreConsole', 83 'CorePluginsAdmin', 84 'CoreVisualizations', 85 'Installation', 86 'SitesManager', 87 'UsersManager', 88 'Intl', 89 'API', 90 'Proxy', 91 'LanguagesManager', 92 'WebsiteMeasurable', 93 94 // default Piwik theme, always enabled 95 self::DEFAULT_THEME, 96 ); 97 98 private $trackerPluginsNotToLoad = array(); 99 100 /** 101 * @var PluginList 102 */ 103 private $pluginList; 104 105 public function __construct(PluginList $pluginList) 106 { 107 $this->pluginList = $pluginList; 108 } 109 110 /** 111 * Loads plugin that are enabled 112 */ 113 public function loadActivatedPlugins() 114 { 115 $pluginsToLoad = $this->getActivatedPluginsFromConfig(); 116 if (!SettingsPiwik::isInternetEnabled()) { 117 $pluginsToLoad = array_filter($pluginsToLoad, function($name) { 118 $plugin = Manager::makePluginClass($name); 119 return !$plugin->requiresInternetConnection(); 120 }); 121 } 122 $this->loadPlugins($pluginsToLoad); 123 } 124 125 /** 126 * Called during Tracker 127 */ 128 public function loadCorePluginsDuringTracker() 129 { 130 $pluginsToLoad = $this->pluginList->getActivatedPlugins(); 131 $pluginsToLoad = array_diff($pluginsToLoad, $this->getTrackerPluginsNotToLoad()); 132 $this->loadPlugins($pluginsToLoad); 133 } 134 135 /** 136 * @return array names of plugins that have been loaded 137 */ 138 public function loadTrackerPlugins() 139 { 140 $cacheId = 'PluginsTracker'; 141 $cache = Cache::getEagerCache(); 142 143 if ($cache->contains($cacheId)) { 144 $pluginsTracker = $cache->fetch($cacheId); 145 } else { 146 $this->unloadPlugins(); 147 $this->loadActivatedPlugins(); 148 149 $pluginsTracker = array(); 150 151 foreach ($this->loadedPlugins as $pluginName => $plugin) { 152 if ($this->isTrackerPlugin($plugin)) { 153 $pluginsTracker[] = $pluginName; 154 } 155 } 156 157 if (!empty($pluginsTracker)) { 158 $cache->save($cacheId, $pluginsTracker); 159 } 160 } 161 162 if (empty($pluginsTracker)) { 163 $this->unloadPlugins(); 164 return array(); 165 } 166 167 $pluginsTracker = array_diff($pluginsTracker, $this->getTrackerPluginsNotToLoad()); 168 $this->doNotLoadAlwaysActivatedPlugins(); 169 $this->loadPlugins($pluginsTracker); 170 171 // we could simply unload all plugins first before loading plugins but this way it is much faster 172 // since we won't have to create each plugin again and we won't have to parse each plugin metadata file 173 // again etc 174 $this->makeSureOnlyActivatedPluginsAreLoaded(); 175 176 return $pluginsTracker; 177 } 178 179 /** 180 * Do not load the specified plugins (used during testing, to disable Provider plugin) 181 * @param array $plugins 182 */ 183 public function setTrackerPluginsNotToLoad($plugins) 184 { 185 $this->trackerPluginsNotToLoad = $plugins; 186 } 187 188 /** 189 * Get list of plugins to not load 190 * 191 * @return array 192 */ 193 public function getTrackerPluginsNotToLoad() 194 { 195 return $this->trackerPluginsNotToLoad; 196 } 197 198 // If a plugin hooks onto at least an event starting with "Tracker.", we load the plugin during tracker 199 const TRACKER_EVENT_PREFIX = 'Tracker.'; 200 201 /** 202 * @param $pluginName 203 * @return bool 204 */ 205 public function isPluginOfficialAndNotBundledWithCore($pluginName) 206 { 207 static $gitModules; 208 if (empty($gitModules)) { 209 $gitModules = file_get_contents(PIWIK_INCLUDE_PATH . '/.gitmodules'); 210 } 211 // All submodules are officially maintained plugins 212 $isSubmodule = false !== strpos($gitModules, "plugins/" . $pluginName . "\n"); 213 return $isSubmodule; 214 } 215 216 /** 217 * Update Plugins config 218 * 219 * @param array $pluginsToLoad Plugins 220 */ 221 private function updatePluginsConfig($pluginsToLoad) 222 { 223 $pluginsToLoad = $this->pluginList->sortPluginsAndRespectDependencies($pluginsToLoad); 224 $section = PiwikConfig::getInstance()->Plugins; 225 $section['Plugins'] = $pluginsToLoad; 226 PiwikConfig::getInstance()->Plugins = $section; 227 } 228 229 /** 230 * Update PluginsInstalled config 231 * 232 * @param array $plugins Plugins 233 */ 234 private function updatePluginsInstalledConfig($plugins) 235 { 236 $section = PiwikConfig::getInstance()->PluginsInstalled; 237 $section['PluginsInstalled'] = $plugins; 238 PiwikConfig::getInstance()->PluginsInstalled = $section; 239 } 240 241 public function clearPluginsInstalledConfig() 242 { 243 $this->updatePluginsInstalledConfig(array()); 244 PiwikConfig::getInstance()->forceSave(); 245 } 246 247 /** 248 * Returns true if plugin is always activated 249 * 250 * @param string $name Name of plugin 251 * @return bool 252 */ 253 private function isPluginAlwaysActivated($name) 254 { 255 return in_array($name, $this->pluginToAlwaysActivate); 256 } 257 258 /** 259 * Returns true if the plugin can be uninstalled. Any non-core plugin can be uninstalled. 260 * 261 * @param $name 262 * @return bool 263 */ 264 private function isPluginUninstallable($name) 265 { 266 return !$this->isPluginBundledWithCore($name); 267 } 268 269 /** 270 * Returns `true` if a plugin has been activated. 271 * 272 * @param string $name Name of plugin, eg, `'Actions'`. 273 * @return bool 274 * @api 275 */ 276 public function isPluginActivated($name) 277 { 278 return in_array($name, $this->pluginsToLoad) 279 || ($this->doLoadAlwaysActivatedPlugins && $this->isPluginAlwaysActivated($name)); 280 } 281 282 /** 283 * Returns `true` if a plugin requires an working internet connection 284 * 285 * @param string $name Name of plugin, eg, `'Actions'`. 286 * @return bool 287 * @throws \Exception 288 */ 289 public function doesPluginRequireInternetConnection($name) 290 { 291 $plugin = $this->makePluginClass($name); 292 return $plugin->requiresInternetConnection(); 293 } 294 295 /** 296 * Checks whether the given plugin is activated, if not triggers an exception. 297 * 298 * @param string $pluginName 299 * @throws PluginDeactivatedException 300 */ 301 public function checkIsPluginActivated($pluginName) 302 { 303 if (!$this->isPluginActivated($pluginName)) { 304 throw new PluginDeactivatedException($pluginName); 305 } 306 } 307 308 /** 309 * Returns `true` if plugin is loaded (in memory). 310 * 311 * @param string $name Name of plugin, eg, `'Acions'`. 312 * @return bool 313 * @api 314 */ 315 public function isPluginLoaded($name) 316 { 317 return isset($this->loadedPlugins[$name]); 318 } 319 320 /** 321 * Reads the directories inside the plugins/ directory and returns their names in an array 322 * 323 * @return array 324 */ 325 public function readPluginsDirectory() 326 { 327 $result = array(); 328 foreach (self::getPluginsDirectories() as $pluginsDir) { 329 $pluginsName = _glob($pluginsDir . '*', GLOB_ONLYDIR); 330 if ($pluginsName != false) { 331 foreach ($pluginsName as $path) { 332 if (self::pluginStructureLooksValid($path)) { 333 $result[] = basename($path); 334 } 335 } 336 } 337 } 338 339 sort($result); 340 341 return $result; 342 } 343 344 public static function initPluginDirectories() 345 { 346 $envDirs = getenv('MATOMO_PLUGIN_DIRS'); 347 if (!empty($envDirs)) { 348 // we expect it in the format `absoluteStorageDir1;webrootPathRelative1:absoluteStorageDir2;webrootPathRelative1` 349 if (empty($GLOBALS['MATOMO_PLUGIN_DIRS'])) { 350 $GLOBALS['MATOMO_PLUGIN_DIRS'] = array(); 351 } 352 353 $envDirs = explode(':', $envDirs); 354 foreach ($envDirs as $envDir) { 355 $envDir = explode(';', $envDir); 356 $absoluteDir = rtrim($envDir[0], '/') . '/'; 357 $GLOBALS['MATOMO_PLUGIN_DIRS'][] = array( 358 'pluginsPathAbsolute' => $absoluteDir, 359 'webrootDirRelativeToMatomo' => isset($envDir[1]) ? $envDir[1] : null, 360 ); 361 } 362 } 363 364 if (!empty($GLOBALS['MATOMO_PLUGIN_DIRS'])) { 365 foreach ($GLOBALS['MATOMO_PLUGIN_DIRS'] as $pluginDir => &$settings) { 366 if (!isset($settings['pluginsPathAbsolute'])) { 367 throw new \Exception('Missing "pluginsPathAbsolute" configuration for plugin dir'); 368 } 369 if (!isset($settings['webrootDirRelativeToMatomo'])) { 370 throw new \Exception('Missing "webrootDirRelativeToMatomo" configuration for plugin dir'); 371 } 372 } 373 374 $pluginDirs = self::getPluginsDirectories(); 375 if (count($pluginDirs) > 1) { 376 self::registerPluginDirAutoload($pluginDirs); 377 } 378 } 379 380 $envCopyDir = getenv('MATOMO_PLUGIN_COPY_DIR'); 381 if (!empty($envCopyDir)) { 382 $GLOBALS['MATOMO_PLUGIN_COPY_DIR'] = $envCopyDir; 383 } 384 385 if (!empty($GLOBALS['MATOMO_PLUGIN_COPY_DIR']) 386 && !in_array($GLOBALS['MATOMO_PLUGIN_COPY_DIR'], self::getPluginsDirectories()) 387 ) { 388 throw new \Exception('"MATOMO_PLUGIN_COPY_DIR" dir must be one of "MATOMO_PLUGIN_DIRS" directories'); 389 } 390 } 391 392 /** 393 * Registers a new autoloader to support the loading of Matomo plugin classes when the plugins are installed 394 * outside the Matomo plugins folder. 395 * @param array $pluginDirs 396 */ 397 public static function registerPluginDirAutoload($pluginDirs) 398 { 399 spl_autoload_register(function ($className) use ($pluginDirs) { 400 if (strpos($className, 'Piwik\Plugins\\') === 0) { 401 $withoutPrefix = str_replace('Piwik\Plugins\\', '', $className); 402 $path = str_replace('\\', DIRECTORY_SEPARATOR, $withoutPrefix) . '.php'; 403 foreach ($pluginDirs as $pluginsDirectory) { 404 if (file_exists($pluginsDirectory . $path)) { 405 require_once $pluginsDirectory . $path; 406 } 407 } 408 } 409 }); 410 } 411 412 public static function getAlternativeWebRootDirectories() 413 { 414 $dirs = array(); 415 416 if (!empty($GLOBALS['MATOMO_PLUGIN_DIRS'])) { 417 foreach ($GLOBALS['MATOMO_PLUGIN_DIRS'] as $pluginDir) { 418 $absolute = rtrim($pluginDir['pluginsPathAbsolute'], '/') . '/'; 419 $relative = rtrim($pluginDir['webrootDirRelativeToMatomo'], '/') . '/'; 420 $dirs[$absolute] = $relative; 421 } 422 } 423 424 return $dirs; 425 } 426 427 public function getWebRootDirectoriesForCustomPluginDirs() 428 { 429 return array_intersect_key(self::$pluginsToWebRootDirCache, array_flip($this->pluginsToLoad)); 430 } 431 432 /** 433 * Returns the path to all plugins directories. Each plugins directory may contain several plugins. 434 * All paths have a trailing slash '/'. 435 * @return string[] 436 * @api 437 */ 438 public static function getPluginsDirectories() 439 { 440 $dirs = array(self::getPluginsDirectory()); 441 442 if (!empty($GLOBALS['MATOMO_PLUGIN_DIRS'])) { 443 $extraDirs = array_map(function ($dir) { 444 return rtrim($dir['pluginsPathAbsolute'], '/') . '/'; 445 }, $GLOBALS['MATOMO_PLUGIN_DIRS']); 446 $dirs = array_merge($dirs, $extraDirs); 447 } 448 449 return $dirs; 450 } 451 452 private static function getPluginRealPath($path) 453 { 454 if (strpos($path, '../') !== false) { 455 // for tests, only do it when needed re performance etc 456 $real = realpath($path); 457 if ($real && Common::stringEndsWith($path, '/')) { 458 return rtrim($real, '/') . '/'; 459 } 460 if ($real) { 461 return $real; 462 } 463 } 464 return $path; 465 } 466 467 /** 468 * Gets the path to a specific plugin. If the plugin does not exist in any plugins folder, the default plugins 469 * folder will be assumed. 470 * 471 * @param $pluginName 472 * @return mixed|string 473 * @api 474 */ 475 public static function getPluginDirectory($pluginName) 476 { 477 if (isset(self::$pluginsToPathCache[$pluginName])) { 478 return self::$pluginsToPathCache[$pluginName]; 479 } 480 481 $corePluginsDir = PIWIK_INCLUDE_PATH . '/plugins/' . $pluginName; 482 if (is_dir($corePluginsDir)) { 483 // for faster performance 484 self::$pluginsToPathCache[$pluginName] = self::getPluginRealPath($corePluginsDir); 485 return self::$pluginsToPathCache[$pluginName]; 486 } 487 488 foreach (self::getAlternativeWebRootDirectories() as $dir => $relative) { 489 $path = $dir . $pluginName; 490 if (is_dir($path)) { 491 self::$pluginsToPathCache[$pluginName] = self::getPluginRealPath($path); 492 self::$pluginsToWebRootDirCache[$pluginName] = rtrim($relative, '/'); 493 return $path; 494 } 495 } 496 497 // assume default directory when plugin does not exist just yet 498 return self::getPluginsDirectory() . $pluginName; 499 } 500 501 /** 502 * Returns the path to the directory where core plugins are located. Please note since Matomo 3.9 503 * plugins may also be located in other directories and therefore this method has been deprecated. 504 * @internal since Matomo 3.9.0 use {@link (getPluginsDirectories())} or {@link getPluginDirectory($pluginName)} instead 505 * @return string 506 */ 507 public static function getPluginsDirectory() 508 { 509 $path = rtrim(PIWIK_INCLUDE_PATH, '/') . '/plugins/'; 510 $path = self::getPluginRealPath($path); 511 return $path; 512 } 513 514 /** 515 * Deactivate plugin 516 * 517 * @param string $pluginName Name of plugin 518 */ 519 public function deactivatePlugin($pluginName) 520 { 521 $plugins = $this->pluginList->getActivatedPlugins(); 522 if (!in_array($pluginName, $plugins)) { 523 // plugin is already deactivated 524 return; 525 } 526 527 $this->clearCache($pluginName); 528 529 // execute deactivate() to let the plugin do cleanups 530 $this->executePluginDeactivate($pluginName); 531 532 $this->savePluginTime(self::LAST_PLUGIN_DEACTIVATION_TIME_OPTION_PREFIX, $pluginName); 533 534 $this->unloadPluginFromMemory($pluginName); 535 536 $this->removePluginFromConfig($pluginName); 537 538 /** 539 * Event triggered after a plugin has been deactivated. 540 * 541 * @param string $pluginName The plugin that has been deactivated. 542 */ 543 Piwik::postEvent('PluginManager.pluginDeactivated', array($pluginName)); 544 } 545 546 /** 547 * Tries to find the given components such as a Menu or Tasks implemented by plugins. 548 * This method won't cache the found components. If you need to find the same component multiple times you might 549 * want to cache the result to save a tiny bit of time. 550 * 551 * @param string $componentName The name of the component you want to look for. In case you request a 552 * component named 'Menu' it'll look for a file named 'Menu.php' within the 553 * root of all plugin folders that implement a class named 554 * Piwik\Plugin\$PluginName\Menu. 555 * @param string $expectedSubclass If not empty, a check will be performed whether a found file extends the 556 * given subclass. If the requested file exists but does not extend this class 557 * a warning will be shown to advice a developer to extend this certain class. 558 * 559 * @return \stdClass[] 560 */ 561 public function findComponents($componentName, $expectedSubclass) 562 { 563 $plugins = $this->getPluginsLoadedAndActivated(); 564 $components = array(); 565 566 foreach ($plugins as $plugin) { 567 $component = $plugin->findComponent($componentName, $expectedSubclass); 568 569 if (!empty($component)) { 570 $components[] = $component; 571 } 572 } 573 574 return $components; 575 } 576 577 public function findMultipleComponents($directoryWithinPlugin, $expectedSubclass) 578 { 579 $plugins = $this->getPluginsLoadedAndActivated(); 580 $found = array(); 581 582 foreach ($plugins as $plugin) { 583 $components = $plugin->findMultipleComponents($directoryWithinPlugin, $expectedSubclass); 584 585 if (!empty($components)) { 586 $found = array_merge($found, $components); 587 } 588 } 589 590 return $found; 591 } 592 593 /** 594 * Uninstalls a Plugin (deletes plugin files from the disk) 595 * Only deactivated plugins can be uninstalled 596 * 597 * @param $pluginName 598 * @throws \Exception 599 * @return bool 600 */ 601 public function uninstallPlugin($pluginName) 602 { 603 if ($this->isPluginLoaded($pluginName)) { 604 throw new \Exception("To uninstall the plugin $pluginName, first disable it in Matomo > Settings > Plugins"); 605 } 606 $this->loadAllPluginsAndGetTheirInfo(); 607 608 SettingsStorage\Backend\PluginSettingsTable::removeAllSettingsForPlugin($pluginName); 609 SettingsStorage\Backend\MeasurableSettingsTable::removeAllSettingsForPlugin($pluginName); 610 611 $this->executePluginDeactivate($pluginName); 612 $this->executePluginUninstall($pluginName); 613 614 $this->removePluginFromPluginsInstalledConfig($pluginName); 615 616 $this->unloadPluginFromMemory($pluginName); 617 618 $this->removePluginFromConfig($pluginName); 619 $this->removeInstalledVersionFromOptionTable($pluginName); 620 $this->clearCache($pluginName); 621 622 self::deletePluginFromFilesystem($pluginName); 623 if ($this->isPluginInFilesystem($pluginName)) { 624 return false; 625 } 626 627 /** 628 * Event triggered after a plugin has been uninstalled. 629 * 630 * @param string $pluginName The plugin that has been uninstalled. 631 */ 632 Piwik::postEvent('PluginManager.pluginUninstalled', array($pluginName)); 633 634 return true; 635 } 636 637 /** 638 * @param string $pluginName 639 */ 640 private function clearCache($pluginName) 641 { 642 $this->resetTransientCache(); 643 Filesystem::deleteAllCacheOnUpdate($pluginName); 644 } 645 646 public static function deletePluginFromFilesystem($plugin) 647 { 648 $pluginDir = self::getPluginDirectory($plugin); 649 if (strpos($pluginDir, PIWIK_INCLUDE_PATH) === 0) { 650 // only delete files for plugins within matomo directory... 651 Filesystem::unlinkRecursive($pluginDir, $deleteRootToo = true); 652 } 653 } 654 655 /** 656 * Install loaded plugins 657 * 658 * @throws 659 * @return array Error messages of plugin install fails 660 */ 661 public function installLoadedPlugins() 662 { 663 Log::debug("Loaded plugins: " . implode(", ", array_keys($this->getLoadedPlugins()))); 664 665 foreach ($this->getLoadedPlugins() as $plugin) { 666 $this->installPluginIfNecessary($plugin); 667 } 668 } 669 670 /** 671 * Activate the specified plugin and install (if needed) 672 * 673 * @param string $pluginName Name of plugin 674 * @throws \Exception 675 */ 676 public function activatePlugin($pluginName) 677 { 678 $plugins = $this->pluginList->getActivatedPlugins(); 679 if (in_array($pluginName, $plugins)) { 680 // plugin is already activated 681 return; 682 } 683 684 if (!$this->isPluginInFilesystem($pluginName)) { 685 throw new \Exception("Plugin '$pluginName' cannot be found in the filesystem in plugins/ directory."); 686 } 687 $this->deactivateThemeIfTheme($pluginName); 688 689 // Load plugin 690 $plugin = $this->loadPlugin($pluginName); 691 if ($plugin === null) { 692 throw new \Exception("The plugin '$pluginName' was found in the filesystem, but could not be loaded.'"); 693 } 694 $this->installPluginIfNecessary($plugin); 695 $plugin->activate(); 696 697 $this->savePluginTime(self::LAST_PLUGIN_ACTIVATION_TIME_OPTION_PREFIX, $pluginName); 698 699 EventDispatcher::getInstance()->postPendingEventsTo($plugin); 700 701 $this->pluginsToLoad[] = $pluginName; 702 703 $this->updatePluginsConfig($this->pluginsToLoad); 704 PiwikConfig::getInstance()->forceSave(); 705 706 $this->clearCache($pluginName); 707 708 /** 709 * Event triggered after a plugin has been activated. 710 * 711 * @param string $pluginName The plugin that has been activated. 712 */ 713 Piwik::postEvent('PluginManager.pluginActivated', array($pluginName)); 714 } 715 716 public function isPluginInFilesystem($pluginName) 717 { 718 $existingPlugins = $this->readPluginsDirectory(); 719 $isPluginInFilesystem = array_search($pluginName, $existingPlugins) !== false; 720 return $this->isValidPluginName($pluginName) 721 && $isPluginInFilesystem; 722 } 723 724 /** 725 * Returns the currently enabled theme. 726 * 727 * If no theme is enabled, the **Morpheus** plugin is returned (this is the base and default theme). 728 * 729 * @return Plugin 730 * @api 731 */ 732 public function getThemeEnabled() 733 { 734 $plugins = $this->getLoadedPlugins(); 735 736 $theme = false; 737 foreach ($plugins as $plugin) { 738 /* @var $plugin Plugin */ 739 if ($plugin->isTheme() 740 && $this->isPluginActivated($plugin->getPluginName()) 741 ) { 742 if ($plugin->getPluginName() != self::DEFAULT_THEME) { 743 return $plugin; // enabled theme (not default) 744 } 745 $theme = $plugin; // default theme 746 } 747 } 748 return $theme; 749 } 750 751 /** 752 * @param string $themeName 753 * @throws \Exception 754 * @return Theme 755 */ 756 public function getTheme($themeName) 757 { 758 $plugins = $this->getLoadedPlugins(); 759 760 foreach ($plugins as $plugin) { 761 if ($plugin->isTheme() && $plugin->getPluginName() == $themeName) { 762 return new Theme($plugin); 763 } 764 } 765 throw new \Exception('Theme not found : ' . $themeName); 766 } 767 768 public function getNumberOfActivatedPluginsExcludingAlwaysActivated() 769 { 770 $counter = 0; 771 772 $pluginNames = $this->getLoadedPluginsName(); 773 foreach ($pluginNames as $pluginName) { 774 if ($this->isPluginActivated($pluginName) 775 && !$this->isPluginAlwaysActivated($pluginName)) { 776 $counter++; 777 } 778 } 779 780 return $counter; 781 } 782 783 /** 784 * Returns info regarding all plugins. Loads plugins that can be loaded. 785 * 786 * @return array An array that maps plugin names with arrays of plugin information. Plugin 787 * information consists of the following entries: 788 * 789 * - **activated**: Whether the plugin is activated. 790 * - **alwaysActivated**: Whether the plugin should always be activated, 791 * or not. 792 * - **uninstallable**: Whether the plugin is uninstallable or not. 793 * - **invalid**: If the plugin is invalid, this property will be set to true. 794 * If the plugin is not invalid, this property will not exist. 795 * - **info**: If the plugin was loaded, will hold the plugin information. 796 * See {@link Piwik\Plugin::getInformation()}. 797 * @api 798 */ 799 public function loadAllPluginsAndGetTheirInfo() 800 { 801 /** @var Translator $translator */ 802 $translator = StaticContainer::get('Piwik\Translation\Translator'); 803 804 $plugins = array(); 805 806 $listPlugins = array_merge( 807 $this->readPluginsDirectory(), 808 $this->pluginList->getActivatedPlugins() 809 ); 810 $listPlugins = array_unique($listPlugins); 811 $internetFeaturesEnabled = SettingsPiwik::isInternetEnabled(); 812 foreach ($listPlugins as $pluginName) { 813 // Hide plugins that are never going to be used 814 if ($this->isPluginBogus($pluginName)) { 815 continue; 816 } 817 818 // If the plugin is not core and looks bogus, do not load 819 if ($this->isPluginThirdPartyAndBogus($pluginName)) { 820 $info = array( 821 'invalid' => true, 822 'activated' => false, 823 'alwaysActivated' => false, 824 'uninstallable' => true, 825 ); 826 } else { 827 $translator->addDirectory(self::getPluginDirectory($pluginName) . '/lang'); 828 $this->loadPlugin($pluginName); 829 $info = array( 830 'activated' => $this->isPluginActivated($pluginName), 831 'alwaysActivated' => $this->isPluginAlwaysActivated($pluginName), 832 'uninstallable' => $this->isPluginUninstallable($pluginName), 833 ); 834 } 835 836 $plugins[$pluginName] = $info; 837 } 838 839 $loadedPlugins = $this->getLoadedPlugins(); 840 foreach ($loadedPlugins as $oPlugin) { 841 $pluginName = $oPlugin->getPluginName(); 842 843 $info = array( 844 'info' => $oPlugin->getInformation(), 845 'activated' => $this->isPluginActivated($pluginName), 846 'alwaysActivated' => $this->isPluginAlwaysActivated($pluginName), 847 'missingRequirements' => $oPlugin->getMissingDependenciesAsString(), 848 'uninstallable' => $this->isPluginUninstallable($pluginName), 849 ); 850 $plugins[$pluginName] = $info; 851 } 852 853 return $plugins; 854 } 855 856 protected static function isManifestFileFound($path) 857 { 858 return file_exists($path . "/" . MetadataLoader::PLUGIN_JSON_FILENAME); 859 } 860 861 /** 862 * Returns `true` if the plugin is bundled with core or `false` if it is third party. 863 * 864 * @param string $name The name of the plugin, eg, `'Actions'`. 865 * @return bool 866 */ 867 public function isPluginBundledWithCore($name) 868 { 869 return $this->isPluginEnabledByDefault($name) 870 || in_array($name, $this->pluginList->getCorePluginsDisabledByDefault()) 871 || $name == self::DEFAULT_THEME; 872 } 873 874 /** 875 * @param $pluginName 876 * @return bool 877 * @ignore 878 */ 879 public function isPluginThirdPartyAndBogus($pluginName) 880 { 881 if ($this->isPluginBundledWithCore($pluginName)) { 882 return false; 883 } 884 if ($this->isPluginBogus($pluginName)) { 885 return true; 886 } 887 888 $path = self::getPluginDirectory($pluginName); 889 890 if (!$this->isManifestFileFound($path)) { 891 return true; 892 } 893 return false; 894 } 895 896 /** 897 * Load AND activates the specified plugins. It will also overwrite all previously loaded plugins, so it acts 898 * as a setter. 899 * 900 * @param array $pluginsToLoad Array of plugins to load. 901 */ 902 public function loadPlugins(array $pluginsToLoad) 903 { 904 $this->resetTransientCache(); 905 $this->pluginsToLoad = $this->makePluginsToLoad($pluginsToLoad); 906 $this->reloadActivatedPlugins(); 907 } 908 909 /** 910 * Disable plugin loading. 911 */ 912 public function doNotLoadPlugins() 913 { 914 $this->doLoadPlugins = false; 915 } 916 917 /** 918 * Disable loading of "always activated" plugins. 919 */ 920 public function doNotLoadAlwaysActivatedPlugins() 921 { 922 $this->doLoadAlwaysActivatedPlugins = false; 923 } 924 925 /** 926 * Execute postLoad() hook for loaded plugins 927 */ 928 public function postLoadPlugins() 929 { 930 $plugins = $this->getLoadedPlugins(); 931 foreach ($plugins as $plugin) { 932 $plugin->postLoad(); 933 } 934 } 935 936 /** 937 * Returns an array containing the plugins class names (eg. 'UserCountry' and NOT 'UserCountry') 938 * 939 * @return array 940 */ 941 public function getLoadedPluginsName() 942 { 943 return array_keys($this->getLoadedPlugins()); 944 } 945 946 /** 947 * Returns an array mapping loaded plugin names with their plugin objects, eg, 948 * 949 * array( 950 * 'UserCountry' => Plugin $pluginObject, 951 * 'UserLanguage' => Plugin $pluginObject, 952 * ); 953 * 954 * @return Plugin[] 955 */ 956 public function getLoadedPlugins() 957 { 958 return $this->loadedPlugins; 959 } 960 961 /** 962 * @param string $piwikVersion 963 * @return Plugin[] 964 */ 965 public function getIncompatiblePlugins($piwikVersion) 966 { 967 $plugins = $this->getLoadedPlugins(); 968 969 $incompatible = array(); 970 foreach ($plugins as $plugin) { 971 if ($plugin->hasMissingDependencies($piwikVersion)) { 972 $incompatible[] = $plugin; 973 } 974 } 975 976 return $incompatible; 977 } 978 979 /** 980 * Returns an array of plugins that are currently loaded and activated, 981 * mapping loaded plugin names with their plugin objects, eg, 982 * 983 * array( 984 * 'UserCountry' => Plugin $pluginObject, 985 * 'UserLanguage' => Plugin $pluginObject, 986 * ); 987 * 988 * @return Plugin[] 989 */ 990 public function getPluginsLoadedAndActivated() 991 { 992 if (is_null($this->pluginsLoadedAndActivated)) { 993 $enabled = $this->getActivatedPlugins(); 994 995 if (empty($enabled)) { 996 return array(); 997 } 998 999 $plugins = $this->getLoadedPlugins(); 1000 $enabled = array_combine($enabled, $enabled); 1001 $plugins = array_intersect_key($plugins, $enabled); 1002 1003 $this->pluginsLoadedAndActivated = $plugins; 1004 } 1005 1006 return $this->pluginsLoadedAndActivated; 1007 } 1008 1009 /** 1010 * Returns a list of all names of currently activated plugin eg, 1011 * 1012 * array( 1013 * 'UserCountry' 1014 * 'UserLanguage' 1015 * ); 1016 * 1017 * @return string[] 1018 */ 1019 public function getActivatedPlugins() 1020 { 1021 return $this->pluginsToLoad; 1022 } 1023 1024 public function getActivatedPluginsFromConfig() 1025 { 1026 $plugins = $this->pluginList->getActivatedPlugins(); 1027 1028 return $this->makePluginsToLoad($plugins); 1029 } 1030 1031 /** 1032 * Returns a Plugin object by name. 1033 * 1034 * @param string $name The name of the plugin, eg, `'Actions'`. 1035 * @throws \Exception If the plugin has not been loaded. 1036 * @return Plugin 1037 */ 1038 public function getLoadedPlugin($name) 1039 { 1040 if (!isset($this->loadedPlugins[$name]) || is_null($this->loadedPlugins[$name])) { 1041 throw new \Exception("The plugin '$name' has not been loaded."); 1042 } 1043 return $this->loadedPlugins[$name]; 1044 } 1045 1046 /** 1047 * Load the plugins classes installed. 1048 * Register the observers for every plugin. 1049 */ 1050 private function reloadActivatedPlugins() 1051 { 1052 $pluginsToPostPendingEventsTo = array(); 1053 foreach ($this->pluginsToLoad as $pluginName) { 1054 $pluginsToPostPendingEventsTo = $this->reloadActivatedPlugin($pluginName, $pluginsToPostPendingEventsTo); 1055 } 1056 1057 // post pending events after all plugins are successfully loaded 1058 foreach ($pluginsToPostPendingEventsTo as $plugin) { 1059 EventDispatcher::getInstance()->postPendingEventsTo($plugin); 1060 } 1061 } 1062 1063 private function reloadActivatedPlugin($pluginName, $pluginsToPostPendingEventsTo) 1064 { 1065 if ($this->isPluginLoaded($pluginName) || $this->isPluginThirdPartyAndBogus($pluginName)) { 1066 return $pluginsToPostPendingEventsTo; 1067 } 1068 1069 $newPlugin = $this->loadPlugin($pluginName); 1070 1071 if ($newPlugin === null) { 1072 return $pluginsToPostPendingEventsTo; 1073 } 1074 1075 $requirements = $newPlugin->getMissingDependencies(); 1076 1077 if (!empty($requirements)) { 1078 foreach ($requirements as $requirement) { 1079 $possiblePluginName = $requirement['requirement']; 1080 if (in_array($possiblePluginName, $this->pluginsToLoad, $strict = true)) { 1081 $pluginsToPostPendingEventsTo = $this->reloadActivatedPlugin($possiblePluginName, $pluginsToPostPendingEventsTo); 1082 } 1083 } 1084 } 1085 1086 if ($newPlugin->hasMissingDependencies()) { 1087 $this->unloadPluginFromMemory($pluginName); 1088 1089 // at this state we do not know yet whether current user has super user access. We do not even know 1090 // if someone is actually logged in. 1091 $message = Piwik::translate('CorePluginsAdmin_WeCouldNotLoadThePluginAsItHasMissingDependencies', array($pluginName, $newPlugin->getMissingDependenciesAsString())); 1092 $message .= ' '; 1093 $message .= Piwik::translate('General_PleaseContactYourPiwikAdministrator'); 1094 1095 $notification = new Notification($message); 1096 $notification->context = Notification::CONTEXT_ERROR; 1097 Notification\Manager::notify('PluginManager_PluginUnloaded', $notification); 1098 return $pluginsToPostPendingEventsTo; 1099 } 1100 1101 if ($newPlugin->isPremiumFeature() 1102 && SettingsPiwik::isInternetEnabled() 1103 && !Development::isEnabled() 1104 && $this->isPluginActivated('Marketplace') 1105 && $this->isPluginActivated($pluginName)) { 1106 1107 $cacheKey = 'MarketplacePluginMissingLicense' . $pluginName; 1108 $cache = self::getLicenseCache(); 1109 1110 if ($cache->contains($cacheKey)) { 1111 $pluginLicenseInfo = $cache->fetch($cacheKey); 1112 } elseif (!SettingsServer::isTrackerApiRequest()) { 1113 // prevent requesting license info during a tracker request see https://github.com/matomo-org/matomo/issues/14401 1114 // as possibly many instances would try to do this at the same time 1115 try { 1116 $plugins = StaticContainer::get('Piwik\Plugins\Marketplace\Plugins'); 1117 $licenseInfo = $plugins->getLicenseValidInfo($pluginName); 1118 } catch (\Exception $e) { 1119 $licenseInfo = array(); 1120 } 1121 1122 $pluginLicenseInfo = array('missing' => !empty($licenseInfo['isMissingLicense'])); 1123 $sixHours = 3600 * 6; 1124 $cache->save($cacheKey, $pluginLicenseInfo, $sixHours); 1125 } else { 1126 // tracker mode, we assume it is not missing until cache is written 1127 $pluginLicenseInfo = array('missing' => false); 1128 } 1129 1130 if (!empty($pluginLicenseInfo['missing']) && (!defined('PIWIK_TEST_MODE') || !PIWIK_TEST_MODE)) { 1131 $this->unloadPluginFromMemory($pluginName); 1132 return $pluginsToPostPendingEventsTo; 1133 } 1134 } 1135 1136 $pluginsToPostPendingEventsTo[] = $newPlugin; 1137 1138 return $pluginsToPostPendingEventsTo; 1139 } 1140 1141 public static function getLicenseCache() 1142 { 1143 return Cache::getLazyCache(); 1144 } 1145 1146 public function getIgnoredBogusPlugins() 1147 { 1148 $ignored = array(); 1149 foreach ($this->pluginsToLoad as $pluginName) { 1150 if ($this->isPluginThirdPartyAndBogus($pluginName)) { 1151 $ignored[] = $pluginName; 1152 } 1153 } 1154 return $ignored; 1155 } 1156 1157 /** 1158 * Returns the name of all plugins found in this Piwik instance 1159 * (including those not enabled and themes) 1160 * 1161 * @return array 1162 */ 1163 public static function getAllPluginsNames() 1164 { 1165 $pluginList = StaticContainer::get('Piwik\Application\Kernel\PluginList'); 1166 1167 $pluginsToLoad = array_merge( 1168 self::getInstance()->readPluginsDirectory(), 1169 $pluginList->getCorePluginsDisabledByDefault() 1170 ); 1171 $pluginsToLoad = array_values(array_unique($pluginsToLoad)); 1172 return $pluginsToLoad; 1173 } 1174 1175 /** 1176 * Loads the plugin filename and instantiates the plugin with the given name, eg. UserCountry. 1177 * Contrary to loadPlugins() it does not activate the plugin, it only loads it. 1178 * 1179 * @param string $pluginName 1180 * @throws \Exception 1181 * @return Plugin|null 1182 */ 1183 public function loadPlugin($pluginName) 1184 { 1185 if (isset($this->loadedPlugins[$pluginName])) { 1186 return $this->loadedPlugins[$pluginName]; 1187 } 1188 1189 $newPlugin = $this->makePluginClass($pluginName); 1190 1191 $this->addLoadedPlugin($pluginName, $newPlugin); 1192 return $newPlugin; 1193 } 1194 1195 public function isValidPluginName($pluginName) 1196 { 1197 return (bool) preg_match('/^[a-zA-Z]([a-zA-Z0-9_]*)$/D', $pluginName); 1198 } 1199 1200 /** 1201 * @param $pluginName 1202 * @return Plugin 1203 * @throws \Exception 1204 */ 1205 protected function makePluginClass($pluginName) 1206 { 1207 $pluginClassName = $pluginName; 1208 1209 if (!$this->isValidPluginName($pluginName)) { 1210 throw new \Exception(sprintf("The plugin name '%s' is not a valid plugin name", $pluginName)); 1211 } 1212 1213 $path = self::getPluginDirectory($pluginName); 1214 $path = sprintf('%s/%s.php', $path, $pluginName); 1215 1216 if (!file_exists($path)) { 1217 // Create the smallest minimal Piwik Plugin 1218 // Eg. Used for Morpheus default theme which does not have a Morpheus.php file 1219 return new Plugin($pluginName); 1220 } 1221 1222 require_once $path; 1223 1224 $namespacedClass = $this->getClassNamePlugin($pluginName); 1225 if (!class_exists($namespacedClass, false)) { 1226 throw new \Exception("The class $pluginClassName couldn't be found in the file '$path'"); 1227 } 1228 $newPlugin = new $namespacedClass; 1229 1230 if (!($newPlugin instanceof Plugin)) { 1231 throw new \Exception("The plugin $pluginClassName in the file $path must inherit from Plugin."); 1232 } 1233 return $newPlugin; 1234 } 1235 1236 protected function getClassNamePlugin($pluginName) 1237 { 1238 $className = $pluginName; 1239 if ($pluginName == 'API') { 1240 $className = 'Plugin'; 1241 } 1242 return "\\Piwik\\Plugins\\$pluginName\\$className"; 1243 } 1244 1245 private function resetTransientCache() 1246 { 1247 $this->pluginsLoadedAndActivated = null; 1248 } 1249 1250 /** 1251 * Unload plugin 1252 * 1253 * @param Plugin|string $plugin 1254 * @throws \Exception 1255 */ 1256 public function unloadPlugin($plugin) 1257 { 1258 $this->resetTransientCache(); 1259 1260 if (!($plugin instanceof Plugin)) { 1261 $oPlugin = $this->loadPlugin($plugin); 1262 if ($oPlugin === null) { 1263 unset($this->loadedPlugins[$plugin]); 1264 return; 1265 } 1266 1267 $plugin = $oPlugin; 1268 } 1269 1270 unset($this->loadedPlugins[$plugin->getPluginName()]); 1271 } 1272 1273 /** 1274 * Unload all loaded plugins 1275 */ 1276 public function unloadPlugins() 1277 { 1278 $this->resetTransientCache(); 1279 1280 $pluginsLoaded = $this->getLoadedPlugins(); 1281 foreach ($pluginsLoaded as $plugin) { 1282 $this->unloadPlugin($plugin); 1283 } 1284 } 1285 1286 /** 1287 * Install a specific plugin 1288 * 1289 * @param Plugin $plugin 1290 * @throws \Piwik\Plugin\PluginException if installation fails 1291 */ 1292 private function executePluginInstall(Plugin $plugin) 1293 { 1294 try { 1295 $plugin->install(); 1296 } catch (\Exception $e) { 1297 throw new \Piwik\Plugin\PluginException($plugin->getPluginName(), $e->getMessage()); 1298 } 1299 } 1300 1301 /** 1302 * Add a plugin in the loaded plugins array 1303 * 1304 * @param string $pluginName plugin name without prefix (eg. 'UserCountry') 1305 * @param Plugin $newPlugin 1306 * @internal 1307 */ 1308 public function addLoadedPlugin($pluginName, Plugin $newPlugin) 1309 { 1310 $this->resetTransientCache(); 1311 1312 $this->loadedPlugins[$pluginName] = $newPlugin; 1313 } 1314 1315 /** 1316 * Return names of all installed plugins. 1317 * 1318 * @return array 1319 * @api 1320 */ 1321 public function getInstalledPluginsName() 1322 { 1323 $pluginNames = Config::getInstance()->PluginsInstalled['PluginsInstalled']; 1324 return $pluginNames; 1325 } 1326 1327 /** 1328 * Returns names of plugins that should be loaded, but cannot be since their 1329 * files cannot be found. 1330 * 1331 * @return array 1332 * @api 1333 */ 1334 public function getMissingPlugins() 1335 { 1336 $missingPlugins = array(); 1337 1338 $plugins = $this->pluginList->getActivatedPlugins(); 1339 1340 foreach ($plugins as $pluginName) { 1341 // if a plugin is listed in the config, but is not loaded, it does not exist in the folder 1342 if (!$this->isPluginLoaded($pluginName) && !$this->isPluginBogus($pluginName) && 1343 !($this->doesPluginRequireInternetConnection($pluginName) && !SettingsPiwik::isInternetEnabled())) { 1344 $missingPlugins[] = $pluginName; 1345 } 1346 } 1347 1348 return $missingPlugins; 1349 } 1350 1351 /** 1352 * Install a plugin, if necessary 1353 * 1354 * @param Plugin $plugin 1355 */ 1356 private function installPluginIfNecessary(Plugin $plugin) 1357 { 1358 $pluginName = $plugin->getPluginName(); 1359 $saveConfig = false; 1360 1361 // is the plugin already installed or is it the first time we activate it? 1362 $pluginsInstalled = $this->getInstalledPluginsName(); 1363 1364 if (!$this->isPluginInstalled($pluginName)) { 1365 $this->executePluginInstall($plugin); 1366 $pluginsInstalled[] = $pluginName; 1367 $this->updatePluginsInstalledConfig($pluginsInstalled); 1368 $updater = new Updater(); 1369 $updater->markComponentSuccessfullyUpdated($plugin->getPluginName(), $plugin->getVersion(), $isNew = true); 1370 $saveConfig = true; 1371 1372 /** 1373 * Event triggered after a new plugin has been installed. 1374 * 1375 * Note: Might be triggered more than once if the config file is not writable 1376 * 1377 * @param string $pluginName The plugin that has been installed. 1378 */ 1379 Piwik::postEvent('PluginManager.pluginInstalled', array($pluginName)); 1380 } 1381 1382 if ($saveConfig) { 1383 PiwikConfig::getInstance()->forceSave(); 1384 $this->clearCache($pluginName); 1385 } 1386 } 1387 1388 public function isTrackerPlugin(Plugin $plugin) 1389 { 1390 if (!$this->isPluginInstalled($plugin->getPluginName())) { 1391 return false; 1392 } 1393 1394 if ($plugin->isTrackerPlugin()) { 1395 return true; 1396 } 1397 1398 $dimensions = VisitDimension::getDimensions($plugin); 1399 if (!empty($dimensions)) { 1400 return true; 1401 } 1402 1403 $dimensions = ActionDimension::getDimensions($plugin); 1404 if (!empty($dimensions)) { 1405 return true; 1406 } 1407 1408 $hooks = $plugin->registerEvents(); 1409 $hookNames = array_keys($hooks); 1410 foreach ($hookNames as $name) { 1411 if (strpos($name, self::TRACKER_EVENT_PREFIX) === 0) { 1412 return true; 1413 } 1414 if ($name === 'Request.initAuthenticationObject') { 1415 return true; 1416 } 1417 } 1418 1419 $dimensions = ConversionDimension::getDimensions($plugin); 1420 if (!empty($dimensions)) { 1421 return true; 1422 } 1423 1424 return false; 1425 } 1426 1427 private static function pluginStructureLooksValid($path) 1428 { 1429 $name = basename($path); 1430 return file_exists($path . "/" . $name . ".php") 1431 || self::isManifestFileFound($path); 1432 } 1433 1434 /** 1435 * @param $pluginName 1436 */ 1437 private function removePluginFromPluginsInstalledConfig($pluginName) 1438 { 1439 $pluginsInstalled = Config::getInstance()->PluginsInstalled['PluginsInstalled']; 1440 $key = array_search($pluginName, $pluginsInstalled); 1441 if ($key !== false) { 1442 unset($pluginsInstalled[$key]); 1443 } 1444 1445 $this->updatePluginsInstalledConfig($pluginsInstalled); 1446 } 1447 1448 /** 1449 * @param $pluginName 1450 */ 1451 private function removePluginFromPluginsConfig($pluginName) 1452 { 1453 $pluginsEnabled = $this->pluginList->getActivatedPlugins(); 1454 $key = array_search($pluginName, $pluginsEnabled); 1455 if ($key !== false) { 1456 unset($pluginsEnabled[$key]); 1457 } 1458 $this->updatePluginsConfig($pluginsEnabled); 1459 } 1460 1461 /** 1462 * @param $pluginName 1463 * @return bool 1464 */ 1465 private function isPluginBogus($pluginName) 1466 { 1467 $bogusPlugins = array( 1468 'PluginMarketplace', //defines a plugin.json but 1.x Piwik plugin 1469 'DoNotTrack', // Removed in 2.0.3 1470 'AnonymizeIP', // Removed in 2.0.3 1471 'wp-optimize', // a WP plugin that has a plugin.json file but is not a matomo plugin 1472 ); 1473 return in_array($pluginName, $bogusPlugins); 1474 } 1475 1476 private function deactivateThemeIfTheme($pluginName) 1477 { 1478 // Only one theme enabled at a time 1479 $themeEnabled = $this->getThemeEnabled(); 1480 if ($themeEnabled 1481 && $themeEnabled->getPluginName() != self::DEFAULT_THEME) { 1482 $themeAlreadyEnabled = $themeEnabled->getPluginName(); 1483 1484 $plugin = $this->loadPlugin($pluginName); 1485 if ($plugin->isTheme()) { 1486 $this->deactivatePlugin($themeAlreadyEnabled); 1487 } 1488 } 1489 } 1490 1491 /** 1492 * @param $pluginName 1493 */ 1494 private function executePluginDeactivate($pluginName) 1495 { 1496 if (!$this->isPluginBogus($pluginName)) { 1497 $plugin = $this->loadPlugin($pluginName); 1498 if ($plugin !== null) { 1499 $plugin->deactivate(); 1500 } 1501 } 1502 } 1503 1504 /** 1505 * @param $pluginName 1506 */ 1507 private function unloadPluginFromMemory($pluginName) 1508 { 1509 $this->unloadPlugin($pluginName); 1510 1511 $key = array_search($pluginName, $this->pluginsToLoad); 1512 if ($key !== false) { 1513 unset($this->pluginsToLoad[$key]); 1514 } 1515 } 1516 1517 /** 1518 * @param $pluginName 1519 */ 1520 private function removePluginFromConfig($pluginName) 1521 { 1522 $this->removePluginFromPluginsConfig($pluginName); 1523 PiwikConfig::getInstance()->forceSave(); 1524 } 1525 1526 /** 1527 * @param $pluginName 1528 */ 1529 private function executePluginUninstall($pluginName) 1530 { 1531 try { 1532 $plugin = $this->getLoadedPlugin($pluginName); 1533 $plugin->uninstall(); 1534 } catch (\Exception $e) { 1535 } 1536 1537 if (empty($plugin)) { 1538 return; 1539 } 1540 1541 try { 1542 $visitDimensions = VisitDimension::getAllDimensions(); 1543 1544 foreach (VisitDimension::getDimensions($plugin) as $dimension) { 1545 $this->uninstallDimension(VisitDimension::INSTALLER_PREFIX, $dimension, $visitDimensions); 1546 } 1547 } catch (\Exception $e) { 1548 } 1549 1550 try { 1551 $actionDimensions = ActionDimension::getAllDimensions(); 1552 1553 foreach (ActionDimension::getDimensions($plugin) as $dimension) { 1554 $this->uninstallDimension(ActionDimension::INSTALLER_PREFIX, $dimension, $actionDimensions); 1555 } 1556 } catch (\Exception $e) { 1557 } 1558 1559 try { 1560 $conversionDimensions = ConversionDimension::getAllDimensions(); 1561 1562 foreach (ConversionDimension::getDimensions($plugin) as $dimension) { 1563 $this->uninstallDimension(ConversionDimension::INSTALLER_PREFIX, $dimension, $conversionDimensions); 1564 } 1565 } catch (\Exception $e) { 1566 } 1567 } 1568 1569 /** 1570 * @param VisitDimension|ActionDimension|ConversionDimension $dimension 1571 * @param VisitDimension[]|ActionDimension[]|ConversionDimension[] $allDimensions 1572 * @return bool 1573 */ 1574 private function doesAnotherPluginDefineSameColumnWithDbEntry($dimension, $allDimensions) 1575 { 1576 $module = $dimension->getModule(); 1577 $columnName = $dimension->getColumnName(); 1578 1579 foreach ($allDimensions as $dim) { 1580 if ($dim->getColumnName() === $columnName && 1581 $dim->hasColumnType() && 1582 $dim->getModule() !== $module) { 1583 return true; 1584 } 1585 } 1586 1587 return false; 1588 } 1589 1590 /** 1591 * @param string $prefix column installer prefix 1592 * @param ConversionDimension|VisitDimension|ActionDimension $dimension 1593 * @param VisitDimension[]|ActionDimension[]|ConversionDimension[] $allDimensions 1594 */ 1595 private function uninstallDimension($prefix, Dimension $dimension, $allDimensions) 1596 { 1597 if (!$this->doesAnotherPluginDefineSameColumnWithDbEntry($dimension, $allDimensions)) { 1598 $dimension->uninstall(); 1599 1600 $this->removeInstalledVersionFromOptionTable($prefix . $dimension->getColumnName()); 1601 } 1602 } 1603 1604 /** 1605 * @param string $pluginName 1606 * @param bool $checkPluginExistsInFilesystem if enabled, it won't rely on the information in the config file only 1607 * but also check the filesystem if the plugin really is installed. 1608 * For performance reasons this is not the case by default. 1609 * @return bool 1610 */ 1611 public function isPluginInstalled($pluginName, $checkPluginExistsInFilesystem = false) 1612 { 1613 $pluginsInstalled = $this->getInstalledPluginsName(); 1614 $isInstalledInConfig = in_array($pluginName, $pluginsInstalled); 1615 1616 if ($isInstalledInConfig && $checkPluginExistsInFilesystem) { 1617 return $this->isPluginInFilesystem($pluginName); 1618 } 1619 1620 return $isInstalledInConfig; 1621 } 1622 1623 private function removeInstalledVersionFromOptionTable($name) 1624 { 1625 $updater = new Updater(); 1626 $updater->markComponentSuccessfullyUninstalled($name); 1627 } 1628 1629 private function makeSureOnlyActivatedPluginsAreLoaded() 1630 { 1631 foreach ($this->getLoadedPlugins() as $pluginName => $plugin) { 1632 if (!in_array($pluginName, $this->pluginsToLoad)) { 1633 $this->unloadPlugin($plugin); 1634 } 1635 } 1636 } 1637 1638 /** 1639 * Reading the plugins from the global.ini.php config file 1640 * 1641 * @return array 1642 */ 1643 protected function getPluginsFromGlobalIniConfigFile() 1644 { 1645 return $this->pluginList->getPluginsBundledWithPiwik(); 1646 } 1647 1648 /** 1649 * @param $name 1650 * @return bool 1651 */ 1652 protected function isPluginEnabledByDefault($name) 1653 { 1654 $pluginsBundledWithPiwik = $this->getPluginsFromGlobalIniConfigFile(); 1655 if (empty($pluginsBundledWithPiwik)) { 1656 return false; 1657 } 1658 return in_array($name, $pluginsBundledWithPiwik); 1659 } 1660 1661 /** 1662 * @param array $pluginsToLoad 1663 * @return array 1664 */ 1665 private function makePluginsToLoad(array $pluginsToLoad) 1666 { 1667 $pluginsToLoad = array_unique($pluginsToLoad); 1668 if ($this->doLoadAlwaysActivatedPlugins) { 1669 $pluginsToLoad = array_merge($pluginsToLoad, $this->pluginToAlwaysActivate); 1670 } 1671 $pluginsToLoad = array_unique($pluginsToLoad); 1672 $pluginsToLoad = $this->pluginList->sortPlugins($pluginsToLoad); 1673 return $pluginsToLoad; 1674 } 1675 1676 public function loadPluginTranslations() 1677 { 1678 /** @var Translator $translator */ 1679 $translator = StaticContainer::get('Piwik\Translation\Translator'); 1680 foreach ($this->getAllPluginsNames() as $pluginName) { 1681 $translator->addDirectory(self::getPluginDirectory($pluginName) . '/lang'); 1682 } 1683 } 1684 1685 public function hasPremiumFeatures() 1686 { 1687 foreach ($this->getPluginsLoadedAndActivated() as $activatedPlugin) { 1688 if ($activatedPlugin->isPremiumFeature()) { 1689 return true; 1690 } 1691 } 1692 return false; 1693 } 1694 1695 private function savePluginTime($timingName, $pluginName) 1696 { 1697 $optionName = $timingName . $pluginName; 1698 1699 try { 1700 Option::set($optionName, time()); 1701 } catch (\Exception $e) { 1702 if (SettingsPiwik::isMatomoInstalled()) { 1703 throw $e; 1704 } 1705 // we ignore any error while Matomo is not installed yet. refs #16741 1706 } 1707 } 1708 1709} 1710