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