1<?php
2/**
3 * Matomo - free/libre analytics platform
4 *
5 * @link https://matomo.org
6 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7 *
8 */
9namespace Piwik;
10
11use Piwik\Archive\ArchiveInvalidator;
12use Piwik\Container\StaticContainer;
13use Piwik\Plugin\Dependency;
14use Piwik\Plugin\Manager;
15use Piwik\Plugin\MetadataLoader;
16
17if (!class_exists('Piwik\Plugin')) {
18
19/**
20 * Base class of all Plugin Descriptor classes.
21 *
22 * Any plugin that wants to add event observers to one of Piwik's {@hook # hooks},
23 * or has special installation/uninstallation logic must implement this class.
24 * Plugins that can specify everything they need to in the _plugin.json_ files,
25 * such as themes, don't need to implement this class.
26 *
27 * Class implementations should be named after the plugin they are a part of
28 * (eg, `class UserCountry extends Plugin`).
29 *
30 * ### Plugin Metadata
31 *
32 * In addition to providing a place for plugins to install/uninstall themselves
33 * and add event observers, this class is also responsible for loading metadata
34 * found in the plugin.json file.
35 *
36 * The plugin.json file must exist in the root directory of a plugin. It can
37 * contain the following information:
38 *
39 * - **description**: An internationalized string description of what the plugin
40 *                    does.
41 * - **homepage**: The URL to the plugin's website.
42 * - **authors**: A list of author arrays with keys for 'name', 'email' and 'homepage'
43 * - **license**: The license the code uses (eg, GPL, MIT, etc.).
44 * - **version**: The plugin version (eg, 1.0.1).
45 * - **theme**: `true` or `false`. If `true`, the plugin will be treated as a theme.
46 *
47 * ### Examples
48 *
49 * **How to extend**
50 *
51 *     use Piwik\Common;
52 *     use Piwik\Plugin;
53 *     use Piwik\Db;
54 *
55 *     class MyPlugin extends Plugin
56 *     {
57 *         public function registerEvents()
58 *         {
59 *             return array(
60 *                 'API.getReportMetadata' => 'getReportMetadata',
61 *                 'Another.event'         => array(
62 *                                                'function' => 'myOtherPluginFunction',
63 *                                                'after'    => true // executes this callback after others
64 *                                            )
65 *             );
66 *         }
67 *
68 *         public function install()
69 *         {
70 *             Db::exec("CREATE TABLE " . Common::prefixTable('mytable') . "...");
71 *         }
72 *
73 *         public function uninstall()
74 *         {
75 *             Db::exec("DROP TABLE IF EXISTS " . Common::prefixTable('mytable'));
76 *         }
77 *
78 *         public function getReportMetadata(&$metadata)
79 *         {
80 *             // ...
81 *         }
82 *
83 *         public function myOtherPluginFunction()
84 *         {
85 *             // ...
86 *         }
87 *     }
88 *
89 * @api
90 */
91class Plugin
92{
93    /**
94     * Name of this plugin.
95     *
96     * @var string
97     */
98    protected $pluginName;
99
100    /**
101     * Holds plugin metadata.
102     *
103     * @var array
104     */
105    private $pluginInformation;
106
107    /**
108     * As the cache is used quite often we avoid having to create instances all the time. We reuse it which is not
109     * perfect but efficient. If the cache is used we need to make sure to call setId() before usage as there
110     * is maybe a different key set since last usage.
111     *
112     * @var \Matomo\Cache\Eager
113     */
114    private $cache;
115
116    /**
117     * Constructor.
118     *
119     * @param string|bool $pluginName A plugin name to force. If not supplied, it is set
120     *                                to the last part of the class name.
121     * @throws \Exception If plugin metadata is defined in both the getInformation() method
122     *                    and the **plugin.json** file.
123     */
124    public function __construct($pluginName = false)
125    {
126        if (empty($pluginName)) {
127            $pluginName = explode('\\', get_class($this));
128            $pluginName = end($pluginName);
129        }
130        $this->pluginName = $pluginName;
131
132        $cacheId = 'Plugin' . $pluginName . 'Metadata';
133        $cache = Cache::getEagerCache();
134
135        if ($cache->contains($cacheId)) {
136            $this->pluginInformation = $cache->fetch($cacheId);
137        } else {
138            $this->reloadPluginInformation();
139
140            $cache->save($cacheId, $this->pluginInformation);
141        }
142    }
143
144    public function reloadPluginInformation()
145    {
146        $metadataLoader = new MetadataLoader($this->pluginName);
147        $this->pluginInformation = $metadataLoader->load();
148
149        if ($this->hasDefinedPluginInformationInPluginClass() && $metadataLoader->hasPluginJson()) {
150            throw new \Exception('Plugin ' . $this->pluginName . ' has defined the method getInformation() and as well as having a plugin.json file. Please delete the getInformation() method from the plugin class. Alternatively, you may delete the plugin directory from plugins/' . $this->pluginName);
151        }
152    }
153
154    private function createCacheIfNeeded()
155    {
156        if (is_null($this->cache)) {
157            $this->cache = Cache::getEagerCache();
158        }
159    }
160
161    private function hasDefinedPluginInformationInPluginClass()
162    {
163        $myClassName = get_class();
164        $pluginClassName = get_class($this);
165
166        if ($pluginClassName == $myClassName) {
167            // plugin has not defined its own class
168            return false;
169        }
170
171        $foo = new \ReflectionMethod(get_class($this), 'getInformation');
172        $declaringClass = $foo->getDeclaringClass()->getName();
173
174        return $declaringClass != $myClassName;
175    }
176
177    /**
178     * Returns plugin information, including:
179     *
180     * - 'description' => string        // 1-2 sentence description of the plugin
181     * - 'author' => string             // plugin author
182     * - 'author_homepage' => string    // author homepage URL (or email "mailto:youremail@example.org")
183     * - 'homepage' => string           // plugin homepage URL
184     * - 'license' => string            // plugin license
185     * - 'version' => string            // plugin version number; examples and 3rd party plugins must not use Version::VERSION; 3rd party plugins must increment the version number with each plugin release
186     * - 'theme' => bool                // Whether this plugin is a theme (a theme is a plugin, but a plugin is not necessarily a theme)
187     *
188     * @return array
189     */
190    public function getInformation()
191    {
192        return $this->pluginInformation;
193    }
194
195    final public function isPremiumFeature()
196    {
197        return !empty($this->pluginInformation['price']['base']);
198    }
199
200    /**
201     * Returns a list of events with associated event observers.
202     *
203     * Derived classes should use this method to associate callbacks with events.
204     *
205     * @return array eg,
206     *
207     *                   array(
208     *                       'API.getReportMetadata' => 'myPluginFunction',
209     *                       'Another.event'         => array(
210     *                                                      'function' => 'myOtherPluginFunction',
211     *                                                      'after'    => true // execute after callbacks w/o ordering
212     *                                                  )
213     *                       'Yet.Another.event'     => array(
214     *                                                      'function' => 'myOtherPluginFunction',
215     *                                                      'before'   => true // execute before callbacks w/o ordering
216     *                                                  )
217     *                   )
218     * @since 2.15.0
219     */
220    public function registerEvents()
221    {
222        return array();
223    }
224
225    /**
226     * This method is executed after a plugin is loaded and translations are registered.
227     * Useful for initialization code that uses translated strings.
228     */
229    public function postLoad()
230    {
231        return;
232    }
233
234    /**
235     * Defines whether the whole plugin requires a working internet connection
236     * If set to true, the plugin will be automatically unloaded if `enable_internet_features` is 0,
237     * even if the plugin is activated
238     *
239     * @return bool
240     */
241    public function requiresInternetConnection()
242    {
243        return false;
244    }
245
246    /**
247     * Installs the plugin. Derived classes should implement this class if the plugin
248     * needs to:
249     *
250     * - create tables
251     * - update existing tables
252     * - etc.
253     *
254     * @throws \Exception if installation of fails for some reason.
255     */
256    public function install()
257    {
258        return;
259    }
260
261    /**
262     * Uninstalls the plugins. Derived classes should implement this method if the changes
263     * made in {@link install()} need to be undone during uninstallation.
264     *
265     * In most cases, if you have an {@link install()} method, you should provide
266     * an {@link uninstall()} method.
267     *
268     * @throws \Exception if uninstallation of fails for some reason.
269     */
270    public function uninstall()
271    {
272        return;
273    }
274
275    /**
276     * Executed every time the plugin is enabled.
277     */
278    public function activate()
279    {
280        return;
281    }
282
283    /**
284     * Executed every time the plugin is disabled.
285     */
286    public function deactivate()
287    {
288        return;
289    }
290
291    /**
292     * Returns the plugin version number.
293     *
294     * @return string
295     */
296    final public function getVersion()
297    {
298        $info = $this->getInformation();
299        return $info['version'];
300    }
301
302    /**
303     * Returns `true` if this plugin is a theme, `false` if otherwise.
304     *
305     * @return bool
306     */
307    public function isTheme()
308    {
309        $info = $this->getInformation();
310        return !empty($info['theme']) && (bool)$info['theme'];
311    }
312
313    /**
314     * Returns the plugin's base class name without the namespace,
315     * e.g., `"UserCountry"` when the plugin class is `"Piwik\Plugins\UserCountry\UserCountry"`.
316     *
317     * @return string
318     */
319    final public function getPluginName()
320    {
321        return $this->pluginName;
322    }
323
324    /**
325     * Tries to find a component such as a Menu or Tasks within this plugin.
326     *
327     * @param string $componentName      The name of the component you want to look for. In case you request a
328     *                                   component named 'Menu' it'll look for a file named 'Menu.php' within the
329     *                                   root of the plugin folder that implements a class named
330     *                                   Piwik\Plugin\$PluginName\Menu . If such a file exists but does not implement
331     *                                   this class it'll silently ignored.
332     * @param string $expectedSubclass   If not empty, a check will be performed whether a found file extends the
333     *                                   given subclass. If the requested file exists but does not extend this class
334     *                                   a warning will be shown to advice a developer to extend this certain class.
335     *
336     * @return string|null  Null if the requested component does not exist or an instance of the found
337     *                         component.
338     */
339    public function findComponent($componentName, $expectedSubclass)
340    {
341        $this->createCacheIfNeeded();
342
343        $cacheId = 'Plugin' . $this->pluginName . $componentName . $expectedSubclass;
344
345        $pluginsDir = Manager::getPluginDirectory($this->pluginName);
346
347        $componentFile = sprintf('%s/%s.php', $pluginsDir, $componentName);
348
349        if ($this->cache->contains($cacheId)) {
350            $classname = $this->cache->fetch($cacheId);
351
352            if (empty($classname)) {
353                return null; // might by "false" in case has no menu, widget, ...
354            }
355
356            if (file_exists($componentFile)) {
357                include_once $componentFile;
358            }
359        } else {
360            $this->cache->save($cacheId, false); // prevent from trying to load over and over again for instance if there is no Menu for a plugin
361
362            if (!file_exists($componentFile)) {
363                return null;
364            }
365
366            require_once $componentFile;
367
368            $classname = sprintf('Piwik\\Plugins\\%s\\%s', $this->pluginName, $componentName);
369
370            if (!class_exists($classname)) {
371                return null;
372            }
373
374            if (!empty($expectedSubclass) && !is_subclass_of($classname, $expectedSubclass)) {
375                Log::warning(sprintf('Cannot use component %s for plugin %s, class %s does not extend %s',
376                    $componentName, $this->pluginName, $classname, $expectedSubclass));
377                return null;
378            }
379
380            $this->cache->save($cacheId, $classname);
381        }
382
383        return $classname;
384    }
385
386    public function findMultipleComponents($directoryWithinPlugin, $expectedSubclass)
387    {
388        $this->createCacheIfNeeded();
389
390        $cacheId = 'Plugin' . $this->pluginName . $directoryWithinPlugin . $expectedSubclass;
391
392        if ($this->cache->contains($cacheId)) {
393            $components = $this->cache->fetch($cacheId);
394
395            if ($this->includeComponents($components)) {
396                return $components;
397            } else {
398                // problem including one cached file, refresh cache
399            }
400        }
401
402        $components = $this->doFindMultipleComponents($directoryWithinPlugin, $expectedSubclass);
403
404        $this->cache->save($cacheId, $components);
405
406        return $components;
407    }
408
409    /**
410     * Detect whether there are any missing dependencies.
411     *
412     * @param null $piwikVersion Defaults to the current Piwik version
413     * @return bool
414     */
415    public function hasMissingDependencies($piwikVersion = null)
416    {
417        $requirements = $this->getMissingDependencies($piwikVersion);
418
419        return !empty($requirements);
420    }
421
422    public function getMissingDependencies($piwikVersion = null)
423    {
424        if (empty($this->pluginInformation['require'])) {
425            return array();
426        }
427
428        $dependency = $this->makeDependency($piwikVersion);
429        return $dependency->getMissingDependencies($this->pluginInformation['require']);
430    }
431
432    /**
433     * Returns a string (translated) describing the missing requirements for this plugin and the given Piwik version
434     *
435     * @param string $piwikVersion
436     * @return string "AnonymousPiwikUsageMeasurement requires PIWIK >=3.0.0"
437     */
438    public function getMissingDependenciesAsString($piwikVersion = null)
439    {
440        if ($this->requiresInternetConnection() && !SettingsPiwik::isInternetEnabled()) {
441            return Piwik::translate('CorePluginsAdmin_PluginRequiresInternet');
442        }
443
444        if (empty($this->pluginInformation['require'])) {
445            return '';
446        }
447        $dependency = $this->makeDependency($piwikVersion);
448
449        $missingDependencies = $dependency->getMissingDependencies($this->pluginInformation['require']);
450
451        if(empty($missingDependencies)) {
452            return '';
453        }
454
455        $causedBy = array();
456        foreach ($missingDependencies as $dependency) {
457            $causedBy[] = ucfirst($dependency['requirement']) . ' ' . $dependency['causedBy'];
458        }
459
460        return Piwik::translate("CorePluginsAdmin_PluginRequirement", array(
461            $this->getPluginName(),
462            implode(', ', $causedBy)
463        ));
464    }
465
466    /**
467     * Schedules re-archiving of this plugin's reports from when this plugin was last
468     * deactivated to now. If the last time core:archive was run is earlier than the
469     * plugin's last deactivation time, then we use that time instead.
470     *
471     * Note: this only works for CLI archiving setups.
472     *
473     * Note: the time frame is limited by the `[General] rearchive_reports_in_past_last_n_months`
474     * INI config value.
475     *
476     * @throws \DI\DependencyException
477     * @throws \DI\NotFoundException
478     */
479    public function schedulePluginReArchiving()
480    {
481        $lastDeactivationTime = $this->getPluginLastDeactivationTime();
482
483        $dateTime = null;
484
485        $lastCronArchiveTime = (int) Option::get(CronArchive::OPTION_ARCHIVING_FINISHED_TS);
486        if (empty($lastCronArchiveTime)) {
487            $dateTime = $lastDeactivationTime;
488        } else if (empty($lastDeactivationTime)) {
489            $dateTime = null; // use default earliest time
490        } else {
491            $lastCronArchiveTime = Date::factory($lastCronArchiveTime);
492            $dateTime = $lastDeactivationTime->isEarlier($lastCronArchiveTime) ? $lastDeactivationTime : $lastCronArchiveTime;
493        }
494
495        if (empty($dateTime)) { // sanity check
496            $dateTime = null;
497        }
498
499        $archiveInvalidator = StaticContainer::get(ArchiveInvalidator::class);
500        $archiveInvalidator->scheduleReArchiving('all', $this->getPluginName(), $report = null, $dateTime);
501    }
502
503    /**
504     * Extracts the plugin name from a backtrace array. Returns `false` if we can't find one.
505     *
506     * @param array $backtrace The result of {@link debug_backtrace()} or
507     *                         [Exception::getTrace()](http://www.php.net/manual/en/exception.gettrace.php).
508     * @return string|false
509     */
510    public static function getPluginNameFromBacktrace($backtrace)
511    {
512        foreach ($backtrace as $tracepoint) {
513            // try and discern the plugin name
514            if (isset($tracepoint['class'])) {
515                $className = self::getPluginNameFromNamespace($tracepoint['class']);
516                if ($className) {
517                    return $className;
518                }
519            }
520        }
521        return false;
522    }
523
524    /**
525     * Extracts the plugin name from a namespace name or a fully qualified class name. Returns `false`
526     * if we can't find one.
527     *
528     * @param string $namespaceOrClassName The namespace or class string.
529     * @return string|false
530     */
531    public static function getPluginNameFromNamespace($namespaceOrClassName)
532    {
533        if ($namespaceOrClassName && preg_match("/Piwik\\\\Plugins\\\\([a-zA-Z_0-9]+)\\\\/", $namespaceOrClassName, $matches)) {
534            return $matches[1];
535        } else {
536            return false;
537        }
538    }
539
540    /**
541     * Override this method in your plugin class if you want your plugin to be loaded during tracking.
542     *
543     * Note: If you define your own dimension or handle a tracker event, your plugin will automatically
544     * be detected as a tracker plugin.
545     *
546     * @return bool
547     * @internal
548     */
549    public function isTrackerPlugin()
550    {
551        return false;
552    }
553
554    /**
555     * @return Date|null
556     * @throws \Exception
557     */
558    public function getPluginLastActivationTime()
559    {
560        $optionName = Manager::LAST_PLUGIN_ACTIVATION_TIME_OPTION_PREFIX . $this->pluginName;
561        $time = Option::get($optionName);
562        if (empty($time)) {
563            return null;
564        }
565        return Date::factory((int) $time);
566    }
567
568    /**
569     * @return Date|null
570     * @throws \Exception
571     */
572    public function getPluginLastDeactivationTime()
573    {
574        $optionName = Manager::LAST_PLUGIN_DEACTIVATION_TIME_OPTION_PREFIX . $this->pluginName;
575        $time = Option::get($optionName);
576        if (empty($time)) {
577            return null;
578        }
579        return Date::factory((int) $time);
580    }
581
582    /**
583     * @param $directoryWithinPlugin
584     * @param $expectedSubclass
585     * @return array
586     */
587    private function doFindMultipleComponents($directoryWithinPlugin, $expectedSubclass)
588    {
589        $components = array();
590
591        $pluginsDir = Manager::getPluginDirectory($this->pluginName);
592        $baseDir = $pluginsDir . '/' . $directoryWithinPlugin;
593
594        $files   = Filesystem::globr($baseDir, '*.php');
595
596        foreach ($files as $file) {
597            require_once $file;
598
599            $fileName  = str_replace(array($baseDir . '/', '.php'), '', $file);
600            $klassName = sprintf('Piwik\\Plugins\\%s\\%s\\%s', $this->pluginName, str_replace('/', '\\', $directoryWithinPlugin), str_replace('/', '\\', $fileName));
601
602            if (!class_exists($klassName)) {
603                continue;
604            }
605
606            if (!empty($expectedSubclass) && !is_subclass_of($klassName, $expectedSubclass)) {
607                continue;
608            }
609
610            $klass = new \ReflectionClass($klassName);
611
612            if ($klass->isAbstract()) {
613                continue;
614            }
615
616            $components[$file] = $klassName;
617        }
618        return $components;
619    }
620
621    /**
622     * @param $components
623     * @return bool true if all files were included, false if any file cannot be read
624     */
625    private function includeComponents($components)
626    {
627        foreach ($components as $file => $klass) {
628            if (!is_readable($file)) {
629                return false;
630            }
631        }
632        foreach ($components as $file => $klass) {
633            include_once $file;
634        }
635        return true;
636    }
637
638    /**
639     * @param $piwikVersion
640     * @return Dependency
641     */
642    private function makeDependency($piwikVersion)
643    {
644        $dependency = new Dependency();
645
646        if (!is_null($piwikVersion)) {
647            $dependency->setPiwikVersion($piwikVersion);
648        }
649        return $dependency;
650    }
651}
652
653}
654
655