[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> ActionRouter.php (source)

   1  <?php
   2  
   3  namespace dokuwiki;
   4  
   5  use dokuwiki\Extension\Event;
   6  use dokuwiki\Action\AbstractAction;
   7  use dokuwiki\Action\Exception\ActionDisabledException;
   8  use dokuwiki\Action\Exception\ActionException;
   9  use dokuwiki\Action\Exception\FatalException;
  10  use dokuwiki\Action\Exception\NoActionException;
  11  use dokuwiki\Action\Plugin;
  12  
  13  /**
  14   * Class ActionRouter
  15   * @package dokuwiki
  16   */
  17  class ActionRouter
  18  {
  19      /** @var  AbstractAction */
  20      protected $action;
  21  
  22      /** @var  ActionRouter */
  23      protected static $instance;
  24  
  25      /** @var int transition counter */
  26      protected $transitions = 0;
  27  
  28      /** maximum loop */
  29      protected const MAX_TRANSITIONS = 5;
  30  
  31      /** @var string[] the actions disabled in the configuration */
  32      protected $disabled;
  33  
  34      /**
  35       * ActionRouter constructor. Singleton, thus protected!
  36       *
  37       * Sets up the correct action based on the $ACT global. Writes back
  38       * the selected action to $ACT
  39       */
  40      protected function __construct()
  41      {
  42          global $ACT;
  43          global $conf;
  44  
  45          $this->disabled = explode(',', $conf['disableactions']);
  46          $this->disabled = array_map('trim', $this->disabled);
  47  
  48          $ACT = act_clean($ACT);
  49          $this->setupAction($ACT);
  50          $ACT = $this->action->getActionName();
  51      }
  52  
  53      /**
  54       * Get the singleton instance
  55       *
  56       * @param bool $reinit
  57       * @return ActionRouter
  58       */
  59      public static function getInstance($reinit = false)
  60      {
  61          if ((!self::$instance instanceof \dokuwiki\ActionRouter) || $reinit) {
  62              self::$instance = new ActionRouter();
  63          }
  64          return self::$instance;
  65      }
  66  
  67      /**
  68       * Setup the given action
  69       *
  70       * Instantiates the right class, runs permission checks and pre-processing and
  71       * sets $action
  72       *
  73       * @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility
  74       * @triggers ACTION_ACT_PREPROCESS
  75       */
  76      protected function setupAction(&$actionname)
  77      {
  78          $presetup = $actionname;
  79  
  80          try {
  81              // give plugins an opportunity to process the actionname
  82              $evt = new Event('ACTION_ACT_PREPROCESS', $actionname);
  83              if ($evt->advise_before()) {
  84                  $this->action = $this->loadAction($actionname);
  85                  $this->checkAction($this->action);
  86                  $this->action->preProcess();
  87              } else {
  88                  // event said the action should be kept, assume action plugin will handle it later
  89                  $this->action = new Plugin($actionname);
  90              }
  91              $evt->advise_after();
  92          } catch (ActionException $e) {
  93              // we should have gotten a new action
  94              $actionname = $e->getNewAction();
  95  
  96              // this one should trigger a user message
  97              if ($e instanceof ActionDisabledException) {
  98                  msg('Action disabled: ' . hsc($presetup), -1);
  99              }
 100  
 101              // some actions may request the display of a message
 102              if ($e->displayToUser()) {
 103                  msg(hsc($e->getMessage()), -1);
 104              }
 105  
 106              // do setup for new action
 107              $this->transitionAction($presetup, $actionname);
 108          } catch (NoActionException $e) {
 109              msg('Action unknown: ' . hsc($actionname), -1);
 110              $actionname = 'show';
 111              $this->transitionAction($presetup, $actionname);
 112          } catch (\Exception $e) {
 113              $this->handleFatalException($e);
 114          }
 115      }
 116  
 117      /**
 118       * Transitions from one action to another
 119       *
 120       * Basically just calls setupAction() again but does some checks before.
 121       *
 122       * @param string $from current action name
 123       * @param string $to new action name
 124       * @param null|ActionException $e any previous exception that caused the transition
 125       */
 126      protected function transitionAction($from, $to, $e = null)
 127      {
 128          $this->transitions++;
 129  
 130          // no infinite recursion
 131          if ($from == $to) {
 132              $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
 133          }
 134  
 135          // larger loops will be caught here
 136          if ($this->transitions >= self::MAX_TRANSITIONS) {
 137              $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
 138          }
 139  
 140          // do the recursion
 141          $this->setupAction($to);
 142      }
 143  
 144      /**
 145       * Aborts all processing with a message
 146       *
 147       * When a FataException instanc is passed, the code is treated as Status code
 148       *
 149       * @param \Exception|FatalException $e
 150       * @throws FatalException during unit testing
 151       */
 152      protected function handleFatalException(\Throwable $e)
 153      {
 154          if ($e instanceof FatalException) {
 155              http_status($e->getCode());
 156          } else {
 157              http_status(500);
 158          }
 159          if (defined('DOKU_UNITTEST')) {
 160              throw $e;
 161          }
 162          ErrorHandler::logException($e);
 163          $msg = 'Something unforeseen has happened: ' . $e->getMessage();
 164          nice_die(hsc($msg));
 165      }
 166  
 167      /**
 168       * Load the given action
 169       *
 170       * This translates the given name to a class name by uppercasing the first letter.
 171       * Underscores translate to camelcase names. For actions with underscores, the different
 172       * parts are removed beginning from the end until a matching class is found. The instatiated
 173       * Action will always have the full original action set as Name
 174       *
 175       * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
 176       *
 177       * @param $actionname
 178       * @return AbstractAction
 179       * @throws NoActionException
 180       */
 181      public function loadAction($actionname)
 182      {
 183          $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
 184          $parts = explode('_', $actionname);
 185          while ($parts !== []) {
 186              $load = implode('_', $parts);
 187              $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
 188              if (class_exists($class)) {
 189                  return new $class($actionname);
 190              }
 191              array_pop($parts);
 192          }
 193  
 194          throw new NoActionException();
 195      }
 196  
 197      /**
 198       * Execute all the checks to see if this action can be executed
 199       *
 200       * @param AbstractAction $action
 201       * @throws ActionDisabledException
 202       * @throws ActionException
 203       */
 204      public function checkAction(AbstractAction $action)
 205      {
 206          global $INFO;
 207          global $ID;
 208  
 209          if (in_array($action->getActionName(), $this->disabled)) {
 210              throw new ActionDisabledException();
 211          }
 212  
 213          $action->checkPreconditions();
 214  
 215          if (isset($INFO)) {
 216              $perm = $INFO['perm'];
 217          } else {
 218              $perm = auth_quickaclcheck($ID);
 219          }
 220  
 221          if ($perm < $action->minimumPermission()) {
 222              throw new ActionException('denied');
 223          }
 224      }
 225  
 226      /**
 227       * Returns the action handling the current request
 228       *
 229       * @return AbstractAction
 230       */
 231      public function getAction()
 232      {
 233          return $this->action;
 234      }
 235  }