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