AdminController.php 61 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911
  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. return $this->object ?: null;
  1353. }
  1354. /**
  1355. * @param string|null $type
  1356. * @return FlexDirectoryInterface|null
  1357. */
  1358. public function getDirectory(string $type = null): ?FlexDirectoryInterface
  1359. {
  1360. $type = $type ?? $this->target;
  1361. if ($type && null === $this->directory) {
  1362. $this->directory = Grav::instance()['flex_objects']->getDirectory($type);
  1363. }
  1364. return $this->directory;
  1365. }
  1366. /**
  1367. * @return FlexCollectionInterface|null
  1368. */
  1369. public function getCollection(): ?FlexCollectionInterface
  1370. {
  1371. if (null === $this->collection) {
  1372. $directory = $this->getDirectory();
  1373. $this->collection = $directory ? $directory->getCollection() : null;
  1374. }
  1375. return $this->collection;
  1376. }
  1377. /**
  1378. * @param string $msg
  1379. * @param string $type
  1380. * @return void
  1381. */
  1382. public function setMessage(string $msg, string $type = 'info'): void
  1383. {
  1384. /** @var Message $messages */
  1385. $messages = $this->grav['messages'];
  1386. $messages->add($msg, $type);
  1387. }
  1388. /**
  1389. * @return bool
  1390. */
  1391. public function isActive(): bool
  1392. {
  1393. return (bool) $this->active;
  1394. }
  1395. /**
  1396. * @param string $location
  1397. * @return void
  1398. */
  1399. public function setLocation(string $location): void
  1400. {
  1401. $this->location = $location;
  1402. }
  1403. /**
  1404. * @return string|null
  1405. */
  1406. public function getLocation(): ?string
  1407. {
  1408. return $this->location;
  1409. }
  1410. /**
  1411. * @param string $action
  1412. * @return void
  1413. */
  1414. public function setAction(string $action): void
  1415. {
  1416. $this->action = $action;
  1417. }
  1418. /**
  1419. * @return string|null
  1420. */
  1421. public function getAction(): ?string
  1422. {
  1423. return $this->action;
  1424. }
  1425. /**
  1426. * @param string $task
  1427. * @return void
  1428. */
  1429. public function setTask(string $task): void
  1430. {
  1431. $this->task = $task;
  1432. }
  1433. /**
  1434. * @return string|null
  1435. */
  1436. public function getTask(): ?string
  1437. {
  1438. return $this->task;
  1439. }
  1440. /**
  1441. * @param string $target
  1442. * @return void
  1443. */
  1444. public function setTarget(string $target): void
  1445. {
  1446. $this->target = $target;
  1447. }
  1448. /**
  1449. * @return string|null
  1450. */
  1451. public function getTarget(): ?string
  1452. {
  1453. return $this->target;
  1454. }
  1455. /**
  1456. * @param string $id
  1457. * @return void
  1458. */
  1459. public function setId(string $id): void
  1460. {
  1461. $this->id = $id;
  1462. }
  1463. /**
  1464. * @return string|null
  1465. */
  1466. public function getId(): ?string
  1467. {
  1468. return $this->id;
  1469. }
  1470. /**
  1471. * Sets the page redirect.
  1472. *
  1473. * @param string $path The path to redirect to
  1474. * @param int $code The HTTP redirect code
  1475. * @return void
  1476. */
  1477. public function setRedirect(string $path, int $code = 303): void
  1478. {
  1479. $this->redirect = $path;
  1480. $this->redirectCode = (int)$code;
  1481. }
  1482. /**
  1483. * Redirect to the route stored in $this->redirect
  1484. *
  1485. * @return void
  1486. */
  1487. public function redirect(): void
  1488. {
  1489. $this->admin->redirect($this->redirect, $this->redirectCode);
  1490. }
  1491. /**
  1492. * @return array
  1493. */
  1494. public function getAdminRoutes(): array
  1495. {
  1496. if (null === $this->adminRoutes) {
  1497. $routes = [];
  1498. /** @var FlexDirectoryInterface $directory */
  1499. foreach ($this->getFlex()->getDirectories() as $directory) {
  1500. $config = $directory->getConfig('admin');
  1501. if (!$directory->isEnabled() || !empty($config['disabled'])) {
  1502. continue;
  1503. }
  1504. // Alias under flex-objects (always exists, but can be redirected).
  1505. $routes["/flex-objects/{$directory->getFlexType()}/"] = ['directory' => $directory];
  1506. $route = $config['router']['path'] ?? $config['menu']['list']['route'] ?? null;
  1507. if ($route) {
  1508. $routes[$route . '/'] = ['directory' => $directory];
  1509. }
  1510. $redirects = (array)($config['router']['redirects'] ?? null);
  1511. foreach ($redirects as $redirectFrom => $redirectTo) {
  1512. $redirectFrom .= '/';
  1513. if (!isset($routes[$redirectFrom])) {
  1514. $routes[$redirectFrom] = ['directory' => $directory, 'redirect' => $redirectTo];
  1515. }
  1516. }
  1517. $actions = (array)($config['router']['actions'] ?? null);
  1518. foreach ($actions as $name => $action) {
  1519. if (is_array($action)) {
  1520. $path = $action['path'] ?? null;
  1521. } else {
  1522. $path = $action;
  1523. }
  1524. if ($path !== null) {
  1525. $routes[$path . '/'] = ['directory' => $directory, 'action' => $name];
  1526. }
  1527. }
  1528. }
  1529. $this->adminRoutes = $routes;
  1530. }
  1531. return $this->adminRoutes;
  1532. }
  1533. /**
  1534. * Return true if multilang is active
  1535. *
  1536. * @return bool True if multilang is active
  1537. */
  1538. protected function isMultilang(): bool
  1539. {
  1540. /** @var Language $language */
  1541. $language = $this->grav['language'];
  1542. return $language->enabled();
  1543. }
  1544. protected function validateNonce(): bool
  1545. {
  1546. $nonce_action = $this->nonce_action;
  1547. $nonce = $this->post[$this->nonce_name] ?? $this->grav['uri']->param($this->nonce_name) ?? $this->grav['uri']->query($this->nonce_name);
  1548. if (!$nonce) {
  1549. $nonce = $this->post['admin-nonce'] ?? $this->grav['uri']->param('admin-nonce') ?? $this->grav['uri']->query('admin-nonce');
  1550. $nonce_action = 'admin-form';
  1551. }
  1552. return $nonce && Utils::verifyNonce($nonce, $nonce_action);
  1553. }
  1554. /**
  1555. * Prepare and return POST data.
  1556. *
  1557. * @param array $post
  1558. * @return array
  1559. */
  1560. protected function getPost(array $post): array
  1561. {
  1562. unset($post['task']);
  1563. // Decode JSON encoded fields and merge them to data.
  1564. if (isset($post['_json'])) {
  1565. $post = array_replace_recursive($post, $this->jsonDecode($post['_json']));
  1566. unset($post['_json']);
  1567. }
  1568. $post = $this->cleanDataKeys($post);
  1569. return $post;
  1570. }
  1571. /**
  1572. * @param ResponseInterface $response
  1573. * @return never-return
  1574. */
  1575. protected function close(ResponseInterface $response): void
  1576. {
  1577. $this->grav->close($response);
  1578. }
  1579. /**
  1580. * Recursively JSON decode data.
  1581. *
  1582. * @param array $data
  1583. * @return array
  1584. */
  1585. protected function jsonDecode(array $data)
  1586. {
  1587. foreach ($data as &$value) {
  1588. if (is_array($value)) {
  1589. $value = $this->jsonDecode($value);
  1590. } else {
  1591. $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
  1592. }
  1593. }
  1594. return $data;
  1595. }
  1596. /**
  1597. * @param array $source
  1598. * @return array
  1599. */
  1600. protected function cleanDataKeys($source = []): array
  1601. {
  1602. $out = [];
  1603. if (is_array($source)) {
  1604. foreach ($source as $key => $value) {
  1605. $key = str_replace(['%5B', '%5D'], ['[', ']'], $key);
  1606. if (is_array($value)) {
  1607. $out[$key] = $this->cleanDataKeys($value);
  1608. } else {
  1609. $out[$key] = $value;
  1610. }
  1611. }
  1612. }
  1613. return $out;
  1614. }
  1615. /**
  1616. * @return string
  1617. */
  1618. public function getLanguage(): string
  1619. {
  1620. return $this->admin->language ?? '';
  1621. }
  1622. /**
  1623. * @return Config
  1624. */
  1625. protected function getConfig(): Config
  1626. {
  1627. return $this->grav['config'];
  1628. }
  1629. /**
  1630. * @return ServerRequestInterface
  1631. */
  1632. protected function getRequest(): ServerRequestInterface
  1633. {
  1634. return $this->grav['request'];
  1635. }
  1636. }