[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/Extension/ -> PluginController.php (source)

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