229 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			229 lines
		
	
	
		
			6.8 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| namespace dokuwiki;
 | |
| 
 | |
| use dokuwiki\Action\AbstractAction;
 | |
| use dokuwiki\Action\Exception\ActionDisabledException;
 | |
| use dokuwiki\Action\Exception\ActionException;
 | |
| use dokuwiki\Action\Exception\FatalException;
 | |
| use dokuwiki\Action\Exception\NoActionException;
 | |
| use dokuwiki\Action\Plugin;
 | |
| 
 | |
| /**
 | |
|  * Class ActionRouter
 | |
|  * @package dokuwiki
 | |
|  */
 | |
| class ActionRouter {
 | |
| 
 | |
|     /** @var  AbstractAction */
 | |
|     protected $action;
 | |
| 
 | |
|     /** @var  ActionRouter */
 | |
|     protected static $instance = null;
 | |
| 
 | |
|     /** @var int transition counter */
 | |
|     protected $transitions = 0;
 | |
| 
 | |
|     /** maximum loop */
 | |
|     const MAX_TRANSITIONS = 5;
 | |
| 
 | |
|     /** @var string[] the actions disabled in the configuration */
 | |
|     protected $disabled;
 | |
| 
 | |
|     /**
 | |
|      * ActionRouter constructor. Singleton, thus protected!
 | |
|      *
 | |
|      * Sets up the correct action based on the $ACT global. Writes back
 | |
|      * the selected action to $ACT
 | |
|      */
 | |
|     protected function __construct() {
 | |
|         global $ACT;
 | |
|         global $conf;
 | |
| 
 | |
|         $this->disabled = explode(',', $conf['disableactions']);
 | |
|         $this->disabled = array_map('trim', $this->disabled);
 | |
|         $this->transitions = 0;
 | |
| 
 | |
|         $ACT = act_clean($ACT);
 | |
|         $this->setupAction($ACT);
 | |
|         $ACT = $this->action->getActionName();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Get the singleton instance
 | |
|      *
 | |
|      * @param bool $reinit
 | |
|      * @return ActionRouter
 | |
|      */
 | |
|     public static function getInstance($reinit = false) {
 | |
|         if((self::$instance === null) || $reinit) {
 | |
|             self::$instance = new ActionRouter();
 | |
|         }
 | |
|         return self::$instance;
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Setup the given action
 | |
|      *
 | |
|      * Instantiates the right class, runs permission checks and pre-processing and
 | |
|      * sets $action
 | |
|      *
 | |
|      * @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility
 | |
|      * @triggers ACTION_ACT_PREPROCESS
 | |
|      */
 | |
|     protected function setupAction(&$actionname) {
 | |
|         $presetup = $actionname;
 | |
| 
 | |
|         try {
 | |
|             // give plugins an opportunity to process the actionname
 | |
|             $evt = new Extension\Event('ACTION_ACT_PREPROCESS', $actionname);
 | |
|             if ($evt->advise_before()) {
 | |
|                 $this->action = $this->loadAction($actionname);
 | |
|                 $this->checkAction($this->action);
 | |
|                 $this->action->preProcess();
 | |
|             } else {
 | |
|                 // event said the action should be kept, assume action plugin will handle it later
 | |
|                 $this->action = new Plugin($actionname);
 | |
|             }
 | |
|             $evt->advise_after();
 | |
| 
 | |
|         } catch(ActionException $e) {
 | |
|             // we should have gotten a new action
 | |
|             $actionname = $e->getNewAction();
 | |
| 
 | |
|             // this one should trigger a user message
 | |
|             if(is_a($e, ActionDisabledException::class)) {
 | |
|                 msg('Action disabled: ' . hsc($presetup), -1);
 | |
|             }
 | |
| 
 | |
|             // some actions may request the display of a message
 | |
|             if($e->displayToUser()) {
 | |
|                 msg(hsc($e->getMessage()), -1);
 | |
|             }
 | |
| 
 | |
|             // do setup for new action
 | |
|             $this->transitionAction($presetup, $actionname);
 | |
| 
 | |
|         } catch(NoActionException $e) {
 | |
|             msg('Action unknown: ' . hsc($actionname), -1);
 | |
|             $actionname = 'show';
 | |
|             $this->transitionAction($presetup, $actionname);
 | |
|         } catch(\Exception $e) {
 | |
|             $this->handleFatalException($e);
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Transitions from one action to another
 | |
|      *
 | |
|      * Basically just calls setupAction() again but does some checks before.
 | |
|      *
 | |
|      * @param string $from current action name
 | |
|      * @param string $to new action name
 | |
|      * @param null|ActionException $e any previous exception that caused the transition
 | |
|      */
 | |
|     protected function transitionAction($from, $to, $e = null) {
 | |
|         $this->transitions++;
 | |
| 
 | |
|         // no infinite recursion
 | |
|         if($from == $to) {
 | |
|             $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e));
 | |
|         }
 | |
| 
 | |
|         // larger loops will be caught here
 | |
|         if($this->transitions >= self::MAX_TRANSITIONS) {
 | |
|             $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e));
 | |
|         }
 | |
| 
 | |
|         // do the recursion
 | |
|         $this->setupAction($to);
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Aborts all processing with a message
 | |
|      *
 | |
|      * When a FataException instanc is passed, the code is treated as Status code
 | |
|      *
 | |
|      * @param \Exception|FatalException $e
 | |
|      * @throws FatalException during unit testing
 | |
|      */
 | |
|     protected function handleFatalException(\Exception $e) {
 | |
|         if(is_a($e, FatalException::class)) {
 | |
|             http_status($e->getCode());
 | |
|         } else {
 | |
|             http_status(500);
 | |
|         }
 | |
|         if(defined('DOKU_UNITTEST')) {
 | |
|             throw $e;
 | |
|         }
 | |
|         $msg = 'Something unforeseen has happened: ' . $e->getMessage();
 | |
|         nice_die(hsc($msg));
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Load the given action
 | |
|      *
 | |
|      * This translates the given name to a class name by uppercasing the first letter.
 | |
|      * Underscores translate to camelcase names. For actions with underscores, the different
 | |
|      * parts are removed beginning from the end until a matching class is found. The instatiated
 | |
|      * Action will always have the full original action set as Name
 | |
|      *
 | |
|      * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export'
 | |
|      *
 | |
|      * @param $actionname
 | |
|      * @return AbstractAction
 | |
|      * @throws NoActionException
 | |
|      */
 | |
|     public function loadAction($actionname) {
 | |
|         $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else?
 | |
|         $parts = explode('_', $actionname);
 | |
|         while(!empty($parts)) {
 | |
|             $load = join('_', $parts);
 | |
|             $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_'));
 | |
|             if(class_exists($class)) {
 | |
|                 return new $class($actionname);
 | |
|             }
 | |
|             array_pop($parts);
 | |
|         }
 | |
| 
 | |
|         throw new NoActionException();
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Execute all the checks to see if this action can be executed
 | |
|      *
 | |
|      * @param AbstractAction $action
 | |
|      * @throws ActionDisabledException
 | |
|      * @throws ActionException
 | |
|      */
 | |
|     public function checkAction(AbstractAction $action) {
 | |
|         global $INFO;
 | |
|         global $ID;
 | |
| 
 | |
|         if(in_array($action->getActionName(), $this->disabled)) {
 | |
|             throw new ActionDisabledException();
 | |
|         }
 | |
| 
 | |
|         $action->checkPreconditions();
 | |
| 
 | |
|         if(isset($INFO)) {
 | |
|             $perm = $INFO['perm'];
 | |
|         } else {
 | |
|             $perm = auth_quickaclcheck($ID);
 | |
|         }
 | |
| 
 | |
|         if($perm < $action->minimumPermission()) {
 | |
|             throw new ActionException('denied');
 | |
|         }
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns the action handling the current request
 | |
|      *
 | |
|      * @return AbstractAction
 | |
|      */
 | |
|     public function getAction() {
 | |
|         return $this->action;
 | |
|     }
 | |
| }
 |