PluginManager   B
last analyzed

Complexity

Total Complexity 43

Size/Duplication

Total Lines 438
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 169
c 2
b 0
f 0
dl 0
loc 438
rs 8.96
wmc 43

14 Methods

Rating   Name   Duplication   Size   Complexity  
A registerPlugin() 0 15 1
A checkCompatibility() 0 17 2
A loadPluginClass() 0 42 4
A load() 0 20 3
A getPlugin() 0 17 3
A initPlugin() 0 22 3
A upgradePlugins() 0 47 5
A getEnabledPlugins() 0 13 3
A getPlugins() 0 24 6
A __construct() 0 11 1
A getLoadedPlugins() 0 3 1
B loadPlugins() 0 43 7
A checkEnabledPlugins() 0 9 3
A getDisabledPlugins() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like PluginManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use PluginManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * sysPass
4
 *
5
 * @author    nuxsmin
6
 * @link      https://syspass.org
7
 * @copyright 2012-2019, Rubén Domínguez nuxsmin@$syspass.org
8
 *
9
 * This file is part of sysPass.
10
 *
11
 * sysPass is free software: you can redistribute it and/or modify
12
 * it under the terms of the GNU General Public License as published by
13
 * the Free Software Foundation, either version 3 of the License, or
14
 * (at your option) any later version.
15
 *
16
 * sysPass is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU General Public License
22
 *  along with sysPass.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24
25
namespace SP\Plugin;
26
27
use Exception;
28
use ReflectionClass;
29
use SP\Bootstrap;
30
use SP\Core\Context\ContextInterface;
31
use SP\Core\Events\Event;
32
use SP\Core\Events\EventDispatcher;
33
use SP\Core\Events\EventMessage;
34
use SP\Core\Exceptions\ConstraintException;
35
use SP\Core\Exceptions\QueryException;
36
use SP\Core\Exceptions\SPException;
37
use SP\Repositories\NoSuchItemException;
38
use SP\Repositories\Plugin\PluginModel;
39
use SP\Services\Install\Installer;
40
use SP\Services\Plugin\PluginDataService;
41
use SP\Services\Plugin\PluginService;
42
use SP\Util\VersionUtil;
43
44
/**
45
 * Class PluginUtil
46
 *
47
 * @package SP\Plugin
48
 */
