AdminController.php 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. <?php
  2. /**
  3. * @package Grav\Plugin\Admin
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. declare(strict_types=1);
  9. namespace Grav\Plugin\Admin\Controllers;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Data\Blueprint;
  12. use Grav\Common\Grav;
  13. use Grav\Common\Language\Language;
  14. use Grav\Common\Page\Interfaces\PageInterface;
  15. use Grav\Common\Page\Page;
  16. use Grav\Common\Page\Pages;
  17. use Grav\Common\Uri;
  18. use Grav\Common\User\Interfaces\UserInterface;
  19. use Grav\Common\Utils;
  20. use Grav\Framework\Controller\Traits\ControllerResponseTrait;
  21. use Grav\Framework\RequestHandler\Exception\PageExpiredException;
  22. use Grav\Framework\Session\SessionInterface;
  23. use Grav\Plugin\Admin\Admin;
  24. use Grav\Plugin\Admin\AdminForm;
  25. use Psr\Http\Message\ResponseInterface;
  26. use Psr\Http\Message\ServerRequestInterface;
  27. use RocketTheme\Toolbox\Session\Message;
  28. abstract class AdminController
  29. {
  30. use ControllerResponseTrait {
  31. createRedirectResponse as traitCreateRedirectResponse;
  32. getErrorJson as traitGetErrorJson;
  33. }
  34. /** @var string */
  35. protected $nonce_action = 'admin-form';
  36. /** @var string */
  37. protected $nonce_name = 'admin-nonce';
  38. /** @var Grav */
  39. protected $grav;
  40. /** @var PageInterface */
  41. protected $page;
  42. /** @var AdminForm|null */
  43. protected $form;
  44. public function __construct(Grav $grav)
  45. {
  46. $this->grav = $grav;
  47. }
  48. /**
  49. * @return PageInterface|null
  50. */
  51. public function getPage(): ?PageInterface
  52. {
  53. return $this->page;
  54. }
  55. /**
  56. * Get currently active form.
  57. *
  58. * @return AdminForm|null
  59. */
  60. public function getActiveForm(): ?AdminForm
  61. {
  62. if (null === $this->form) {
  63. $post = $this->getPost();
  64. $active = $post['__form-name__'] ?? null;
  65. $this->form = $active ? $this->getForm($active) : null;
  66. }
  67. return $this->form;
  68. }
  69. /**
  70. * Get a form.
  71. *
  72. * @param string $name
  73. * @param array $options
  74. * @return AdminForm|null
  75. */
  76. public function getForm(string $name, array $options = []): ?AdminForm
  77. {
  78. $post = $this->getPost();
  79. $page = $this->getPage();
  80. $forms = $page ? $page->forms() : [];
  81. $blueprint = $forms[$name] ?? null;
  82. if (null === $blueprint) {
  83. return null;
  84. }
  85. $active = $post['__form-name__'] ?? null;
  86. $unique_id = $active && $active === $name ? ($post['__unique_form_id__'] ?? null) : null;
  87. $options += [
  88. 'unique_id' => $unique_id,
  89. 'blueprint' => new Blueprint(null, ['form' => $blueprint]),
  90. 'submit_method' => $this->getFormSubmitMethod($name),
  91. 'nonce_name' => $this->nonce_name,
  92. 'nonce_action' => $this->nonce_action,
  93. ];
  94. return new AdminForm($name, $options);
  95. }
  96. abstract protected function getFormSubmitMethod(string $name): callable;
  97. /**
  98. * @param string $route
  99. * @param string|null $lang
  100. * @return string
  101. */
  102. public function getAdminUrl(string $route, string $lang = null): string
  103. {
  104. /** @var Pages $pages */
  105. $pages = $this->grav['pages'];
  106. $admin = $this->getAdmin();
  107. return $pages->baseUrl($lang) . $admin->base . $route;
  108. }
  109. /**
  110. * @param string $route
  111. * @param string|null $lang
  112. * @return string
  113. */
  114. public function getAbsoluteAdminUrl(string $route, string $lang = null): string
  115. {
  116. /** @var Pages $pages */
  117. $pages = $this->grav['pages'];
  118. $admin = $this->getAdmin();
  119. return $pages->baseUrl($lang, true) . $admin->base . $route;
  120. }
  121. /**
  122. * Get session.
  123. *
  124. * @return SessionInterface
  125. */
  126. public function getSession(): SessionInterface
  127. {
  128. return $this->grav['session'];
  129. }
  130. /**
  131. * @return Admin
  132. */
  133. protected function getAdmin(): Admin
  134. {
  135. return $this->grav['admin'];
  136. }
  137. /**
  138. * @return UserInterface
  139. */
  140. protected function getUser(): UserInterface
  141. {
  142. return $this->getAdmin()->user;
  143. }
  144. /**
  145. * @return ServerRequestInterface
  146. */
  147. public function getRequest(): ServerRequestInterface
  148. {
  149. return $this->getAdmin()->request;
  150. }
  151. /**
  152. * @return array
  153. */
  154. public function getPost(): array
  155. {
  156. return (array)($this->getRequest()->getParsedBody() ?? []);
  157. }
  158. /**
  159. * Translate a string.
  160. *
  161. * @param string $string
  162. * @param mixed ...$args
  163. * @return string
  164. */
  165. public function translate(string $string, ...$args): string
  166. {
  167. /** @var Language $language */
  168. $language = $this->grav['language'];
  169. array_unshift($args, $string);
  170. return $language->translate($args);
  171. }
  172. /**
  173. * Set message to be shown in the admin.
  174. *
  175. * @param string $message
  176. * @param string $type
  177. * @return $this
  178. */
  179. public function setMessage(string $message, string $type = 'info'): AdminController
  180. {
  181. /** @var Message $messages */
  182. $messages = $this->grav['messages'];
  183. $messages->add($message, $type);
  184. return $this;
  185. }
  186. /**
  187. * @return Config
  188. */
  189. protected function getConfig(): Config
  190. {
  191. return $this->grav['config'];
  192. }
  193. /**
  194. * Check if request nonce is valid.
  195. *
  196. * @return void
  197. * @throws PageExpiredException If nonce is not valid.
  198. */
  199. protected function checkNonce(): void
  200. {
  201. $nonce = null;
  202. $nonce_name = $this->form ? $this->form->getNonceName() : $this->nonce_name;
  203. $nonce_action = $this->form ? $this->form->getNonceAction() : $this->nonce_action;
  204. if (\in_array(strtoupper($this->getRequest()->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
  205. $post = $this->getPost();
  206. $nonce = $post[$nonce_name] ?? null;
  207. }
  208. /** @var Uri $uri */
  209. $uri = $this->grav['uri'];
  210. if (!$nonce) {
  211. $nonce = $uri->param($nonce_name);
  212. }
  213. if (!$nonce) {
  214. $nonce = $uri->query($nonce_name);
  215. }
  216. if (!$nonce || !Utils::verifyNonce($nonce, $nonce_action)) {
  217. throw new PageExpiredException($this->getRequest());
  218. }
  219. }
  220. /**
  221. * Return the best matching mime type for the request.
  222. *
  223. * @param string[] $compare
  224. * @return string|null
  225. */
  226. protected function getAccept(array $compare): ?string
  227. {
  228. $accepted = [];
  229. foreach ($this->getRequest()->getHeader('Accept') as $accept) {
  230. foreach (explode(',', $accept) as $item) {
  231. if (!$item) {
  232. continue;
  233. }
  234. $split = explode(';q=', $item);
  235. $mime = array_shift($split);
  236. $priority = array_shift($split) ?? 1.0;
  237. $accepted[$mime] = $priority;
  238. }
  239. }
  240. arsort($accepted);
  241. // TODO: add support for image/* etc
  242. $list = array_intersect($compare, array_keys($accepted));
  243. if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) {
  244. return reset($compare) ?: null;
  245. }
  246. return reset($list) ?: null;
  247. }
  248. /**
  249. * @param string $template
  250. * @return PageInterface
  251. */
  252. protected function createPage(string $template): PageInterface
  253. {
  254. $page = new Page();
  255. // Plugins may not have the correct Cache-Control header set, force no-store for the proxies.
  256. $page->expires(0);
  257. $filename = "plugin://admin/pages/admin/{$template}.md";
  258. if (!file_exists($filename)) {
  259. throw new \RuntimeException(sprintf('Creating admin page %s failed: not found', $template));
  260. }
  261. Admin::DEBUG && Admin::addDebugMessage("Admin page: {$template}");
  262. $page->init(new \SplFileInfo($filename));
  263. $page->slug($template);
  264. return $page;
  265. }
  266. /**
  267. * @param string|null $url
  268. * @param int|null $code
  269. * @return ResponseInterface
  270. */
  271. protected function createRedirectResponse(string $url = null, int $code = null): ResponseInterface
  272. {
  273. $request = $this->getRequest();
  274. if (null === $url || '' === $url) {
  275. $url = (string)$request->getUri();
  276. } elseif (mb_strpos($url, '/') === 0) {
  277. $url = $this->getAbsoluteAdminUrl($url);
  278. }
  279. if (null === $code) {
  280. if (in_array($request->getMethod(), ['GET', 'HEAD'])) {
  281. $code = 302;
  282. } else {
  283. $code = 303;
  284. }
  285. }
  286. return $this->traitCreateRedirectResponse($url, $code);
  287. }
  288. /**
  289. * @param \Throwable $e
  290. * @return array
  291. */
  292. protected function getErrorJson(\Throwable $e): array
  293. {
  294. $json = $this->traitGetErrorJson($e);
  295. $code = $e->getCode();
  296. if ($code === 401) {
  297. $json['redirect'] = $this->getAbsoluteAdminUrl('/');
  298. }
  299. return $json;
  300. }
  301. }