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