Test Setup Failed
Push — master ( 41e37a...724119 )
by Matthieu
26:44 queued 19:08
created

Manager::reloadActivatedPlugin()   C

Complexity

Conditions 8
Paths 6

Size

Total Lines 42
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 10.0155

Importance

Changes 0
Metric Value
cc 8
eloc 23
nc 6
nop 2
dl 0
loc 42
ccs 13
cts 19
cp 0.6842
crap 10.0155
rs 5.3846
c 0
b 0
f 0
1
<?php
2
/**
3
 * Piwik - free/libre analytics platform
4
 *
5
 * @link http://piwik.org
6
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
7
 *
8
 */
9
10
namespace Piwik\Plugin;
11
12
use Piwik\Application\Kernel\PluginList;
13
use Piwik\Cache;
14
use Piwik\Columns\Dimension;
15
use Piwik\Config;
16
use Piwik\Config as PiwikConfig;
17
use Piwik\Container\StaticContainer;
18
use Piwik\EventDispatcher;
19
use Piwik\Exception\PluginDeactivatedException;
20
use Piwik\Filesystem;
21
use Piwik\Log;
22
use Piwik\Notification;
23
use Piwik\Piwik;
24
use Piwik\Plugin;
25
use Piwik\Plugin\Dimension\ActionDimension;
26
use Piwik\Plugin\Dimension\ConversionDimension;
27
use Piwik\Plugin\Dimension\VisitDimension;
28
use Piwik\Settings\Storage as SettingsStorage;
29
use Piwik\Theme;
30
use Piwik\Translation\Translator;
31
use Piwik\Updater;
32
33
require_once PIWIK_INCLUDE_PATH . '/core/EventDispatcher.php';
34
35
/**
36
 * The singleton that manages plugin loading/unloading and installation/uninstallation.
37
 */
