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