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 $inf = confToHash(DOKU_PLUGIN . "$plugin/plugin.info.txt"); 104 if ($inf['base'] && $inf['base'] != $plugin) { 105 msg( 106 sprintf( 107 "Plugin installed incorrectly. Rename plugin directory '%s' to '%s'.", 108 hsc($plugin), 109 hsc( 110 $inf['base'] 111 ) 112 ), -1 113 ); 114 } elseif (preg_match('/^' . DOKU_PLUGIN_NAME_REGEX . '$/', $plugin) !== 1) { 115 msg( 116 sprintf( 117 "Plugin name '%s' is not a valid plugin name, only the characters a-z and 0-9 are allowed. " . 118 'Maybe the plugin has been installed in the wrong directory?', hsc($plugin) 119 ), -1 120 ); 121 } 122 return null; 123 } 124 125 $DOKU_PLUGINS[$type][$name] = new $class; 126 return $DOKU_PLUGINS[$type][$name]; 127 } 128 129 /** 130 * Whether plugin is disabled 131 * 132 * @param string $plugin name of plugin 133 * @return bool true disabled, false enabled 134 * @deprecated in favor of the more sensible isEnabled where the return value matches the enabled state 135 */ 136 public function isDisabled($plugin) 137 { 138 dbg_deprecated('isEnabled()'); 139 return !$this->isEnabled($plugin); 140 } 141 142 /** 143 * Check whether plugin is disabled 144 * 145 * @param string $plugin name of plugin 146 * @return bool true enabled, false disabled 147 */ 148 public function isEnabled($plugin) 149 { 150 return !empty($this->tmp_plugins[$plugin]); 151 } 152 153 /** 154 * Disable the plugin 155 * 156 * @param string $plugin name of plugin 157 * @return bool true saving succeed, false saving failed 158 */ 159 public function disable($plugin) 160 { 161 if (array_key_exists($plugin, $this->plugin_cascade['protected'])) return false; 162 $this->tmp_plugins[$plugin] = 0; 163 return $this->saveList(); 164 } 165 166 /** 167 * Enable the plugin 168 * 169 * @param string $plugin name of plugin 170 * @return bool true saving succeed, false saving failed 171 */ 172 public function enable($plugin) 173 { 174 if (array_key_exists($plugin, $this->plugin_cascade['protected'])) return false; 175 $this->tmp_plugins[$plugin] = 1; 176 return $this->saveList(); 177 } 178 179 /** 180 * Returns cascade of the config files 181 * 182 * @return array with arrays of plugin configs 183 */ 184 public function getCascade() 185 { 186 return $this->plugin_cascade; 187 } 188 189 protected function _populateMasterList() 190 { 191 global $conf; 192 193 if ($dh = @opendir(DOKU_PLUGIN)) { 194 $all_plugins = array(); 195 while (false !== ($plugin = readdir($dh))) { 196 if ($plugin[0] == '.') continue; // skip hidden entries 197 if (is_file(DOKU_PLUGIN . $plugin)) continue; // skip files, we're only interested in directories 198 199 if (array_key_exists($plugin, $this->tmp_plugins) && $this->tmp_plugins[$plugin] == 0) { 200 $all_plugins[$plugin] = 0; 201 202 } elseif ((array_key_exists($plugin, $this->tmp_plugins) && $this->tmp_plugins[$plugin] == 1)) { 203 $all_plugins[$plugin] = 1; 204 } else { 205 $all_plugins[$plugin] = 1; 206 } 207 } 208 $this->tmp_plugins = $all_plugins; 209 if (!file_exists($this->last_local_config_file)) { 210 $this->saveList(true); 211 } 212 } 213 } 214 215 /** 216 * Includes the plugin config $files 217 * and returns the entries of the $plugins array set in these files 218 * 219 * @param array $files list of files to include, latter overrides previous 220 * @return array with entries of the $plugins arrays of the included files 221 */ 222 protected function checkRequire($files) 223 { 224 $plugins = array(); 225 foreach ($files as $file) { 226 if (file_exists($file)) { 227 include_once($file); 228 } 229 } 230 return $plugins; 231 } 232 233 /** 234 * Save the current list of plugins 235 * 236 * @param bool $forceSave ; 237 * false to save only when config changed 238 * true to always save 239 * @return bool true saving succeed, false saving failed 240 */ 241 protected function saveList($forceSave = false) 242 { 243 global $conf; 244 245 if (empty($this->tmp_plugins)) return false; 246 247 // Rebuild list of local settings 248 $local_plugins = $this->rebuildLocal(); 249 if ($local_plugins != $this->plugin_cascade['local'] || $forceSave) { 250 $file = $this->last_local_config_file; 251 $out = "<?php\n/*\n * Local plugin enable/disable settings\n" . 252 " * Auto-generated through plugin/extension manager\n *\n" . 253 " * NOTE: Plugins will not be added to this file unless there " . 254 "is a need to override a default setting. Plugins are\n" . 255 " * enabled by default.\n */\n"; 256 foreach ($local_plugins as $plugin => $value) { 257 $out .= "\$plugins['$plugin'] = $value;\n"; 258 } 259 // backup current file (remove any existing backup) 260 if (file_exists($file)) { 261 $backup = $file . '.bak'; 262 if (file_exists($backup)) @unlink($backup); 263 if (!@copy($file, $backup)) return false; 264 if (!empty($conf['fperm'])) chmod($backup, $conf['fperm']); 265 } 266 //check if can open for writing, else restore 267 return io_saveFile($file, $out); 268 } 269 return false; 270 } 271 272 /** 273 * Rebuild the set of local plugins 274 * 275 * @return array array of plugins to be saved in end($config_cascade['plugins']['local']) 276 */ 277 protected function rebuildLocal() 278 { 279 //assign to local variable to avoid overwriting 280 $backup = $this->tmp_plugins; 281 //Can't do anything about protected one so rule them out completely 282 $local_default = array_diff_key($backup, $this->plugin_cascade['protected']); 283 //Diff between local+default and default 284 //gives us the ones we need to check and save 285 $diffed_ones = array_diff_key($local_default, $this->plugin_cascade['default']); 286 //The ones which we are sure of (list of 0s not in default) 287 $sure_plugins = array_filter($diffed_ones, array($this, 'negate')); 288 //the ones in need of diff 289 $conflicts = array_diff_key($local_default, $diffed_ones); 290 //The final list 291 return array_merge($sure_plugins, array_diff_assoc($conflicts, $this->plugin_cascade['default'])); 292 } 293 294 /** 295 * Build the list of plugins and cascade 296 * 297 */ 298 protected function loadConfig() 299 { 300 global $config_cascade; 301 foreach (array('default', 'protected') as $type) { 302 if (array_key_exists($type, $config_cascade['plugins'])) { 303 $this->plugin_cascade[$type] = $this->checkRequire($config_cascade['plugins'][$type]); 304 } 305 } 306 $local = $config_cascade['plugins']['local']; 307 $this->last_local_config_file = array_pop($local); 308 $this->plugin_cascade['local'] = $this->checkRequire(array($this->last_local_config_file)); 309 if (is_array($local)) { 310 $this->plugin_cascade['default'] = array_merge( 311 $this->plugin_cascade['default'], 312 $this->checkRequire($local) 313 ); 314 } 315 $this->tmp_plugins = array_merge( 316 $this->plugin_cascade['default'], 317 $this->plugin_cascade['local'], 318 $this->plugin_cascade['protected'] 319 ); 320 } 321 322 /** 323 * Returns a list of available plugin components of given type 324 * 325 * @param string $type plugin_type name; the type of plugin to return, 326 * @param bool $enabled true to return enabled plugins, 327 * false to return disabled plugins 328 * @return array of plugin components of requested type 329 */ 330 protected function _getListByType($type, $enabled) 331 { 332 $master_list = $enabled 333 ? array_keys(array_filter($this->tmp_plugins)) 334 : array_keys(array_filter($this->tmp_plugins, array($this, 'negate'))); 335 $plugins = array(); 336 337 foreach ($master_list as $plugin) { 338 339 if (file_exists(DOKU_PLUGIN . "$plugin/$type.php")) { 340 $plugins[] = $plugin; 341 continue; 342 } 343 344 $typedir = DOKU_PLUGIN . "$plugin/$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