ActionRouter.php 6.9 KB

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