164ab5140SAndreas Gohr<?php 264ab5140SAndreas Gohr 364ab5140SAndreas Gohrnamespace dokuwiki; 464ab5140SAndreas Gohr 524870174SAndreas Gohruse dokuwiki\Extension\Event; 664ab5140SAndreas Gohruse dokuwiki\Action\AbstractAction; 764ab5140SAndreas Gohruse dokuwiki\Action\Exception\ActionDisabledException; 864ab5140SAndreas Gohruse dokuwiki\Action\Exception\ActionException; 964ab5140SAndreas Gohruse dokuwiki\Action\Exception\FatalException; 1064ab5140SAndreas Gohruse dokuwiki\Action\Exception\NoActionException; 11a3f6fae6SAndreas Gohruse dokuwiki\Action\Plugin; 1264ab5140SAndreas Gohr 13a3f6fae6SAndreas Gohr/** 14a3f6fae6SAndreas Gohr * Class ActionRouter 15a3f6fae6SAndreas Gohr * @package dokuwiki 16a3f6fae6SAndreas Gohr */ 178c7c53b0SAndreas Gohrclass ActionRouter 188c7c53b0SAndreas Gohr{ 1964ab5140SAndreas Gohr /** @var AbstractAction */ 2064ab5140SAndreas Gohr protected $action; 2164ab5140SAndreas Gohr 2264ab5140SAndreas Gohr /** @var ActionRouter */ 2324870174SAndreas Gohr protected static $instance; 2464ab5140SAndreas Gohr 2550701b66SAndreas Gohr /** @var int transition counter */ 2650701b66SAndreas Gohr protected $transitions = 0; 2750701b66SAndreas Gohr 2850701b66SAndreas Gohr /** maximum loop */ 2974981a4eSAndreas Gohr protected const MAX_TRANSITIONS = 5; 3050701b66SAndreas Gohr 31480336a3SAndreas Gohr /** @var string[] the actions disabled in the configuration */ 32480336a3SAndreas Gohr protected $disabled; 33480336a3SAndreas Gohr 3464ab5140SAndreas Gohr /** 3564ab5140SAndreas Gohr * ActionRouter constructor. Singleton, thus protected! 36a3f6fae6SAndreas Gohr * 37a3f6fae6SAndreas Gohr * Sets up the correct action based on the $ACT global. Writes back 38a3f6fae6SAndreas Gohr * the selected action to $ACT 3964ab5140SAndreas Gohr */ 40*d868eb89SAndreas Gohr protected function __construct() 41*d868eb89SAndreas Gohr { 42a3f6fae6SAndreas Gohr global $ACT; 43480336a3SAndreas Gohr global $conf; 44480336a3SAndreas Gohr 45480336a3SAndreas Gohr $this->disabled = explode(',', $conf['disableactions']); 46480336a3SAndreas Gohr $this->disabled = array_map('trim', $this->disabled); 47480336a3SAndreas Gohr 48a3f6fae6SAndreas Gohr $ACT = act_clean($ACT); 49a3f6fae6SAndreas Gohr $this->setupAction($ACT); 50a3f6fae6SAndreas Gohr $ACT = $this->action->getActionName(); 5164ab5140SAndreas Gohr } 5264ab5140SAndreas Gohr 5364ab5140SAndreas Gohr /** 5464ab5140SAndreas Gohr * Get the singleton instance 5564ab5140SAndreas Gohr * 5664ab5140SAndreas Gohr * @param bool $reinit 5764ab5140SAndreas Gohr * @return ActionRouter 5864ab5140SAndreas Gohr */ 59*d868eb89SAndreas Gohr public static function getInstance($reinit = false) 60*d868eb89SAndreas Gohr { 6124870174SAndreas Gohr if ((!self::$instance instanceof \dokuwiki\ActionRouter) || $reinit) { 62ae7bcdc7SAndreas Gohr self::$instance = new ActionRouter(); 6364ab5140SAndreas Gohr } 64ae7bcdc7SAndreas Gohr return self::$instance; 6564ab5140SAndreas Gohr } 6664ab5140SAndreas Gohr 6764ab5140SAndreas Gohr /** 6864ab5140SAndreas Gohr * Setup the given action 6964ab5140SAndreas Gohr * 7064ab5140SAndreas Gohr * Instantiates the right class, runs permission checks and pre-processing and 71a3f6fae6SAndreas Gohr * sets $action 7264ab5140SAndreas Gohr * 73c7d61a4eSAndreas Gohr * @param string $actionname this is passed as a reference to $ACT, for plugin backward compatibility 74a3f6fae6SAndreas Gohr * @triggers ACTION_ACT_PREPROCESS 7564ab5140SAndreas Gohr */ 76*d868eb89SAndreas Gohr protected function setupAction(&$actionname) 77*d868eb89SAndreas Gohr { 78a3f6fae6SAndreas Gohr $presetup = $actionname; 79a3f6fae6SAndreas Gohr 8064ab5140SAndreas Gohr try { 8168667f4aSMichael Große // give plugins an opportunity to process the actionname 8224870174SAndreas Gohr $evt = new Event('ACTION_ACT_PREPROCESS', $actionname); 8368667f4aSMichael Große if ($evt->advise_before()) { 8464ab5140SAndreas Gohr $this->action = $this->loadAction($actionname); 85480336a3SAndreas Gohr $this->checkAction($this->action); 8664ab5140SAndreas Gohr $this->action->preProcess(); 8768667f4aSMichael Große } else { 8868667f4aSMichael Große // event said the action should be kept, assume action plugin will handle it later 8968667f4aSMichael Große $this->action = new Plugin($actionname); 9068667f4aSMichael Große } 9168667f4aSMichael Große $evt->advise_after(); 9264ab5140SAndreas Gohr } catch (ActionException $e) { 9364ab5140SAndreas Gohr // we should have gotten a new action 94a3f6fae6SAndreas Gohr $actionname = $e->getNewAction(); 9564ab5140SAndreas Gohr 9664ab5140SAndreas Gohr // this one should trigger a user message 9724870174SAndreas Gohr if ($e instanceof ActionDisabledException) { 98a3f6fae6SAndreas Gohr msg('Action disabled: ' . hsc($presetup), -1); 9964ab5140SAndreas Gohr } 10064ab5140SAndreas Gohr 10181f9e22bSAndreas Gohr // some actions may request the display of a message 10281f9e22bSAndreas Gohr if ($e->displayToUser()) { 10381f9e22bSAndreas Gohr msg(hsc($e->getMessage()), -1); 10481f9e22bSAndreas Gohr } 10581f9e22bSAndreas Gohr 10664ab5140SAndreas Gohr // do setup for new action 10750701b66SAndreas Gohr $this->transitionAction($presetup, $actionname); 10864ab5140SAndreas Gohr } catch (NoActionException $e) { 109a3f6fae6SAndreas Gohr msg('Action unknown: ' . hsc($actionname), -1); 110a3f6fae6SAndreas Gohr $actionname = 'show'; 11150701b66SAndreas Gohr $this->transitionAction($presetup, $actionname); 11264ab5140SAndreas Gohr } catch (\Exception $e) { 11364ab5140SAndreas Gohr $this->handleFatalException($e); 11464ab5140SAndreas Gohr } 11564ab5140SAndreas Gohr } 11664ab5140SAndreas Gohr 11764ab5140SAndreas Gohr /** 11850701b66SAndreas Gohr * Transitions from one action to another 11950701b66SAndreas Gohr * 12058528803SAndreas Gohr * Basically just calls setupAction() again but does some checks before. 12150701b66SAndreas Gohr * 12250701b66SAndreas Gohr * @param string $from current action name 12350701b66SAndreas Gohr * @param string $to new action name 12450701b66SAndreas Gohr * @param null|ActionException $e any previous exception that caused the transition 12550701b66SAndreas Gohr */ 126*d868eb89SAndreas Gohr protected function transitionAction($from, $to, $e = null) 127*d868eb89SAndreas Gohr { 12850701b66SAndreas Gohr $this->transitions++; 12950701b66SAndreas Gohr 13050701b66SAndreas Gohr // no infinite recursion 13150701b66SAndreas Gohr if ($from == $to) { 13250701b66SAndreas Gohr $this->handleFatalException(new FatalException('Infinite loop in actions', 500, $e)); 13350701b66SAndreas Gohr } 13450701b66SAndreas Gohr 13550701b66SAndreas Gohr // larger loops will be caught here 13650701b66SAndreas Gohr if ($this->transitions >= self::MAX_TRANSITIONS) { 13750701b66SAndreas Gohr $this->handleFatalException(new FatalException('Maximum action transitions reached', 500, $e)); 13850701b66SAndreas Gohr } 13950701b66SAndreas Gohr 14050701b66SAndreas Gohr // do the recursion 14150701b66SAndreas Gohr $this->setupAction($to); 14250701b66SAndreas Gohr } 14350701b66SAndreas Gohr 14450701b66SAndreas Gohr /** 14564ab5140SAndreas Gohr * Aborts all processing with a message 14664ab5140SAndreas Gohr * 14764ab5140SAndreas Gohr * When a FataException instanc is passed, the code is treated as Status code 14864ab5140SAndreas Gohr * 14964ab5140SAndreas Gohr * @param \Exception|FatalException $e 1507675e707SAndreas Gohr * @throws FatalException during unit testing 15164ab5140SAndreas Gohr */ 152*d868eb89SAndreas Gohr protected function handleFatalException(\Throwable $e) 153*d868eb89SAndreas Gohr { 15424870174SAndreas Gohr if ($e instanceof FatalException) { 15564ab5140SAndreas Gohr http_status($e->getCode()); 15664ab5140SAndreas Gohr } else { 15764ab5140SAndreas Gohr http_status(500); 15864ab5140SAndreas Gohr } 1597675e707SAndreas Gohr if (defined('DOKU_UNITTEST')) { 1607675e707SAndreas Gohr throw $e; 1617675e707SAndreas Gohr } 162ecad51ddSAndreas Gohr ErrorHandler::logException($e); 163985f440fSDamien Regad $msg = 'Something unforeseen has happened: ' . $e->getMessage(); 16464ab5140SAndreas Gohr nice_die(hsc($msg)); 16564ab5140SAndreas Gohr } 16664ab5140SAndreas Gohr 16764ab5140SAndreas Gohr /** 16864ab5140SAndreas Gohr * Load the given action 16964ab5140SAndreas Gohr * 17073522543SAndreas Gohr * This translates the given name to a class name by uppercasing the first letter. 17173522543SAndreas Gohr * Underscores translate to camelcase names. For actions with underscores, the different 17273522543SAndreas Gohr * parts are removed beginning from the end until a matching class is found. The instatiated 17373522543SAndreas Gohr * Action will always have the full original action set as Name 17473522543SAndreas Gohr * 17573522543SAndreas Gohr * Example: 'export_raw' -> ExportRaw then 'export' -> 'Export' 17673522543SAndreas Gohr * 17764ab5140SAndreas Gohr * @param $actionname 17864ab5140SAndreas Gohr * @return AbstractAction 17964ab5140SAndreas Gohr * @throws NoActionException 18064ab5140SAndreas Gohr */ 181*d868eb89SAndreas Gohr public function loadAction($actionname) 182*d868eb89SAndreas Gohr { 18373522543SAndreas Gohr $actionname = strtolower($actionname); // FIXME is this needed here? should we run a cleanup somewhere else? 18473522543SAndreas Gohr $parts = explode('_', $actionname); 18524870174SAndreas Gohr while ($parts !== []) { 18624870174SAndreas Gohr $load = implode('_', $parts); 18773522543SAndreas Gohr $class = 'dokuwiki\\Action\\' . str_replace('_', '', ucwords($load, '_')); 18864ab5140SAndreas Gohr if (class_exists($class)) { 18973522543SAndreas Gohr return new $class($actionname); 19064ab5140SAndreas Gohr } 19173522543SAndreas Gohr array_pop($parts); 19273522543SAndreas Gohr } 19373522543SAndreas Gohr 19464ab5140SAndreas Gohr throw new NoActionException(); 19564ab5140SAndreas Gohr } 19664ab5140SAndreas Gohr 19764ab5140SAndreas Gohr /** 198480336a3SAndreas Gohr * Execute all the checks to see if this action can be executed 199480336a3SAndreas Gohr * 200480336a3SAndreas Gohr * @param AbstractAction $action 201480336a3SAndreas Gohr * @throws ActionDisabledException 202480336a3SAndreas Gohr * @throws ActionException 203480336a3SAndreas Gohr */ 204*d868eb89SAndreas Gohr public function checkAction(AbstractAction $action) 205*d868eb89SAndreas Gohr { 206480336a3SAndreas Gohr global $INFO; 207480336a3SAndreas Gohr global $ID; 208480336a3SAndreas Gohr 209480336a3SAndreas Gohr if (in_array($action->getActionName(), $this->disabled)) { 210480336a3SAndreas Gohr throw new ActionDisabledException(); 211480336a3SAndreas Gohr } 212480336a3SAndreas Gohr 213b2c9cd19SAndreas Gohr $action->checkPreconditions(); 214480336a3SAndreas Gohr 215480336a3SAndreas Gohr if (isset($INFO)) { 216480336a3SAndreas Gohr $perm = $INFO['perm']; 217480336a3SAndreas Gohr } else { 218480336a3SAndreas Gohr $perm = auth_quickaclcheck($ID); 219480336a3SAndreas Gohr } 220480336a3SAndreas Gohr 221480336a3SAndreas Gohr if ($perm < $action->minimumPermission()) { 222480336a3SAndreas Gohr throw new ActionException('denied'); 223480336a3SAndreas Gohr } 224480336a3SAndreas Gohr } 225480336a3SAndreas Gohr 226480336a3SAndreas Gohr /** 22764ab5140SAndreas Gohr * Returns the action handling the current request 22864ab5140SAndreas Gohr * 22964ab5140SAndreas Gohr * @return AbstractAction 23064ab5140SAndreas Gohr */ 231*d868eb89SAndreas Gohr public function getAction() 232*d868eb89SAndreas Gohr { 23364ab5140SAndreas Gohr return $this->action; 23464ab5140SAndreas Gohr } 23564ab5140SAndreas Gohr} 236