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