about summary refs log tree commit diff stats
path: root/wiki/inc/plugincontroller.class.php
blob: fd8cd9663289922177a202ce27c2060474b4dd09 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
<?php
/**
 * Class to encapsulate access to dokuwiki plugins
 *
 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 * @author     Christopher Smith <chris@jalakai.co.uk>
 */

// plugin related constants
if(!defined('DOKU_PLUGIN'))  define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/');

class Doku_Plugin_Controller {

    protected $list_bytype = array();
    protected $tmp_plugins = array();
    protected $plugin_cascade = array('default'=>array(),'local'=>array(),'protected'=>array());
    protected $last_local_config_file = '';

    /**
     * Populates the master list of plugins
     */
    public function __construct() {
        $this->loadConfig();
        $this->_populateMasterList();
    }

    /**
     * Returns a list of available plugins of given type
     *
     * @param $type  string, plugin_type name;
     *               the type of plugin to return,
     *               use empty string for all types
     * @param $all   bool;
     *               false to only return enabled plugins,
     *               true to return both enabled and disabled plugins
     *
     * @return       array of
     *                  - plugin names when $type = ''
     *                  - or plugin component names when a $type is given
     *
     * @author Andreas Gohr <andi@splitbrain.org>
     */
    public function getList($type='',$all=false){

        // request the complete list
        if (!$type) {
            return $all ? array_keys($this->tmp_plugins) : array_keys(array_filter($this->tmp_plugins));
        }

        if (!isset($this->list_bytype[$type]['enabled'])) {
            $this->list_bytype[$type]['enabled'] = $this->_getListByType($type,true);
        }
        if ($all && !isset($this->list_bytype[$type]['disabled'])) {
            $this->list_bytype[$type]['disabled'] = $this->_getListByType($type,false);
        }

        return $all ? array_merge($this->list_bytype[$type]['enabled'],$this->list_bytype[$type]['disabled']) : $this->list_bytype[$type]['enabled'];
    }

    /**
     * Loads the given plugin and creates an object of it
     *
     * @author Andreas Gohr <andi@splitbrain.org>
     *
     * @param  $type     string type of plugin to load
     * @param  $name     string name of the plugin to load
     * @param  $new      bool   true to return a new instance of the plugin, false to use an already loaded instance
     * @param  $disabled bool   true to load even disabled plugins
     * @return DokuWiki_PluginInterface|null  the plugin object or null on failure
     */
    public function load($type,$name,$new=false,$disabled=false){

        //we keep all loaded plugins available in global scope for reuse
        global $DOKU_PLUGINS;

        list($plugin, /* $component */) = $this->_splitName($name);

        // check if disabled
        if(!$disabled && $this->isdisabled($plugin)){
            return null;
        }

        $class = $type.'_plugin_'.$name;

        //plugin already loaded?
        if(!empty($DOKU_PLUGINS[$type][$name])){
            if ($new || !$DOKU_PLUGINS[$type][$name]->isSingleton()) {
                return class_exists($class, true) ? new $class : null;
            } else {
                return $DOKU_PLUGINS[$type][$name];
            }
        }

        //construct class and instantiate
        if (!class_exists($class, true)) {

            # the plugin might be in the wrong directory
            $dir = $this->get_directory($plugin);
            $inf = confToHash(DOKU_PLUGIN."$dir/plugin.info.txt");
            if($inf['base'] && $inf['base'] != $plugin){
                msg(sprintf("Plugin installed incorrectly. Rename plugin directory '%s' to '%s'.", hsc($plugin), hsc($inf['base'])), -1);
            } elseif (preg_match('/^'.DOKU_PLUGIN_NAME_REGEX.'$/', $plugin) !== 1) {
                msg(sprintf("Plugin name '%s' is not a valid plugin name, only the characters a-z and 0-9 are allowed. ".
                                'Maybe the plugin has been installed in the wrong directory?', hsc($plugin)), -1);
            }
            return null;
        }

        $DOKU_PLUGINS[$type][$name] = new $class;
        return $DOKU_PLUGINS[$type][$name];
    }

    /**
     * Whether plugin is disabled
     *
     * @param string $plugin name of plugin
     * @return bool  true disabled, false enabled
     */
    public function isdisabled($plugin) {
        return empty($this->tmp_plugins[$plugin]);
    }

    /**
     * Disable the plugin
     *
     * @param string $plugin name of plugin
     * @return bool  true saving succeed, false saving failed
     */
    public function disable($plugin) {
        if(array_key_exists($plugin,$this->plugin_cascade['protected'])) return false;
        $this->tmp_plugins[$plugin] = 0;
        return $this->saveList();
    }

    /**
     * Enable the plugin
     *
     * @param string $plugin name of plugin
     * @return bool  true saving succeed, false saving failed
     */
    public function enable($plugin) {
        if(array_key_exists($plugin,$this->plugin_cascade['protected'])) return false;
        $this->tmp_plugins[$plugin] = 1;
        return $this->saveList();
    }

    /**
     * Returns directory name of plugin
     *
     * @param string $plugin name of plugin
     * @return string name of directory
     */
    public function get_directory($plugin) {
        return $plugin;
    }

    /**
     * Returns cascade of the config files
     *
     * @return array with arrays of plugin configs
     */
    public function getCascade() {
        return $this->plugin_cascade;
    }

