Completed
Push — psr2-plugin ( 6f343c...628d18 )
by Andreas
07:52 queued 04:08
created

PluginController::getInstance()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 2
nop 1
dl 0
loc 7
rs 10
c 0
b 0
f 0
1
<?php
2
namespace dokuwiki\Extension;
3
4
/**
5
 * Class to encapsulate access to dokuwiki plugins
6
 *
7
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
8
 * @author     Christopher Smith <[email protected]>
9
 */
10
class PluginController
11
{
12
    /** @var PluginController */
13
    protected static $instance;
14
15
    /** The different types of plugins DokuWiki supports */
16
    const PLUGIN_TYPES = array('auth', 'admin','syntax','action','renderer', 'helper','remote');
17
18
    protected $list_bytype = array();
19
    protected $tmp_plugins = array();
20
    protected $plugin_cascade = array('default' => array(), 'local' => array(), 'protected' => array());
21
    protected $last_local_config_file = '';
22
23
    /**
24
     * Populates the master list of plugins
25
     * @param bool $usedGetInstance temporary to find deprecated uses
26
     * @deprecated 2018-06-16 This constructor will be made private
27
     */
28
    public function __construct($usedGetInstance=false)
29
    {
30
        if(!$usedGetInstance) {
31
            dbg_deprecated('\dokuwiki\Extension\PluginController::getInstance');
32
        }
33
        $this->loadConfig();
34
        $this->_populateMasterList();
35
    }
36
37
    /**
38
     * Get the singleton instance of the Plugin Controller
39
     *
40
     * @param bool $init force a reload of the controller
41
     * @return PluginController
42
     */
43
    public static function getInstance($init=false) {
44
        if(self::$instance === null || $init) {
45
            self::$instance = new PluginController(true);
46
        }
47
48
        return self::$instance;
49
    }
50
51
    /**
52
     * Returns a list of available plugins of given type
53
     *
54
     * @param $type  string, plugin_type name;
55
     *               the type of plugin to return,
56
     *               use empty string for all types
57
     * @param $all   bool;
58
     *               false to only return enabled plugins,
59
     *               true to return both enabled and disabled plugins
60
     *
61
     * @return       array of
62
     *                  - plugin names when $type = ''
63
     *                  - or plugin component names when a $type is given
64
     *
65
     * @author Andreas Gohr <[email protected]>
66
     */
67
    public function getList($type = '', $all = false)
68
    {
69
70
        // request the complete list
71
        if (!$type) {
72
            return $all ? array_keys($this->tmp_plugins) : array_keys(array_filter($this->tmp_plugins));
73
        }
74
75
        if (!isset($this->list_bytype[$type]['enabled'])) {
76
            $this->list_bytype[$type]['enabled'] = $this->_getListByType($type, true);
77
        }
78
        if ($all && !isset($this->list_bytype[$type]['disabled'])) {
79
            $this->list_bytype[$type]['disabled'] = $this->_getListByType($type, false);
80
        }
81
82
        return $all
83
            ? array_merge($this->list_bytype[$type]['enabled'], $this->list_bytype[$type]['disabled'])
84
            : $this->list_bytype[$type]['enabled'];
85
    }
86
87
    /**
88
     * Loads the given plugin and creates an object of it
89
     *
90
     * @author Andreas Gohr <[email protected]>
91
     *
92
     * @param  $type     string type of plugin to load
93
     * @param  $name     string name of the plugin to load
94
     * @param  $new      bool   true to return a new instance of the plugin, false to use an already loaded instance
95
     * @param  $disabled bool   true to load even disabled plugins
96
     * @return PluginInterface|null  the plugin object or null on failure
97
     */
98
    public function load($type, $name, $new = false, $disabled = false)
99
    {
100
101
        //we keep all loaded plugins available in global scope for reuse
102
        global $DOKU_PLUGINS;
103
104
        list($plugin, /* $component */) = $this->_splitName($name);
105
106
        // check if disabled
107
        if (!$disabled && !$this->isEnabled($plugin)) {
108
            return null;
109
        }
110
111
        $class = $type . '_plugin_' . $name;
112
113
        //plugin already loaded?
114
        if (!empty($DOKU_PLUGINS[$type][$name])) {
115
            if ($new || !$DOKU_PLUGINS[$type][$name]->isSingleton()) {
116
                return class_exists($class, true) ? new $class : null;
117
            } else {
118
                return $DOKU_PLUGINS[$type][$name];
119
            }
120
        }
121
122
        //construct class and instantiate
123
        if (!class_exists($class, true)) {
124
125
            # the plugin might be in the wrong directory
126
            $inf = confToHash(DOKU_PLUGIN . "$plugin/plugin.info.txt");
127
            if ($inf['base'] && $inf['base'] != $plugin) {
128
                msg(
129
                    sprintf(
130
                        "Plugin installed incorrectly. Rename plugin directory '%s' to '%s'.",
131
                        hsc($plugin),
132
                        hsc(
133
                            $inf['base']
134
                        )
135
                    ), -1
136
                );
137
            } elseif (preg_match('/^' . DOKU_PLUGIN_NAME_REGEX . '$/', $plugin) !== 1) {
138
                msg(
139
                    sprintf(
140
                        "Plugin name '%s' is not a valid plugin name, only the characters a-z and 0-9 are allowed. " .
141
                        'Maybe the plugin has been installed in the wrong directory?', hsc($plugin)
142
                    ), -1
143
                );
144
            }
145
            return null;
146
        }
147
148
        $DOKU_PLUGINS[$type][$name] = new $class;
149
        return $DOKU_PLUGINS[$type][$name];
150
    }
151
152
    /**
153
     * Whether plugin is disabled
154
     *
155
     * @param string $plugin name of plugin
156
     * @return bool  true disabled, false enabled
157
     * @deprecated in favor of the more sensible isEnabled where the return value matches the enabled state
158
     */
159
    public function isDisabled($plugin)
160
    {
161
        dbg_deprecated('isEnabled()');
162
        return !$this->isEnabled($plugin);
163
    }
164
165
    /**
166
     * Check whether plugin is disabled
167
     *
168
     * @param string $plugin name of plugin
169
     * @return bool  true enabled, false disabled
170
     */
171
    public function isEnabled($plugin)
172
    {
173
        return !empty($this->tmp_plugins[$plugin]);
174
    }
175
176
    /**
177
     * Disable the plugin
178
     *
179
     * @param string $plugin name of plugin
180
     * @return bool  true saving succeed, false saving failed
181
     */
182
    public function disable($plugin)
183
    {
184
        if (array_key_exists($plugin, $this->plugin_cascade['protected'])) return false;
185
        $this->tmp_plugins[$plugin] = 0;
186
        return $this->saveList();
187
    }
188
189
    /**
190
     * Enable the plugin
191
     *
192
     * @param string $plugin name of plugin
193
     * @return bool  true saving succeed, false saving failed
194
     */
195
    public function enable($plugin)
196
    {
197
        if (array_key_exists($plugin, $this->plugin_cascade['protected'])) return false;
198
        $this->tmp_plugins[$plugin] = 1;
199
        return $this->saveList();
200
    }
201
202
    /**
203
     * Returns cascade of the config files
204
     *
205
     * @return array with arrays of plugin configs
206
     */
207
    public function getCascade()
208
    {
209
        return $this->plugin_cascade;
210
    }
211
212
    protected function _populateMasterList()
213
    {
214
        global $conf;
215
216
        if ($dh = @opendir(DOKU_PLUGIN)) {
217
            $all_plugins = array();
218
            while (false !== ($plugin = readdir($dh))) {
219
                if ($plugin[0] == '.') continue;               // skip hidden entries
220
                if (is_file(DOKU_PLUGIN . $plugin)) continue;    // skip files, we're only interested in directories
221
222
                if (array_key_exists($plugin, $this->tmp_plugins) && $this->tmp_plugins[$plugin] == 0) {
223
                    $all_plugins[$plugin] = 0;
224
225
                } elseif ((array_key_exists($plugin, $this->tmp_plugins) && $this->tmp_plugins[$plugin] == 1)) {
226
                    $all_plugins[$plugin] = 1;
227
                } else {
228
                    $all_plugins[$plugin] = 1;
229
                }
230
            }
231
            $this->tmp_plugins = $all_plugins;
232
            if (!file_exists($this->last_local_config_file)) {
233
                $this->saveList(true);
234
            }
235
        }
236
    }
237
238
    /**
239
     * Includes the plugin config $files
240
     * and returns the entries of the $plugins array set in these files
241
     *
242
     * @param array $files list of files to include, latter overrides previous
243
     * @return array with entries of the $plugins arrays of the included files
244
     */
245
    protected function checkRequire($files)
246
    {
247
        $plugins = array();
248
        foreach ($files as $file) {
249
            if (file_exists($file)) {
250
                include_once($file);
251
            }
252
        }
253
        return $plugins;
254
    }
255
256
    /**
257
     * Save the current list of plugins
258
     *
259
     * @param bool $forceSave ;
260
     *              false to save only when config changed
261
     *              true to always save
262
     * @return bool  true saving succeed, false saving failed
263
     */
264
    protected function saveList($forceSave = false)
265
    {
266
        global $conf;
267
268
        if (empty($this->tmp_plugins)) return false;
269
270
        // Rebuild list of local settings
271
        $local_plugins = $this->rebuildLocal();
272
        if ($local_plugins != $this->plugin_cascade['local'] || $forceSave) {
273
            $file = $this->last_local_config_file;
274
            $out = "<?php\n/*\n * Local plugin enable/disable settings\n" .
275
                " * Auto-generated through plugin/extension manager\n *\n" .
276
                " * NOTE: Plugins will not be added to this file unless there " .
277
                "is a need to override a default setting. Plugins are\n" .
278
                " *       enabled by default.\n */\n";
279
            foreach ($local_plugins as $plugin => $value) {
280
                $out .= "\$plugins['$plugin'] = $value;\n";
281
            }
282
            // backup current file (remove any existing backup)
283
            if (file_exists($file)) {
284
                $backup = $file . '.bak';
285
                if (file_exists($backup)) @unlink($backup);
286
                if (!@copy($file, $backup)) return false;
287
                if (!empty($conf['fperm'])) chmod($backup, $conf['fperm']);
288
            }
289
            //check if can open for writing, else restore
290
            return io_saveFile($file, $out);
291
        }
292
        return false;
293
    }
294
295
    /**
296
     * Rebuild the set of local plugins
297
     *
298
     * @return array array of plugins to be saved in end($config_cascade['plugins']['local'])
299
     */
300
    protected function rebuildLocal()
301
    {
302
        //assign to local variable to avoid overwriting
303
        $backup = $this->tmp_plugins;
304
        //Can't do anything about protected one so rule them out completely
305
        $local_default = array_diff_key($backup, $this->plugin_cascade['protected']);
306
        //Diff between local+default and default
307
        //gives us the ones we need to check and save
308
        $diffed_ones = array_diff_key($local_default, $this->plugin_cascade['default']);
309
        //The ones which we are sure of (list of 0s not in default)
310
        $sure_plugins = array_filter($diffed_ones, array($this, 'negate'));
311
        //the ones in need of diff
312
        $conflicts = array_diff_key($local_default, $diffed_ones);
313
        //The final list
314
        return array_merge($sure_plugins, array_diff_assoc($conflicts, $this->plugin_cascade['default']));
315
    }
316
317
    /**
318
     * Build the list of plugins and cascade
319
     *
320
     */
321
    protected function loadConfig()
322
    {
323
        global $config_cascade;
324
        foreach (array('default', 'protected') as $type) {
325
            if (array_key_exists($type, $config_cascade['plugins'])) {
326
                $this->plugin_cascade[$type] = $this->checkRequire($config_cascade['plugins'][$type]);
327
            }
328
        }
329
        $local = $config_cascade['plugins']['local'];
330
        $this->last_local_config_file = array_pop($local);
331
        $this->plugin_cascade['local'] = $this->checkRequire(array($this->last_local_config_file));
332
        if (is_array($local)) {
333
            $this->plugin_cascade['default'] = array_merge(
334
                $this->plugin_cascade['default'],
335
                $this->checkRequire($local)
336
            );
337
        }
338
        $this->tmp_plugins = array_merge(
339
            $this->plugin_cascade['default'],
340
            $this->plugin_cascade['local'],
341
            $this->plugin_cascade['protected']
342
        );
343
    }
344
345
    /**
346
     * Returns a list of available plugin components of given type
347
     *
348
     * @param string $type plugin_type name; the type of plugin to return,
349
     * @param bool $enabled true to return enabled plugins,
350
     *                          false to return disabled plugins
351
     * @return array of plugin components of requested type
352
     */
353
    protected function _getListByType($type, $enabled)
354
    {
355
        $master_list = $enabled
356
            ? array_keys(array_filter($this->tmp_plugins))
357
            : array_keys(array_filter($this->tmp_plugins, array($this, 'negate')));
358
        $plugins = array();
359
360
        foreach ($master_list as $plugin) {
361
362
            if (file_exists(DOKU_PLUGIN . "$plugin/$type.php")) {
363
                $plugins[] = $plugin;
364
                continue;
365
            }
366
367
            $typedir = DOKU_PLUGIN . "$plugin/$type/";
368
            if (is_dir($typedir)) {
369
                if ($dp = opendir($typedir)) {
370
                    while (false !== ($component = readdir($dp))) {
371
                        if (substr($component, 0, 1) == '.' || strtolower(substr($component, -4)) != ".php") continue;
372
                        if (is_file($typedir . $component)) {
373
                            $plugins[] = $plugin . '_' . substr($component, 0, -4);
374
                        }
375
                    }
376
                    closedir($dp);
377
                }
378
            }
379
380
        }//foreach
381
382
        return $plugins;
383
    }
384
385
    /**
386
     * Split name in a plugin name and a component name
387
     *
388
     * @param string $name
389
     * @return array with
390
     *              - plugin name
391
     *              - and component name when available, otherwise empty string
392
     */
393
    protected function _splitName($name)
394
    {
395
        if (array_search($name, array_keys($this->tmp_plugins)) === false) {
396
            return explode('_', $name, 2);
397
        }
398
399
        return array($name, '');
400
    }
401
402
    /**
403
     * Returns inverse boolean value of the input
404
     *
405
     * @param mixed $input
406
     * @return bool inversed boolean value of input
407
     */
408
    protected function negate($input)
409
    {
410
        return !(bool)$input;
411
    }
412
}
413