[ 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          $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                  continue;
 349              }
 350  
 351              $typedir = DOKU_PLUGIN . "$plugin/$type/";
 352              if (is_dir($typedir)) {
 353                  if ($dp = opendir($typedir)) {
 354                      while (false !== ($component = readdir($dp))) {
 355                          if (
 356                              str_starts_with($component, '.') ||
 357                              !str_ends_with(strtolower($component), '.php')
 358                          ) continue;
 359                          if (is_file($typedir . $component)) {
 360                              $plugins[] = $plugin . '_' . substr($component, 0, -4);
 361                          }
 362                      }
 363                      closedir($dp);
 364                  }
 365              }
 366          }//foreach
 367  
 368          return $plugins;
 369      }
 370  
 371      /**
 372       * Split name in a plugin name and a component name
 373       *
 374       * @param string $name
 375       * @return array with
 376       *              - plugin name
 377       *              - and component name when available, otherwise empty string
 378       */
 379      protected function splitName($name)
 380      {
 381          if (!isset($this->masterList[$name])) {
 382              return sexplode('_', $name, 2, '');
 383          }
 384  
 385          return [$name, ''];
 386      }
 387  
 388      /**
 389       * Returns inverse boolean value of the input
 390       *
 391       * @param mixed $input
 392       * @return bool inversed boolean value of input
 393       */
 394      protected function negate($input)
 395      {
 396          return !(bool)$input;
 397      }
 398  
 399      /**
 400       * Initialize vendor autoloaders for all plugins that have them
 401       */
 402      protected function initAutoloaders()
 403      {
 404          $plugins = $this->getList();
 405          foreach ($plugins as $plugin) {
 406              if (file_exists(DOKU_PLUGIN . $plugin . '/vendor/autoload.php')) {
 407                  try {
 408                      require_once (DOKU_PLUGIN . $plugin . '/vendor/autoload.php');
 409                  } catch (\Throwable $e) {
 410                      ErrorHandler::showExceptionMsg($e, sprintf('Failed to init plugin %s autoloader', $plugin));
 411                  }
 412              }
 413          }
 414      }
 415  }