You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

236 lines
6.9 KiB

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