38
class Manager
39
{
40
    /**
41
     * @return self
42 235
     */
43
    public static function getInstance()
44 235
    {
45
        return StaticContainer::get('Piwik\Plugin\Manager');
46
    }
47
48
    protected $pluginsToLoad = array();
49
50
    protected $doLoadPlugins = true;
51
52
    private $pluginsLoadedAndActivated;
53
54
    /**
55
     * @var Plugin[]
56
     */
57
    protected $loadedPlugins = array();
58
    /**
59
     * Default theme used in Piwik.
60
     */
61
    const DEFAULT_THEME = "Morpheus";
62
63
    protected $doLoadAlwaysActivatedPlugins = true;
64
65
    // These are always activated and cannot be deactivated
66
    protected $pluginToAlwaysActivate = array(
67
        'CoreHome',
68
        'Diagnostics',
69
        'CoreUpdater',
70
        'CoreAdminHome',
71
        'CoreConsole',
72
        'CorePluginsAdmin',
73
        'CoreVisualizations',
74
        'Installation',
75
        'SitesManager',
76
        'UsersManager',
77
        'Intl',
78
        'API',
79
        'Proxy',
80
        'LanguagesManager',
81
        'WebsiteMeasurable',
82
83
        // default Piwik theme, always enabled
84
        self::DEFAULT_THEME,
85
    );
86
87
    private $trackerPluginsNotToLoad = array();
88
89
    /**
90
     * @var PluginList
91
     */
92
    private $pluginList;
93 85
94
    public function __construct(PluginList $pluginList)
95 85
    {
96 85
        $this->pluginList = $pluginList;
97
    }
98
99
    /**
100
     * Loads plugin that are enabled
101
     */
102
    public function loadActivatedPlugins()
103
    {
104
        $pluginsToLoad = $this->getActivatedPluginsFromConfig();
105
        $this->loadPlugins($pluginsToLoad);
106
    }
107
108
    /**
109
     * Called during Tracker
110
     */
111
    public function loadCorePluginsDuringTracker()
112
    {
113
        $pluginsToLoad = $this->pluginList->getActivatedPlugins();
114
        $pluginsToLoad = array_diff($pluginsToLoad, $this->getTrackerPluginsNotToLoad());
115
        $this->loadPlugins($pluginsToLoad);
116
    }
117
118
    /**
119
     * @return array names of plugins that have been loaded
120
     */
121
    public function loadTrackerPlugins()
122
    {
123
        $cacheId = 'PluginsTracker';
124
        $cache = Cache::getEagerCache();
125
126
        if ($cache->contains($cacheId)) {
127
            $pluginsTracker = $cache->fetch($cacheId);
128
        } else {
129
            $this->unloadPlugins();
130
            $this->loadActivatedPlugins();
131
132
            $pluginsTracker = array();
133
134
            foreach ($this->loadedPlugins as $pluginName => $plugin) {
135
                if ($this->isTrackerPlugin($plugin)) {
136
                    $pluginsTracker[] = $pluginName;
137
                }
138
            }
139
140
            if (!empty($pluginsTracker)) {
141
                $cache->save($cacheId, $pluginsTracker);
142
            }
143
        }
144
145
        if (empty($pluginsTracker)) {
146
            $this->unloadPlugins();
147
            return array();
148
        }
149
150
        $pluginsTracker = array_diff($pluginsTracker, $this->getTrackerPluginsNotToLoad());
151
        $this->doNotLoadAlwaysActivatedPlugins();
152
        $this->loadPlugins($pluginsTracker);
153
154
        // we could simply unload all plugins first before loading plugins but this way it is much faster
155
        // since we won't have to create each plugin again and we won't have to parse each plugin metadata file
156
        // again etc
157
        $this->makeSureOnlyActivatedPluginsAreLoaded();
158
159
        return $pluginsTracker;
160
    }
161
162
    /**
163
     * Do not load the specified plugins (used during testing, to disable Provider plugin)
164
     * @param array $plugins
165
     */
166
    public function setTrackerPluginsNotToLoad($plugins)
167
    {
168
        $this->trackerPluginsNotToLoad = $plugins;
169
    }
170
171
    /**
172
     * Get list of plugins to not load
173
     *
174
     * @return array
175
     */
176
    public function getTrackerPluginsNotToLoad()
177
    {
178
        return $this->trackerPluginsNotToLoad;
179
    }
180
181
    // If a plugin hooks onto at least an event starting with "Tracker.", we load the plugin during tracker
182
    const TRACKER_EVENT_PREFIX = 'Tracker.';
183
184
    /**
185
     * @param $pluginName
186
     * @return bool
187 85
     */
188
    public function isPluginOfficialAndNotBundledWithCore($pluginName)
189 85
    {
190 85
        static $gitModules;
191
        if (empty($gitModules)) {
192
            $gitModules = file_get_contents(PIWIK_INCLUDE_PATH . '/.gitmodules');
193
        }
194 85
        // All submodules are officially maintained plugins
195 85
        $isSubmodule = false !== strpos($gitModules, "plugins/" . $pluginName . "\n");
196
        return $isSubmodule;
197
    }
198
199
    /**
200
     * Update Plugins config
201
     *
202
     * @param array $pluginsToLoad Plugins
203
     */
204
    private function updatePluginsConfig($pluginsToLoad)
205
    {
206
        $pluginsToLoad = $this->pluginList->sortPluginsAndRespectDependencies($pluginsToLoad);
207
        $section = PiwikConfig::getInstance()->Plugins;
0 ignored issues
show
Documentation introduced by
The property Plugins does not exist on object<Piwik\Config>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
208
        $section['Plugins'] = $pluginsToLoad;
209
        PiwikConfig::getInstance()->Plugins = $section;
0 ignored issues
show
Documentation introduced by
The property Plugins does not exist on object<Piwik\Config>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
210
    }
211
212
    /**
213
     * Update PluginsInstalled config
214
     *
215
     * @param array $plugins Plugins
216
     */
217
    private function updatePluginsInstalledConfig($plugins)
218
    {
219
        $section = PiwikConfig::getInstance()->PluginsInstalled;
0 ignored issues
show
Documentation introduced by
The property PluginsInstalled does not exist on object<Piwik\Config>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
220
        $section['PluginsInstalled'] = $plugins;
221
        PiwikConfig::getInstance()->PluginsInstalled = $section;
0 ignored issues
show
Documentation introduced by
The property PluginsInstalled does not exist on object<Piwik\Config>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
222
    }
223
224
    public function clearPluginsInstalledConfig()
225
    {
226
        $this->updatePluginsInstalledConfig(array());
227
        PiwikConfig::getInstance()->forceSave();
228
    }
229
230
    /**
231
     * Returns true if plugin is always activated
232
     *
233
     * @param string $name Name of plugin
234
     * @return bool
235 4
     */
236
    private function isPluginAlwaysActivated($name)
237 4
    {
238
        return in_array($name, $this->pluginToAlwaysActivate);
239
    }
240
241
    /**
242
     * Returns true if the plugin can be uninstalled. Any non-core plugin can be uninstalled.
243
     *
244
     * @param $name
245
     * @return bool
246
     */
247
    private function isPluginUninstallable($name)
248
    {
249
        return !$this->isPluginBundledWithCore($name);
250
    }
251
252
    /**
253
     * Returns `true` if a plugin has been activated.
254
     *
255
     * @param string $name Name of plugin, eg, `'Actions'`.
256
     * @return bool
257
     * @api
258 9
     */
259
    public function isPluginActivated($name)
260 9
    {
261 9
        return in_array($name, $this->pluginsToLoad)
262
        || ($this->doLoadAlwaysActivatedPlugins && $this->isPluginAlwaysActivated($name));
263
    }
264
265
    /**
266
     * Checks whether the given plugin is activated, if not triggers an exception.
267
     *
268
     * @param  string $pluginName
269
     * @throws PluginDeactivatedException
270
     */
271
    public function checkIsPluginActivated($pluginName)
272
    {
273
        if (!$this->isPluginActivated($pluginName)) {
274
            throw new PluginDeactivatedException($pluginName);
275
        }
276
    }
277
278
    /**
279
     * Returns `true` if plugin is loaded (in memory).
280
     *
281
     * @param string $name Name of plugin, eg, `'Acions'`.
282
     * @return bool
283
     * @api
284 36
     */
285
    public function isPluginLoaded($name)
286 36
    {
287
        return isset($this->loadedPlugins[$name]);
288
    }
289
290
    /**
291
     * Reads the directories inside the plugins/ directory and returns their names in an array
292
     *
293
     * @return array
294 283
     */
295
    public function readPluginsDirectory()
296 283
    {
297 283
        $pluginsName = _glob(self::getPluginsDirectory() . '*', GLOB_ONLYDIR);
298 283
        $result = array();
299 283
        if ($pluginsName != false) {
300 283
            foreach ($pluginsName as $path) {
301 283
                if (self::pluginStructureLooksValid($path)) {
302 283
                    $result[] = basename($path);
303 283
                }
304 283
            }
305 283
        }
306
        return $result;
307
    }
308 288
309
    public static function getPluginsDirectory()
310 288
    {
311
        return PIWIK_INCLUDE_PATH . '/plugins/';
312
    }
313
314
    /**
315
     * Deactivate plugin
316
     *
317
     * @param string $pluginName Name of plugin
318
     */
319
    public function deactivatePlugin($pluginName)
320
    {
321
        $this->clearCache($pluginName);
322
323
        // execute deactivate() to let the plugin do cleanups
324
        $this->executePluginDeactivate($pluginName);
325
326
        $this->unloadPluginFromMemory($pluginName);
327
328
        $this->removePluginFromConfig($pluginName);
329
330
        /**
331
         * Event triggered after a plugin has been deactivated.
332
         *
333
         * @param string $pluginName The plugin that has been deactivated.
334
         */
335
        Piwik::postEvent('PluginManager.pluginDeactivated', array($pluginName));
336
    }
337
338
    /**
339
     * Tries to find the given components such as a Menu or Tasks implemented by plugins.
340
     * This method won't cache the found components. If you need to find the same component multiple times you might
341
     * want to cache the result to save a tiny bit of time.
342
     *
343
     * @param string $componentName     The name of the component you want to look for. In case you request a
344
     *                                  component named 'Menu' it'll look for a file named 'Menu.php' within the
345
     *                                  root of all plugin folders that implement a class named
346
     *                                  Piwik\Plugin\$PluginName\Menu.
347
     * @param string $expectedSubclass  If not empty, a check will be performed whether a found file extends the
348
     *                                  given subclass. If the requested file exists but does not extend this class
349
     *                                  a warning will be shown to advice a developer to extend this certain class.
350
     *
351
     * @return \stdClass[]
352
     */
353 View Code Duplication
    public function findComponents($componentName, $expectedSubclass)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
354
    {
355
        $plugins    = $this->getPluginsLoadedAndActivated();
356
        $components = array();
357
358
        foreach ($plugins as $plugin) {
359
            $component = $plugin->findComponent($componentName, $expectedSubclass);
360
361
            if (!empty($component)) {
362
                $components[] = $component;
363
            }
364
        }
365
366
        return $components;
367
    }
368 28
369 View Code Duplication
    public function findMultipleComponents($directoryWithinPlugin, $expectedSubclass)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
370 28
    {
371 28
        $plugins = $this->getPluginsLoadedAndActivated();
372
        $found   = array();
373 28
374 28
        foreach ($plugins as $plugin) {
375
            $components = $plugin->findMultipleComponents($directoryWithinPlugin, $expectedSubclass);
376 28
377 28
            if (!empty($components)) {
378 28
                $found = array_merge($found, $components);
379 28
            }
380
        }
381 28
382
        return $found;
383
    }
384
385
    /**
386
     * Uninstalls a Plugin (deletes plugin files from the disk)
387
     * Only deactivated plugins can be uninstalled
388
     *
389
     * @param $pluginName
390
     * @throws \Exception
391
     * @return bool
392
     */
393
    public function uninstallPlugin($pluginName)
394
    {
395
        if ($this->isPluginLoaded($pluginName)) {
396
            throw new \Exception("To uninstall the plugin $pluginName, first disable it in Piwik > Settings > Plugins");
397
        }
398
        $this->loadAllPluginsAndGetTheirInfo();
399
400
        SettingsStorage\Backend\PluginSettingsTable::removeAllSettingsForPlugin($pluginName);
401
        SettingsStorage\Backend\MeasurableSettingsTable::removeAllSettingsForPlugin($pluginName);
402
403
        $this->executePluginDeactivate($pluginName);
404
        $this->executePluginUninstall($pluginName);
405
406
        $this->removePluginFromPluginsInstalledConfig($pluginName);
407
408
        $this->unloadPluginFromMemory($pluginName);
409
410
        $this->removePluginFromConfig($pluginName);
411
        $this->removeInstalledVersionFromOptionTable($pluginName);
412
        $this->clearCache($pluginName);
413
414
        self::deletePluginFromFilesystem($pluginName);
415
        if ($this->isPluginInFilesystem($pluginName)) {
416
            return false;
417
        }
418
419
        /**
420
         * Event triggered after a plugin has been uninstalled.
421
         *
422
         * @param string $pluginName The plugin that has been uninstalled.
423
         */
424
        Piwik::postEvent('PluginManager.pluginUninstalled', array($pluginName));
425
426
        return true;
427
    }
428
429
    /**
430
     * @param string $pluginName
431
     */
432
    private function clearCache($pluginName)
433
    {
434
        $this->resetTransientCache();
435
        Filesystem::deleteAllCacheOnUpdate($pluginName);
436
    }
437
438
    public static function deletePluginFromFilesystem($plugin)
439
    {
440
        Filesystem::unlinkRecursive(PIWIK_INCLUDE_PATH . '/plugins/' . $plugin, $deleteRootToo = true);
441
    }
442
443
    /**
444
     * Install loaded plugins
445
     *
446
     * @throws
447
     * @return array Error messages of plugin install fails
448
     */
449
    public function installLoadedPlugins()
450
    {
451
        Log::debug("Loaded plugins: " . implode(", ", array_keys($this->getLoadedPlugins())));
0 ignored issues
show
Deprecated Code introduced by
The method Piwik\Log::debug() has been deprecated with message: Inject and call Psr\Log\LoggerInterface::debug() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
452
        
453
        foreach ($this->getLoadedPlugins() as $plugin) {
454
            $this->installPluginIfNecessary($plugin);
455
        }
456
    }
457
458
    /**
459
     * Activate the specified plugin and install (if needed)
460
     *
461
     * @param string $pluginName Name of plugin
462
     * @throws \Exception
463
     */
464
    public function activatePlugin($pluginName)
465
    {
466
        $plugins = $this->pluginList->getActivatedPlugins();
467
        if (in_array($pluginName, $plugins)) {
468
            // plugin is already activated
469
            return;
470
        }
471
472
        if (!$this->isPluginInFilesystem($pluginName)) {
473
            throw new \Exception("Plugin '$pluginName' cannot be found in the filesystem in plugins/ directory.");
474
        }
475
        $this->deactivateThemeIfTheme($pluginName);
476
477
        // Load plugin
478
        $plugin = $this->loadPlugin($pluginName);
479
        if ($plugin === null) {
480
            throw new \Exception("The plugin '$pluginName' was found in the filesystem, but could not be loaded.'");
481
        }
482
        $this->installPluginIfNecessary($plugin);
483
        $plugin->activate();
0 ignored issues
show
Unused Code introduced by
The call to the method Piwik\Plugin::activate() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
484
485
        EventDispatcher::getInstance()->postPendingEventsTo($plugin);
486
487
        $this->pluginsToLoad[] = $pluginName;
488
489
        $this->updatePluginsConfig($this->pluginsToLoad);
490
        PiwikConfig::getInstance()->forceSave();
491
492
        $this->clearCache($pluginName);
493
494
        /**
495
         * Event triggered after a plugin has been activated.
496
         *
497
         * @param string $pluginName The plugin that has been activated.
498
         */
499
        Piwik::postEvent('PluginManager.pluginActivated', array($pluginName));
500
    }
501
502
    public function isPluginInFilesystem($pluginName)
503
    {
504
        $existingPlugins = $this->readPluginsDirectory();
505
        $isPluginInFilesystem = array_search($pluginName, $existingPlugins) !== false;
506
        return $this->isValidPluginName($pluginName)
507 1
        && $isPluginInFilesystem;
508
    }
509 1
510
    /**
511 1
     * Returns the currently enabled theme.
512 1
     *
513
     * If no theme is enabled, the **Morpheus** plugin is returned (this is the base and default theme).
514 1
     *
515 1
     * @return Plugin
516 1
     * @api
517 1
     */
518
    public function getThemeEnabled()
519
    {
520 1
        $plugins = $this->getLoadedPlugins();
521 1
522 1
        $theme = false;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The expression false; of type false adds false to the return on line 534 which is incompatible with the return type documented by Piwik\Plugin\Manager::getThemeEnabled of type Piwik\Plugin. It seems like you forgot to handle an error condition.
Loading history...
523 1
        foreach ($plugins as $plugin) {
524
            /* @var $plugin Plugin */
525
            if ($plugin->isTheme()
526
                && $this->isPluginActivated($plugin->getPluginName())
527
            ) {
528
                if ($plugin->getPluginName() != self::DEFAULT_THEME) {
529
                    return $plugin; // enabled theme (not default)
530
                }
531
                $theme = $plugin; // default theme
532
            }
533
        }
534
        return $theme;
535
    }
536
537
    /**
538
     * @param string $themeName
539
     * @throws \Exception
540
     * @return Theme
541
     */
542
    public function getTheme($themeName)
543
    {
544
        $plugins = $this->getLoadedPlugins();
545
546
        foreach ($plugins as $plugin) {
547
            if ($plugin->isTheme() && $plugin->getPluginName() == $themeName) {
548
                return new Theme($plugin);
549
            }
550
        }
551
        throw new \Exception('Theme not found : ' . $themeName);
552
    }
553
554
    public function getNumberOfActivatedPluginsExcludingAlwaysActivated()
555
    {
556
        $counter = 0;
557
558
        $pluginNames = $this->getLoadedPluginsName();
559
        foreach ($pluginNames as $pluginName) {
560
            if ($this->isPluginActivated($pluginName)
561
                && !$this->isPluginAlwaysActivated($pluginName)) {
562
                $counter++;
563
            }
564
        }
565
566
        return $counter;
567
    }
568
569
    /**
570
     * Returns info regarding all plugins. Loads plugins that can be loaded.
571
     *
572
     * @return array An array that maps plugin names with arrays of plugin information. Plugin
573
     *               information consists of the following entries:
574
     *
575
     *               - **activated**: Whether the plugin is activated.
576
     *               - **alwaysActivated**: Whether the plugin should always be activated,
577
     *                                      or not.
578
     *               - **uninstallable**: Whether the plugin is uninstallable or not.
579
     *               - **invalid**: If the plugin is invalid, this property will be set to true.
580
     *                              If the plugin is not invalid, this property will not exist.
581
     *               - **info**: If the plugin was loaded, will hold the plugin information.
582
     *                           See {@link Piwik\Plugin::getInformation()}.
583
     * @api
584
     */
585
    public function loadAllPluginsAndGetTheirInfo()
586
    {
587
        /** @var Translator $translator */
588
        $translator = StaticContainer::get('Piwik\Translation\Translator');
589
590
        $plugins = array();
591
592
        $listPlugins = array_merge(
593
            $this->readPluginsDirectory(),
594
            $this->pluginList->getActivatedPlugins()
595
        );
596
        $listPlugins = array_unique($listPlugins);
597
        foreach ($listPlugins as $pluginName) {
598
            // Hide plugins that are never going to be used
599
            if ($this->isPluginBogus($pluginName)) {
600
                continue;
601
            }
602
603
            // If the plugin is not core and looks bogus, do not load
604
            if ($this->isPluginThirdPartyAndBogus($pluginName)) {
605
                $info = array(
606
                    'invalid'         => true,
607
                    'activated'       => false,
608
                    'alwaysActivated' => false,
609
                    'uninstallable'   => true,
610
                );
611
            } else {
612
                $translator->addDirectory(self::getPluginsDirectory() . $pluginName . '/lang');
613
                $this->loadPlugin($pluginName);
614
                $info = array(
615
                    'activated'       => $this->isPluginActivated($pluginName),
616
                    'alwaysActivated' => $this->isPluginAlwaysActivated($pluginName),
617
                    'uninstallable'   => $this->isPluginUninstallable($pluginName),
618
                );
619
            }
620
621
            $plugins[$pluginName] = $info;
622
        }
623
624
        $loadedPlugins = $this->getLoadedPlugins();
625
        foreach ($loadedPlugins as $oPlugin) {
626
            $pluginName = $oPlugin->getPluginName();
627
628
            $info = array(
629 283
                'info' => $oPlugin->getInformation(),
0 ignored issues
show
Deprecated Code introduced by
The method Piwik\Plugin::getInformation() has been deprecated.

This method has been deprecated.

Loading history...
630
                'activated'       => $this->isPluginActivated($pluginName),
631 283
                'alwaysActivated' => $this->isPluginAlwaysActivated($pluginName),
632
                'missingRequirements' => $oPlugin->getMissingDependenciesAsString(),
633
                'uninstallable' => $this->isPluginUninstallable($pluginName),
634
            );
635
            $plugins[$pluginName] = $info;
636
        }
637
638
        return $plugins;
639
    }
640 88
641
    protected static function isManifestFileFound($path)
642 88
    {
643 86
        return file_exists($path . "/" . MetadataLoader::PLUGIN_JSON_FILENAME);
644 88
    }
645
646
    /**
647 3
     * Returns `true` if the plugin is bundled with core or `false` if it is third party.
648
     *
649 3
     * @param string $name The name of the plugin, eg, `'Actions'`.
650 3
     * @return bool
651
     */
652
    public function isPluginBundledWithCore($name)
653
    {
654
        return $this->isPluginEnabledByDefault($name)
655
        || in_array($name, $this->pluginList->getCorePluginsDisabledByDefault())
656
        || $name == self::DEFAULT_THEME;
657
    }
658
659
    /**
660
     * @param $pluginName
661
     * @return bool
662
     * @ignore
663
     */
664
    public function isPluginThirdPartyAndBogus($pluginName)
665
    {
666
        if ($this->isPluginBundledWithCore($pluginName)) {
667
            return false;
668
        }
669 36
        if ($this->isPluginBogus($pluginName)) {
670
            return true;
671 36
        }
672 36
673 36
        $path = $this->getPluginsDirectory() . $pluginName;
674 36
        if (!$this->isManifestFileFound($path)) {
675
            return true;
676
        }
677
        return false;
678
    }
679
680
    /**
681
     * Load AND activates the specified plugins. It will also overwrite all previously loaded plugins, so it acts
682
     * as a setter.
683
     *
684
     * @param array $pluginsToLoad Array of plugins to load.
685
     */
686
    public function loadPlugins(array $pluginsToLoad)
687
    {
688
        $this->resetTransientCache();
689
        $this->pluginsToLoad = $this->makePluginsToLoad($pluginsToLoad);
690
        $this->reloadActivatedPlugins();
691
    }
692
693
    /**
694
     * Disable plugin loading.
695
     */
696
    public function doNotLoadPlugins()
697
    {
698
        $this->doLoadPlugins = false;
699
    }
700
701
    /**
702
     * Disable loading of "always activated" plugins.
703
     */
704
    public function doNotLoadAlwaysActivatedPlugins()
705
    {
706
        $this->doLoadAlwaysActivatedPlugins = false;
707
    }
708 1
709
    /**
710 1
     * Execute postLoad() hook for loaded plugins
711
     */
712
    public function postLoadPlugins()
713
    {
714
        $plugins = $this->getLoadedPlugins();
715
        foreach ($plugins as $plugin) {
716
            $plugin->postLoad();
717
        }
718
    }
719
720
    /**
721
     * Returns an array containing the plugins class names (eg. 'UserCountry' and NOT 'UserCountry')
722
     *
723 29
     * @return array
724
     */
725 29
    public function getLoadedPluginsName()
726
    {
727
        return array_keys($this->getLoadedPlugins());
728
    }
729
730
    /**
731
     * Returns an array mapping loaded plugin names with their plugin objects, eg,
732
     *
733
     *     array(
734
     *         'UserCountry' => Plugin $pluginObject,
735
     *         'UserLanguage' => Plugin $pluginObject,
736
     *     );
737
     *
738
     * @return Plugin[]
739
     */
740
    public function getLoadedPlugins()
741
    {
742
        return $this->loadedPlugins;
743
    }
744
745
    /**
746
     * @param  string $piwikVersion
747
     * @return Plugin[]
748
     */
749
    public function getIncompatiblePlugins($piwikVersion)
750
    {
751
        $plugins = $this->getLoadedPlugins();
752
753
        $incompatible = array();
754
        foreach ($plugins as $plugin) {
755
            if ($plugin->hasMissingDependencies($piwikVersion)) {
756
                $incompatible[] = $plugin;
757 123
            }
758
        }
759 123
760 121
        return $incompatible;
761
    }
762 121
763 93
    /**
764
     * Returns an array of plugins that are currently loaded and activated,
765
     * mapping loaded plugin names with their plugin objects, eg,
766 28
     *
767 28
     *     array(
768 28
     *         'UserCountry' => Plugin $pluginObject,
769
     *         'UserLanguage' => Plugin $pluginObject,
770 28
     *     );
771 28
     *
772
     * @return Plugin[]
773 30
     */
774
    public function getPluginsLoadedAndActivated()
775
    {
776
        if (is_null($this->pluginsLoadedAndActivated)) {
777
            $enabled = $this->getActivatedPlugins();
778
779
            if (empty($enabled)) {
780
                return array();
781
            }
782
783
            $plugins = $this->getLoadedPlugins();
784
            $enabled = array_combine($enabled, $enabled);
785
            $plugins = array_intersect_key($plugins, $enabled);
786 121
787
            $this->pluginsLoadedAndActivated = $plugins;
788 121
        }
789
790
        return $this->pluginsLoadedAndActivated;
791
    }
792
793
    /**
794
     * Returns a list of all names of currently activated plugin eg,
795
     *
796
     *     array(
797
     *         'UserCountry'
798
     *         'UserLanguage'
799
     *     );
800
     *
801
     * @return string[]
802
     */
803
    public function getActivatedPlugins()
804
    {
805 5
        return $this->pluginsToLoad;
806
    }
807 5
808
    public function getActivatedPluginsFromConfig()
809
    {
810 5
        $plugins = $this->pluginList->getActivatedPlugins();
811
812
        return $this->makePluginsToLoad($plugins);
813
    }
814
815
    /**
816
     * Returns a Plugin object by name.
817 36
     *
818
     * @param string $name The name of the plugin, eg, `'Actions'`.
819 36
     * @throws \Exception If the plugin has not been loaded.
820 36
     * @return Plugin
821 36
     */
822 36
    public function getLoadedPlugin($name)
823 36
    {
824 3
        if (!isset($this->loadedPlugins[$name]) || is_null($this->loadedPlugins[$name])) {
825 3
            throw new \Exception("The plugin '$name' has not been loaded.");
826
        }
827
        return $this->loadedPlugins[$name];
828
    }
829 3
830
    /**
831
     * Load the plugins classes installed.
832
     * Register the observers for every plugin.
833
     */
834 3
    private function reloadActivatedPlugins()
835 3
    {
836 36
        $pluginsToPostPendingEventsTo = array();
837
        foreach ($this->pluginsToLoad as $pluginName) {
838
            $pluginsToPostPendingEventsTo = $this->reloadActivatedPlugin($pluginName, $pluginsToPostPendingEventsTo);
839 36
        }
840 3
841 36
        // post pending events after all plugins are successfully loaded
842 36
        foreach ($pluginsToPostPendingEventsTo as $plugin) {
843
            EventDispatcher::getInstance()->postPendingEventsTo($plugin);
844
        }
845
    }
846
847
    private function reloadActivatedPlugin($pluginName, $pluginsToPostPendingEventsTo)
848
    {
849
        if ($this->isPluginLoaded($pluginName) || $this->isPluginThirdPartyAndBogus($pluginName)) {
850
            return $pluginsToPostPendingEventsTo;
851
        }
852
853
        $newPlugin = $this->loadPlugin($pluginName);
854
855
        if ($newPlugin === null) {
856
            return $pluginsToPostPendingEventsTo;
857
        }
858
859
        $requirements = $newPlugin->getMissingDependencies();
860
861 192
        if (!empty($requirements)) {
862
            foreach ($requirements as $requirement) {
863 192
                $possiblePluginName = $requirement['requirement'];
864
                if (in_array($possiblePluginName, $this->pluginsToLoad, $strict = true)) {
865 192
                    $pluginsToPostPendingEventsTo = $this->reloadActivatedPlugin($possiblePluginName, $pluginsToPostPendingEventsTo);
866 192
                }
867 192
            }
868 192
        }
869 192
870 192
        if ($newPlugin->hasMissingDependencies()) {
871
            $this->unloadPluginFromMemory($pluginName);
872
873
            // at this state we do not know yet whether current user has super user access. We do not even know
874
            // if someone is actually logged in.
875
            $message  = Piwik::translate('CorePluginsAdmin_WeCouldNotLoadThePluginAsItHasMissingDependencies', array($pluginName, $newPlugin->getMissingDependenciesAsString()));
876
            $message .= ' ';
877
            $message .= Piwik::translate('General_PleaseContactYourPiwikAdministrator');
878
879
            $notification = new Notification($message);
880
            $notification->context = Notification::CONTEXT_ERROR;
881 5
            Notification\Manager::notify('PluginManager_PluginUnloaded', $notification);
882
            return $pluginsToPostPendingEventsTo;
883 5
        }
884 2
885
        $pluginsToPostPendingEventsTo[] = $newPlugin;
886 3
887
        return $pluginsToPostPendingEventsTo;
888 3
    }
889 3
890
    public function getIgnoredBogusPlugins()
891
    {
892 3
        $ignored = array();
893
        foreach ($this->pluginsToLoad as $pluginName) {
894 3
            if ($this->isPluginThirdPartyAndBogus($pluginName)) {
895
                $ignored[] = $pluginName;
896
            }
897
        }
898
        return $ignored;
899
    }
900
901
    /**
902 3
     * Returns the name of all plugins found in this Piwik instance
903
     * (including those not enabled and themes)
904 3
     *
905 3
     * @return array
906
     */
907 3
    public static function getAllPluginsNames()
908
    {
909
        $pluginList = StaticContainer::get('Piwik\Application\Kernel\PluginList');
910
911 3
        $pluginsToLoad = array_merge(
912
            self::getInstance()->readPluginsDirectory(),
913 3
            $pluginList->getCorePluginsDisabledByDefault()
914
        );
915
        $pluginsToLoad = array_values(array_unique($pluginsToLoad));
916 1
        return $pluginsToLoad;
917
    }
918
919 3
    /**
920
     * Loads the plugin filename and instantiates the plugin with the given name, eg. UserCountry.
921 3
     * Contrary to loadPlugins() it does not activate the plugin, it only loads it.
922 3
     *
923
     * @param string $pluginName
924
     * @throws \Exception
925 3
     * @return Plugin|null
926
     */
927 3
    public function loadPlugin($pluginName)
928
    {
929
        if (isset($this->loadedPlugins[$pluginName])) {
930 3
            return $this->loadedPlugins[$pluginName];
931
        }
932
        $newPlugin = $this->makePluginClass($pluginName);
933 3
934
        $this->addLoadedPlugin($pluginName, $newPlugin);
935 3
        return $newPlugin;
936 3
    }
937 1
938 1
    public function isValidPluginName($pluginName)
939 3
    {
940
        return (bool) preg_match('/^[a-zA-Z]([a-zA-Z0-9_]*)$/D', $pluginName);
941
    }
942 36
943
    /**
944 36
     * @param $pluginName
945 36
     * @return Plugin
946
     * @throws \Exception
947
     */
948
    protected function makePluginClass($pluginName)
949
    {
950
        $pluginFileName = sprintf("%s/%s.php", $pluginName, $pluginName);
951
        $pluginClassName = $pluginName;
952
953
        if (!$this->isValidPluginName($pluginName)) {
954
            throw new \Exception(sprintf("The plugin filename '%s' is not a valid plugin name", $pluginFileName));
955
        }
956
957
        $path = self::getPluginsDirectory() . $pluginFileName;
958
959
        if (!file_exists($path)) {
960
            // Create the smallest minimal Piwik Plugin
961
            // Eg. Used for Morpheus default theme which does not have a Morpheus.php file
962
            return new Plugin($pluginName);
963
        }
964
965
        require_once $path;
966
967
        $namespacedClass = $this->getClassNamePlugin($pluginName);
968
        if (!class_exists($namespacedClass, false)) {
969
            throw new \Exception("The class $pluginClassName couldn't be found in the file '$path'");
970
        }
971
        $newPlugin = new $namespacedClass;
972
973
        if (!($newPlugin instanceof Plugin)) {
974
            throw new \Exception("The plugin $pluginClassName in the file $path must inherit from Plugin.");
975
        }
976
        return $newPlugin;
977
    }
978
979
    protected function getClassNamePlugin($pluginName)
980
    {
981
        $className = $pluginName;
982
        if ($pluginName == 'API') {
983
            $className = 'Plugin';
984
        }
985
        return "\\Piwik\\Plugins\\$pluginName\\$className";
986
    }
987
988
    private function resetTransientCache()
989
    {
990
        $this->pluginsLoadedAndActivated = null;
991
    }
992
993
    /**
994
     * Unload plugin
995
     *
996
     * @param Plugin|string $plugin
997
     * @throws \Exception
998
     */
999
    public function unloadPlugin($plugin)
1000
    {
1001
        $this->resetTransientCache();
1002
1003
        if (!($plugin instanceof Plugin)) {
1004
            $oPlugin = $this->loadPlugin($plugin);
1005 3
            if ($oPlugin === null) {
1006
                unset($this->loadedPlugins[$plugin]);
1007 3
                return;
1008
            }
1009 3
1010 3
            $plugin = $oPlugin;
1011
        }
1012
1013
        unset($this->loadedPlugins[$plugin->getPluginName()]);
1014
    }
1015
1016
    /**
1017
     * Unload all loaded plugins
1018
     */
1019
    public function unloadPlugins()
1020
    {
1021
        $this->resetTransientCache();
1022
1023
        $pluginsLoaded = $this->getLoadedPlugins();
1024
        foreach ($pluginsLoaded as $plugin) {
1025
            $this->unloadPlugin($plugin);
1026
        }
1027
    }
1028
1029
    /**
1030
     * Install a specific plugin
1031
     *
1032
     * @param Plugin $plugin
1033
     * @throws \Piwik\Plugin\PluginException if installation fails
1034
     */
1035
    private function executePluginInstall(Plugin $plugin)
1036
    {
1037
        try {
1038
            $plugin->install();
1039
        } catch (\Exception $e) {
1040
            throw new \Piwik\Plugin\PluginException($plugin->getPluginName(), $e->getMessage());
1041
        }
1042
    }
1043
1044
    /**
1045
     * Add a plugin in the loaded plugins array
1046
     *
1047
     * @param string $pluginName plugin name without prefix (eg. 'UserCountry')
1048
     * @param Plugin $newPlugin
1049
     * @internal
1050
     */
1051
    public function addLoadedPlugin($pluginName, Plugin $newPlugin)
1052
    {
1053
        $this->resetTransientCache();
1054
1055
        $this->loadedPlugins[$pluginName] = $newPlugin;
1056
    }
1057
1058
    /**
1059
     * Return names of all installed plugins.
1060
     *
1061
     * @return array
1062
     * @api
1063
     */
1064
    public function getInstalledPluginsName()
1065
    {
1066
        $pluginNames = Config::getInstance()->PluginsInstalled['PluginsInstalled'];
0 ignored issues
show
Documentation introduced by
The property PluginsInstalled does not exist on object<Piwik\Config>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1067
        return $pluginNames;
1068
    }
1069
1070
    /**
1071
     * Returns names of plugins that should be loaded, but cannot be since their
1072
     * files cannot be found.
1073
     *
1074
     * @return array
1075
     * @api
1076
     */
1077
    public function getMissingPlugins()
1078
    {
1079
        $missingPlugins = array();
1080
1081
        $plugins = $this->pluginList->getActivatedPlugins();
1082
1083
        foreach ($plugins as $pluginName) {
1084
            // if a plugin is listed in the config, but is not loaded, it does not exist in the folder
1085
            if (!$this->isPluginLoaded($pluginName) && !$this->isPluginBogus($pluginName) ) {
1086
                $missingPlugins[] = $pluginName;
1087
            }
1088
        }
1089
1090
        return $missingPlugins;
1091
    }
1092
1093
    /**
1094
     * Install a plugin, if necessary
1095
     *
1096
     * @param Plugin $plugin
1097
     */
1098
    private function installPluginIfNecessary(Plugin $plugin)
1099
    {
1100
        $pluginName = $plugin->getPluginName();
1101
        $saveConfig = false;
1102
1103
        // is the plugin already installed or is it the first time we activate it?
1104
        $pluginsInstalled = $this->getInstalledPluginsName();
1105
1106
        if (!$this->isPluginInstalled($pluginName)) {
1107
            $this->executePluginInstall($plugin);
1108
            $pluginsInstalled[] = $pluginName;
1109
            $this->updatePluginsInstalledConfig($pluginsInstalled);
1110
            $updater = new Updater();
1111
            $updater->markComponentSuccessfullyUpdated($plugin->getPluginName(), $plugin->getVersion(), $isNew = true);
1112
            $saveConfig = true;
1113
1114
            /**
1115 283
             * Event triggered after a new plugin has been installed.
1116
             *
1117 283
             * Note: Might be triggered more than once if the config file is not writable
1118 283
             *
1119 283
             * @param string $pluginName The plugin that has been installed.
1120
             */
1121
            Piwik::postEvent('PluginManager.pluginInstalled', array($pluginName));
1122
        }
1123
1124
        if ($saveConfig) {
1125
            PiwikConfig::getInstance()->forceSave();
1126
            $this->clearCache($pluginName);
1127
        }
1128
    }
1129
1130
    public function isTrackerPlugin(Plugin $plugin)
1131
    {
1132
        if (!$this->isPluginInstalled($plugin->getPluginName())) {
1133
            return false;
1134
        }
1135
        
1136
        if ($plugin->isTrackerPlugin()) {
1137
            return true;
1138
        }
1139
1140
        $dimensions = VisitDimension::getDimensions($plugin);
1141
        if (!empty($dimensions)) {
1142
            return true;
1143
        }
1144
1145
        $dimensions = ActionDimension::getDimensions($plugin);
1146
        if (!empty($dimensions)) {
1147
            return true;
1148
        }
1149
1150
        $hooks = $plugin->getListHooksRegistered();
0 ignored issues
show
Deprecated Code introduced by
The method Piwik\Plugin::getListHooksRegistered() has been deprecated with message: since 2.15.0 use {@link registerEvents()} instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
1151
        $hookNames = array_keys($hooks);
1152
        foreach ($hookNames as $name) {
1153
            if (strpos($name, self::TRACKER_EVENT_PREFIX) === 0) {
1154
                return true;
1155
            }
1156
            if ($name === 'Request.initAuthenticationObject') {
1157
                return true;
1158
            }
1159
        }
1160
1161
        $dimensions = ConversionDimension::getDimensions($plugin);
1162
        if (!empty($dimensions)) {
1163
            return true;
1164
        }
1165
1166
        return false;
1167
    }
1168
1169
    private static function pluginStructureLooksValid($path)
1170
    {
1171
        $name = basename($path);
1172
        return file_exists($path . "/" . $name . ".php")
1173
        || self::isManifestFileFound($path);
1174
    }
1175
1176
    /**
1177
     * @param $pluginName
1178
     */
1179
    private function removePluginFromPluginsInstalledConfig($pluginName)
1180
    {
1181
        $pluginsInstalled = Config::getInstance()->PluginsInstalled['PluginsInstalled'];
0 ignored issues
show
Documentation introduced by
The property PluginsInstalled does not exist on object<Piwik\Config>. Since you implemented __get, maybe consider adding a @property annotation.

Since your code implements the magic getter _get, this function will be called for any read access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

If the property has read access only, you can use the @property-read annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
1182
        $key = array_search($pluginName, $pluginsInstalled);
1183
        if ($key !== false) {
1184
            unset($pluginsInstalled[$key]);
1185
        }
1186
1187
        $this->updatePluginsInstalledConfig($pluginsInstalled);
1188
    }
1189
1190
    /**
1191
     * @param $pluginName
1192
     */
1193
    private function removePluginFromPluginsConfig($pluginName)
1194
    {
1195
        $pluginsEnabled = $this->pluginList->getActivatedPlugins();
1196
        $key = array_search($pluginName, $pluginsEnabled);
1197
        if ($key !== false) {
1198
            unset($pluginsEnabled[$key]);
1199
        }
1200
        $this->updatePluginsConfig($pluginsEnabled);
1201
    }
1202
1203
    /**
1204
     * @param $pluginName
1205
     * @return bool
1206
     */
1207
    private function isPluginBogus($pluginName)
1208
    {
1209
        $bogusPlugins = array(
1210
            'PluginMarketplace', //defines a plugin.json but 1.x Piwik plugin
1211
            'DoNotTrack', // Removed in 2.0.3
1212
            'AnonymizeIP', // Removed in 2.0.3
1213
        );
1214
        return in_array($pluginName, $bogusPlugins);
1215
    }
1216
1217
    private function deactivateThemeIfTheme($pluginName)
1218
    {
1219
        // Only one theme enabled at a time
1220
        $themeEnabled = $this->getThemeEnabled();
1221
        if ($themeEnabled
1222
            && $themeEnabled->getPluginName() != self::DEFAULT_THEME) {
1223
            $themeAlreadyEnabled = $themeEnabled->getPluginName();
1224
1225
            $plugin = $this->loadPlugin($pluginName);
1226
            if ($plugin->isTheme()) {
1227
                $this->deactivatePlugin($themeAlreadyEnabled);
1228
            }
1229
        }
1230
    }
1231
1232
    /**
1233
     * @param $pluginName
1234
     */
1235
    private function executePluginDeactivate($pluginName)
1236
    {
1237
        if (!$this->isPluginBogus($pluginName)) {
1238
            $plugin = $this->loadPlugin($pluginName);
1239
            if ($plugin !== null) {
1240
                $plugin->deactivate();
1241
            }
1242
        }
1243
    }
1244
1245
    /**
1246
     * @param $pluginName
1247
     */
1248
    private function unloadPluginFromMemory($pluginName)
1249
    {
1250
        $this->unloadPlugin($pluginName);
1251
1252
        $key = array_search($pluginName, $this->pluginsToLoad);
1253
        if ($key !== false) {
1254
            unset($this->pluginsToLoad[$key]);
1255
        }
1256
    }
1257
1258
    /**
1259
     * @param $pluginName
1260
     */
1261
    private function removePluginFromConfig($pluginName)
1262
    {
1263
        $this->removePluginFromPluginsConfig($pluginName);
1264
        PiwikConfig::getInstance()->forceSave();
1265
    }
1266
1267
    /**
1268
     * @param $pluginName
1269
     */
1270
    private function executePluginUninstall($pluginName)
1271
    {
1272
        try {
1273
            $plugin = $this->getLoadedPlugin($pluginName);
1274
            $plugin->uninstall();
1275
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1276
        }
1277
1278
        if (empty($plugin)) {
1279
            return;
1280
        }
1281
1282
        try {
1283
            $visitDimensions = VisitDimension::getAllDimensions();
1284
1285
            foreach (VisitDimension::getDimensions($plugin) as $dimension) {
1286
                $this->uninstallDimension(VisitDimension::INSTALLER_PREFIX, $dimension, $visitDimensions);
1287
            }
1288
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1289
        }
1290
1291
        try {
1292
            $actionDimensions = ActionDimension::getAllDimensions();
1293
1294
            foreach (ActionDimension::getDimensions($plugin) as $dimension) {
1295
                $this->uninstallDimension(ActionDimension::INSTALLER_PREFIX, $dimension, $actionDimensions);
1296
            }
1297
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1298
        }
1299
1300
        try {
1301
            $conversionDimensions = ConversionDimension::getAllDimensions();
1302
1303
            foreach (ConversionDimension::getDimensions($plugin) as $dimension) {
1304
                $this->uninstallDimension(ConversionDimension::INSTALLER_PREFIX, $dimension, $conversionDimensions);
1305
            }
1306
        } catch (\Exception $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
1307
        }
1308
    }
1309
1310
    /**
1311
     * @param VisitDimension|ActionDimension|ConversionDimension $dimension
1312
     * @param VisitDimension[]|ActionDimension[]|ConversionDimension[] $allDimensions
1313
     * @return bool
1314
     */
1315
    private function doesAnotherPluginDefineSameColumnWithDbEntry($dimension, $allDimensions)
1316
    {
1317
        $module = $dimension->getModule();
1318
        $columnName = $dimension->getColumnName();
1319
1320
        foreach ($allDimensions as $dim) {
1321 88
            if ($dim->getColumnName() === $columnName &&
1322
                $dim->hasColumnType() &&
1323 88
                $dim->getModule() !== $module) {
1324
                return true;
1325
            }
1326
        }
1327
1328
        return false;
1329
    }
1330 88
1331
    /**
1332 88
     * @param string $prefix column installer prefix
1333 88
     * @param ConversionDimension|VisitDimension|ActionDimension $dimension
1334
     * @param VisitDimension[]|ActionDimension[]|ConversionDimension[] $allDimensions
1335
     */
1336 88
    private function uninstallDimension($prefix, Dimension $dimension, $allDimensions)
1337
    {
1338
        if (!$this->doesAnotherPluginDefineSameColumnWithDbEntry($dimension, $allDimensions)) {
1339
            $dimension->uninstall();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Piwik\Columns\Dimension as the method uninstall() does only exist in the following sub-classes of Piwik\Columns\Dimension: Piwik\Plugin\Dimension\ActionDimension, Piwik\Plugin\Dimension\ConversionDimension, Piwik\Plugin\Dimension\VisitDimension, Piwik\Plugins\Actions\Columns\ActionType, Piwik\Plugins\Actions\Columns\ActionUrl, Piwik\Plugins\Actions\Columns\ClickedUrl, Piwik\Plugins\Actions\Columns\DownloadUrl, Piwik\Plugins\Actions\Columns\EntryPageTitle, Piwik\Plugins\Actions\Columns\EntryPageUrl, Piwik\Plugins\Actions\Columns\ExitPageTitle, Piwik\Plugins\Actions\Columns\ExitPageUrl, Piwik\Plugins\Actions\Columns\IdPageview, Piwik\Plugins\Actions\Columns\InteractionPosition, Piwik\Plugins\Actions\Columns\PageTitle, Piwik\Plugins\Actions\Columns\PageUrl, Piwik\Plugins\Actions\Columns\SearchKeyword, Piwik\Plugins\Actions\Columns\TimeSpentRefAction, Piwik\Plugins\Actions\Columns\VisitTotalActions, Piwik\Plugins\Actions\Co...\VisitTotalInteractions, Piwik\Plugins\Actions\Columns\VisitTotalSearches, Piwik\Plugins\Contents\Columns\ContentInteraction, Piwik\Plugins\Contents\Columns\ContentName, Piwik\Plugins\Contents\Columns\ContentPiece, Piwik\Plugins\Contents\Columns\ContentTarget, Piwik\Plugins\CoreHome\Columns\IdSite, Piwik\Plugins\CoreHome\Columns\ServerTime, Piwik\Plugins\CoreHome\Columns\UserId, Piwik\Plugins\CoreHome\C...ns\VisitFirstActionTime, Piwik\Plugins\CoreHome\Columns\VisitGoalBuyer, Piwik\Plugins\CoreHome\Columns\VisitGoalConverted, Piwik\Plugins\CoreHome\Columns\VisitId, Piwik\Plugins\CoreHome\Columns\VisitIp, Piwik\Plugins\CoreHome\Columns\VisitLastActionTime, Piwik\Plugins\CoreHome\Columns\VisitTotalTime, Piwik\Plugins\CoreHome\C...s\VisitorDaysSinceFirst, Piwik\Plugins\CoreHome\C...s\VisitorDaysSinceOrder, Piwik\Plugins\CoreHome\Columns\VisitorId, Piwik\Plugins\CoreHome\Columns\VisitorReturning, Piwik\Plugins\CoreHome\Columns\VisitsCount, Piwik\Plugins\CustomVariables\Columns\Base, Piwik\Plugins\CustomVari...umns\CustomVariableName, Piwik\Plugins\CustomVari...mns\CustomVariableValue, Piwik\Plugins\DevicePlugins\Columns\PluginCookie, Piwik\Plugins\DevicePlugins\Columns\PluginDirector, Piwik\Plugins\DevicePlugins\Columns\PluginFlash, Piwik\Plugins\DevicePlugins\Columns\PluginGears, Piwik\Plugins\DevicePlugins\Columns\PluginJava, Piwik\Plugins\DevicePlugins\Columns\PluginPdf, Piwik\Plugins\DevicePlug...Columns\PluginQuickTime, Piwik\Plugins\DevicePlug...olumns\PluginRealPlayer, Piwik\Plugins\DevicePlug...lumns\PluginSilverlight, Piwik\Plugins\DevicePlug...umns\PluginWindowsMedia, Piwik\Plugins\DevicesDetection\Columns\Base, Piwik\Plugins\DevicesDet...n\Columns\BrowserEngine, Piwik\Plugins\DevicesDetection\Columns\BrowserName, Piwik\Plugins\DevicesDet...\Columns\BrowserVersion, Piwik\Plugins\DevicesDetection\Columns\DeviceBrand, Piwik\Plugins\DevicesDetection\Columns\DeviceModel, Piwik\Plugins\DevicesDetection\Columns\DeviceType, Piwik\Plugins\DevicesDetection\Columns\Os, Piwik\Plugins\DevicesDetection\Columns\OsVersion, Piwik\Plugins\Ecommerce\Columns\BaseConversion, Piwik\Plugins\Ecommerce\Columns\Revenue, Piwik\Plugins\Ecommerce\Columns\RevenueDiscount, Piwik\Plugins\Ecommerce\Columns\RevenueShipping, Piwik\Plugins\Ecommerce\Columns\RevenueSubtotal, Piwik\Plugins\Ecommerce\Columns\RevenueTax, Piwik\Plugins\Events\Columns\EventAction, Piwik\Plugins\Events\Columns\EventCategory, Piwik\Plugins\Events\Columns\EventName, Piwik\Plugins\Events\Columns\TotalEvents, Piwik\Plugins\ExampleTra...\ExampleActionDimension, Piwik\Plugins\ExampleTra...mpleConversionDimension, Piwik\Plugins\ExampleTra...s\ExampleVisitDimension, Piwik\Plugins\Goals\Columns\IdGoal, Piwik\Plugins\Provider\Columns\Provider, Piwik\Plugins\Referrers\Columns\Base, Piwik\Plugins\Referrers\Columns\Campaign, Piwik\Plugins\Referrers\Columns\Keyword, Piwik\Plugins\Referrers\Columns\ReferrerName, Piwik\Plugins\Referrers\Columns\ReferrerType, Piwik\Plugins\Referrers\Columns\ReferrerUrl, Piwik\Plugins\Referrers\Columns\Website, Piwik\Plugins\Resolution\Columns\Resolution, Piwik\Plugins\UserCountry\Columns\Base, Piwik\Plugins\UserCountry\Columns\City, Piwik\Plugins\UserCountry\Columns\Country, Piwik\Plugins\UserCountry\Columns\Latitude, Piwik\Plugins\UserCountry\Columns\Longitude, Piwik\Plugins\UserCountry\Columns\Provider, Piwik\Plugins\UserCountry\Columns\Region, Piwik\Plugins\UserId\Columns\UserId, Piwik\Plugins\UserLanguage\Columns\Language, Piwik\Plugins\VisitTime\Columns\LocalTime, Piwik\Plugins\VisitTime\Columns\ServerTime, Piwik\Plugins\VisitorInt...itsByDaysSinceLastVisit. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
1340
1341
            $this->removeInstalledVersionFromOptionTable($prefix . $dimension->getColumnName());
1342
        }
1343 36
    }
1344
1345 36
    /**
1346 36
     * @param $pluginName
1347 36
     * @return bool
1348 36
     */
1349 36
    public function isPluginInstalled($pluginName)
1350 36
    {
1351 36
        $pluginsInstalled = $this->getInstalledPluginsName();
1352
        return in_array($pluginName, $pluginsInstalled);
1353
    }
1354 192
1355
    private function removeInstalledVersionFromOptionTable($name)
1356
    {
1357 192
        $updater = new Updater();
1358 192
        $updater->markComponentSuccessfullyUninstalled($name);
1359 192
    }
1360 192
1361 192
    private function makeSureOnlyActivatedPluginsAreLoaded()
1362
    {
1363
        foreach ($this->getLoadedPlugins() as $pluginName => $plugin) {
1364
            if (!in_array($pluginName, $this->pluginsToLoad)) {
1365
                $this->unloadPlugin($plugin);
1366
            }
1367
        }
1368
    }
1369
1370
    /**
1371
     * Reading the plugins from the global.ini.php config file
1372
     *
1373
     * @return array
1374
     */
1375
    protected function getPluginsFromGlobalIniConfigFile()
1376
    {
1377
        return $this->pluginList->getPluginsBundledWithPiwik();
1378
    }
1379
1380
    /**
1381
     * @param $name
1382
     * @return bool
1383
     */
1384
    protected function isPluginEnabledByDefault($name)
1385
    {
1386
        $pluginsBundledWithPiwik = $this->getPluginsFromGlobalIniConfigFile();
1387
        if (empty($pluginsBundledWithPiwik)) {
1388
            return false;
1389
        }
1390
        return in_array($name, $pluginsBundledWithPiwik);
1391
    }
1392
1393
    /**
1394
     * @param array $pluginsToLoad
1395
     * @return array
1396
     */
1397
    private function makePluginsToLoad(array $pluginsToLoad)
1398
    {
1399
        $pluginsToLoad = array_unique($pluginsToLoad);
1400
        if ($this->doLoadAlwaysActivatedPlugins) {
1401
            $pluginsToLoad = array_merge($pluginsToLoad, $this->pluginToAlwaysActivate);
1402
        }
1403
        $pluginsToLoad = array_unique($pluginsToLoad);
1404
        $pluginsToLoad = $this->pluginList->sortPlugins($pluginsToLoad);
1405
        return $pluginsToLoad;
1406
    }
1407
1408
    public function loadPluginTranslations()
1409
    {
1410
        /** @var Translator $translator */
1411
        $translator = StaticContainer::get('Piwik\Translation\Translator');
1412
        foreach ($this->getAllPluginsNames() as $pluginName) {
1413
            $translator->addDirectory(self::getPluginsDirectory() . $pluginName . '/lang');
1414
        }
1415
    }
1416
}
1417