    protected function _populateMasterList() {
        global $conf;

        if ($dh = @opendir(DOKU_PLUGIN)) {
            $all_plugins = array();
            while (false !== ($plugin = readdir($dh))) {
                if ($plugin[0] == '.') continue;               // skip hidden entries
                if (is_file(DOKU_PLUGIN.$plugin)) continue;    // skip files, we're only interested in directories

                if (array_key_exists($plugin,$this->tmp_plugins) && $this->tmp_plugins[$plugin] == 0){
                    $all_plugins[$plugin] = 0;

                } elseif ((array_key_exists($plugin,$this->tmp_plugins) && $this->tmp_plugins[$plugin] == 1)) {
                    $all_plugins[$plugin] = 1;
                } else {
                    $all_plugins[$plugin] = 1;
                }
            }
            $this->tmp_plugins = $all_plugins;
            if (!file_exists($this->last_local_config_file)) {
                $this->saveList(true);
            }
        }
    }

    /**
     * Includes the plugin config $files
     * and returns the entries of the $plugins array set in these files
     *
     * @param array $files list of files to include, latter overrides previous
     * @return array with entries of the $plugins arrays of the included files
     */
    protected function checkRequire($files) {
        $plugins = array();
        foreach($files as $file) {
            if(file_exists($file)) {
                include_once($file);
            }
        }
        return $plugins;
    }

    /**
     * Save the current list of plugins
     *
     * @param bool $forceSave;
     *              false to save only when config changed
     *              true to always save
     * @return bool  true saving succeed, false saving failed
     */
    protected function saveList($forceSave = false) {
        global $conf;

        if (empty($this->tmp_plugins)) return false;

        // Rebuild list of local settings
        $local_plugins = $this->rebuildLocal();
        if($local_plugins != $this->plugin_cascade['local'] || $forceSave) {
            $file = $this->last_local_config_file;
            $out = "<?php\n/*\n * Local plugin enable/disable settings\n * Auto-generated through plugin/extension manager\n *\n".
                   " * NOTE: Plugins will not be added to this file unless there is a need to override a default setting. Plugins are\n".
                   " *       enabled by default.\n */\n";
            foreach ($local_plugins as $plugin => $value) {
                $out .= "\$plugins['$plugin'] = $value;\n";
            }
            // backup current file (remove any existing backup)
            if (file_exists($file)) {
                $backup = $file.'.bak';
                if (file_exists($backup)) @unlink($backup);
                if (!@copy($file,$backup)) return false;
                if (!empty($conf['fperm'])) chmod($backup, $conf['fperm']);
            }
            //check if can open for writing, else restore
            return io_saveFile($file,$out);
        }
        return false;
    }

    /**
     * Rebuild the set of local plugins
     *
     * @return array array of plugins to be saved in end($config_cascade['plugins']['local'])
     */
    protected function rebuildLocal() {
        //assign to local variable to avoid overwriting
        $backup = $this->tmp_plugins;
        //Can't do anything about protected one so rule them out completely
        $local_default = array_diff_key($backup,$this->plugin_cascade['protected']);
        //Diff between local+default and default
        //gives us the ones we need to check and save
        $diffed_ones = array_diff_key($local_default,$this->plugin_cascade['default']);
        //The ones which we are sure of (list of 0s not in default)
        $sure_plugins = array_filter($diffed_ones,array($this,'negate'));
        //the ones in need of diff
        $conflicts = array_diff_key($local_default,$diffed_ones);
        //The final list
        return array_merge($sure_plugins,array_diff_assoc($conflicts,$this->plugin_cascade['default']));
    }

    /**
     * Build the list of plugins and cascade
     *
     */
    protected function loadConfig() {
        global $config_cascade;
        foreach(array('default','protected') as $type) {
            if(array_key_exists($type,$config_cascade['plugins']))
                $this->plugin_cascade[$type] = $this->checkRequire($config_cascade['plugins'][$type]);
        }
        $local = $config_cascade['plugins']['local'];
        $this->last_local_config_file = array_pop($local);
        $this->plugin_cascade['local'] = $this->checkRequire(array($this->last_local_config_file));
        if(is_array($local)) {
            $this->plugin_cascade['default'] = array_merge($this->plugin_cascade['default'],$this->checkRequire($local));
        }
        $this->tmp_plugins = array_merge($this->plugin_cascade['default'],$this->plugin_cascade['local'],$this->plugin_cascade['protected']);
    }

    /**
     * Returns a list of available plugin components of given type
     *
     * @param string $type      plugin_type name; the type of plugin to return,
     * @param bool   $enabled   true to return enabled plugins,
     *                          false to return disabled plugins
     * @return array of plugin components of requested type
     */
    protected function _getListByType($type, $enabled) {
        $master_list = $enabled ? array_keys(array_filter($this->tmp_plugins)) : array_keys(array_filter($this->tmp_plugins,array($this,'negate')));
        $plugins = array();

        foreach ($master_list as $plugin) {

            $basedir = $this->get_directory($plugin);
            if (file_exists(DOKU_PLUGIN."$basedir/$type.php")){
                $plugins[] = $plugin;
                continue;
            }

            $typedir = DOKU_PLUGIN."$basedir/$type/";
            if (is_dir($typedir)) {
                if ($dp = opendir($typedir)) {
                    while (false !== ($component = readdir($dp))) {
                        if (substr($component,0,1) == '.' || strtolower(substr($component, -4)) != ".php") continue;
                        if (is_file($typedir.$component)) {
                            $plugins[] = $plugin.'_'.substr($component, 0, -4);
                        }
                    }
                    closedir($dp);
                }
            }

        }//foreach

        return $plugins;
    }

    /**
     * Split name in a plugin name and a component name
     *
     * @param string $name
     * @return array with
     *              - plugin name
     *              - and component name when available, otherwise empty string
     */
    protected function _splitName($name) {
        if (array_search($name, array_keys($this->tmp_plugins)) === false) {
            return explode('_',$name,2);
        }

        return array($name,'');
    }

    /**
     * Returns inverse boolean value of the input
     *
     * @param mixed $input
     * @return bool inversed boolean value of input
     */
    protected function negate($input) {
        return !(bool) $input;
    }
}