49
final class PluginManager
50
{
51
    /**
52
     * @var array
53
     */
54
    private static $pluginsAvailable;
55
    /**
56
     * @var array Plugins habilitados
57
     */
58
    private $enabledPlugins;
59
    /**
60
     * @var PluginInterface[] Plugins ya cargados
61
     */
62
    private $loadedPlugins = [];
63
    /**
64
     * @var array Plugins deshabilitados
65
     */
66
    private $disabledPlugins = [];
67
    /**
68
     * @var PluginService
69
     */
70
    private $pluginService;
71
    /**
72
     * @var ContextInterface
73
     */
74
    private $context;
75
    /**
76
     * @var EventDispatcher
77
     */
78
    private $eventDispatcher;
79
    /**
80
     * @var PluginDataService
81
     */
82
    private $pluginDataService;
83
84
    /**
85
     * PluginManager constructor.
86
     *
87
     * @param PluginService     $pluginService
88
     * @param PluginDataService $pluginDataService
89
     * @param ContextInterface  $context
90
     * @param EventDispatcher   $eventDispatcher
91
     */
92
    public function __construct(PluginService $pluginService,
93
                                PluginDataService $pluginDataService,
94
                                ContextInterface $context,
95
                                EventDispatcher $eventDispatcher)
96
    {
97
        $this->pluginService = $pluginService;
98
        $this->pluginDataService = $pluginDataService;
99
        $this->context = $context;
100
        $this->eventDispatcher = $eventDispatcher;
101
102
        self::$pluginsAvailable = self::getPlugins();
103
    }
104
105
    /**
106
     * Devuelve la lista de Plugins disponibles em el directorio
107
     *
108
     * @return array
109
     */
110
    public static function getPlugins()
111
    {
112
        $dir = dir(PLUGINS_PATH);
113
        $plugins = [];
114
115
        if ($dir) {
116
            while (false !== ($entry = $dir->read())) {
117
                $pluginDir = PLUGINS_PATH . DIRECTORY_SEPARATOR . $entry;
118
                $pluginFile = $pluginDir . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'Plugin.php';
119
120
                if (strpos($entry, '.') === false
121
                    && is_dir($pluginDir)
122
                    && file_exists($pluginFile)
123
                ) {
124
                    logger(sprintf('Plugin found: %s', $pluginDir));
125
126
                    $plugins[$entry] = require $pluginDir . DIRECTORY_SEPARATOR . 'base.php';
127
                }
128
            }
129
130
            $dir->close();
131
        }
132
133
        return $plugins;
134
    }
135
136
    /**
137
     * Obtener la información de un plugin
138
     *
139
     * @param string $name Nombre del plugin
140
     * @param bool   $initialize
141
     *
142
     * @return PluginInterface
143
     */
144
    public function getPlugin($name, bool $initialize = false)
145
    {
146
        if (isset(self::$pluginsAvailable[$name])) {
147
            $plugin = $this->loadPluginClass(
148
                $name,
149
                self::$pluginsAvailable[$name]['namespace']
150
            );
151
152
            if ($initialize) {
153
                $this->initPlugin($plugin);
154
                $plugin->onLoad();
155
            }
156
157
            return $plugin;
158
        }
159
160
        return null;
161
    }
162
163
    /**
164
     * Cargar un plugin
165
     *
166
     * @param string $name Nombre del plugin
167
     * @param string $namespace
168
     *
169
     * @return PluginInterface
170
     */
171
    private function loadPluginClass(string $name, string $namespace)
172
    {
173
        $pluginName = ucfirst($name);
174
175
        if (isset($this->loadedPlugins[$pluginName])) {
176
            return $this->loadedPlugins[$pluginName];
177
        }
178
179
        try {
180
            $class = $namespace . 'Plugin';
181
            $reflectionClass = new ReflectionClass($class);
182
183
            /** @var PluginInterface $plugin */
184
            $plugin = $reflectionClass->newInstance(
185
                Bootstrap::getContainer(),
186
                new PluginOperation($this->pluginDataService, $pluginName)
187
            );
188
189
            // Do not load plugin's data if not compatible.
190
            // Just return the plugin instance before disabling it
191
            if (self::checkCompatibility($plugin) === false) {
0 ignored issues
show
Bug Best Practice introduced by
The method SP\Plugin\PluginManager::checkCompatibility() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

191
            if (self::/** @scrutinizer ignore-call */ checkCompatibility($plugin) === false) {
Loading history...
192
                $this->eventDispatcher->notifyEvent('plugin.load.error',
193
                    new Event($this, EventMessage::factory()
194
                        ->addDescription(sprintf(__('Plugin version not compatible (%s)'), implode('.', $plugin->getVersion()))))
195
                );
196
197
                $this->disabledPlugins[] = $pluginName;
198
            }
199
200
            return $plugin;
201
        } catch (Exception $e) {
202
            processException($e);
203
204
            $this->eventDispatcher->notifyEvent('exception',
205
                new Event($e, EventMessage::factory()
206
                    ->addDescription(sprintf(__('Unable to load the "%s" plugin'), $pluginName))
207
                    ->addDescription($e->getMessage())
208
                    ->addDetail(__u('Plugin'), $pluginName))
209
            );
210
        }
211
212
        return null;
213
    }
214
215
    /**
216
     * @param PluginInterface $plugin
217
     *
218
     * @return bool
219
     * @throws ConstraintException
220
     * @throws NoSuchItemException
221
     * @throws QueryException
222
     */
223
    public function checkCompatibility(PluginInterface $plugin)
224
    {
225
        $pluginVersion = implode('.', $plugin->getCompatibleVersion());
226
        $appVersion = implode('.', array_slice(Installer::VERSION, 0, 2));
227
228
        if (version_compare($pluginVersion, $appVersion) === -1) {
229
            $this->pluginService->toggleEnabledByName($plugin->getName(), false);
230
231
            $this->eventDispatcher->notifyEvent('edit.plugin.disable',
232
                new Event($this, EventMessage::factory()
233
                    ->addDetail(__u('Plugin disabled'), $plugin->getName()))
234
            );
235
236
            return false;
237
        }
238
239
        return true;
240
    }
241
242
    /**
243
     * @param PluginInterface $plugin
244
     *
245
     * @return bool
246
     */
247
    private function initPlugin(PluginInterface $plugin)
248
    {
249
        try {
250
            $pluginModel = $this->pluginService->getByName($plugin->getName());
251
252
            if ($pluginModel->getEnabled() !== 1) {
253
                $this->disabledPlugins[] = $plugin->getName();
254
            }
255
256
            return true;
257
        } catch (Exception $e) {
258
            processException($e);
259
260
            $this->eventDispatcher->notifyEvent('exception',
261
                new Event($e, EventMessage::factory()
262
                    ->addDescription(sprintf(__('Unable to load the "%s" plugin'), $plugin->getName()))
263
                    ->addDescription($e->getMessage())
264
                    ->addDetail(__u('Plugin'), $plugin->getName()))
265
            );
266
        }
267
268
        return false;
269
    }
270
271
    /**
272
     * Loads the available and enabled plugins
273
     *
274
     * @throws ConstraintException
275
     * @throws QueryException
276
     * @throws SPException
277
     */
278
    public function loadPlugins()
279
    {
280
        $available = array_keys(self::$pluginsAvailable);
281
        $processed = [];
282
283
        // Process registered plugins in the database
284
        foreach ($this->pluginService->getAll() as $plugin) {
285
            $in = in_array($plugin->getName(), $available, true);
286
287
            if ($in === true) {
288
                if ($plugin->getEnabled() === 1) {
289
                    $this->load($plugin->getName());
290
                }
291
292
                if ($plugin->getAvailable() === 0) {
293
                    $this->pluginService->toggleAvailable($plugin->getId(), true);
294
295
                    $this->eventDispatcher->notifyEvent('edit.plugin.available',
296
                        new Event($this, EventMessage::factory()
297
                            ->addDetail(__u('Plugin available'), $plugin->getName()))
298
                    );
299
300
                    $this->load($plugin->getName());
301
                }
302
            } else {
303
                if ($plugin->getAvailable() === 1) {
304
                    $this->pluginService->toggleAvailable($plugin->getId(), false);
305
306
                    $this->eventDispatcher->notifyEvent('edit.plugin.unavailable',
307
                        new Event($this, EventMessage::factory()
308
                            ->addDetail(__u('Plugin unavailable'), $plugin->getName()))
309
                    );
310
                }
311
            }
312
313
            $processed[] = $plugin->getName();
314
        }
315
316
        // Search for available plugins and not registered in the database
317
        foreach (array_diff($available, $processed) as $plugin) {
318
            $this->registerPlugin($plugin);
319
320
            $this->load($plugin);
321
        }
322
    }
323
324
    /**
325
     * @param string $pluginName
326
     */
327
    private function load(string $pluginName)
328
    {
329
        $plugin = $this->loadPluginClass(
330
            $pluginName,
331
            self::$pluginsAvailable[$pluginName]['namespace']
332
        );
333
334
        if ($plugin !== null
335
            && $this->initPlugin($plugin)
336
        ) {
337
            logger(sprintf('Plugin loaded: %s', $pluginName));
338
339
            $this->eventDispatcher->notifyEvent('plugin.load',
340
                new Event($this, EventMessage::factory()
341
                    ->addDetail(__u('Plugin loaded'), $pluginName))
342
            );
343
344
            $this->loadedPlugins[$pluginName] = $plugin;
345
346
            $this->eventDispatcher->attach($plugin);
347
        }
348
    }
349
350
    /**
351
     * @param string $name
352
     *
353
     * @throws ConstraintException
354
     * @throws QueryException
355
     */
356
    private function registerPlugin(string $name)
357
    {
358
        $pluginData = new PluginModel();
359
        $pluginData->setName($name);
360
        $pluginData->setEnabled(false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $enabled of SP\Repositories\Plugin\PluginModel::setEnabled(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

360
        $pluginData->setEnabled(/** @scrutinizer ignore-type */ false);
Loading history...
361
362
        $this->pluginService->create($pluginData);
363
364
        $this->eventDispatcher->notifyEvent('create.plugin',
365
            new Event($this, EventMessage::factory()
366
                ->addDescription(__u('New Plugin'))
367
                ->addDetail(__u('Name'), $name)
368
            ));
369
370
        $this->disabledPlugins[] = $name;
371
    }
372
373
    /**
374
     * @param string $version
375
     */
376
    public function upgradePlugins(string $version)
377
    {
378
        $available = array_keys(self::$pluginsAvailable);
379
380
        foreach ($available as $pluginName) {
381
            $plugin = $this->loadPluginClass(
382
                $pluginName,
383
                self::$pluginsAvailable[$pluginName]['namespace']
384
            );
385
386
            try {
387
                $pluginModel = $this->pluginService->getByName($pluginName);
388
389
                if ($pluginModel->getVersionLevel() === null
390
                    || VersionUtil::checkVersion($pluginModel->getVersionLevel(), $version)
391
                ) {
392
                    $this->eventDispatcher->notifyEvent('upgrade.plugin.process',
393
                        new Event($this, EventMessage::factory()
394
                            ->addDescription(__u('Upgrading plugin'))
395
                            ->addDetail(__u('Name'), $pluginName))
396
                    );
397
398
                    $plugin->upgrade(
399
                        $version,
400
                        new PluginOperation($this->pluginDataService, $pluginName),
401
                        $pluginModel
402
                    );
403
404
                    $pluginModel->setData(null);
405
                    $pluginModel->setVersionLevel($version);
406
407
                    $this->pluginService->update($pluginModel);
408
409
                    $this->eventDispatcher->notifyEvent('upgrade.plugin.process',
410
                        new Event($this, EventMessage::factory()
411
                            ->addDescription(__u('Plugin upgraded'))
412
                            ->addDetail(__u('Name'), $pluginName))
413
                    );
414
                }
415
            } catch (Exception $e) {
416
                processException($e);
417
418
                $this->eventDispatcher->notifyEvent('exception',
419
                    new Event($e, EventMessage::factory()
420
                        ->addDescription(sprintf(__('Unable to upgrade the "%s" plugin'), $pluginName))
421
                        ->addDescription($e->getMessage())
422
                        ->addDetail(__u('Plugin'), $pluginName))
423
                );
424
            }
425
        }
426
    }
427
428
    /**
429
     * Comprobar disponibilidad de plugins habilitados
430
     *
431
     * @throws SPException
432
     */
433
    public function checkEnabledPlugins()
434
    {
435
        foreach ($this->getEnabledPlugins() as $plugin) {
436
            if (!in_array($plugin, $this->loadedPlugins)) {
437
                $this->pluginService->toggleAvailableByName($plugin, false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type integer expected by parameter $available of SP\Services\Plugin\Plugi...toggleAvailableByName(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

437
                $this->pluginService->toggleAvailableByName($plugin, /** @scrutinizer ignore-type */ false);
Loading history...
438
439
                $this->eventDispatcher->notifyEvent('edit.plugin.unavailable',
440
                    new Event($this, EventMessage::factory()
441
                        ->addDetail(__u('Plugin disabled'), $plugin->getName()))
442
                );
443
            }
444
        }
445
    }
446
447
    /**
448
     * Devolver los plugins habilitados
449
     *
450
     * @return array
451
     * @throws ConstraintException
452
     * @throws QueryException
453
     */
454
    public function getEnabledPlugins()
455
    {
456
        if ($this->enabledPlugins !== null) {
457
            return $this->enabledPlugins;
458
        }
459
460
        $this->enabledPlugins = [];
461
462
        foreach ($this->pluginService->getEnabled() as $plugin) {
463
            $this->enabledPlugins[] = $plugin->getName();
464
        }
465
466
        return $this->enabledPlugins;
467
    }
468
469
    /**
470
     * Devolver los plugins cargados
471
     *
472
     * @return PluginInterface[]
473
     */
474
    public function getLoadedPlugins()
475
    {
476
        return $this->loadedPlugins;
477
    }
478
479
    /**
480
     * Devolver los plugins deshabilidatos
481
     *
482
     * @return string[]
483
     */
484
    public function getDisabledPlugins()
485
    {
486
        return $this->disabledPlugins;
487
    }
488
}