Passed
Push — devel-3.0 ( 6b6d4a...0e3c8b )
by Rubén
03:12
created

PluginManager::loadPluginClass()   A

Complexity

Conditions 5
Paths 13

Size

Total Lines 49
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 27
nc 13
nop 2
dl 0
loc 49
rs 9.1768
c 0
b 0
f 0
1
<?php
2
/**
3
 * sysPass
4
 *
5
 * @author    nuxsmin
6
 * @link      https://syspass.org
7
 * @copyright 2012-2018, 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 ReflectionClass;
28
use SP\Bootstrap;
29
use SP\Core\Context\ContextInterface;
30
use SP\Core\Events\Event;
31
use SP\Core\Events\EventDispatcher;
32
use SP\Core\Events\EventMessage;
33
use SP\Core\Exceptions\ConstraintException;
34
use SP\Core\Exceptions\QueryException;
35
use SP\DataModel\PluginData;
36
use SP\Repositories\NoSuchItemException;
37
use SP\Services\Install\Installer;
38
use SP\Services\Plugin\PluginService;
39
40
/**
41
 * Class PluginUtil
42
 *
43
 * @package SP\Plugin
44
 */
45
final class PluginManager
46
{
47
    /**
48
     * @var array
49
     */
50
    private static $pluginsAvailable;
51
    /**
52
     * @var array Plugins habilitados
53
     */
54
    private $enabledPlugins;
55
    /**
56
     * @var PluginInterface[] Plugins ya cargados
57
     */
58
    private $loadedPlugins = [];
59
    /**
60
     * @var array Plugins deshabilitados
61
     */
62
    private $disabledPlugins = [];
63
    /**
64
     * @var PluginService
65
     */
66
    private $pluginService;
67
    /**
68
     * @var ContextInterface
69
     */
70
    private $context;
71
    /**
72
     * @var EventDispatcher
73
     */
74
    private $eventDispatcher;
75
76
    /**
77
     * PluginManager constructor.
78
     *
79
     * @param PluginService    $pluginService
80
     * @param ContextInterface $context
81
     * @param EventDispatcher  $eventDispatcher
82
     */
83
    public function __construct(PluginService $pluginService, ContextInterface $context, EventDispatcher $eventDispatcher)
84
    {
85
        $this->pluginService = $pluginService;
86
        $this->context = $context;
87
        $this->eventDispatcher = $eventDispatcher;
88
89
        self::$pluginsAvailable = self::getPlugins();
90
    }
91
92
    /**
93
     * Devuelve la lista de Plugins disponibles em el directorio
94
     *
95
     * @return array
96
     */
97
    public static function getPlugins()
98
    {
99
        $dir = dir(PLUGINS_PATH);
100
        $plugins = [];
101
102
        if ($dir) {
0 ignored issues
show
introduced by
$dir is of type Directory, thus it always evaluated to true.
Loading history...
103
            while (false !== ($entry = $dir->read())) {
104
                $pluginDir = PLUGINS_PATH . DIRECTORY_SEPARATOR . $entry;
105
                $pluginFile = $pluginDir . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'Plugin.php';
106
107
                if (strpos($entry, '.') === false
108
                    && is_dir($pluginDir)
109
                    && file_exists($pluginFile)
110
                ) {
111
                    logger(sprintf('Plugin found: %s', $pluginDir));
112
113
                    $plugins[$entry] = require $pluginDir . DIRECTORY_SEPARATOR . 'base.php';
114
                }
115
            }
116
117
            $dir->close();
118
        }
119
120
        return $plugins;
121
    }
122
123
    /**
124
     * Obtener la información de un plugin
125
     *
126
     * @param string $name Nombre del plugin
127
     *
128
     * @return PluginInterface
129
     */
130
    public function getPluginInfo($name)
131
    {
132
        if (isset(self::$pluginsAvailable[$name])) {
133
            return $this->loadPluginClass(
134
                $name,
135
                self::$pluginsAvailable[$name]['namespace']
136
            );
137
        }
138
139
        return null;
140
    }
141
142
    /**
143
     * Cargar un plugin
144
     *
145
     * @param string $name Nombre del plugin
146
     * @param string $namespace
147
     *
148
     * @return PluginInterface
149
     */
150
    public function loadPluginClass(string $name, string $namespace)
151
    {
152
        $name = ucfirst($name);
153
154
        if (isset($this->loadedPlugins[$name])) {
155
            return $this->loadedPlugins[$name];
156
        }
157
158
        try {
159
            $class = $namespace . 'Plugin';
160
            $reflectionClass = new ReflectionClass($class);
161
162
            /** @var PluginInterface $plugin */
163
            $plugin = $reflectionClass->newInstance(Bootstrap::getContainer());
164
165
            // Do not load plugin's data if not compatible.
166
            // Just return the plugin instance before disabling it
167
            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

167
            if (self::/** @scrutinizer ignore-call */ checkCompatibility($plugin) === false) {
Loading history...
168
                $this->eventDispatcher->notifyEvent('plugin.load.error',
169
                    new Event($this, EventMessage::factory()
170
                        ->addDescription(sprintf(__('Versión de plugin no compatible (%s)'), implode('.', $plugin->getVersion()))))
171
                );
172
173
                $this->disabledPlugins[] = $name;
174
175
                return $plugin;
176
            }
177
178
            $pluginData = $this->pluginService->getByName($name);
179
180
            if ($pluginData->getEnabled() === 1) {
181
                $plugin->onLoadData($pluginData);
182
183
                return $plugin;
184
            } else {
185
                $this->disabledPlugins[] = $name;
186
            }
187
        } catch (\Exception $e) {
188
            processException($e);
189
190
            $this->eventDispatcher->notifyEvent('exception',
191
                new Event($e, EventMessage::factory()
192
                    ->addDescription(sprintf(__('No es posible cargar el plugin "%s"'), $name))
193
                    ->addDescription($e->getMessage())
194
                    ->addDetail(__u('Plugin'), $name))
195
            );
196
        }
197
198
        return null;
199
    }
200
201
    /**
202
     * @param PluginInterface $plugin
203
     *
204
     * @return bool
205
     * @throws ConstraintException
206
     * @throws NoSuchItemException
207
     * @throws QueryException
208
     */
209
    public function checkCompatibility(PluginInterface $plugin)
210
    {
211
        $pluginVersion = implode('.', $plugin->getCompatibleVersion());
212
        $appVersion = implode('.', array_slice(Installer::VERSION, 0, 2));
213
214
        if (version_compare($pluginVersion, $appVersion) === -1) {
215
            $this->pluginService->toggleEnabledByName($plugin->getName(), false);
216
217
            $this->eventDispatcher->notifyEvent('edit.plugin.disable',
218
                new Event($this, EventMessage::factory()
219
                    ->addDetail(__u('Plugin deshabilitado'), $plugin->getName()))
220
            );
221
222
            return false;
223
        }
224
225
        return true;
226
    }
227
228
    /**
229
     * Loads the available and enabled plugins
230
     *
231
     * @throws ConstraintException
232
     * @throws QueryException
233
     * @throws \SP\Core\Exceptions\SPException
234
     */
235
    public function loadPlugins()
236
    {
237
        $available = array_keys(self::$pluginsAvailable);
238
        $processed = [];
239
240
        // Process registered plugins in the database
241
        foreach ($this->pluginService->getAll() as $plugin) {
242
            $in = in_array($plugin->getName(), $available, true);
243
244
            if ($in === true) {
245
                if ($plugin->getEnabled() === 1) {
246
                    $this->load($plugin->getName());
247
                }
248
249
                if ($plugin->getAvailable() === 0) {
250
                    $this->pluginService->toggleAvailable($plugin->getId(), true);
251
252
                    $this->eventDispatcher->notifyEvent('edit.plugin.available',
253
                        new Event($this, EventMessage::factory()
254
                            ->addDetail(__u('Plugin disponible'), $plugin->getName()))
255
                    );
256
257
                    $this->load($plugin->getName());
258
                }
259
            } else {
260
                $this->pluginService->toggleAvailable($plugin->getId(), false);
261
262
                $this->eventDispatcher->notifyEvent('edit.plugin.unavailable',
263
                    new Event($this, EventMessage::factory()
264
                        ->addDetail(__u('Plugin no disponible'), $plugin->getName()))
265
                );
266
            }
267
268
            $processed[] = $plugin->getName();
269
        }
270
271
        // Search for available plugins and not registered in the database
272
        foreach (array_diff($available, $processed) as $plugin) {
273
            $this->registerPlugin($plugin);
274
275
            $this->load($plugin);
276
        }
277
    }
278
279
    /**
280
     * @param string $pluginName
281
     */
282
    private function load(string $pluginName)
283
    {
284
        $plugin = $this->loadPluginClass(
285
            $pluginName,
286
            self::$pluginsAvailable[$pluginName]['namespace']
287
        );
288
289
        if ($plugin !== null) {
290
            logger(sprintf('Plugin loaded: %s', $pluginName));
291
292
            $this->eventDispatcher->notifyEvent('plugin.load',
293
                new Event($this, EventMessage::factory()
294
                    ->addDetail(__u('Plugin cargado'), $pluginName))
295
            );
296
297
            $this->loadedPlugins[$pluginName] = $plugin;
298
299
            $this->eventDispatcher->attach($plugin);
300
        }
301
    }
302
303
    /**
304
     * @param string $name
305
     *
306
     * @throws ConstraintException
307
     * @throws QueryException
308
     */
309
    private function registerPlugin(string $name)
310
    {
311
        $pluginData = new PluginData();
312
        $pluginData->setName($name);
313
        $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\DataModel\PluginData::setEnabled(). ( Ignorable by Annotation )

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

313
        $pluginData->setEnabled(/** @scrutinizer ignore-type */ false);
Loading history...
314
315
        $this->pluginService->create($pluginData);
316
317
        $this->eventDispatcher->notifyEvent('create.plugin',
318
            new Event($this, EventMessage::factory()
319
                ->addDescription(__u('Nuevo Plugin'))
320
                ->addDetail(__u('Nombre'), $name)
321
            ));
322
323
        $this->disabledPlugins[] = $name;
324
    }
325
326
    /**
327
     * Comprobar disponibilidad de plugins habilitados
328
     *
329
     * @throws \SP\Core\Exceptions\SPException
330
     */
331
    public function checkEnabledPlugins()
332
    {
333
        foreach ($this->getEnabledPlugins() as $plugin) {
334
            if (!in_array($plugin, $this->loadedPlugins)) {
335
                $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

335
                $this->pluginService->toggleAvailableByName($plugin, /** @scrutinizer ignore-type */ false);
Loading history...
336
337
                $this->eventDispatcher->notifyEvent('edit.plugin.unavailable',
338
                    new Event($this, EventMessage::factory()
339
                        ->addDetail(__u('Plugin deshabilitado'), $plugin->getName()))
340
                );
341
            }
342
        }
343
    }
344
345
    /**
346
     * Devolver los plugins habilitados
347
     *
348
     * @return array
349
     * @throws \SP\Core\Exceptions\ConstraintException
350
     * @throws \SP\Core\Exceptions\QueryException
351
     */
352
    public function getEnabledPlugins()
353
    {
354
        if ($this->enabledPlugins !== null) {
355
            return $this->enabledPlugins;
356
        }
357
358
        $this->enabledPlugins = [];
359
360
        foreach ($this->pluginService->getEnabled() as $plugin) {
361
            $this->enabledPlugins[] = $plugin->getName();
362
        }
363
364
        return $this->enabledPlugins;
365
    }
366
367
    /**
368
     * Devolver los plugins cargados
369
     *
370
     * @return PluginInterface[]
371
     */
372
    public function getLoadedPlugins()
373
    {
374
        return $this->loadedPlugins;
375
    }
376
377
    /**
378
     * Devolver los plugins deshabilidatos
379
     *
380
     * @return string[]
381
     */
382
    public function getDisabledPlugins()
383
    {
384
        return $this->disabledPlugins;
385
    }
386
}