[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> ActionRouter.php (source)

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