Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
No puede seleccionar más de 25 temas Los temas deben comenzar con una letra o número, pueden incluir guiones ('-') y pueden tener hasta 35 caracteres de largo.
 
 
 
 
 
 

1914 líneas
61 KiB

  1. <?php
  2. namespace Grav\Plugin\FlexObjects\Admin;
  3. use Exception;
  4. use Grav\Common\Cache;
  5. use Grav\Common\Config\Config;
  6. use Grav\Common\Data\Data;
  7. use Grav\Common\Debugger;
  8. use Grav\Common\Filesystem\Folder;
  9. use Grav\Common\Flex\Types\Pages\PageCollection;
  10. use Grav\Common\Flex\Types\Pages\PageIndex;
  11. use Grav\Common\Flex\Types\Pages\PageObject;
  12. use Grav\Common\Grav;
  13. use Grav\Common\Helpers\Excerpts;
  14. use Grav\Common\Language\Language;
  15. use Grav\Common\Page\Interfaces\PageInterface;
  16. use Grav\Common\Uri;
  17. use Grav\Common\User\Interfaces\UserInterface;
  18. use Grav\Common\Utils;
  19. use Grav\Framework\Controller\Traits\ControllerResponseTrait;
  20. use Grav\Framework\File\Formatter\CsvFormatter;
  21. use Grav\Framework\File\Formatter\YamlFormatter;
  22. use Grav\Framework\File\Interfaces\FileFormatterInterface;
  23. use Grav\Framework\Flex\FlexForm;
  24. use Grav\Framework\Flex\FlexFormFlash;
  25. use Grav\Framework\Flex\FlexObject;
  26. use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
  27. use Grav\Framework\Flex\Interfaces\FlexCollectionInterface;
  28. use Grav\Framework\Flex\Interfaces\FlexDirectoryInterface;
  29. use Grav\Framework\Flex\Interfaces\FlexFormInterface;
  30. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  31. use Grav\Framework\Flex\Interfaces\FlexTranslateInterface;
  32. use Grav\Framework\Object\Interfaces\ObjectInterface;
  33. use Grav\Framework\Psr7\Response;
  34. use Grav\Framework\RequestHandler\Exception\RequestException;
  35. use Grav\Framework\Route\Route;
  36. use Grav\Framework\Route\RouteFactory;
  37. use Grav\Plugin\Admin\Admin;
  38. use Grav\Plugin\FlexObjects\Controllers\MediaController;
  39. use Grav\Plugin\FlexObjects\Flex;
  40. use Nyholm\Psr7\ServerRequest;
  41. use Psr\Http\Message\ResponseInterface;
  42. use Psr\Http\Message\ServerRequestInterface;
  43. use RocketTheme\Toolbox\Event\Event;
  44. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  45. use RocketTheme\Toolbox\Session\Message;
  46. use RuntimeException;
  47. use function dirname;
  48. use function in_array;
  49. use function is_array;
  50. use function is_callable;
  51. /**
  52. * Class AdminController
  53. * @package Grav\Plugin\FlexObjects
  54. */
  55. class AdminController
  56. {
  57. use ControllerResponseTrait;
  58. /** @var AdminController|null */
  59. private static $instance;
  60. /** @var Grav */
  61. public $grav;
  62. /** @var string */
  63. public $view;
  64. /** @var string */
  65. public $task;
  66. /** @var Route|null */
  67. public $route;
  68. /** @var array */
  69. public $post;
  70. /** @var array|null */
  71. public $data;
  72. /** @var array */
  73. protected $adminRoutes;
  74. /** @var Uri */
  75. protected $uri;
  76. /** @var Admin */
  77. protected $admin;
  78. /** @var UserInterface */
  79. protected $user;
  80. /** @var string */
  81. protected $redirect;
  82. /** @var int */
  83. protected $redirectCode;
  84. /** @var Route */
  85. protected $currentRoute;
  86. /** @var Route */
  87. protected $referrerRoute;
  88. /** @var string|null */
  89. protected $action;
  90. /** @var string|null */
  91. protected $location;
  92. /** @var string|null */
  93. protected $target;
  94. /** @var string|null */
  95. protected $id;
  96. /** @var bool */
  97. protected $active;
  98. /** @var FlexObjectInterface|false|null */
  99. protected $object;
  100. /** @var FlexCollectionInterface|null */
  101. protected $collection;
  102. /** @var FlexDirectoryInterface|null */
  103. protected $directory;
  104. /** @var string */
  105. protected $nonce_name = 'admin-nonce';
  106. /** @var string */
  107. protected $nonce_action = 'admin-form';
  108. /** @var string */
  109. protected $task_prefix = 'task';
  110. /** @var string */
  111. protected $action_prefix = 'action';
  112. /**
  113. * Unknown task, call onFlexTask[NAME] event.
  114. *
  115. * @return void
  116. */
  117. public function taskDefault(): void
  118. {
  119. $type = $this->target;
  120. $directory = $this->getDirectory($type);
  121. if (!$directory) {
  122. throw new RuntimeException('Not Found', 404);
  123. }
  124. $object = $this->getObject();
  125. $key = $this->id;
  126. if ($object && $object->exists()) {
  127. $event = new Event(
  128. [
  129. 'type' => $type,
  130. 'key' => $key,
  131. 'admin' => $this->admin,
  132. 'flex' => $this->getFlex(),
  133. 'directory' => $directory,
  134. 'object' => $object,
  135. 'data' => $this->data,
  136. 'user' => $this->user,
  137. 'redirect' => $this->redirect
  138. ]
  139. );
  140. try {
  141. $this->grav->fireEvent('onFlexTask' . ucfirst($this->task), $event);
  142. } catch (Exception $e) {
  143. /** @var Debugger $debugger */
  144. $debugger = $this->grav['debugger'];
  145. $debugger->addException($e);
  146. $this->admin->setMessage($e->getMessage(), 'error');
  147. }
  148. $redirect = $event['redirect'];
  149. if ($redirect) {
  150. $this->setRedirect($redirect);
  151. }
  152. }
  153. }
  154. /**
  155. * Default action, onFlexAction[NAME] event.
  156. *
  157. * @return void
  158. */
  159. public function actionDefault(): void
  160. {
  161. $type = $this->target;
  162. $directory = $this->getDirectory($type);
  163. if (!$directory) {
  164. throw new RuntimeException('Not Found', 404);
  165. }
  166. $object = $this->getObject();
  167. $key = $this->id;
  168. if ($object && $object->exists()) {
  169. $event = new Event(
  170. [
  171. 'type' => $type,
  172. 'key' => $key,
  173. 'admin' => $this->admin,
  174. 'flex' => $this->getFlex(),
  175. 'directory' => $directory,
  176. 'object' => $object,
  177. 'user' => $this->user,
  178. 'redirect' => $this->redirect
  179. ]
  180. );
  181. try {
  182. $this->grav->fireEvent('onFlexAction' . ucfirst($this->action), $event);
  183. } catch (Exception $e) {
  184. /** @var Debugger $debugger */
  185. $debugger = $this->grav['debugger'];
  186. $debugger->addException($e);
  187. $this->admin->setMessage($e->getMessage(), 'error');
  188. }
  189. $redirect = $event['redirect'];
  190. if ($redirect) {
  191. $this->setRedirect($redirect);
  192. }
  193. }
  194. }
  195. /**
  196. * Get datatable for list view.
  197. *
  198. * @return ResponseInterface|null
  199. */
  200. public function actionList(): ?ResponseInterface
  201. {
  202. $directory = $this->getDirectory();
  203. if (!$directory) {
  204. throw new RuntimeException('Not Found', 404);
  205. }
  206. // Check authorization.
  207. if (!$directory->isAuthorized('list', 'admin', $this->user)) {
  208. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' list.', 403);
  209. }
  210. /** @var Uri $uri */
  211. $uri = $this->grav['uri'];
  212. if ($uri->extension() === 'json') {
  213. $options = [
  214. 'collection' => $this->getCollection(),
  215. 'url' => $uri->path(),
  216. 'page' => $uri->query('page'),
  217. 'limit' => $uri->query('per_page'),
  218. 'sort' => $uri->query('sort'),
  219. 'search' => $uri->query('filter'),
  220. 'filters' => $uri->query('filters'),
  221. ];
  222. $table = $this->getFlex()->getDataTable($directory, $options);
  223. return $this->createJsonResponse($table->jsonSerialize());
  224. }
  225. return null;
  226. }
  227. /**
  228. * Alias for Export action.
  229. *
  230. * @return ResponseInterface|null
  231. */
  232. public function actionCsv(): ?ResponseInterface
  233. {
  234. return $this->actionExport();
  235. }
  236. /**
  237. * Export action. Defaults to CVS export.
  238. *
  239. * @return ResponseInterface|null
  240. */
  241. public function actionExport(): ?ResponseInterface
  242. {
  243. $collection = $this->getCollection();
  244. if (!$collection) {
  245. throw new RuntimeException('Not Found', 404);
  246. }
  247. // Check authorization.
  248. $directory = $collection->getFlexDirectory();
  249. $authorized = is_callable([$collection, 'isAuthorized'])
  250. ? $collection->isAuthorized('read', 'admin', $this->user)
  251. : $directory->isAuthorized('read', 'admin', $this->user);
  252. if (!$authorized) {
  253. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' read.', 403);
  254. }
  255. $config = $collection->getFlexDirectory()->getConfig('admin.views.export') ?? $collection->getFlexDirectory()->getConfig('admin.export') ?? false;
  256. if (!$config || empty($config['enabled'])) {
  257. throw new RuntimeException($this->admin::translate('Not Found'), 404);
  258. }
  259. $queryParams = $this->getRequest()->getQueryParams();
  260. $type = $queryParams['type'] ?? null;
  261. if ($type) {
  262. $config = $config['options'][$type] ?? null;
  263. if (!$config) {
  264. throw new RuntimeException($this->admin::translate('Not Found'), 404);
  265. }
  266. }
  267. $defaultFormatter = CsvFormatter::class;
  268. $class = trim($config['formatter']['class'] ?? $defaultFormatter, '\\');
  269. $method = $config['method'] ?? ($class === $defaultFormatter ? 'csvSerialize' : 'jsonSerialize');
  270. if (!class_exists($class)) {
  271. throw new RuntimeException($this->admin::translate('Formatter Not Found'), 404);
  272. }
  273. /** @var FileFormatterInterface $formatter */
  274. $formatter = new $class($config['formatter']['options'] ?? []);
  275. $filename = ($config['filename'] ?? 'export') . $formatter->getDefaultFileExtension();
  276. if (method_exists($collection, $method)) {
  277. $list = $type ? $collection->{$method}($type) : $collection->{$method}();
  278. } else {
  279. $list = [];
  280. /** @var ObjectInterface $object */
  281. foreach ($collection as $object) {
  282. if (method_exists($object, $method)) {
  283. $data = $object->{$method}();
  284. if ($data) {
  285. $list[] = $data;
  286. }
  287. } else {
  288. $list[] = $object->jsonSerialize();
  289. }
  290. }
  291. }
  292. $response = new Response(
  293. 200,
  294. [
  295. 'Content-Type' => $formatter->getMimeType(),
  296. 'Content-Disposition' => 'inline; filename="' . $filename . '"',
  297. 'Expires' => 'Mon, 26 Jul 1997 05:00:00 GMT',
  298. 'Last-Modified' => gmdate('D, d M Y H:i:s') . ' GMT',
  299. 'Cache-Control' => 'no-store, no-cache, must-revalidate',
  300. 'Pragma' => 'no-cache',
  301. ],
  302. $formatter->encode($list)
  303. );
  304. return $response;
  305. }
  306. /**
  307. * Delete object from directory.
  308. *
  309. * @return void
  310. */
  311. public function taskDelete(): void
  312. {
  313. $directory = $this->getDirectory();
  314. if (!$directory) {
  315. throw new RuntimeException('Not Found', 404);
  316. }
  317. $object = null;
  318. try {
  319. $object = $this->getObject();
  320. if ($object && $object->exists()) {
  321. $authorized = $object instanceof FlexAuthorizeInterface
  322. ? $object->isAuthorized('delete', 'admin', $this->user)
  323. : $directory->isAuthorized('delete', 'admin', $this->user);
  324. if (!$authorized) {
  325. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' delete.', 403);
  326. }
  327. $object->delete();
  328. $this->admin->setMessage($this->admin::translate('PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_DELETE_SUCCESS'));
  329. if ($this->currentRoute->withoutGravParams()->getRoute() === $this->referrerRoute->getRoute()) {
  330. $redirect = dirname($this->currentRoute->withoutGravParams()->toString(true));
  331. } else {
  332. $redirect = $this->referrerRoute->toString(true);
  333. }
  334. $this->setRedirect($redirect);
  335. $this->grav->fireEvent('onFlexAfterDelete', new Event(['type' => 'flex', 'object' => $object]));
  336. }
  337. } catch (RuntimeException $e) {
  338. $this->admin->setMessage($this->admin::translate(['PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_DELETE_FAILURE', $e->getMessage()]), 'error');
  339. $this->setRedirect($this->referrerRoute->toString(true), 302);
  340. }
  341. }
  342. /**
  343. * Create a new empty folder (from modal).
  344. *
  345. * TODO: Move pages specific logic
  346. *
  347. * @return void
  348. */
  349. public function taskSaveNewFolder(): void
  350. {
  351. $directory = $this->getDirectory();
  352. if (!$directory) {
  353. throw new RuntimeException('Not Found', 404);
  354. }
  355. $collection = $directory->getIndex();
  356. if (!($collection instanceof PageCollection || $collection instanceof PageIndex)) {
  357. throw new RuntimeException('Task saveNewFolder works only for pages', 400);
  358. }
  359. $data = $this->data;
  360. $route = trim($data['route'] ?? '', '/');
  361. // TODO: Folder name needs to be validated! However we test against /="' as they are dangerous characters.
  362. $folder = mb_strtolower($data['folder'] ?? '');
  363. if ($folder === '' || preg_match('![="\']!u', $folder) !== 0) {
  364. throw new RuntimeException('Creating folder failed, bad folder name', 400);
  365. }
  366. $parent = $route ? $directory->getObject($route) : $collection->getRoot();
  367. if (!$parent instanceof PageObject) {
  368. throw new RuntimeException('Creating folder failed, bad parent route', 400);
  369. }
  370. if (!$parent->isAuthorized('create', 'admin', $this->user)) {
  371. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' create.', 403);
  372. }
  373. $path = $parent->getFlexDirectory()->getStorageFolder($parent->getStorageKey());
  374. if (!$path) {
  375. throw new RuntimeException('Creating folder failed, bad parent storage path', 400);
  376. }
  377. // Ordering
  378. $orders = $parent->children()->visible()->getProperty('order');
  379. $maxOrder = 0;
  380. foreach ($orders as $order) {
  381. $maxOrder = max($maxOrder, (int)$order);
  382. }
  383. $orderOfNewFolder = $maxOrder ? sprintf('%02d.', $maxOrder+1) : '';
  384. $new_path = $path . '/' . $orderOfNewFolder . $folder;
  385. /** @var UniformResourceLocator $locator */
  386. $locator = $this->grav['locator'];
  387. if ($locator->isStream($new_path)) {
  388. $new_path = $locator->findResource($new_path, true, true);
  389. } else {
  390. $new_path = GRAV_ROOT . '/' . $new_path;
  391. }
  392. Folder::create($new_path);
  393. Cache::clearCache('invalidate');
  394. $directory->getCache('index')->clear();
  395. $this->grav->fireEvent('onAdminAfterSaveAs', new Event(['path' => $new_path]));
  396. $this->admin->setMessage($this->admin::translate('PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_NEW_FOLDER_SUCCESS'));
  397. $this->setRedirect($this->referrerRoute->toString(true));
  398. }
  399. /**
  400. * Create a new object (from modal).
  401. *
  402. * TODO: Move pages specific logic
  403. *
  404. * @return void
  405. */
  406. public function taskContinue(): void
  407. {
  408. $directory = $this->getDirectory();
  409. if (!$directory) {
  410. throw new RuntimeException('Not Found', 404);
  411. }
  412. if ($directory->getObject() instanceof PageInterface) {
  413. $this->continuePages($directory);
  414. } else {
  415. $this->continue($directory);
  416. }
  417. }
  418. /**
  419. * @param FlexDirectoryInterface $directory
  420. * @return void
  421. */
  422. protected function continue(FlexDirectoryInterface $directory): void
  423. {
  424. $config = $directory->getConfig('admin');
  425. $supported = !empty($config['modals']['add']);
  426. if (!$supported) {
  427. throw new RuntimeException('Task continue is not supported by the type', 400);
  428. }
  429. $authorized = $directory->isAuthorized('create', 'admin', $this->user);
  430. if (!$authorized) {
  431. $this->setRedirect($this->referrerRoute->toString(true));
  432. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' add.', 403);
  433. }
  434. $this->object = $directory->createObject($this->data, '');
  435. // Reset form, we are starting from scratch.
  436. /** @var FlexForm $form */
  437. $form = $this->object->getForm('', ['reset' => true]);
  438. /** @var FlexFormFlash $flash */
  439. $flash = $form->getFlash();
  440. $flash->setUrl($this->getFlex()->adminRoute($this->object));
  441. $flash->save(true);
  442. $this->setRedirect($flash->getUrl());
  443. }
  444. /**
  445. * Create a new page (from modal).
  446. *
  447. * TODO: Move pages specific logic
  448. *
  449. * @return void
  450. */
  451. protected function continuePages(FlexDirectoryInterface $directory): void
  452. {
  453. $this->data['route'] = '/' . trim($this->data['route'] ?? '', '/');
  454. $route = trim($this->data['route'], '/');
  455. if ($route) {
  456. $parent = $directory->getObject($route);
  457. } else {
  458. // Use root page or fail back to directory auth.
  459. $index = $directory->getIndex();
  460. $parent = $index->getRoot() ?? $directory;
  461. }
  462. $authorized = $parent instanceof FlexAuthorizeInterface
  463. ? $parent->isAuthorized('create', 'admin', $this->user)
  464. : $directory->isAuthorized('create', 'admin', $this->user);
  465. if (!$authorized) {
  466. $this->setRedirect($this->referrerRoute->toString(true));
  467. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' add.', 403);
  468. }
  469. $folder = $this->data['folder'] ?? null;
  470. $title = $this->data['title'] ?? null;
  471. if ($title) {
  472. $this->data['header']['title'] = $this->data['title'];
  473. unset($this->data['title']);
  474. }
  475. if (null !== $folder && 0 === strpos($folder, '@slugify-')) {
  476. $folder = \Grav\Plugin\Admin\Utils::slug($this->data[substr($folder, 9)] ?? '');
  477. }
  478. if (!$folder) {
  479. $folder = \Grav\Plugin\Admin\Utils::slug($title) ?: '';
  480. }
  481. $folder = ltrim($folder, '_');
  482. if ($folder === '' || mb_strpos($folder, '/') !== false) {
  483. throw new RuntimeException('Creating page failed: bad folder name', 400);
  484. }
  485. if (!isset($this->data['name'])) {
  486. // Get default child type.
  487. $this->data['name'] = $parent->header()->child_type ?? $parent->getBlueprint()->child_type ?? 'default';
  488. }
  489. if (strpos($this->data['name'], 'modular/') === 0) {
  490. $this->data['header']['body_classes'] = 'modular';
  491. $folder = '_' . $folder;
  492. }
  493. $this->data['folder'] = $folder;
  494. unset($this->data['blueprint']);
  495. $key = trim("{$route}/{$folder}", '/');
  496. if ($directory->getObject($key)) {
  497. throw new RuntimeException("Page '/{$key}' already exists!", 403);
  498. }
  499. $max = 0;
  500. if (isset($this->data['visible'])) {
  501. $auto = $this->data['visible'] === '';
  502. $visible = (bool)($this->data['visible'] ?? false);
  503. unset($this->data['visible']);
  504. // Empty string on visible means auto.
  505. if ($auto || $visible) {
  506. $children = $parent ? $parent->children()->visible() : [];
  507. $max = $auto ? 0 : 1;
  508. foreach ($children as $child) {
  509. $max = max($max, (int)$child->order());
  510. }
  511. }
  512. $this->data['order'] = $max ? $max + 1 : false;
  513. }
  514. $this->data['lang'] = $this->getLanguage();
  515. $header = $this->data['header'] ?? [];
  516. $this->grav->fireEvent('onAdminCreatePageFrontmatter', new Event(['header' => &$header,
  517. 'data' => $this->data]));
  518. $formatter = new YamlFormatter();
  519. $this->data['frontmatter'] = $formatter->encode($header);
  520. $this->data['header'] = $header;
  521. $this->object = $directory->createObject($this->data, $key);
  522. // Reset form, we are starting from scratch.
  523. /** @var FlexForm $form */
  524. $form = $this->object->getForm('', ['reset' => true]);
  525. /** @var FlexFormFlash $flash */
  526. $flash = $form->getFlash();
  527. $flash->setUrl($this->getFlex()->adminRoute($this->object));
  528. $flash->save(true);
  529. // Store the name and route of a page, to be used pre-filled defaults of the form in the future
  530. $this->admin->session()->lastPageName = $this->data['name'] ?? '';
  531. $this->admin->session()->lastPageRoute = $this->data['route'] ?? '';
  532. $this->setRedirect($flash->getUrl());
  533. }
  534. /**
  535. * Save page as a new copy.
  536. *
  537. * Route: /pages
  538. *
  539. * @return void
  540. * @throws RuntimeException
  541. */
  542. protected function taskCopy(): void
  543. {
  544. try {
  545. $directory = $this->getDirectory();
  546. if (!$directory) {
  547. throw new RuntimeException('Not Found', 404);
  548. }
  549. $object = $this->getObject();
  550. if (!$object || !$object->exists() || !is_callable([$object, 'createCopy'])) {
  551. throw new RuntimeException('Not Found', 404);
  552. }
  553. // Pages are a special case.
  554. $parent = $object instanceof PageInterface ? $object->parent() : $object;
  555. $authorized = $parent instanceof FlexAuthorizeInterface
  556. ? $parent->isAuthorized('create', 'admin', $this->user)
  557. : $directory->isAuthorized('create', 'admin', $this->user);
  558. if (!$authorized || !$parent) {
  559. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' copy.',
  560. 403);
  561. }
  562. if ($object instanceof PageInterface && is_array($this->data)) {
  563. $data = $this->data;
  564. $blueprints = $this->admin->blueprints('admin/pages/move');
  565. $blueprints->validate($data);
  566. $data = $blueprints->filter($data, true, true);
  567. // Hack for pages
  568. $data['name'] = $data['name'] ?? $object->template();
  569. $data['ordering'] = (int)$object->order() > 0;
  570. $data['order'] = null;
  571. if (isset($data['title'])) {
  572. $data['header']['title'] = $data['title'];
  573. unset($data['title']);
  574. }
  575. $object->order(false);
  576. $object->update($data);
  577. }
  578. $object = $object->createCopy();
  579. $this->admin->setMessage($this->admin::translate('PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_COPY_SUCCESS'));
  580. $this->setRedirect($this->getFlex()->adminRoute($object));
  581. } catch (RuntimeException $e) {
  582. $this->admin->setMessage($this->admin::translate(['PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_COPY_FAILURE', $e->getMessage()]), 'error');
  583. $this->setRedirect($this->referrerRoute->toString(true), 302);
  584. }
  585. }
  586. /**
  587. * $data['route'] = $this->grav['uri']->param('route');
  588. * $data['sortby'] = $this->grav['uri']->param('sortby', null);
  589. * $data['filters'] = $this->grav['uri']->param('filters', null);
  590. * $data['page'] $this->grav['uri']->param('page', true);
  591. * $data['base'] = $this->grav['uri']->param('base');
  592. * $initial = (bool) $this->grav['uri']->param('initial');
  593. *
  594. * @return ResponseInterface
  595. * @throws RequestException
  596. * @TODO: Move pages specific logic
  597. */
  598. protected function actionGetLevelListing(): ResponseInterface
  599. {
  600. /** @var PageInterface|FlexObjectInterface $object */
  601. $object = $this->getObject($this->id ?? '');
  602. if (!$object || !method_exists($object, 'getLevelListing')) {
  603. throw new RuntimeException('Not Found', 404);
  604. }
  605. $request = $this->getRequest();
  606. $data = $request->getParsedBody();
  607. if (!isset($data['field'])) {
  608. throw new RequestException($request, 'Bad Request', 400);
  609. }
  610. // Base64 decode the route
  611. $data['route'] = isset($data['route']) ? base64_decode($data['route']) : null;
  612. $data['filters'] = json_decode($data['filters'] ?? '{}', true, 512, JSON_THROW_ON_ERROR) + ['type' => ['root', 'dir']];
  613. $initial = $data['initial'] ?? null;
  614. if ($initial) {
  615. $data['leaf_route'] = $data['route'];
  616. $data['route'] = null;
  617. $data['level'] = 1;
  618. }
  619. [$status, $message, $response,$route] = $object->getLevelListing($data);
  620. $json = [
  621. 'status' => $status,
  622. 'message' => $this->admin::translate($message ?? 'PLUGIN_ADMIN.NO_ROUTE_PROVIDED'),
  623. 'route' => $route,
  624. 'initial' => (bool)$initial,
  625. 'data' => array_values($response)
  626. ];
  627. return $this->createJsonResponse($json, 200);
  628. }
  629. /**
  630. * $data['route'] = $this->grav['uri']->param('route');
  631. * $data['sortby'] = $this->grav['uri']->param('sortby', null);
  632. * $data['filters'] = $this->grav['uri']->param('filters', null);
  633. * $data['page'] $this->grav['uri']->param('page', true);
  634. * $data['base'] = $this->grav['uri']->param('base');
  635. * $initial = (bool) $this->grav['uri']->param('initial');
  636. *
  637. * @return ResponseInterface
  638. * @throws RequestException
  639. * @TODO: Move pages specific logic
  640. */
  641. protected function actionListLevel(): ResponseInterface
  642. {
  643. try {
  644. /** @var PageInterface|FlexObjectInterface $object */
  645. $object = $this->getObject('');
  646. if (!$object || !method_exists($object, 'getLevelListing')) {
  647. throw new RuntimeException('Not Found', 404);
  648. }
  649. $directory = $object->getFlexDirectory();
  650. if (!$directory->isAuthorized('list', 'admin', $this->user)) {
  651. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' getLevelListing.',
  652. 403);
  653. }
  654. $request = $this->getRequest();
  655. $data = $request->getParsedBody();
  656. // Base64 decode the route
  657. $data['route'] = isset($data['route']) ? base64_decode($data['route']) : null;
  658. $data['filters'] = ($data['filters'] ?? []) + ['type' => ['dir']];
  659. $data['lang'] = $this->getLanguage();
  660. // Display root if permitted.
  661. $action = $directory->getConfig('admin.views.configure.authorize') ?? $directory->getConfig('admin.configure.authorize') ?? 'admin.super';
  662. if ($this->user->authorize($action)) {
  663. $data['filters']['type'][] = 'root';
  664. }
  665. $initial = $data['initial'] ?? null;
  666. if ($initial) {
  667. $data['leaf_route'] = $data['route'];
  668. $data['route'] = null;
  669. $data['level'] = 1;
  670. }
  671. [$status, $message, $response, $route] = $object->getLevelListing($data);
  672. $json = [
  673. 'status' => $status,
  674. 'message' => $this->admin::translate($message ?? 'PLUGIN_ADMIN.NO_ROUTE_PROVIDED'),
  675. 'route' => $route,
  676. 'initial' => (bool)$initial,
  677. 'data' => array_values($response)
  678. ];
  679. } catch (Exception $e) {
  680. return $this->createErrorResponse($e);
  681. }
  682. return $this->createJsonResponse($json, 200);
  683. }
  684. /**
  685. * @return ResponseInterface
  686. */
  687. public function taskReset(): ResponseInterface
  688. {
  689. $key = $this->id;
  690. $object = $this->getObject($key);
  691. if (!$object) {
  692. throw new RuntimeException('Not Found', 404);
  693. }
  694. /** @var FlexForm $form */
  695. $form = $this->getForm($object);
  696. $form->getFlash()->delete();
  697. return $this->createRedirectResponse($this->referrerRoute->toString(true));
  698. }
  699. /**
  700. * @return void
  701. */
  702. public function taskSaveas(): void
  703. {
  704. $this->taskSave();
  705. }
  706. /**
  707. * @return void
  708. */
  709. public function taskSave(): void
  710. {
  711. $directory = $this->getDirectory();
  712. if (!$directory) {
  713. throw new RuntimeException('Not Found', 404);
  714. }
  715. $key = $this->id;
  716. try {
  717. $object = $this->getObject($key);
  718. if (!$object) {
  719. throw new RuntimeException('Not Found', 404);
  720. }
  721. $authorized = $object instanceof FlexAuthorizeInterface
  722. ? $object->isAuthorized('save', 'admin', $this->user)
  723. : $directory->isAuthorized($object->exists() ? 'update' : 'create', 'admin', $this->user);
  724. if (!$authorized) {
  725. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' save.',
  726. 403);
  727. }
  728. /** @var ServerRequestInterface $request */
  729. $request = $this->grav['request'];
  730. /** @var FlexForm $form */
  731. $form = $this->getForm($object);
  732. $callable = function (array $data, array $files, FlexObject $object) use ($form) {
  733. if (method_exists($object, 'storeOriginal')) {
  734. $object->storeOriginal();
  735. }
  736. $object->update($data, $files);
  737. // Support for expert mode.
  738. if (str_ends_with($form->getId(), '-raw') && isset($data['frontmatter']) && is_callable([$object, 'frontmatter'])) {
  739. if (!$this->user->authorize('admin.super')) {
  740. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' save raw.',
  741. 403);
  742. }
  743. $object->frontmatter($data['frontmatter']);
  744. unset($data['frontmatter']);
  745. }
  746. if (is_callable([$object, 'check'])) {
  747. $object->check($this->user);
  748. }
  749. $object->save();
  750. };
  751. $form->setSubmitMethod($callable);
  752. $form->handleRequest($request);
  753. $error = $form->getError();
  754. $errors = $form->getErrors();
  755. if ($errors) {
  756. if ($error) {
  757. $this->admin->setMessage($error, 'error');
  758. }
  759. foreach ($errors as $field => $list) {
  760. foreach ((array)$list as $message) {
  761. $this->admin->setMessage($message, 'error');
  762. }
  763. }
  764. throw new RuntimeException('Form validation failed, please check your input');
  765. }
  766. if ($error) {
  767. throw new RuntimeException($error);
  768. }
  769. $object = $form->getObject();
  770. $objectKey = $object->getKey();
  771. $this->admin->setMessage($this->admin::translate('PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_SAVE_SUCCESS'));
  772. // Set route to point to the current page.
  773. if (!$this->redirect) {
  774. $postAction = $request->getParsedBody()['_post_entries_save'] ?? 'edit';
  775. $this->grav['session']->post_entries_save = $postAction;
  776. if ($postAction === 'create-new') {
  777. // Create another.
  778. $route = $this->referrerRoute->withGravParam('action', null)->withGravParam('', 'add');
  779. } elseif ($postAction === 'list') {
  780. // Back to listing.
  781. $route = $this->currentRoute;
  782. // Remove :add action.
  783. $actionAdd = $key === '' || $route->getGravParam('action') === 'add' || $route->getGravParam('') === 'add';
  784. if ($actionAdd) {
  785. $route = $route->withGravParam('action', null)->withGravParam('', null);
  786. }
  787. $len = ($key === '' ? 0 : -1) - \substr_count($key, '/');
  788. if ($len) {
  789. $route = $route->withRoute($route->getRoute(0, $len));
  790. }
  791. } else {
  792. // Back to edit.
  793. $route = $this->currentRoute;
  794. $isRoot = $object instanceof PageInterface && $object->root();
  795. $hasKeyChanged = !$isRoot && $key !== $objectKey;
  796. // Remove :add action.
  797. $actionAdd = $key === '' || $route->getGravParam('action') === 'add' || $route->getGravParam('') === 'add';
  798. if ($actionAdd) {
  799. $route = $route->withGravParam('action', null)->withGravParam('', null);
  800. }
  801. if ($hasKeyChanged) {
  802. if ($key === '') {
  803. // Append new key.
  804. $path = $route->getRoute() . '/' . $objectKey;
  805. } elseif ($objectKey === '') {
  806. // Remove old key.
  807. $path = preg_replace('|/' . preg_quote($key, '|') . '$|u', '/', $route->getRoute());
  808. } else {
  809. // Replace old key with new key.
  810. $path = preg_replace('|/' . preg_quote($key, '|') . '$|u', '/' . $objectKey, $route->getRoute());
  811. }
  812. $route = $route->withRoute($path);
  813. }
  814. // Make sure we're using the correct language.
  815. $lang = null;
  816. if ($object instanceof FlexTranslateInterface) {
  817. $lang = $object->getLanguage();
  818. $route = $route->withLanguage($lang);
  819. }
  820. }
  821. $this->setRedirect($route->toString(true));
  822. }
  823. $this->grav->fireEvent('onFlexAfterSave', new Event(['type' => 'flex', 'object' => $object]));
  824. } catch (RuntimeException $e) {
  825. $this->admin->setMessage($this->admin::translate(['PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_SAVE_FAILURE', $e->getMessage()]), 'error');
  826. if (isset($object, $form)) {
  827. $data = $form->getData();
  828. if (null !== $data) {
  829. $flash = $form->getFlash();
  830. $flash->setObject($object);
  831. if ($data instanceof Data) {
  832. $flash->setData($data->toArray());
  833. }
  834. $flash->save();
  835. }
  836. }
  837. // $this->setRedirect($this->referrerRoute->withQueryParam('uid', $flash->getUniqueId())->toString(true), 302);
  838. $this->setRedirect($this->referrerRoute->toString(true), 302);
  839. }
  840. }
  841. /**
  842. * @return void
  843. */
  844. public function taskConfigure(): void
  845. {
  846. $directory = $this->getDirectory();
  847. if (!$directory) {
  848. throw new RuntimeException('Not Found', 404);
  849. }
  850. try {
  851. $config = $directory->getConfig('admin.views.configure.authorize') ?? $directory->getConfig('admin.configure.authorize') ?? 'admin.super';
  852. if (!$this->user->authorize($config)) {
  853. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' configure.', 403);
  854. }
  855. /** @var ServerRequestInterface $request */
  856. $request = $this->grav['request'];
  857. /** @var FlexForm $form */
  858. $form = $this->getDirectoryForm();
  859. $form->handleRequest($request);
  860. $error = $form->getError();
  861. $errors = $form->getErrors();
  862. if ($errors) {
  863. if ($error) {
  864. $this->admin->setMessage($error, 'error');
  865. }
  866. foreach ($errors as $field => $list) {
  867. foreach ((array)$list as $message) {
  868. $this->admin->setMessage($message, 'error');
  869. }
  870. }
  871. throw new RuntimeException('Form validation failed, please check your input');
  872. }
  873. if ($error) {
  874. throw new RuntimeException($error);
  875. }
  876. $this->admin->setMessage($this->admin::translate('PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_CONFIGURE_SUCCESS'));
  877. if (!$this->redirect) {
  878. $this->referrerRoute = $this->currentRoute;
  879. $this->setRedirect($this->referrerRoute->toString(true));
  880. }
  881. } catch (RuntimeException $e) {
  882. $this->admin->setMessage($this->admin::translate(['PLUGIN_FLEX_OBJECTS.CONTROLLER.TASK_CONFIGURE_FAILURE', $e->getMessage()]), 'error');
  883. $this->setRedirect($this->referrerRoute->toString(true), 302);
  884. }
  885. }
  886. /**
  887. * Used in 3rd party editors (e.g. next-gen).
  888. *
  889. * @return ResponseInterface
  890. */
  891. public function actionConvertUrls(): ResponseInterface
  892. {
  893. $directory = $this->getDirectory();
  894. if (!$directory) {
  895. throw new RuntimeException('Not Found', 404);
  896. }
  897. $key = $this->id;
  898. $object = $this->getObject($key);
  899. if (!$object instanceof PageInterface) {
  900. throw new RuntimeException('Not Found', 404);
  901. }
  902. $authorized = $object instanceof FlexAuthorizeInterface
  903. ? $object->isAuthorized('read', 'admin', $this->user)
  904. : $directory->isAuthorized($object->exists() ? 'read' : 'create', 'admin', $this->user);
  905. if (!$authorized) {
  906. throw new RuntimeException($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' save.',
  907. 403);
  908. }
  909. $request = $this->getRequest();
  910. $data = $request->getParsedBody();
  911. $data['data'] = json_decode($data['data'] ?? '{}', true, 512, JSON_THROW_ON_ERROR);
  912. if (!isset($data['data'])) {
  913. throw new RequestException($request, 'Bad Request', 400);
  914. }
  915. $converted_links = [];
  916. foreach ($data['data']['a'] ?? [] as $link) {
  917. $converted_links[$link] = Excerpts::processLinkHtml($link, $object);
  918. }
  919. $converted_images = [];
  920. foreach ($data['data']['img'] ?? [] as $image) {
  921. $converted_images[$image] = Excerpts::processImageHtml($image, $object);
  922. }
  923. $json = [
  924. 'status' => 'success',
  925. 'message' => 'All links converted',
  926. 'data' => ['links' => $converted_links, 'images' => $converted_images]
  927. ];
  928. return $this->createJsonResponse($json, 200);
  929. }
  930. /**
  931. * @return ResponseInterface
  932. */
  933. public function taskMediaList(): ResponseInterface
  934. {
  935. return $this->forwardMediaTask('action', 'media.list');
  936. }
  937. /**
  938. * @return ResponseInterface
  939. */
  940. public function taskMediaUpload(): ResponseInterface
  941. {
  942. return $this->forwardMediaTask('task', 'media.upload');
  943. }
  944. /**
  945. * @return ResponseInterface
  946. */
  947. public function taskMediaUploadMeta(): ResponseInterface
  948. {
  949. return $this->forwardMediaTask('task', 'media.upload.meta');
  950. }
  951. /**
  952. * @return ResponseInterface
  953. */
  954. public function taskMediaReorder(): ResponseInterface
  955. {
  956. return $this->forwardMediaTask('task', 'media.reorder');
  957. }
  958. /**
  959. * @return ResponseInterface
  960. */
  961. public function taskMediaDelete(): ResponseInterface
  962. {
  963. return $this->forwardMediaTask('task', 'media.delete');
  964. }
  965. /**
  966. * @return ResponseInterface
  967. */
  968. public function taskListmedia(): ResponseInterface
  969. {
  970. return $this->taskMediaList();
  971. }
  972. /**
  973. * @return ResponseInterface
  974. */
  975. public function taskAddmedia(): ResponseInterface
  976. {
  977. return $this->forwardMediaTask('task', 'media.copy');
  978. }
  979. /**
  980. * @return ResponseInterface
  981. */
  982. public function taskDelmedia(): ResponseInterface
  983. {
  984. return $this->forwardMediaTask('task', 'media.remove');
  985. }
  986. /**
  987. * @return ResponseInterface
  988. * @deprecated Do not use
  989. */
  990. public function taskFilesUpload(): ResponseInterface
  991. {
  992. throw new RuntimeException('Task filesUpload should not be called!');
  993. }
  994. /**
  995. * @param string|null $filename
  996. * @return ResponseInterface
  997. * @deprecated Do not use
  998. */
  999. public function taskRemoveMedia($filename = null): ResponseInterface
  1000. {
  1001. throw new RuntimeException('Task removeMedia should not be called!');
  1002. }
  1003. /**
  1004. * @return ResponseInterface
  1005. */
  1006. public function taskGetFilesInFolder(): ResponseInterface
  1007. {
  1008. return $this->forwardMediaTask('action', 'media.picker');
  1009. }
  1010. /**
  1011. * @param string $type
  1012. * @param string $name
  1013. * @return ResponseInterface
  1014. */
  1015. protected function forwardMediaTask(string $type, string $name): ResponseInterface
  1016. {
  1017. $directory = $this->getDirectory();
  1018. if (!$directory) {
  1019. throw new RuntimeException('Not Found', 404);
  1020. }
  1021. $route = Uri::getCurrentRoute()->withGravParam('task', null);
  1022. $object = $this->getObject();
  1023. /** @var ServerRequest $request */
  1024. $request = $this->grav['request'];
  1025. $request = $request
  1026. ->withAttribute($type, $name)
  1027. ->withAttribute('type', $this->target)
  1028. ->withAttribute('key', $this->id)
  1029. ->withAttribute('storage_key', $object && $object->exists() ? $object->getStorageKey() : null)
  1030. ->withAttribute('route', $route)
  1031. ->withAttribute('forwarded', true)
  1032. ->withAttribute('object', $object);
  1033. $controller = new MediaController();
  1034. $controller->setUser($this->user);
  1035. return $controller->handle($request);
  1036. }
  1037. /**
  1038. * @return Flex
  1039. */
  1040. protected function getFlex(): Flex
  1041. {
  1042. return Grav::instance()['flex_objects'];
  1043. }
  1044. public static function getInstance(): ?AdminController
  1045. {
  1046. return self::$instance;
  1047. }
  1048. /**
  1049. * AdminController constructor.
  1050. */
  1051. public function __construct()
  1052. {
  1053. self::$instance = $this;
  1054. $this->grav = Grav::instance();
  1055. $this->admin = $this->grav['admin'];
  1056. $this->user = $this->admin->user;
  1057. $this->active = false;
  1058. // Controller can only be run in admin.
  1059. if (!Utils::isAdminPlugin()) {
  1060. return;
  1061. }
  1062. [, $location, $target] = $this->grav['admin']->getRouteDetails();
  1063. if (!$location) {
  1064. return;
  1065. }
  1066. $target = \is_string($target) ? urldecode($target) : null;
  1067. /** @var Uri $uri */
  1068. $uri = $this->grav['uri'];
  1069. $routeObject = $uri::getCurrentRoute();
  1070. $routeObject->withExtension('');
  1071. $routes = $this->getAdminRoutes();
  1072. // Match route to the flex directory.
  1073. $path = '/' . ($target ? $location . '/' . $target : $location) . '/';
  1074. $test = $routes[$path] ?? null;
  1075. $directory = null;
  1076. if ($test) {
  1077. $directory = $test['directory'];
  1078. $location = trim($path, '/');
  1079. $target = '';
  1080. } else {
  1081. krsort($routes);
  1082. foreach ($routes as $route => $test) {
  1083. if (strpos($path, $route) === 0) {
  1084. $directory = $test['directory'];
  1085. $location = trim($route, '/');
  1086. $target = trim(substr($path, strlen($route)), '/');
  1087. break;
  1088. }
  1089. $test = null;
  1090. }
  1091. }
  1092. if ($directory) {
  1093. // Redirect aliases.
  1094. if (isset($test['redirect'])) {
  1095. $route = $test['redirect'];
  1096. // If directory route starts with alias and path continues, stop.
  1097. if ($target && strpos($route, $location) === 0) {
  1098. // We are not in a directory.
  1099. return;
  1100. }
  1101. $redirect = '/' . $route . ($target ? '/' . $target : '');
  1102. $this->setRedirect($redirect, 302);
  1103. $this->redirect();
  1104. } elseif (isset($test['action'])) {
  1105. $routeObject = $routeObject->withGravParam('', $test['action']);
  1106. }
  1107. $id = $target;
  1108. $target = $directory->getFlexType();
  1109. } else {
  1110. // We are not in a directory.
  1111. if ($location !== 'flex-objects') {
  1112. return;
  1113. }
  1114. $array = explode('/', $target, 2);
  1115. $target = array_shift($array) ?: null;
  1116. $id = array_shift($array) ?: null;
  1117. }
  1118. // Post
  1119. $post = $_POST;
  1120. if (isset($post['data'])) {
  1121. $data = $post['data'];
  1122. if (is_string($data)) {
  1123. $data = json_decode($data, true);
  1124. }
  1125. $this->data = $this->getPost($data);
  1126. unset($post['data']);
  1127. }
  1128. // Task
  1129. $task = $this->grav['task'];
  1130. if ($task) {
  1131. $this->task = $task;
  1132. }
  1133. $this->post = $this->getPost($post);
  1134. $this->location = 'flex-objects';
  1135. $this->target = $target;
  1136. $this->id = $this->post['id'] ?? $id;
  1137. $this->action = $this->post['action'] ?? $uri->param('action', null) ?? $uri->param('', null) ?? $routeObject->getGravParam('');
  1138. $this->active = true;
  1139. $this->currentRoute = $uri::getCurrentRoute();
  1140. $this->route = $routeObject;
  1141. $base = $this->grav['pages']->base();
  1142. if ($base) {
  1143. // Fix referrer for sub-folder multi-site setups.
  1144. $referrer = preg_replace('`^' . $base . '`', '', $uri->referrer());
  1145. } else {
  1146. $referrer = $uri->referrer();
  1147. }
  1148. $this->referrerRoute = $referrer ? RouteFactory::createFromString($referrer) : $this->currentRoute;
  1149. }
  1150. public function getInfo(): array
  1151. {
  1152. if (!$this->isActive()) {
  1153. return [];
  1154. }
  1155. $class = AdminController::class;
  1156. return [
  1157. 'controller' => [
  1158. 'name' => $this->location,
  1159. 'instance' => [$class, 'getInstance']
  1160. ],
  1161. 'location' => $this->location,
  1162. 'type' => $this->target,
  1163. 'key' => $this->id,
  1164. 'action' => $this->action,
  1165. 'task' => $this->task
  1166. ];
  1167. }
  1168. /**
  1169. * Performs a task or action on a post or target.
  1170. *
  1171. * @return ResponseInterface|bool|null
  1172. */
  1173. public function execute()
  1174. {
  1175. if (!$this->user->authorize('admin.login')) {
  1176. // TODO: improve
  1177. return false;
  1178. }
  1179. $params = [];
  1180. $event = new Event(
  1181. [
  1182. 'type' => &$this->target,
  1183. 'key' => &$this->id,
  1184. 'directory' => &$this->directory,
  1185. 'collection' => &$this->collection,
  1186. 'object' => &$this->object
  1187. ]
  1188. );
  1189. $this->grav->fireEvent("flex.{$this->target}.admin.route", $event);
  1190. if ($this->isFormSubmit()) {
  1191. $form = $this->getForm();
  1192. $this->nonce_name = $form->getNonceName();
  1193. $this->nonce_action = $form->getNonceAction();
  1194. }
  1195. // Handle Task & Action
  1196. if ($this->task) {
  1197. // validate nonce
  1198. if (!$this->validateNonce()) {
  1199. $e = new RequestException($this->getRequest(), 'Page Expired', 400);
  1200. $this->close($this->createErrorResponse($e));
  1201. }
  1202. $method = $this->task_prefix . ucfirst(str_replace('.', '', $this->task));
  1203. if (!method_exists($this, $method)) {
  1204. $method = $this->task_prefix . 'Default';
  1205. }
  1206. } elseif ($this->target) {
  1207. if (!$this->action) {
  1208. if ($this->id) {
  1209. $this->action = 'edit';
  1210. $params[] = $this->id;
  1211. } else {
  1212. $this->action = 'list';
  1213. }
  1214. }
  1215. $method = 'action' . ucfirst(strtolower(str_replace('.', '', $this->action)));
  1216. if (!method_exists($this, $method)) {
  1217. $method = $this->action_prefix . 'Default';
  1218. }
  1219. } else {
  1220. return null;
  1221. }
  1222. if (!method_exists($this, $method)) {
  1223. return null;
  1224. }
  1225. try {
  1226. $response = $this->{$method}(...$params);
  1227. } catch (RequestException $e) {
  1228. $response = $this->createErrorResponse($e);
  1229. } catch (RuntimeException $e) {
  1230. // If task fails to run, redirect back to the previous page and display the error message.
  1231. if ($this->task && !$this->redirect) {
  1232. $this->setRedirect($this->referrerRoute->toString(true));
  1233. }
  1234. $response = null;
  1235. $this->setMessage($e->getMessage(), 'error');
  1236. }
  1237. if ($response instanceof ResponseInterface) {
  1238. $this->close($response);
  1239. }
  1240. // Grab redirect parameter.
  1241. $redirect = $this->post['_redirect'] ?? null;
  1242. unset($this->post['_redirect']);
  1243. // Redirect if requested.
  1244. if ($redirect) {
  1245. $this->setRedirect($redirect);
  1246. }
  1247. return $response;
  1248. }
  1249. /**
  1250. * @return bool
  1251. */
  1252. public function isFormSubmit(): bool
  1253. {
  1254. return (bool)($this->post['__form-name__'] ?? null);
  1255. }
  1256. /**
  1257. * @param FlexObjectInterface|null $object
  1258. * @return FlexFormInterface
  1259. */
  1260. public function getForm(FlexObjectInterface $object = null): FlexFormInterface
  1261. {
  1262. $object = $object ?? $this->getObject();
  1263. if (!$object) {
  1264. throw new RuntimeException('Not Found', 404);
  1265. }
  1266. $formName = $this->post['__form-name__'] ?? '';
  1267. $name = '';
  1268. $uniqueId = null;
  1269. // Get the form name. This defines the blueprint which is being used.
  1270. if (strpos($formName, '-')) {
  1271. $parts = explode('-', $formName);
  1272. $prefix = $parts[0] ?? '';
  1273. $type = $parts[1] ?? '';
  1274. if ($prefix === 'flex' && $type === $object->getFlexType()) {
  1275. $name = $parts[2] ?? '';
  1276. if ($name === 'object') {
  1277. $name = '';
  1278. }
  1279. $uniqueId = $this->post['__unique_form_id__'] ?? null;
  1280. }
  1281. }
  1282. $options = [
  1283. 'unique_id' => $uniqueId,
  1284. ];
  1285. return $object->getForm($name, $options);
  1286. }
  1287. /**
  1288. * @param FlexDirectoryInterface|null $directory
  1289. * @return FlexFormInterface
  1290. */
  1291. public function getDirectoryForm(FlexDirectoryInterface $directory = null): FlexFormInterface
  1292. {
  1293. $directory = $directory ?? $this->getDirectory();
  1294. if (!$directory) {
  1295. throw new RuntimeException('Not Found', 404);
  1296. }
  1297. $formName = $this->post['__form-name__'] ?? '';
  1298. $name = '';
  1299. $uniqueId = null;
  1300. // Get the form name. This defines the blueprint which is being used.
  1301. if (strpos($formName, '-')) {
  1302. $parts = explode('-', $formName);
  1303. $prefix = $parts[0] ?? '';
  1304. $type = $parts[1] ?? '';
  1305. if ($prefix === 'flex_conf' && $type === $directory->getFlexType()) {
  1306. $name = $parts[2] ?? '';
  1307. $uniqueId = $this->post['__unique_form_id__'] ?? null;
  1308. }
  1309. }
  1310. $options = [
  1311. 'unique_id' => $uniqueId,
  1312. ];
  1313. return $directory->getDirectoryForm($name, $options);
  1314. }
  1315. /**
  1316. * @param string|null $key
  1317. * @return FlexObjectInterface|null
  1318. */
  1319. public function getObject(string $key = null): ?FlexObjectInterface
  1320. {
  1321. if (null === $this->object) {
  1322. $key = $key ?? $this->id;
  1323. $object = false;
  1324. $directory = $this->getDirectory();
  1325. if ($directory) {
  1326. // FIXME: hack for pages.
  1327. if ($key === '_root') {
  1328. $index = $directory->getIndex();
  1329. if ($index instanceof PageIndex) {
  1330. $object = $index->getRoot();
  1331. }
  1332. } elseif (null !== $key) {
  1333. $object = $directory->getObject($key) ?? $directory->createObject([], $key);
  1334. } elseif ($this->action === 'add') {
  1335. $object = $directory->createObject([], '');
  1336. }
  1337. if ($object instanceof FlexTranslateInterface && $this->isMultilang()) {
  1338. $language = $this->getLanguage();
  1339. if ($object->hasTranslation($language)) {
  1340. $object = $object->getTranslation($language);
  1341. } elseif (!in_array('', $object->getLanguages(true), true)) {
  1342. $object->language($language);
  1343. }
  1344. }
  1345. if (is_callable([$object, 'refresh'])) {
  1346. $object->refresh();
  1347. }
  1348. // Get updated object via form.
  1349. $this->object = $object ? $object->getForm()->getObject() : false;
  1350. }
  1351. }
  1352. $this->grav->fireEvent('onAdminObjectGet', new Event(['object' => $this->object]));
  1353. return $this->object ?: null;
  1354. }
  1355. /**
  1356. * @param string|null $type
  1357. * @return FlexDirectoryInterface|null
  1358. */
  1359. public function getDirectory(string $type = null): ?FlexDirectoryInterface
  1360. {
  1361. $type = $type ?? $this->target;
  1362. if ($type && null === $this->directory) {
  1363. $this->directory = Grav::instance()['flex_objects']->getDirectory($type);
  1364. }
  1365. return $this->directory;
  1366. }
  1367. /**
  1368. * @return FlexCollectionInterface|null
  1369. */
  1370. public function getCollection(): ?FlexCollectionInterface
  1371. {
  1372. if (null === $this->collection) {
  1373. $directory = $this->getDirectory();
  1374. $this->collection = $directory ? $directory->getCollection() : null;
  1375. }
  1376. return $this->collection;
  1377. }
  1378. /**
  1379. * @param string $msg
  1380. * @param string $type
  1381. * @return void
  1382. */
  1383. public function setMessage(string $msg, string $type = 'info'): void
  1384. {
  1385. /** @var Message $messages */
  1386. $messages = $this->grav['messages'];
  1387. $messages->add($msg, $type);
  1388. }
  1389. /**
  1390. * @return bool
  1391. */
  1392. public function isActive(): bool
  1393. {
  1394. return (bool) $this->active;
  1395. }
  1396. /**
  1397. * @param string $location
  1398. * @return void
  1399. */
  1400. public function setLocation(string $location): void
  1401. {
  1402. $this->location = $location;
  1403. }
  1404. /**
  1405. * @return string|null
  1406. */
  1407. public function getLocation(): ?string
  1408. {
  1409. return $this->location;
  1410. }
  1411. /**
  1412. * @param string $action
  1413. * @return void
  1414. */
  1415. public function setAction(string $action): void
  1416. {
  1417. $this->action = $action;
  1418. }
  1419. /**
  1420. * @return string|null
  1421. */
  1422. public function getAction(): ?string
  1423. {
  1424. return $this->action;
  1425. }
  1426. /**
  1427. * @param string $task
  1428. * @return void
  1429. */
  1430. public function setTask(string $task): void
  1431. {
  1432. $this->task = $task;
  1433. }
  1434. /**
  1435. * @return string|null
  1436. */
  1437. public function getTask(): ?string
  1438. {
  1439. return $this->task;
  1440. }
  1441. /**
  1442. * @param string $target
  1443. * @return void
  1444. */
  1445. public function setTarget(string $target): void
  1446. {
  1447. $this->target = $target;
  1448. }
  1449. /**
  1450. * @return string|null
  1451. */
  1452. public function getTarget(): ?string
  1453. {
  1454. return $this->target;
  1455. }
  1456. /**
  1457. * @param string $id
  1458. * @return void
  1459. */
  1460. public function setId(string $id): void
  1461. {
  1462. $this->id = $id;
  1463. }
  1464. /**
  1465. * @return string|null
  1466. */
  1467. public function getId(): ?string
  1468. {
  1469. return $this->id;
  1470. }
  1471. /**
  1472. * Sets the page redirect.
  1473. *
  1474. * @param string $path The path to redirect to
  1475. * @param int $code The HTTP redirect code
  1476. * @return void
  1477. */
  1478. public function setRedirect(string $path, int $code = 303): void
  1479. {
  1480. $this->redirect = $path;
  1481. $this->redirectCode = (int)$code;
  1482. }
  1483. /**
  1484. * Redirect to the route stored in $this->redirect
  1485. *
  1486. * @return void
  1487. */
  1488. public function redirect(): void
  1489. {
  1490. $this->admin->redirect($this->redirect, $this->redirectCode);
  1491. }
  1492. /**
  1493. * @return array
  1494. */
  1495. public function getAdminRoutes(): array
  1496. {
  1497. if (null === $this->adminRoutes) {
  1498. $routes = [];
  1499. /** @var FlexDirectoryInterface $directory */
  1500. foreach ($this->getFlex()->getDirectories() as $directory) {
  1501. $config = $directory->getConfig('admin');
  1502. if (!$directory->isEnabled() || !empty($config['disabled'])) {
  1503. continue;
  1504. }
  1505. // Alias under flex-objects (always exists, but can be redirected).
  1506. $routes["/flex-objects/{$directory->getFlexType()}/"] = ['directory' => $directory];
  1507. $route = $config['router']['path'] ?? $config['menu']['list']['route'] ?? null;
  1508. if ($route) {
  1509. $routes[$route . '/'] = ['directory' => $directory];
  1510. }
  1511. $redirects = (array)($config['router']['redirects'] ?? null);
  1512. foreach ($redirects as $redirectFrom => $redirectTo) {
  1513. $redirectFrom .= '/';
  1514. if (!isset($routes[$redirectFrom])) {
  1515. $routes[$redirectFrom] = ['directory' => $directory, 'redirect' => $redirectTo];
  1516. }
  1517. }
  1518. $actions = (array)($config['router']['actions'] ?? null);
  1519. foreach ($actions as $name => $action) {
  1520. if (is_array($action)) {
  1521. $path = $action['path'] ?? null;
  1522. } else {
  1523. $path = $action;
  1524. }
  1525. if ($path !== null) {
  1526. $routes[$path . '/'] = ['directory' => $directory, 'action' => $name];
  1527. }
  1528. }
  1529. }
  1530. $this->adminRoutes = $routes;
  1531. }
  1532. return $this->adminRoutes;
  1533. }
  1534. /**
  1535. * Return true if multilang is active
  1536. *
  1537. * @return bool True if multilang is active
  1538. */
  1539. protected function isMultilang(): bool
  1540. {
  1541. /** @var Language $language */
  1542. $language = $this->grav['language'];
  1543. return $language->enabled();
  1544. }
  1545. protected function validateNonce(): bool
  1546. {
  1547. $nonce_action = $this->nonce_action;
  1548. $nonce = $this->post[$this->nonce_name] ?? $this->grav['uri']->param($this->nonce_name) ?? $this->grav['uri']->query($this->nonce_name);
  1549. if (!$nonce) {
  1550. $nonce = $this->post['admin-nonce'] ?? $this->grav['uri']->param('admin-nonce') ?? $this->grav['uri']->query('admin-nonce');
  1551. $nonce_action = 'admin-form';
  1552. }
  1553. return $nonce && Utils::verifyNonce($nonce, $nonce_action);
  1554. }
  1555. /**
  1556. * Prepare and return POST data.
  1557. *
  1558. * @param array $post
  1559. * @return array
  1560. */
  1561. protected function getPost(array $post): array
  1562. {
  1563. unset($post['task']);
  1564. // Decode JSON encoded fields and merge them to data.
  1565. if (isset($post['_json'])) {
  1566. $post = array_replace_recursive($post, $this->jsonDecode($post['_json']));
  1567. unset($post['_json']);
  1568. }
  1569. $post = $this->cleanDataKeys($post);
  1570. return $post;
  1571. }
  1572. /**
  1573. * @param ResponseInterface $response
  1574. * @return never-return
  1575. */
  1576. protected function close(ResponseInterface $response): void
  1577. {
  1578. $this->grav->close($response);
  1579. }
  1580. /**
  1581. * Recursively JSON decode data.
  1582. *
  1583. * @param array $data
  1584. * @return array
  1585. */
  1586. protected function jsonDecode(array $data)
  1587. {
  1588. foreach ($data as &$value) {
  1589. if (is_array($value)) {
  1590. $value = $this->jsonDecode($value);
  1591. } else {
  1592. $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
  1593. }
  1594. }
  1595. return $data;
  1596. }
  1597. /**
  1598. * @param array $source
  1599. * @return array
  1600. */
  1601. protected function cleanDataKeys($source = []): array
  1602. {
  1603. $out = [];
  1604. if (is_array($source)) {
  1605. foreach ($source as $key => $value) {
  1606. $key = str_replace(['%5B', '%5D'], ['[', ']'], $key);
  1607. if (is_array($value)) {
  1608. $out[$key] = $this->cleanDataKeys($value);
  1609. } else {
  1610. $out[$key] = $value;
  1611. }
  1612. }
  1613. }
  1614. return $out;
  1615. }
  1616. /**
  1617. * @return string
  1618. */
  1619. public function getLanguage(): string
  1620. {
  1621. return $this->admin->language ?? '';
  1622. }
  1623. /**
  1624. * @return Config
  1625. */
  1626. protected function getConfig(): Config
  1627. {
  1628. return $this->grav['config'];
  1629. }
  1630. /**
  1631. * @return ServerRequestInterface
  1632. */
  1633. protected function getRequest(): ServerRequestInterface
  1634. {
  1635. return $this->grav['request'];
  1636. }
  1637. }