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 */ 135 public function isdisabled($plugin) 136 { 137 return empty($this->tmp_plugins[$plugin]); 138 } 139 140 /** 141 * Disable the plugin 142 * 143 * @param string $plugin name of plugin 144 * @return bool true saving succeed, false saving failed 145 */ 146 public function disable($plugin) 147 { 148 if (array_key_exists($plugin, $this->plugin_cascade['protected'])) return false; 149 $this->tmp_plugins[$plugin] = 0; 150 return $this->saveList(); 151 } 152 153 /** 154 * Enable the plugin 155 * 156 * @param string $plugin name of plugin 157 * @return bool true saving succeed, false saving failed 158 */ 159 public function enable($plugin) 160 { 161 if (array_key_exists($plugin, $this->plugin_cascade['protected'])) return false; 162 $this->tmp_plugins[$plugin] = 1; 163 return $this->saveList(); 164 } 165 166 /** 167 * Returns cascade of the config files 168 * 169 * @return array with arrays of plugin configs 170 */ 171 public function getCascade() 172 { 173 return $this->plugin_cascade; 174 } 175 176 protected function _populateMasterList() 177 { 178 global $conf; 179 180 if ($dh = @opendir(DOKU_PLUGIN)) { 181 $all_plugins = array(); 182 while (false !== ($plugin = readdir($dh))) { 183 if ($plugin[0] == '.') continue; // skip hidden entries 184 if (is_file(DOKU_PLUGIN . $plugin)) continue; // skip files, we're only interested in directories 185 186 if (array_key_exists($plugin, $this->tmp_plugins) && $this->tmp_plugins[$plugin] == 0) { 187 $all_plugins[$plugin] = 0; 188 189 } elseif ((array_key_exists($plugin, $this->tmp_plugins) && $this->tmp_plugins[$plugin] == 1)) { 190 $all_plugins[$plugin] = 1; 191 } else { 192 $all_plugins[$plugin] = 1; 193 } 194 } 195 $this->tmp_plugins = $all_plugins; 196 if (!file_exists($this->last_local_config_file)) { 197 $this->saveList(true); 198 } 199 } 200 } 201 202 /** 203 * Includes the plugin config $files 204 * and returns the entries of the $plugins array set in these files 205 * 206 * @param array $files list of files to include, latter overrides previous 207 * @return array with entries of the $plugins arrays of the included files 208 */ 209 protected function checkRequire($files) 210 { 211 $plugins = array(); 212 foreach ($files as $file) { 213 if (file_exists($file)) { 214 include_once($file); 215 } 216 } 217 return $plugins; 218 } 219 220 /** 221 * Save the current list of plugins 222 * 223 * @param bool $forceSave ; 224 * false to save only when config changed 225 * true to always save 226 * @return bool true saving succeed, false saving failed 227 */ 228 protected function saveList($forceSave = false) 229 { 230 global $conf; 231 232 if (empty($this->tmp_plugins)) return false; 233 234 // Rebuild list of local settings 235 $local_plugins = $this->rebuildLocal(); 236 if ($local_plugins != $this->plugin_cascade['local'] || $forceSave) { 237 $file = $this->last_local_config_file; 238 $out = "<?php\n/*\n * Local plugin enable/disable settings\n" . 239 " * Auto-generated through plugin/extension manager\n *\n" . 240 " * NOTE: Plugins will not be added to this file unless there " . 241 "is a need to override a default setting. Plugins are\n" . 242 " * enabled by default.\n */\n"; 243 foreach ($local_plugins as $plugin => $value) { 244 $out .= "\$plugins['$plugin'] = $value;\n"; 245 } 246 // backup current file (remove any existing backup) 247 if (file_exists($file)) { 248 $backup = $file . '.bak'; 249 if (file_exists($backup)) @unlink($backup); 250 if (!@copy($file, $backup)) return false; 251 if (!empty($conf['fperm'])) chmod($backup, $conf['fperm']); 252 } 253 //check if can open for writing, else restore 254 return io_saveFile($file, $out); 255 } 256 return false; 257 } 258 259 /** 260 * Rebuild the set of local plugins 261 * 262 * @return array array of plugins to be saved in end($config_cascade['plugins']['local']) 263 */ 264 protected function rebuildLocal() 265 { 266 //assign to local variable to avoid overwriting 267 $backup = $this->tmp_plugins; 268 //Can't do anything about protected one so rule them out completely 269 $local_default = array_diff_key($backup, $this->plugin_cascade['protected']); 270 //Diff between local+default and default 271 //gives us the ones we need to check and save 272 $diffed_ones = array_diff_key($local_default, $this->plugin_cascade['default']); 273 //The ones which we are sure of (list of 0s not in default) 274 $sure_plugins = array_filter($diffed_ones, array($this, 'negate')); 275 //the ones in need of diff 276 $conflicts = array_diff_key($local_default, $diffed_ones); 277 //The final list 278 return array_merge($sure_plugins, array_diff_assoc($conflicts, $this->plugin_cascade['default'])); 279 } 280 281 /** 282 * Build the list of plugins and cascade 283 * 284 */ 285 protected function loadConfig() 286 { 287 global $config_cascade; 288 foreach (array('default', 'protected') as $type) { 289 if (array_key_exists($type, $config_cascade['plugins'])) { 290 $this->plugin_cascade[$type] = $this->checkRequire($config_cascade['plugins'][$type]); 291 } 292 } 293 $local = $config_cascade['plugins']['local']; 294 $this->last_local_config_file = array_pop($local); 295 $this->plugin_cascade['local'] = $this->checkRequire(array($this->last_local_config_file)); 296 if (is_array($local)) { 297 $this->plugin_cascade['default'] = array_merge( 298 $this->plugin_cascade['default'], 299 $this->checkRequire($local) 300 ); 301 } 302 $this->tmp_plugins = array_merge( 303 $this->plugin_cascade['default'], 304 $this->plugin_cascade['local'], 305 $this->plugin_cascade['protected'] 306 ); 307 } 308 309 /** 310 * Returns a list of available plugin components of given type 311 * 312 * @param string $type plugin_type name; the type of plugin to return, 313 * @param bool $enabled true to return enabled plugins, 314 * false to return disabled plugins 315 * @return array of plugin components of requested type 316 */ 317 protected function _getListByType($type, $enabled) 318 { 319 $master_list = $enabled 320 ? array_keys(array_filter($this->tmp_plugins)) 321 : array_keys(array_filter($this->tmp_plugins, array($this, 'negate'))); 322 $plugins = array(); 323 324 foreach ($master_list as $plugin) { 325 326 if (file_exists(DOKU_PLUGIN . "$plugin/$type.php")) { 327 $plugins[] = $plugin; 328 continue; 329 } 330 331 $typedir = DOKU_PLUGIN . "$plugin/$type/"; 332 if (is_dir($typedir)) { 333 if ($dp = opendir($typedir)) { 334 while (false !== ($component = readdir($dp))) { 335 if (substr($component, 0, 1) == '.' || strtolower(substr($component, -4)) != ".php") continue; 336 if (is_file($typedir . $component)) { 337 $plugins[] = $plugin . '_' . substr($component, 0, -4); 338 } 339 } 340 closedir($dp); 341 } 342 } 343 344 }//foreach 345 346 return $plugins; 347 } 348 349 /** 350 * Split name in a plugin name and a component name 351 * 352 * @param string $name 353 * @return array with 354 * - plugin name 355 * - and component name when available, otherwise empty string 356 */ 357 protected function _splitName($name) 358 { 359 if (array_search($name, array_keys($this->tmp_plugins)) === false) { 360 return explode('_', $name, 2); 361 } 362 363 return array($name, ''); 364 } 365 366 /** 367 * Returns inverse boolean value of the input 368 * 369 * @param mixed $input 370 * @return bool inversed boolean value of input 371 */ 372 protected function negate($input) 373 { 374 return !(bool)$input; 375 } 376} 377