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