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