Failed Conditions
Pull Request — master (#3203)
by Andreas
08:35 queued 05:38
created

inc/Extension/PluginController.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace dokuwiki\Extension;
4
5
use dokuwiki\ErrorHandler;
6
7
/**
8
 * Class to encapsulate access to dokuwiki plugins
9
 *
10
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
11
 * @author     Christopher Smith <[email protected]>
12
 */
13
class PluginController
14
{
15
    /** @var array the types of plugins DokuWiki supports */
16
    const PLUGIN_TYPES = ['auth', 'admin', 'syntax', 'action', 'renderer', 'helper', 'remote', 'cli'];
17
18
    protected $listByType = [];
19
    /** @var array all installed plugins and their enabled state [plugin=>enabled] */
20
    protected $masterList = [];
21
    protected $pluginCascade = ['default' => [], 'local' => [], 'protected' => []];
22
    protected $lastLocalConfigFile = '';
23
24
    /**
25
     * Populates the master list of plugins
26
     */
27
    public function __construct()
28
    {
29
        $this->loadConfig();
30
        $this->populateMasterList();
31
    }
32
33
    /**
34
     * Returns a list of available plugins of given type
35
     *
36
     * @param $type  string, plugin_type name;
37
     *               the type of plugin to return,
38
     *               use empty string for all types
39
     * @param $all   bool;
40
     *               false to only return enabled plugins,
41
     *               true to return both enabled and disabled plugins
42
     *
43
     * @return       array of
44
     *                  - plugin names when $type = ''
45
     *                  - or plugin component names when a $type is given
46
     *
47
     * @author Andreas Gohr <[email protected]>
48
     */
49
    public function getList($type = '', $all = false)
50
    {
51
52
        // request the complete list
53
        if (!$type) {
54
            return $all ? array_keys($this->masterList) : array_keys(array_filter($this->masterList));
55
        }
56
57
        if (!isset($this->listByType[$type]['enabled'])) {
58
            $this->listByType[$type]['enabled'] = $this->getListByType($type, true);
59
        }
60
        if ($all && !isset($this->listByType[$type]['disabled'])) {
61
            $this->listByType[$type]['disabled'] = $this->getListByType($type, false);
62
        }
63
64
        return $all
65
            ? array_merge($this->listByType[$type]['enabled'], $this->listByType[$type]['disabled'])
66
            : $this->listByType[$type]['enabled'];
67
    }
68
69
    /**
70
     * Loads the given plugin and creates an object of it
71
     *
72
     * @param  $type     string type of plugin to load
73
     * @param  $name     string name of the plugin to load
74
     * @param  $new      bool   true to return a new instance of the plugin, false to use an already loaded instance
75
     * @param  $disabled bool   true to load even disabled plugins
76
     * @return PluginInterface|null  the plugin object or null on failure
77
     * @author Andreas Gohr <[email protected]>
78
     *
79
     */
80
    public function load($type, $name, $new = false, $disabled = false)
81
    {
82
83
        //we keep all loaded plugins available in global scope for reuse
84
        global $DOKU_PLUGINS;
85
86
        list($plugin, /* $component */) = $this->splitName($name);
87
88
        // check if disabled
89
        if (!$disabled && !$this->isEnabled($plugin)) {
90
            return null;
91
        }
92
93
        $class = $type . '_plugin_' . $name;
94
95
        try {
96
            //plugin already loaded?
97
            if (!empty($DOKU_PLUGINS[$type][$name])) {
98
                if ($new || !$DOKU_PLUGINS[$type][$name]->isSingleton()) {
99
100
                    return class_exists($class, true) ? new $class : null;
101
                }
102
103
                return $DOKU_PLUGINS[$type][$name];
104
            }
105
106
            //construct class and instantiate
107
            if (!class_exists($class, true)) {
108
                # the plugin might be in the wrong directory
109
                $inf = confToHash(DOKU_PLUGIN . "$plugin/plugin.info.txt");
110
                if ($inf['base'] && $inf['base'] != $plugin) {
111
                    msg(
112
                        sprintf(
113
                            "Plugin installed incorrectly. Rename plugin directory '%s' to '%s'.",
114
                            hsc($plugin),
115
                            hsc(
116
                                $inf['base']
117
                            )
118
                        ), -1
119
                    );
120
                } elseif (preg_match('/^' . DOKU_PLUGIN_NAME_REGEX . '$/', $plugin) !== 1) {
121
                    msg(
122
                        sprintf(
123
                            "Plugin name '%s' is not a valid plugin name, only the characters a-z ".
124
                            "and 0-9 are allowed. " .
125
                            'Maybe the plugin has been installed in the wrong directory?', hsc($plugin)
126
                        ), -1
127
                    );
128
                }
129
                return null;
130
            }
131
            $DOKU_PLUGINS[$type][$name] = new $class;
132
133
        } catch (\Throwable $e) {
0 ignored issues
show
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
134
            ErrorHandler::showExceptionMsg($e, sprintf('Failed to load plugin %s', $plugin));
135
            return null;
136
        }
137
138
        return $DOKU_PLUGINS[$type][$name];
139
    }
140
141
    /**
142
     * Whether plugin is disabled
143
     *
144
     * @param string $plugin name of plugin
145
     * @return bool  true disabled, false enabled
146
     * @deprecated in favor of the more sensible isEnabled where the return value matches the enabled state
147
     */
148
    public function isDisabled($plugin)
149
    {
150
        dbg_deprecated('isEnabled()');
151
        return !$this->isEnabled($plugin);
152
    }
153
154
    /**
155
     * Check whether plugin is disabled
156
     *
157
     * @param string $plugin name of plugin
158
     * @return bool  true enabled, false disabled
159
     */
160
    public function isEnabled($plugin)
161
    {
162
        return !empty($this->masterList[$plugin]);
163
    }
164
165
    /**
166
     * Disable the plugin
167
     *
168
     * @param string $plugin name of plugin
169
     * @return bool  true saving succeed, false saving failed
170
     */
171
    public function disable($plugin)
172
    {
173
        if (array_key_exists($plugin, $this->pluginCascade['protected'])) return false;
174
        $this->masterList[$plugin] = 0;
175
        return $this->saveList();
176
    }
177
178
    /**
179
     * Enable the plugin
180
     *
181
     * @param string $plugin name of plugin
182
     * @return bool  true saving succeed, false saving failed
183
     */
184
    public function enable($plugin)
185
    {
186
        if (array_key_exists($plugin, $this->pluginCascade['protected'])) return false;
187
        $this->masterList[$plugin] = 1;
188
        return $this->saveList();
189
    }
190
191
    /**
192
     * Returns cascade of the config files
193
     *
194
     * @return array with arrays of plugin configs
195
     */
196
    public function getCascade()
197
    {
198
        return $this->pluginCascade;
199
    }
200
201
    /**
202
     * Read all installed plugins and their current enabled state
203
     */
204
    protected function populateMasterList()
205
    {
206
        if ($dh = @opendir(DOKU_PLUGIN)) {
207
            $all_plugins = array();
208
            while (false !== ($plugin = readdir($dh))) {
209
                if ($plugin[0] === '.') continue;               // skip hidden entries
210
                if (is_file(DOKU_PLUGIN . $plugin)) continue;    // skip files, we're only interested in directories
211
212
                if (array_key_exists($plugin, $this->masterList) && $this->masterList[$plugin] == 0) {
213
                    $all_plugins[$plugin] = 0;
214
215
                } elseif (array_key_exists($plugin, $this->masterList) && $this->masterList[$plugin] == 1) {
216
                    $all_plugins[$plugin] = 1;
217
                } else {
218
                    $all_plugins[$plugin] = 1;
219
                }
220
            }
221
            $this->masterList = $all_plugins;
222
            if (!file_exists($this->lastLocalConfigFile)) {
223
                $this->saveList(true);
224
            }
225
        }
226
    }
227
228
    /**
229
     * Includes the plugin config $files
230
     * and returns the entries of the $plugins array set in these files
231
     *
232
     * @param array $files list of files to include, latter overrides previous
233
     * @return array with entries of the $plugins arrays of the included files
234
     */
235
    protected function checkRequire($files)
236
    {
237
        $plugins = array();
238
        foreach ($files as $file) {
239
            if (file_exists($file)) {
240
                include_once($file);
241
            }
242
        }
243
        return $plugins;
244
    }
245
246
    /**
247
     * Save the current list of plugins
248
     *
249
     * @param bool $forceSave ;
250
     *              false to save only when config changed
251
     *              true to always save
252
     * @return bool  true saving succeed, false saving failed
253
     */
254
    protected function saveList($forceSave = false)
255
    {
256
        global $conf;
257
258
        if (empty($this->masterList)) return false;
259
260
        // Rebuild list of local settings
261
        $local_plugins = $this->rebuildLocal();
262
        if ($local_plugins != $this->pluginCascade['local'] || $forceSave) {
263
            $file = $this->lastLocalConfigFile;
264
            $out = "<?php\n/*\n * Local plugin enable/disable settings\n" .
265
                " * Auto-generated through plugin/extension manager\n *\n" .
266
                " * NOTE: Plugins will not be added to this file unless there " .
267
                "is a need to override a default setting. Plugins are\n" .
268
                " *       enabled by default.\n */\n";
269
            foreach ($local_plugins as $plugin => $value) {
270
                $out .= "\$plugins['$plugin'] = $value;\n";
271
            }
272
            // backup current file (remove any existing backup)
273
            if (file_exists($file)) {
274
                $backup = $file . '.bak';
275
                if (file_exists($backup)) @unlink($backup);
276
                if (!@copy($file, $backup)) return false;
277
                if (!empty($conf['fperm'])) chmod($backup, $conf['fperm']);
278
            }
279
            //check if can open for writing, else restore
280
            return io_saveFile($file, $out);
281
        }
282
        return false;
283
    }
284
285
    /**
286
     * Rebuild the set of local plugins
287
     *
288
     * @return array array of plugins to be saved in end($config_cascade['plugins']['local'])
289
     */
290
    protected function rebuildLocal()
291
    {
292
        //assign to local variable to avoid overwriting
293
        $backup = $this->masterList;
294
        //Can't do anything about protected one so rule them out completely
295
        $local_default = array_diff_key($backup, $this->pluginCascade['protected']);
296
        //Diff between local+default and default
297
        //gives us the ones we need to check and save
298
        $diffed_ones = array_diff_key($local_default, $this->pluginCascade['default']);
299
        //The ones which we are sure of (list of 0s not in default)
300
        $sure_plugins = array_filter($diffed_ones, array($this, 'negate'));
301
        //the ones in need of diff
302
        $conflicts = array_diff_key($local_default, $diffed_ones);
303
        //The final list
304
        return array_merge($sure_plugins, array_diff_assoc($conflicts, $this->pluginCascade['default']));
305
    }
306
307
    /**
308
     * Build the list of plugins and cascade
309
     *
310
     */
311
    protected function loadConfig()
312
    {
313
        global $config_cascade;
314
        foreach (array('default', 'protected') as $type) {
315
            if (array_key_exists($type, $config_cascade['plugins'])) {
316
                $this->pluginCascade[$type] = $this->checkRequire($config_cascade['plugins'][$type]);
317
            }
318
        }
319
        $local = $config_cascade['plugins']['local'];
320
        $this->lastLocalConfigFile = array_pop($local);
321
        $this->pluginCascade['local'] = $this->checkRequire(array($this->lastLocalConfigFile));
322
        if (is_array($local)) {
323
            $this->pluginCascade['default'] = array_merge(
324
                $this->pluginCascade['default'],
325
                $this->checkRequire($local)
326
            );
327
        }
328
        $this->masterList = array_merge(
329
            $this->pluginCascade['default'],
330
            $this->pluginCascade['local'],
331
            $this->pluginCascade['protected']
332
        );
333
    }
334
335
    /**
336
     * Returns a list of available plugin components of given type
337
     *
338
     * @param string $type plugin_type name; the type of plugin to return,
339
     * @param bool $enabled true to return enabled plugins,
340
     *                          false to return disabled plugins
341
     * @return array of plugin components of requested type
342
     */
343
    protected function getListByType($type, $enabled)
344
    {
345
        $master_list = $enabled
346
            ? array_keys(array_filter($this->masterList))
347
            : array_keys(array_filter($this->masterList, array($this, 'negate')));
348
        $plugins = array();
349
350
        foreach ($master_list as $plugin) {
351
352
            if (file_exists(DOKU_PLUGIN . "$plugin/$type.php")) {
353
                $plugins[] = $plugin;
354
                continue;
355
            }
356
357
            $typedir = DOKU_PLUGIN . "$plugin/$type/";
358
            if (is_dir($typedir)) {
359
                if ($dp = opendir($typedir)) {
360
                    while (false !== ($component = readdir($dp))) {
361
                        if (strpos($component, '.') === 0 || strtolower(substr($component, -4)) !== '.php') continue;
362
                        if (is_file($typedir . $component)) {
363
                            $plugins[] = $plugin . '_' . substr($component, 0, -4);
364
                        }
365
                    }
366
                    closedir($dp);
367
                }
368
            }
369
370
        }//foreach
371
372
        return $plugins;
373
    }
374
375
    /**
376
     * Split name in a plugin name and a component name
377
     *
378
     * @param string $name
379
     * @return array with
380
     *              - plugin name
381
     *              - and component name when available, otherwise empty string
382
     */
383
    protected function splitName($name)
384
    {
385
        if (!isset($this->masterList[$name])) {
386
            return explode('_', $name, 2);
387
        }
388
389
        return array($name, '');
390
    }
391
392
    /**
393
     * Returns inverse boolean value of the input
394
     *
395
     * @param mixed $input
396
     * @return bool inversed boolean value of input
397
     */
398
    protected function negate($input)
399
    {
400
        return !(bool)$input;
401
    }
402
}
403