Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Nie możesz wybrać więcej, niż 25 tematów Tematy muszą się zaczynać od litery lub cyfry, mogą zawierać myślniki ('-') i mogą mieć do 35 znaków.
 
 
 
 
 
 

3074 wiersze
99 KiB

  1. <?php
  2. /**
  3. * @package Grav\Plugin\Admin
  4. *
  5. * @copyright Copyright (c) 2015 - 2023 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Plugin\Admin;
  9. use Grav\Common\Backup\Backups;
  10. use Grav\Common\Cache;
  11. use Grav\Common\Config\Config;
  12. use Grav\Common\Debugger;
  13. use Grav\Common\File\CompiledYamlFile;
  14. use Grav\Common\Filesystem\Folder;
  15. use Grav\Common\Flex\Types\Pages\PageIndex;
  16. use Grav\Common\GPM\GPM as GravGPM;
  17. use Grav\Common\GPM\Installer;
  18. use Grav\Common\Grav;
  19. use Grav\Common\Data;
  20. use Grav\Common\Helpers\Excerpts;
  21. use Grav\Common\Language\Language;
  22. use Grav\Common\Page\Interfaces\PageInterface;
  23. use Grav\Common\Page\Media;
  24. use Grav\Common\Page\Medium\ImageMedium;
  25. use Grav\Common\Page\Medium\Medium;
  26. use Grav\Common\Page\Page;
  27. use Grav\Common\Page\Pages;
  28. use Grav\Common\Page\Collection;
  29. use Grav\Common\Plugins;
  30. use Grav\Common\Security;
  31. use Grav\Common\User\Interfaces\UserCollectionInterface;
  32. use Grav\Common\User\Interfaces\UserInterface;
  33. use Grav\Common\Utils;
  34. use Grav\Framework\Flex\Flex;
  35. use Grav\Framework\Psr7\Response;
  36. use Grav\Framework\RequestHandler\Exception\RequestException;
  37. use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
  38. use Grav\Common\Yaml;
  39. use PicoFeed\Parser\MalformedXmlException;
  40. use Psr\Http\Message\ResponseInterface;
  41. use RocketTheme\Toolbox\Event\Event;
  42. use RocketTheme\Toolbox\File\File;
  43. use RocketTheme\Toolbox\File\YamlFile;
  44. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  45. use Twig\Loader\FilesystemLoader;
  46. /**
  47. * Class AdminController
  48. *
  49. * @package Grav\Plugin
  50. */
  51. class AdminController extends AdminBaseController
  52. {
  53. /**
  54. * @param Grav|null $grav
  55. * @param string|null $view
  56. * @param string|null $task
  57. * @param string|null $route
  58. * @param array|null $post
  59. * @return void
  60. */
  61. public function initialize(Grav $grav = null, $view = null, $task = null, $route = null, $post = null)
  62. {
  63. $this->grav = $grav;
  64. $this->admin = $this->grav['admin'];
  65. $this->view = $view;
  66. $this->task = $task ?: 'display';
  67. if (isset($post['data'])) {
  68. $this->data = $this->getPost($post['data']);
  69. unset($post['data']);
  70. } else {
  71. // Backwards compatibility for Form plugin <= 1.2
  72. $this->data = $this->getPost($post);
  73. }
  74. $this->post = $this->getPost($post);
  75. $this->route = $route;
  76. $this->grav->fireEvent('onAdminControllerInit', new Event(['controller' => &$this]));
  77. }
  78. // GENERAL TASKS
  79. /**
  80. * Keep alive
  81. *
  82. * Route: POST /task:keepAlive (AJAX call)
  83. *
  84. * @return void
  85. */
  86. protected function taskKeepAlive(): void
  87. {
  88. // This task is available for all admin users.
  89. $response = new Response(200);
  90. $this->close($response);
  91. }
  92. /**
  93. * Clear the cache.
  94. *
  95. * Route: GET /cache.json/task:clearCache (AJAX call)
  96. *
  97. * @return bool True if the action was performed.
  98. */
  99. protected function taskClearCache()
  100. {
  101. if (!$this->authorizeTask('clear cache', ['admin.cache', 'admin.maintenance', 'admin.super'])) {
  102. $this->admin->json_response = [
  103. 'status' => 'error',
  104. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  105. ];
  106. return false;
  107. }
  108. // get optional cleartype param
  109. $clear_type = $this->grav['uri']->param('cleartype');
  110. if ($clear_type) {
  111. $clear = $clear_type;
  112. } else {
  113. $clear = 'standard';
  114. }
  115. if ($clear === 'purge') {
  116. $msg = Cache::purgeJob();
  117. $this->admin->json_response = [
  118. 'status' => 'success',
  119. 'message' => $msg,
  120. ];
  121. } else {
  122. $results = Cache::clearCache($clear);
  123. if (count($results) > 0) {
  124. $this->admin->json_response = [
  125. 'status' => 'success',
  126. 'message' => $this->admin::translate('PLUGIN_ADMIN.CACHE_CLEARED') . ' <br />' . $this->admin::translate('PLUGIN_ADMIN.METHOD') . ': ' . $clear . ''
  127. ];
  128. } else {
  129. $this->admin->json_response = [
  130. 'status' => 'error',
  131. 'message' => $this->admin::translate('PLUGIN_ADMIN.ERROR_CLEARING_CACHE')
  132. ];
  133. }
  134. }
  135. return true;
  136. }
  137. /**
  138. * Handles form and saves the input data if its valid.
  139. *
  140. * Route: POST /pages?task:save
  141. * Route: POST /user?task:save
  142. * Route: POST /*?task:save
  143. *
  144. * @return bool True if the action was performed.
  145. */
  146. public function taskSave()
  147. {
  148. if (!$this->authorizeTask('save', $this->dataPermissions())) {
  149. return false;
  150. }
  151. $this->grav['twig']->twig_vars['current_form_data'] = (array)$this->data;
  152. switch ($this->view) {
  153. case 'pages':
  154. // Not used if Flex-Objects plugin handles pages.
  155. return $this->savePage();
  156. case 'user':
  157. // Not used if Flex-Objects plugin handles users.
  158. return $this->saveUser();
  159. default:
  160. if ($this->saveDefault()) {
  161. $route = $this->grav['uri']::getCurrentRoute();
  162. $this->setRedirect($route->withGravParam('task', null)->toString(), 302);
  163. $this->redirect();
  164. }
  165. return false;
  166. }
  167. }
  168. /**
  169. * @return bool
  170. */
  171. protected function saveDefault()
  172. {
  173. try {
  174. // Handle standard data types.
  175. $type = $this->getDataType();
  176. $obj = $this->admin->getConfigurationData($type, $this->data);
  177. $obj->validate();
  178. } catch (\Exception $e) {
  179. /** @var Debugger $debugger */
  180. $debugger = $this->grav['debugger'];
  181. $debugger->addException($e);
  182. $this->admin->setMessage($e->getMessage(), 'error');
  183. return false;
  184. }
  185. $obj->filter(false, true);
  186. $obj = $this->storeFiles($obj);
  187. if ($obj) {
  188. // Event to manipulate data before saving the object
  189. $this->grav->fireEvent('onAdminSave', new Event(['object' => &$obj]));
  190. $obj->save();
  191. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SAVED'), 'info');
  192. $this->grav->fireEvent('onAdminAfterSave', new Event(['object' => $obj]));
  193. }
  194. Cache::clearCache('invalidate');
  195. // Force configuration reload.
  196. /** @var Config $config */
  197. $config = $this->grav['config'];
  198. $config->reload();
  199. if ($this->view === 'config') {
  200. $this->setRedirect($this->admin->getAdminRoute("/{$this->view}/{$this->route}")->toString());
  201. }
  202. return true;
  203. }
  204. // USER TASKS
  205. /**
  206. * Handle logout.
  207. *
  208. * Route: GET /task:logout
  209. * Route: POST ?task=logout
  210. *
  211. * @return bool True if the action was performed.
  212. */
  213. protected function taskLogout()
  214. {
  215. if (!$this->authorizeTask('logout', ['admin.login', 'admin.super'])) {
  216. return false;
  217. }
  218. $this->admin->logout($this->data, $this->post);
  219. }
  220. /**
  221. * Route: POST /ajax.json/task:regenerate2FASecret (AJAX call)
  222. *
  223. * @return bool
  224. */
  225. public function taskRegenerate2FASecret()
  226. {
  227. if (!$this->authorizeTask('regenerate 2FA Secret', ['admin.login', 'admin.super'])) {
  228. $this->admin->json_response = [
  229. 'status' => 'error',
  230. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  231. ];
  232. return false;
  233. }
  234. try {
  235. /** @var UserInterface $user */
  236. $user = $this->grav['user'];
  237. /** @var TwoFactorAuth $twoFa */
  238. $twoFa = $this->grav['login']->twoFactorAuth();
  239. $secret = $twoFa->createSecret();
  240. $image = $twoFa->getQrImageData($user->username, $secret);
  241. $user->set('twofa_secret', $secret);
  242. // TODO: data user can also use save, but please test it before removing this code.
  243. if ($user instanceof \Grav\Common\User\DataUser\User) {
  244. // Save secret into the user file.
  245. $file = $user->file();
  246. if ($file->exists()) {
  247. $content = (array)$file->content();
  248. $content['twofa_secret'] = $secret;
  249. $file->save($content);
  250. $file->free();
  251. }
  252. } else {
  253. $user->save();
  254. }
  255. $this->admin->json_response = ['status' => 'success', 'image' => $image, 'secret' => preg_replace('|(\w{4})|', '\\1 ', $secret)];
  256. } catch (\Exception $e) {
  257. /** @var Debugger $debugger */
  258. $debugger = $this->grav['debugger'];
  259. $debugger->addException($e);
  260. $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
  261. return false;
  262. }
  263. return true;
  264. }
  265. /**
  266. * Save user account.
  267. *
  268. * Called by more general save task.
  269. *
  270. * @note Not used if Flex-Objects plugin handles users.
  271. *
  272. * @return bool
  273. */
  274. protected function saveUser()
  275. {
  276. /** @var UserCollectionInterface $users */
  277. $users = $this->grav['accounts'];
  278. $user = $users->load($this->admin->route);
  279. if (!$this->admin->authorize(['admin.super', 'admin.users'])) {
  280. // no user file or not admin.super or admin.users
  281. if ($user->username !== $this->grav['user']->username) {
  282. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' save.','error');
  283. return false;
  284. }
  285. }
  286. /** @var Data\Blueprint $blueprint */
  287. $blueprint = $user->blueprints();
  288. $data = $blueprint->processForm($this->admin->cleanUserPost((array)$this->data));
  289. $data = new Data\Data($data, $blueprint);
  290. try {
  291. $data->validate();
  292. $data->filter();
  293. } catch (\Exception $e) {
  294. /** @var Debugger $debugger */
  295. $debugger = $this->grav['debugger'];
  296. $debugger->addException($e);
  297. $this->admin->setMessage($e->getMessage(), 'error');
  298. return false;
  299. }
  300. $user->update($data->toArray());
  301. $user = $this->storeFiles($user);
  302. if ($user) {
  303. // Event to manipulate data before saving the object
  304. $this->grav->fireEvent('onAdminSave', new Event(['object' => &$user]));
  305. $user->save();
  306. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SAVED'), 'info');
  307. $this->grav->fireEvent('onAdminAfterSave', new Event(['object' => $user]));
  308. }
  309. if ($user->username === $this->grav['user']->username) {
  310. /** @var UserCollectionInterface $users */
  311. $users = $this->grav['accounts'];
  312. //Editing current user. Reload user object
  313. $this->grav['user']->undef('avatar');
  314. $this->grav['user']->merge($users->load($this->admin->route)->toArray());
  315. }
  316. return true;
  317. }
  318. // DASHBOARD TASKS
  319. /**
  320. * Get Notifications
  321. *
  322. * Route: POST /task:getNotifications (AJAX call)
  323. *
  324. * @return bool
  325. */
  326. protected function taskGetNotifications()
  327. {
  328. if (!$this->authorizeTask('dashboard', ['admin.login', 'admin.super'])) {
  329. $this->admin->json_response = [
  330. 'status' => 'error',
  331. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  332. ];
  333. return false;
  334. }
  335. // do we need to force a reload
  336. $refresh = $this->data['refresh'] === 'true';
  337. $filter = $this->data['filter'] ?? '';
  338. $filter_types = !empty($filter) ? array_map('trim', explode(',', $filter)) : [];
  339. try {
  340. $notifications = $this->admin->getNotifications($refresh);
  341. $notification_data = [];
  342. foreach ($notifications as $type => $type_notifications) {
  343. if ($filter_types && in_array($type, $filter_types, true)) {
  344. $twig_template = 'partials/notification-' . $type . '-block.html.twig';
  345. $notification_data[$type] = $this->grav['twig']->processTemplate($twig_template, ['notifications' => $type_notifications]);
  346. }
  347. }
  348. $json_response = [
  349. 'status' => 'success',
  350. 'notifications' => $notification_data
  351. ];
  352. } catch (\Exception $e) {
  353. /** @var Debugger $debugger */
  354. $debugger = $this->grav['debugger'];
  355. $debugger->addException($e);
  356. $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
  357. }
  358. $this->sendJsonResponse($json_response);
  359. }
  360. /**
  361. * Hide notifications.
  362. *
  363. * Route: POST /notifications.json/task:hideNotification/notification_id:ID (AJAX call)
  364. *
  365. * @return bool True if the action was performed.
  366. */
  367. protected function taskHideNotification()
  368. {
  369. if (!$this->authorizeTask('hide notification', ['admin.login', 'admin.super'])) {
  370. $this->admin->json_response = [
  371. 'status' => 'error',
  372. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  373. ];
  374. return false;
  375. }
  376. $notification_id = $this->grav['uri']->param('notification_id');
  377. if (!$notification_id) {
  378. $this->admin->json_response = [
  379. 'status' => 'error'
  380. ];
  381. return false;
  382. }
  383. $filename = $this->grav['locator']->findResource('user://data/notifications/' . $this->grav['user']->username . YAML_EXT, true, true);
  384. $file = CompiledYamlFile::instance($filename);
  385. $data = (array)$file->content();
  386. $date = new \DateTime();
  387. $data[$notification_id] = $date->format('r');
  388. $file->save($data);
  389. $this->admin->json_response = [
  390. 'status' => 'success'
  391. ];
  392. return true;
  393. }
  394. /**
  395. * Get Newsfeeds
  396. *
  397. * Route: POST /ajax.json/task:getNewsFeed (AJAX call)
  398. *
  399. * @return bool
  400. */
  401. protected function taskGetNewsFeed()
  402. {
  403. if (!$this->authorizeTask('dashboard', ['admin.login', 'admin.super'])) {
  404. $this->admin->json_response = [
  405. 'status' => 'error',
  406. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  407. ];
  408. return false;
  409. }
  410. $refresh = $this->data['refresh'] === 'true' ? true : false;
  411. try {
  412. $feed = $this->admin->getFeed($refresh);
  413. $feed_data = $this->grav['twig']->processTemplate('partials/feed-block.html.twig', ['feed' => $feed]);
  414. $json_response = [
  415. 'status' => 'success',
  416. 'feed_data' => $feed_data
  417. ];
  418. } catch (MalformedXmlException $e) {
  419. /** @var Debugger $debugger */
  420. $debugger = $this->grav['debugger'];
  421. $debugger->addException($e);
  422. $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
  423. }
  424. $this->sendJsonResponse($json_response);
  425. }
  426. // BACKUP TASKS
  427. /**
  428. * Handle the backup action
  429. *
  430. * Route: GET /backup.json/id:BACKUP_ID/task:backup (AJAX call)
  431. *
  432. * @return bool True if the action was performed.
  433. */
  434. protected function taskBackup()
  435. {
  436. if (!$this->authorizeTask('backup', ['admin.maintenance', 'admin.super'])) {
  437. $this->admin->json_response = [
  438. 'status' => 'error',
  439. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  440. ];
  441. return false;
  442. }
  443. $param_sep = $this->grav['config']->get('system.param_sep', ':');
  444. $download = $this->grav['uri']->param('download');
  445. try {
  446. if ($download) {
  447. $filename = Utils::basename(base64_decode(urldecode($download)));
  448. $file = $this->grav['locator']->findResource("backup://{$filename}", true);
  449. if (!$file || !Utils::endsWith($filename, '.zip', false)) {
  450. header('HTTP/1.1 401 Unauthorized');
  451. exit();
  452. }
  453. Utils::download($file, true);
  454. }
  455. $id = $this->grav['uri']->param('id', 0);
  456. $backup = Backups::backup($id);
  457. } catch (\Exception $e) {
  458. /** @var Debugger $debugger */
  459. $debugger = $this->grav['debugger'];
  460. $debugger->addException($e);
  461. $this->admin->json_response = [
  462. 'status' => 'error',
  463. 'message' => $this->admin::translate('PLUGIN_ADMIN.AN_ERROR_OCCURRED') . '. ' . htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')
  464. ];
  465. return true;
  466. }
  467. $download = urlencode(base64_encode($backup));
  468. $url = rtrim($this->grav['uri']->rootUrl(false), '/') . '/' . trim($this->admin->base,
  469. '/') . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
  470. $this->admin->json_response = [
  471. 'status' => 'success',
  472. 'message' => $this->admin::translate('PLUGIN_ADMIN.YOUR_BACKUP_IS_READY_FOR_DOWNLOAD') . '. <a href="' . $url . '" class="button">' . $this->admin::translate('PLUGIN_ADMIN.DOWNLOAD_BACKUP') . '</a>',
  473. 'toastr' => [
  474. 'timeOut' => 0,
  475. 'extendedTimeOut' => 0,
  476. 'closeButton' => true
  477. ]
  478. ];
  479. return true;
  480. }
  481. /**
  482. * Handle delete backup action
  483. *
  484. * Route: GET /backup.json/backup:BACKUP_FILE/task:backupDelete (AJAX call)
  485. *
  486. * @return bool
  487. */
  488. protected function taskBackupDelete()
  489. {
  490. if (!$this->authorizeTask('backup', ['admin.maintenance', 'admin.super'])) {
  491. $this->admin->json_response = [
  492. 'status' => 'error',
  493. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  494. ];
  495. return false;
  496. }
  497. $backup = $this->grav['uri']->param('backup', null);
  498. if (null !== $backup) {
  499. $filename = Utils::basename(base64_decode(urldecode($backup)));
  500. $file = $this->grav['locator']->findResource("backup://{$filename}", true);
  501. if ($file && Utils::endsWith($filename, '.zip', false)) {
  502. unlink($file);
  503. $this->admin->json_response = [
  504. 'status' => 'success',
  505. 'message' => $this->admin::translate('PLUGIN_ADMIN.BACKUP_DELETED'),
  506. 'toastr' => [
  507. 'closeButton' => true
  508. ]
  509. ];
  510. return true;
  511. }
  512. }
  513. $this->admin->json_response = [
  514. 'status' => 'error',
  515. 'message' => $this->admin::translate('PLUGIN_ADMIN.BACKUP_NOT_FOUND'),
  516. ];
  517. return true;
  518. }
  519. // PLUGIN / THEME TASKS
  520. /**
  521. * Enable a plugin.
  522. *
  523. * Route: GET /plugins/SLUG/task:enable
  524. *
  525. * @return bool True if the action was performed.
  526. */
  527. public function taskEnable()
  528. {
  529. if ($this->view !== 'plugins') {
  530. return false;
  531. }
  532. if (!$this->authorizeTask('enable plugin', ['admin.plugins', 'admin.super'])) {
  533. return false;
  534. }
  535. $type = $this->getDataType();
  536. $this->updatePluginState($type, ['enabled' => true]);
  537. $this->post = ['_redirect' => 'plugins'];
  538. if ($this->grav['uri']->param('redirect')) {
  539. $this->post = ['_redirect' => 'plugins/' . $this->route];
  540. }
  541. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_ENABLED_PLUGIN'), 'info');
  542. Cache::clearCache('invalidate');
  543. return true;
  544. }
  545. /**
  546. * Disable a plugin.
  547. *
  548. * Route: GET /plugins/SLUG/task:disable
  549. *
  550. * @return bool True if the action was performed.
  551. */
  552. public function taskDisable()
  553. {
  554. if ($this->view !== 'plugins') {
  555. return false;
  556. }
  557. if (!$this->authorizeTask('disable plugin', ['admin.plugins', 'admin.super'])) {
  558. return false;
  559. }
  560. $type = $this->getDataType();
  561. $this->updatePluginState($type, ['enabled' => false]);
  562. $this->post = ['_redirect' => 'plugins'];
  563. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_DISABLED_PLUGIN'), 'info');
  564. Cache::clearCache('invalidate');
  565. return true;
  566. }
  567. /**
  568. * @param string $type
  569. * @param array $value
  570. * @return void
  571. */
  572. protected function updatePluginState(string $type, array $value): void
  573. {
  574. $obj = Plugins::get(preg_replace('|plugins/|', '', $type));
  575. if (null === $obj) {
  576. throw new \RuntimeException("Plugin '{$type}' doesn't exist!");
  577. }
  578. /** @var UniformResourceLocator $locator */
  579. $locator = $this->grav['locator'];
  580. // Configuration file will be saved to the existing config stream.
  581. $filename = $locator->findResource('config://') . "/{$type}.yaml";
  582. $file = YamlFile::instance($filename);
  583. $contents = $value + $file->content();
  584. $file->save($contents);
  585. }
  586. /**
  587. * Set the default theme.
  588. *
  589. * Route: GET /themes/SLUG/task:activate
  590. *
  591. * @return bool True if the action was performed.
  592. */
  593. public function taskActivate()
  594. {
  595. if ($this->view !== 'themes') {
  596. return false;
  597. }
  598. if (!$this->authorizeTask('activate theme', ['admin.themes', 'admin.super'])) {
  599. return false;
  600. }
  601. $this->post = ['_redirect' => 'themes' ];
  602. // Make sure theme exists (throws exception)
  603. $name = $this->route;
  604. $this->grav['themes']->get($name);
  605. // Store system configuration.
  606. $system = $this->admin->getConfigurationData('config/system');
  607. $system->set('pages.theme', $name);
  608. $system->save();
  609. // Force configuration reload and save.
  610. /** @var Config $config */
  611. $config = $this->grav['config'];
  612. $config->reload()->save();
  613. $config->set('system.pages.theme', $name);
  614. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_CHANGED_THEME'), 'info');
  615. Cache::clearCache('invalidate');
  616. $this->post = ['_redirect' => 'themes/' . $name ];
  617. return true;
  618. }
  619. // INSTALL & UPGRADE
  620. /**
  621. * Handles updating Grav
  622. *
  623. * Route: GET /update.json/task:updategrav (AJAX call)
  624. *
  625. * @return bool False if user has no permissions.
  626. */
  627. public function taskUpdategrav()
  628. {
  629. if (!$this->authorizeTask('install grav', ['admin.super'])) {
  630. $this->admin->json_response = [
  631. 'status' => 'error',
  632. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  633. ];
  634. return false;
  635. }
  636. $gpm = Gpm::GPM();
  637. $version = $gpm->grav->getVersion();
  638. $result = Gpm::selfupgrade();
  639. if ($result) {
  640. $json_response = [
  641. 'status' => 'success',
  642. 'type' => 'updategrav',
  643. 'version' => $version,
  644. 'message' => $this->admin::translate('PLUGIN_ADMIN.GRAV_WAS_SUCCESSFULLY_UPDATED_TO') . ' ' . $version
  645. ];
  646. } else {
  647. $json_response = [
  648. 'status' => 'error',
  649. 'type' => 'updategrav',
  650. 'version' => GRAV_VERSION,
  651. 'message' => $this->admin::translate('PLUGIN_ADMIN.GRAV_UPDATE_FAILED') . ' <br>' . Installer::lastErrorMsg()
  652. ];
  653. }
  654. $this->sendJsonResponse($json_response);
  655. }
  656. /**
  657. * Handles uninstalling plugins and themes
  658. *
  659. * @return bool True if the action was performed
  660. * @deprecated Not being used anymore
  661. */
  662. public function taskUninstall()
  663. {
  664. $type = $this->view;
  665. if ($type !== 'plugins' && $type !== 'themes') {
  666. return false;
  667. }
  668. if (!$this->authorizeTask('uninstall ' . $type, ['admin.' . $type, 'admin.super'])) {
  669. return false;
  670. }
  671. $package = $this->route;
  672. $result = Gpm::uninstall($package, []);
  673. if ($result) {
  674. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.UNINSTALL_SUCCESSFUL'), 'info');
  675. } else {
  676. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.UNINSTALL_FAILED'), 'error');
  677. }
  678. $this->post = ['_redirect' => $this->view];
  679. return true;
  680. }
  681. /**
  682. * Toggle the gpm.releases setting
  683. *
  684. * Route: POST /ajax.json/task:gpmRelease (AJAX call)
  685. *
  686. * @return bool
  687. */
  688. protected function taskGpmRelease()
  689. {
  690. if (!$this->authorizeTask('configuration', ['admin.super'])) {
  691. $this->admin->json_response = [
  692. 'status' => 'error',
  693. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  694. ];
  695. return false;
  696. }
  697. // Default release state
  698. $release = 'stable';
  699. $reload = false;
  700. // Get the testing release value if set
  701. if ($this->post['release'] === 'testing') {
  702. $release = 'testing';
  703. }
  704. $config = $this->grav['config'];
  705. $current_release = $config->get('system.gpm.releases');
  706. // If the releases setting is different, save it in the system config
  707. if ($current_release !== $release) {
  708. $data = new Data\Data($config->get('system'));
  709. $data->set('gpm.releases', $release);
  710. // Get the file location
  711. $file = CompiledYamlFile::instance($this->grav['locator']->findResource('config://system.yaml'));
  712. $data->file($file);
  713. // Save the configuration
  714. $data->save();
  715. $config->reload();
  716. $reload = true;
  717. }
  718. $this->admin->json_response = ['status' => 'success', 'reload' => $reload];
  719. return true;
  720. }
  721. /**
  722. * Get update status from GPM
  723. *
  724. * Request: POST /update.json/task:getUpdates (AJAX call)
  725. *
  726. * @return bool
  727. */
  728. protected function taskGetUpdates()
  729. {
  730. if ($this->view !== 'update') {
  731. return false;
  732. }
  733. if (!$this->authorizeTask('dashboard', ['admin.plugins', 'admin.themes', 'admin.super'])) {
  734. $this->admin->json_response = [
  735. 'status' => 'error',
  736. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  737. ];
  738. return false;
  739. }
  740. $data = $this->post;
  741. $flush = !empty($data['flush']);
  742. if (isset($this->grav['session'])) {
  743. $this->grav['session']->close();
  744. }
  745. try {
  746. $gpm = new GravGPM($flush);
  747. $resources_updates = $gpm->getUpdatable();
  748. foreach ($resources_updates as $key => $update) {
  749. if (!is_iterable($update)) {
  750. continue;
  751. }
  752. foreach ($update as $slug => $item) {
  753. $resources_updates[$key][$slug] = $item;
  754. }
  755. }
  756. if ($gpm->grav !== null) {
  757. $grav_updates = [
  758. 'isUpdatable' => $gpm->grav->isUpdatable(),
  759. 'assets' => $gpm->grav->getAssets(),
  760. 'version' => GRAV_VERSION,
  761. 'available' => $gpm->grav->getVersion(),
  762. 'date' => $gpm->grav->getDate(),
  763. 'isSymlink' => $gpm->grav->isSymlink()
  764. ];
  765. $this->admin->json_response = [
  766. 'status' => 'success',
  767. 'payload' => [
  768. 'resources' => $resources_updates,
  769. 'grav' => $grav_updates,
  770. 'installed' => $gpm->countInstalled(),
  771. 'flushed' => $flush
  772. ]
  773. ];
  774. } else {
  775. $this->admin->json_response = ['status' => 'error', 'message' => 'Cannot connect to the GPM'];
  776. return false;
  777. }
  778. } catch (\Exception $e) {
  779. /** @var Debugger $debugger */
  780. $debugger = $this->grav['debugger'];
  781. $debugger->addException($e);
  782. $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
  783. return false;
  784. }
  785. return true;
  786. }
  787. /**
  788. * Handle getting a new package dependencies needed to be installed.
  789. *
  790. * Route: /plugins.json/task:getPackagesDependencies (AJAX call)
  791. * Route: /themes.json/task:getPackagesDependencies (AJAX call)
  792. *
  793. * @return bool
  794. */
  795. protected function taskGetPackagesDependencies()
  796. {
  797. $type = $this->view;
  798. if ($type !== 'plugins' && $type !== 'themes') {
  799. return false;
  800. }
  801. if (!$this->authorizeTask('get package dependencies', ['admin.' . $type, 'admin.super'])) {
  802. $this->admin->json_response = [
  803. 'status' => 'error',
  804. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  805. ];
  806. return false;
  807. }
  808. $data = $this->post;
  809. $packages = isset($data['packages']) ? explode(',', $data['packages']) : '';
  810. $packages = (array)$packages;
  811. try {
  812. $this->admin->checkPackagesCanBeInstalled($packages);
  813. $dependencies = $this->admin->getDependenciesNeededToInstall($packages);
  814. } catch (\Exception $e) {
  815. /** @var Debugger $debugger */
  816. $debugger = $this->grav['debugger'];
  817. $debugger->addException($e);
  818. $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
  819. return false;
  820. }
  821. $this->admin->json_response = ['status' => 'success', 'dependencies' => $dependencies];
  822. return true;
  823. }
  824. /**
  825. * Route: /plugins.json/task:installDependenciesOfPackages (AJAX call)
  826. * Route: /themes.json/task:installDependenciesOfPackages (AJAX call)
  827. *
  828. * @return bool
  829. */
  830. protected function taskInstallDependenciesOfPackages()
  831. {
  832. $type = $this->view;
  833. if ($type !== 'plugins' && $type !== 'themes') {
  834. return false;
  835. }
  836. if (!$this->authorizeTask('install dependencies', ['admin.' . $type, 'admin.super'])) {
  837. $this->admin->json_response = [
  838. 'status' => 'error',
  839. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  840. ];
  841. return false;
  842. }
  843. $data = $this->post;
  844. $packages = isset($data['packages']) ? explode(',', $data['packages']) : '';
  845. $packages = (array)$packages;
  846. try {
  847. $dependencies = $this->admin->getDependenciesNeededToInstall($packages);
  848. } catch (\Exception $e) {
  849. /** @var Debugger $debugger */
  850. $debugger = $this->grav['debugger'];
  851. $debugger->addException($e);
  852. $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
  853. return false;
  854. }
  855. $result = Gpm::install(array_keys($dependencies), ['theme' => $type === 'themes']);
  856. if ($result) {
  857. $this->admin->json_response = ['status' => 'success', 'message' => 'Dependencies installed successfully'];
  858. } else {
  859. $this->admin->json_response = [
  860. 'status' => 'error',
  861. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSTALLATION_FAILED')
  862. ];
  863. }
  864. return true;
  865. }
  866. /**
  867. * Route: /plugins.json/task:installPackage (AJAX call)
  868. * Route: /themes.json/task:installPackage (AJAX call)
  869. *
  870. * @param bool $reinstall
  871. * @return bool
  872. */
  873. protected function taskInstallPackage($reinstall = false)
  874. {
  875. $type = $this->view;
  876. if ($type !== 'plugins' && $type !== 'themes') {
  877. return false;
  878. }
  879. if (!$this->authorizeTask('install ' . $type, ['admin.' . $type, 'admin.super'])) {
  880. $this->admin->json_response = [
  881. 'status' => 'error',
  882. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  883. ];
  884. return false;
  885. }
  886. $data = $this->post;
  887. $package = $data['package'] ?? '';
  888. try {
  889. $result = Gpm::install($package, ['theme' => $type === 'themes']);
  890. } catch (\Exception $e) {
  891. /** @var Debugger $debugger */
  892. $debugger = $this->grav['debugger'];
  893. $debugger->addException($e);
  894. $msg = $e->getMessage();
  895. $msg = Utils::contains($msg, '401 Unauthorized') ? "ERROR: License key for this resource is invalid." : $msg;
  896. $msg = Utils::contains($msg, '404 Not Found') ? "ERROR: Resource not found" : $msg;
  897. $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($msg, ENT_QUOTES | ENT_HTML5, 'UTF-8')];
  898. return false;
  899. }
  900. if ($result) {
  901. $this->admin->json_response = [
  902. 'status' => 'success',
  903. 'message' => $this->admin::translate(is_string($result) ? $result : sprintf($this->admin::translate($reinstall ?: 'PLUGIN_ADMIN.PACKAGE_X_REINSTALLED_SUCCESSFULLY',
  904. null), $package))
  905. ];
  906. } else {
  907. $this->admin->json_response = [
  908. 'status' => 'error',
  909. 'message' => $this->admin::translate($reinstall ?: 'PLUGIN_ADMIN.INSTALLATION_FAILED')
  910. ];
  911. }
  912. return true;
  913. }
  914. /**
  915. * Handle removing a package
  916. *
  917. * Route: /plugins.json/task:removePackage (AJAX call)
  918. * Route: /themes.json/task:removePackage (AJAX call)
  919. *
  920. * @return bool
  921. */
  922. protected function taskRemovePackage(): bool
  923. {
  924. $type = $this->view;
  925. if ($type !== 'plugins' && $type !== 'themes') {
  926. return false;
  927. }
  928. if (!$this->authorizeTask('uninstall ' . $type, ['admin.' . $type, 'admin.super'])) {
  929. $json_response = [
  930. 'status' => 'error',
  931. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  932. ];
  933. $this->sendJsonResponse($json_response, 403);
  934. }
  935. $data = $this->post;
  936. $package = $data['package'] ?? '';
  937. $result = false;
  938. //check if there are packages that have this as a dependency. Abort and show which ones
  939. $dependent_packages = $this->admin->getPackagesThatDependOnPackage($package);
  940. if (count($dependent_packages) > 0) {
  941. if (count($dependent_packages) > 1) {
  942. $message = 'The installed packages <cyan>' . implode('</cyan>, <cyan>',
  943. $dependent_packages) . '</cyan> depends on this package. Please remove those first.';
  944. } else {
  945. $message = 'The installed package <cyan>' . implode('</cyan>, <cyan>',
  946. $dependent_packages) . '</cyan> depends on this package. Please remove it first.';
  947. }
  948. $json_response = ['status' => 'error', 'message' => $message];
  949. $this->sendJsonResponse($json_response, 200);
  950. }
  951. $dependencies = false;
  952. try {
  953. $dependencies = $this->admin->dependenciesThatCanBeRemovedWhenRemoving($package);
  954. $result = Gpm::uninstall($package, []);
  955. } catch (\Exception $e) {
  956. /** @var Debugger $debugger */
  957. $debugger = $this->grav['debugger'];
  958. $debugger->addException($e);
  959. $json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
  960. $this->sendJsonResponse($json_response, 200);
  961. }
  962. if ($result) {
  963. $json_response = [
  964. 'status' => 'success',
  965. 'dependencies' => $dependencies,
  966. 'message' => $this->admin::translate(is_string($result) ? $result : 'PLUGIN_ADMIN.UNINSTALL_SUCCESSFUL')
  967. ];
  968. $this->sendJsonResponse($json_response, 200);
  969. }
  970. $json_response = [
  971. 'status' => 'error',
  972. 'message' => $this->admin::translate('PLUGIN_ADMIN.UNINSTALL_FAILED')
  973. ];
  974. $this->sendJsonResponse($json_response, 200);
  975. }
  976. /**
  977. * Handle reinstalling a package
  978. *
  979. * Route: /plugins.json/task:reinstallPackage (AJAX call)
  980. * Route: /themes.json/task:reinstallPackage (AJAX call)
  981. *
  982. * @return bool
  983. */
  984. protected function taskReinstallPackage()
  985. {
  986. $type = $this->view;
  987. if ($type !== 'plugins' && $type !== 'themes') {
  988. return false;
  989. }
  990. if (!$this->authorizeTask('install ' . $type, ['admin.' . $type, 'admin.super'])) {
  991. $json_response = [
  992. 'status' => 'error',
  993. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  994. ];
  995. $this->sendJsonResponse($json_response, 403);
  996. }
  997. $data = $this->post;
  998. $slug = $data['slug'] ?? '';
  999. $package_name = $data['package_name'] ?? '';
  1000. $current_version = $data['current_version'] ?? '';
  1001. $url = "https://getgrav.org/download/{$type}s/$slug/$current_version";
  1002. $result = Gpm::directInstall($url);
  1003. if ($result === true) {
  1004. $this->admin->json_response = [
  1005. 'status' => 'success',
  1006. 'message' => $this->admin::translate(sprintf($this->admin::translate('PLUGIN_ADMIN.PACKAGE_X_REINSTALLED_SUCCESSFULLY',
  1007. null), $package_name))
  1008. ];
  1009. } else {
  1010. $this->admin->json_response = [
  1011. 'status' => 'error',
  1012. 'message' => $this->admin::translate('PLUGIN_ADMIN.REINSTALLATION_FAILED')
  1013. ];
  1014. }
  1015. return true;
  1016. }
  1017. /**
  1018. * Handle direct install.
  1019. *
  1020. * Request: POST /tools/direct-install?task=directInstall
  1021. *
  1022. * @return bool
  1023. */
  1024. protected function taskDirectInstall()
  1025. {
  1026. if (!$this->authorizeTask('install', ['admin.super'])) {
  1027. return false;
  1028. }
  1029. $file_path = $this->data['file_path'] ?? null;
  1030. if (isset($_FILES['uploaded_file'])) {
  1031. // Check $_FILES['file']['error'] value.
  1032. switch ($_FILES['uploaded_file']['error']) {
  1033. case UPLOAD_ERR_OK:
  1034. break;
  1035. case UPLOAD_ERR_NO_FILE:
  1036. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.NO_FILES_SENT'), 'error');
  1037. return false;
  1038. case UPLOAD_ERR_INI_SIZE:
  1039. case UPLOAD_ERR_FORM_SIZE:
  1040. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 'error');
  1041. return false;
  1042. case UPLOAD_ERR_NO_TMP_DIR:
  1043. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 'error');
  1044. return false;
  1045. default:
  1046. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 'error');
  1047. return false;
  1048. }
  1049. $file_name = $_FILES['uploaded_file']['name'];
  1050. $file_path = $_FILES['uploaded_file']['tmp_name'];
  1051. // Handle bad filenames.
  1052. if (!Utils::checkFilename($file_name)) {
  1053. $this->admin->json_response = [
  1054. 'status' => 'error',
  1055. 'message' => $this->admin::translate('PLUGIN_ADMIN.UNKNOWN_ERRORS')
  1056. ];
  1057. return false;
  1058. }
  1059. }
  1060. $result = Gpm::directInstall($file_path);
  1061. if ($result === true) {
  1062. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INSTALLATION_SUCCESSFUL'), 'info');
  1063. } else {
  1064. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INSTALLATION_FAILED') . ': ' . $result,
  1065. 'error');
  1066. }
  1067. $this->setRedirect('/tools');
  1068. return true;
  1069. }
  1070. // PAGE TASKS
  1071. /**
  1072. * Handles creating an empty page folder (without markdown file)
  1073. *
  1074. * Route: /pages
  1075. *
  1076. * @note Not used if Flex-Objects plugin handles pages.
  1077. *
  1078. * @return bool True if the action was performed.
  1079. */
  1080. public function taskSaveNewFolder()
  1081. {
  1082. if ($this->view !== 'pages') {
  1083. return false;
  1084. }
  1085. if (!$this->authorizeTask('new folder', ['admin.pages', 'admin.pages.create', 'admin.super'])) {
  1086. return false;
  1087. }
  1088. $data = (array)$this->data;
  1089. $folder = $data['folder'] ?? '';
  1090. if ($folder === '' || mb_strpos($folder, '/') !== false) {
  1091. throw new \RuntimeException('Creating folder failed: bad folder name', 400);
  1092. }
  1093. if ($data['route'] === '' || $data['route'] === '/') {
  1094. $path = $this->grav['locator']->findResource('page://');
  1095. } else {
  1096. $pages = $this->admin::enablePages();
  1097. $page = $pages->find($data['route']);
  1098. if (!$page) {
  1099. return false;
  1100. }
  1101. $path = $page->path();
  1102. }
  1103. $orderOfNewFolder = static::getNextOrderInFolder($path);
  1104. $new_path = $path . '/' . $orderOfNewFolder . '.' . $folder;
  1105. Folder::create($new_path);
  1106. Cache::clearCache('invalidate');
  1107. $this->grav->fireEvent('onAdminAfterSaveAs', new Event(['path' => $new_path]));
  1108. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SAVED'), 'info');
  1109. $this->setRedirect($this->admin->getAdminRoute("/{$this->view}")->toString());
  1110. return true;
  1111. }
  1112. /**
  1113. * @note Not used if Flex-Objects plugin handles pages.
  1114. *
  1115. * @return bool
  1116. */
  1117. protected function savePage()
  1118. {
  1119. $reorder = true;
  1120. $data = (array)$this->data;
  1121. $this->grav['twig']->twig_vars['current_form_data'] = $data;
  1122. $pages = $this->admin::enablePages();
  1123. // Find new parent page in order to build the path.
  1124. $path = trim($data['route'] ?? dirname($this->admin->route), '/');
  1125. if ($path === '.') {
  1126. $path = '';
  1127. }
  1128. /** @var PageInterface $obj */
  1129. $obj = $this->admin->page(true);
  1130. $folder = $data['folder'] ?? null;
  1131. if ($folder === '' || mb_strpos($folder, '/') !== false) {
  1132. throw new \RuntimeException('Saving page failed: bad folder name', 400);
  1133. }
  1134. if (!isset($data['folder']) || !$data['folder']) {
  1135. $data['folder'] = $obj->slug();
  1136. $this->data['folder'] = $obj->slug();
  1137. }
  1138. // Check for valid frontmatter
  1139. if (isset($data['frontmatter']) && !$this->checkValidFrontmatter($data['frontmatter'])) {
  1140. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INVALID_FRONTMATTER_COULD_NOT_SAVE'),
  1141. 'error');
  1142. return false;
  1143. }
  1144. // XSS Checks for page content
  1145. $xss_whitelist = $this->grav['config']->get('security.xss_whitelist', 'admin.super');
  1146. if (!$this->admin->authorize($xss_whitelist)) {
  1147. $check_what = ['header' => $data['header'] ?? '', 'frontmatter' => $data['frontmatter'] ?? '', 'content' => $data['content'] ?? ''];
  1148. $results = Security::detectXssFromArray($check_what);
  1149. if (!empty($results)) {
  1150. $this->admin->setMessage('<i class="fa fa-ban"></i> ' . $this->admin::translate('PLUGIN_ADMIN.XSS_ONSAVE_ISSUE'),
  1151. 'error');
  1152. return false;
  1153. }
  1154. }
  1155. if ($path !== '') {
  1156. // First try to get page by its path.
  1157. $parent = $pages->get(GRAV_ROOT . '/' . $path);
  1158. if (!$parent) {
  1159. // Fall back using the route.
  1160. $route = '/' . preg_replace(PageIndex::PAGE_ROUTE_REGEX, '/', $path);
  1161. $parent = $pages->find($route, true);
  1162. if (!$parent) {
  1163. throw new \RuntimeException('New parent page cannot be resolved!');
  1164. }
  1165. }
  1166. } else {
  1167. $parent = $pages->root();
  1168. }
  1169. $original_order = (int)trim($obj->order(), '.');
  1170. try {
  1171. // Change parent if needed and initialize move (might be needed also on ordering/folder change).
  1172. $obj = $obj->move($parent);
  1173. $this->preparePage($obj, false, $obj->language());
  1174. $obj->validate();
  1175. } catch (\Exception $e) {
  1176. /** @var Debugger $debugger */
  1177. $debugger = $this->grav['debugger'];
  1178. $debugger->addException($e);
  1179. $this->admin->setMessage($e->getMessage(), 'error');
  1180. return false;
  1181. }
  1182. $obj->filter();
  1183. // rename folder based on visible
  1184. if ($original_order === 1000) {
  1185. // increment order to force reshuffle
  1186. $obj->order($original_order + 1);
  1187. }
  1188. if (isset($data['order']) && !empty($data['order'])) {
  1189. $reorder = explode(',', $data['order']);
  1190. }
  1191. // add or remove numeric prefix based on ordering value
  1192. if (isset($data['ordering'])) {
  1193. if ($data['ordering'] && !$obj->order()) {
  1194. $obj->order(static::getNextOrderInFolder($obj->parent()->path()));
  1195. $reorder = false;
  1196. } elseif (!$data['ordering'] && $obj->order()) {
  1197. $obj->folder($obj->slug());
  1198. }
  1199. }
  1200. $obj = $this->storeFiles($obj);
  1201. if ($obj) {
  1202. // Event to manipulate data before saving the object
  1203. // DEPRECATED: page
  1204. $this->grav->fireEvent('onAdminSave', new Event(['object' => &$obj, 'page' => &$obj]));
  1205. $obj->save($reorder);
  1206. Cache::clearCache('invalidate');
  1207. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SAVED'), 'info');
  1208. // DEPRECATED: page
  1209. $this->grav->fireEvent('onAdminAfterSave', new Event(['object' => $obj, 'page' => $obj]));
  1210. }
  1211. if (method_exists($obj, 'unsetRouteSlug')) {
  1212. $obj->unsetRouteSlug();
  1213. }
  1214. $multilang = $this->isMultilang();
  1215. if ($multilang && !$obj->language()) {
  1216. $obj->language($this->admin->getLanguage());
  1217. }
  1218. $admin_route = $this->admin->base;
  1219. $route = $obj->rawRoute();
  1220. $redirect_url = ($multilang ? '/' . $obj->language() : '') . $admin_route . '/' . $this->view . $route;
  1221. $this->setRedirect($redirect_url);
  1222. return true;
  1223. }
  1224. /**
  1225. * Save page as a new copy.
  1226. *
  1227. * Route: /pages
  1228. *
  1229. * @note Not used if Flex-Objects plugin handles pages.
  1230. *
  1231. * @return bool True if the action was performed.
  1232. * @throws \RuntimeException
  1233. */
  1234. protected function taskCopy()
  1235. {
  1236. if ($this->view !== 'pages') {
  1237. return false;
  1238. }
  1239. if (!$this->authorizeTask('copy page', ['admin.pages', 'admin.pages.create', 'admin.super'])) {
  1240. return false;
  1241. }
  1242. try {
  1243. $pages = $this->admin::enablePages();
  1244. // Get the current page.
  1245. $original_page = $this->admin->page(true);
  1246. // Find new parent page in order to build the path.
  1247. $parent = $original_page->parent() ?: $pages->root();
  1248. // Make a copy of the current page and fill the updated information into it.
  1249. $page = $original_page->copy($parent);
  1250. $order = 0;
  1251. if ($page->order()) {
  1252. $order = $this->getNextOrderInFolder($page->parent()->path());
  1253. }
  1254. // Make sure the header is loaded in case content was set through raw() (expert mode)
  1255. $page->header();
  1256. if ($page->order()) {
  1257. $page->order($order);
  1258. }
  1259. $folder = $this->findFirstAvailable('folder', $page);
  1260. $slug = $this->findFirstAvailable('slug', $page);
  1261. $page->path($page->parent()->path() . DS . $page->order() . $folder);
  1262. $page->route($page->parent()->route() . '/' . $slug);
  1263. $page->rawRoute($page->parent()->rawRoute() . '/' . $slug);
  1264. // Append progressive number to the copied page title
  1265. $match = preg_split('/(\d+)(?!.*\d)/', $original_page->title(), 2, PREG_SPLIT_DELIM_CAPTURE);
  1266. $header = $page->header();
  1267. if (!isset($match[1])) {
  1268. $header->title = $match[0] . ' 2';
  1269. } else {
  1270. $header->title = $match[0] . ((int)$match[1] + 1);
  1271. }
  1272. $page->header($header);
  1273. $page->save(false);
  1274. $redirect = $this->view . $page->rawRoute();
  1275. $header = $page->header();
  1276. if (isset($header->slug)) {
  1277. $match = preg_split('/-(\d+)$/', $header->slug, 2, PREG_SPLIT_DELIM_CAPTURE);
  1278. $header->slug = $match[0] . '-' . (isset($match[1]) ? (int)$match[1] + 1 : 2);
  1279. }
  1280. $page->header($header);
  1281. $page->save();
  1282. Cache::clearCache('invalidate');
  1283. // DEPRECATED: page
  1284. $this->grav->fireEvent('onAdminAfterSave', new Event(['object' => $page, 'page' => $page]));
  1285. // Enqueue message and redirect to new location.
  1286. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_COPIED'), 'info');
  1287. $this->setRedirect($redirect);
  1288. } catch (\Exception $e) {
  1289. throw new \RuntimeException('Copying page failed on error: ' . $e->getMessage());
  1290. }
  1291. return true;
  1292. }
  1293. /**
  1294. * Reorder pages.
  1295. *
  1296. * Route: /pages
  1297. *
  1298. * @note Not used if Flex-Objects plugin handles pages.
  1299. *
  1300. * @return bool True if the action was performed.
  1301. */
  1302. protected function taskReorder()
  1303. {
  1304. if ($this->view !== 'pages') {
  1305. return false;
  1306. }
  1307. if (!$this->authorizeTask('reorder pages', ['admin.pages', 'admin.pages.update', 'admin.super'])) {
  1308. return false;
  1309. }
  1310. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.REORDERING_WAS_SUCCESSFUL'), 'info');
  1311. return true;
  1312. }
  1313. /**
  1314. * Delete page.
  1315. *
  1316. * Route: /pages
  1317. *
  1318. * @note Not used if Flex-Objects plugin handles pages.
  1319. *
  1320. * @return bool True if the action was performed.
  1321. * @throws \RuntimeException
  1322. */
  1323. protected function taskDelete()
  1324. {
  1325. if ($this->view !== 'pages') {
  1326. return false;
  1327. }
  1328. if (!$this->authorizeTask('delete page', ['admin.pages', 'admin.pages.delete', 'admin.super'])) {
  1329. return false;
  1330. }
  1331. try {
  1332. $page = $this->admin->page();
  1333. if (count($page->translatedLanguages()) > 1) {
  1334. $page->file()->delete();
  1335. } else {
  1336. Folder::delete($page->path());
  1337. }
  1338. // DEPRECATED: page
  1339. $this->grav->fireEvent('onAdminAfterDelete', new Event(['object' => $page, 'page' => $page]));
  1340. Cache::clearCache('invalidate');
  1341. // Set redirect to pages list.
  1342. $redirect = 'pages';
  1343. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_DELETED'), 'info');
  1344. $this->setRedirect($redirect);
  1345. } catch (\Exception $e) {
  1346. throw new \RuntimeException('Deleting page failed on error: ' . $e->getMessage());
  1347. }
  1348. return true;
  1349. }
  1350. /**
  1351. * Switch the content language. Optionally redirect to a different page.
  1352. *
  1353. * Route: /pages
  1354. *
  1355. * @note Not used if Flex-Objects plugin handles pages.
  1356. *
  1357. * @return bool
  1358. */
  1359. protected function taskSwitchlanguage()
  1360. {
  1361. if ($this->view !== 'pages') {
  1362. return false;
  1363. }
  1364. if (!$this->authorizeTask('switch language', ['admin.pages', 'admin.pages.list', 'admin.super'])) {
  1365. return false;
  1366. }
  1367. $data = (array)$this->data;
  1368. $language = $data['lang'] ?? $this->grav['uri']->param('lang');
  1369. if (isset($data['redirect'])) {
  1370. $redirect = '/pages/' . $data['redirect'];
  1371. } else {
  1372. $redirect = '/pages';
  1373. }
  1374. if ($language) {
  1375. $this->grav['session']->admin_lang = $language ?: 'en';
  1376. }
  1377. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SWITCHED_LANGUAGE'), 'info');
  1378. $this->setRedirect($this->admin->getAdminRoute($redirect)->toString());
  1379. return true;
  1380. }
  1381. /**
  1382. * Save the current page in a different language. Automatically switches to that language.
  1383. *
  1384. * Route: /pages
  1385. *
  1386. * @note Not used if Flex-Objects plugin handles pages.
  1387. *
  1388. * @return bool True if the action was performed.
  1389. */
  1390. protected function taskSaveas()
  1391. {
  1392. if ($this->view !== 'pages') {
  1393. return false;
  1394. }
  1395. if (!$this->authorizeTask('save as', ['admin.pages', 'admin.pages.create', 'admin.super'])) {
  1396. return false;
  1397. }
  1398. /** @var Language $language */
  1399. $language = $this->grav['language'];
  1400. $data = (array)$this->data;
  1401. $lang = $data['lang'] ?? null;
  1402. if ($lang) {
  1403. $this->grav['session']->admin_lang = $lang ?: 'en';
  1404. }
  1405. $uri = $this->grav['uri'];
  1406. $obj = $this->admin->page($uri->route());
  1407. $this->preparePage($obj, false, $lang);
  1408. $file = $obj->file();
  1409. if ($file) {
  1410. $filename = $this->determineFilenameIncludingLanguage($obj->name(), $lang);
  1411. $path = $obj->path() . DS . $filename;
  1412. $aFile = File::instance($path);
  1413. $aFile->save();
  1414. $aPage = new Page();
  1415. $aPage->init(new \SplFileInfo($path), $lang . '.md');
  1416. $aPage->header($obj->header());
  1417. $aPage->rawMarkdown($obj->rawMarkdown());
  1418. $aPage->template($obj->template());
  1419. $aPage->validate();
  1420. $aPage->filter();
  1421. // DEPRECATED: page
  1422. $this->grav->fireEvent('onAdminSave', new Event(['object' => $aPage, 'page' => &$aPage]));
  1423. $aPage->save();
  1424. Cache::clearCache('invalidate');
  1425. // DEPRECATED: page
  1426. $this->grav->fireEvent('onAdminAfterSave', new Event(['object' => $aPage, 'page' => $aPage]));
  1427. }
  1428. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.SUCCESSFULLY_SWITCHED_LANGUAGE'), 'info');
  1429. // TODO: better multilanguage support needed.
  1430. $this->setRedirect($language->getLanguageURLPrefix($lang) . $uri->route());
  1431. return true;
  1432. }
  1433. /**
  1434. * Continue to the new page.
  1435. *
  1436. * @note Not used if Flex-Objects plugin handles pages, users and user groups.
  1437. *
  1438. * @return bool True if the action was performed.
  1439. */
  1440. public function taskContinue()
  1441. {
  1442. $data = (array)$this->data;
  1443. if ($this->view === 'users') {
  1444. $username = strip_tags(strtolower($data['username']));
  1445. $this->setRedirect("{$this->view}/{$username}");
  1446. return true;
  1447. }
  1448. if ($this->view !== 'pages') {
  1449. return false;
  1450. }
  1451. $route = $data['route'] !== '/' ? $data['route'] : '';
  1452. $folder = $data['folder'] ?? null;
  1453. $title = $data['title'] ?? null;
  1454. // Handle @slugify-{field} value, automatically slugifies the specified field
  1455. if (null !== $folder && 0 === strpos($folder, '@slugify-')) {
  1456. $folder = \Grav\Plugin\Admin\Utils::slug($data[substr($folder, 9)] ?? '');
  1457. }
  1458. if (!$folder) {
  1459. $folder = \Grav\Plugin\Admin\Utils::slug($title) ?: '';
  1460. }
  1461. $folder = ltrim($folder, '_');
  1462. if ($folder === '' || mb_strpos($folder, '/') !== false) {
  1463. throw new \RuntimeException('Creating page failed: bad folder name', 400);
  1464. }
  1465. if (!empty($data['modular'])) {
  1466. $folder = '_' . $folder;
  1467. }
  1468. $data['folder'] = $folder;
  1469. $path = $route . '/' . $folder;
  1470. $this->admin->session()->{$path} = $data;
  1471. // Store the name and route of a page, to be used pre-filled defaults of the form in the future
  1472. $this->admin->session()->lastPageName = $data['name'];
  1473. $this->admin->session()->lastPageRoute = $data['route'];
  1474. $this->setRedirect("{$this->view}/" . ltrim($path, '/'));
  1475. return true;
  1476. }
  1477. /**
  1478. * $data['route'] = $this->grav['uri']->param('route');
  1479. * $data['sortby'] = $this->grav['uri']->param('sortby', null);
  1480. * $data['filters'] = $this->grav['uri']->param('filters', null);
  1481. * $data['page'] $this->grav['uri']->param('page', true);
  1482. * $data['base'] = $this->grav['uri']->param('base');
  1483. * $initial = (bool) $this->grav['uri']->param('initial');
  1484. *
  1485. * @return ResponseInterface
  1486. * @throws RequestException
  1487. */
  1488. protected function taskGetLevelListing(): ResponseInterface
  1489. {
  1490. $this->checkTaskAuthorization('save', $this->dataPermissions());
  1491. $request = $this->getRequest();
  1492. $data = $request->getParsedBody();
  1493. if (!isset($data['field'])) {
  1494. throw new RequestException($request, 'Bad Request', 400);
  1495. }
  1496. // Base64 decode the route
  1497. $data['route'] = isset($data['route']) ? base64_decode($data['route']) : null;
  1498. $initial = $data['initial'] ?? null;
  1499. if ($initial) {
  1500. $data['leaf_route'] = $data['route'];
  1501. $data['route'] = null;
  1502. $data['level'] = 1;
  1503. }
  1504. [$status, $message, $response,] = $this->getLevelListing($data);
  1505. $json = [
  1506. 'status' => $status,
  1507. 'message' => $this->admin::translate($message ?? 'PLUGIN_ADMIN.NO_ROUTE_PROVIDED'),
  1508. 'data' => array_values($response)
  1509. ];
  1510. return $this->createJsonResponse($json, 200);
  1511. }
  1512. /**
  1513. * Route: /ajax.json
  1514. *
  1515. * @note Not used if Flex-Objects plugin handles pages.
  1516. *
  1517. * @return bool
  1518. */
  1519. protected function taskGetChildTypes()
  1520. {
  1521. if ($this->view !== 'ajax') {
  1522. return false;
  1523. }
  1524. if (!$this->authorizeTask('get childtypes', ['admin.pages', 'admin.pages.list', 'admin.super'])) {
  1525. $this->admin->json_response = [
  1526. 'status' => 'error',
  1527. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  1528. ];
  1529. return false;
  1530. }
  1531. $data = $this->post;
  1532. $route = $data['rawroute'] ?? null;
  1533. if ($route) {
  1534. /** @var Flex $flex */
  1535. $flex = $this->grav['flex'];
  1536. $page = $flex->getObject(trim($route, '/'), 'pages');
  1537. if ($page instanceof PageInterface) {
  1538. $child_type = $page->childType();
  1539. if ($child_type !== '') {
  1540. $this->admin->json_response = [
  1541. 'status' => 'success',
  1542. 'child_type' => $child_type
  1543. ];
  1544. return true;
  1545. }
  1546. }
  1547. }
  1548. $this->admin->json_response = [
  1549. 'status' => 'success',
  1550. 'child_type' => '',
  1551. ];
  1552. return true;
  1553. }
  1554. /**
  1555. * Handles filtering the page by modular/visible/routable in the pages list.
  1556. *
  1557. * @note Used only in legacy pages.
  1558. *
  1559. * @return bool
  1560. */
  1561. protected function taskFilterPages()
  1562. {
  1563. if ($this->view !== 'pages-filter') {
  1564. return false;
  1565. }
  1566. if (!$this->authorizeTask('filter pages', ['admin.pages', 'admin.pages.list', 'admin.super'])) {
  1567. $this->admin->json_response = [
  1568. 'status' => 'error',
  1569. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  1570. ];
  1571. return false;
  1572. }
  1573. $data = $this->post;
  1574. $flags = !empty($data['flags']) ? array_map('strtolower', explode(',', $data['flags'])) : [];
  1575. $queries = !empty($data['query']) ? explode(',', $data['query']) : [];
  1576. $pages = $this->admin::enablePages();
  1577. /** @var Collection $collection */
  1578. $collection = $pages->all();
  1579. if (count($flags)) {
  1580. // Filter by state
  1581. $pageStates = [
  1582. 'modular',
  1583. 'nonmodular',
  1584. 'visible',
  1585. 'nonvisible',
  1586. 'routable',
  1587. 'nonroutable',
  1588. 'published',
  1589. 'nonpublished'
  1590. ];
  1591. if (count(array_intersect($pageStates, $flags)) > 0) {
  1592. if (in_array('modular', $flags, true)) {
  1593. $collection = $collection->modular();
  1594. }
  1595. if (in_array('nonmodular', $flags, true)) {
  1596. $collection = $collection->nonModular();
  1597. }
  1598. if (in_array('visible', $flags, true)) {
  1599. $collection = $collection->visible();
  1600. }
  1601. if (in_array('nonvisible', $flags, true)) {
  1602. $collection = $collection->nonVisible();
  1603. }
  1604. if (in_array('routable', $flags, true)) {
  1605. $collection = $collection->routable();
  1606. }
  1607. if (in_array('nonroutable', $flags, true)) {
  1608. $collection = $collection->nonRoutable();
  1609. }
  1610. if (in_array('published', $flags, true)) {
  1611. $collection = $collection->published();
  1612. }
  1613. if (in_array('nonpublished', $flags, true)) {
  1614. $collection = $collection->nonPublished();
  1615. }
  1616. }
  1617. foreach ($pageStates as $pageState) {
  1618. if (($pageState = array_search($pageState, $flags, true)) !== false) {
  1619. unset($flags[$pageState]);
  1620. }
  1621. }
  1622. // Filter by page type
  1623. if ($flags) {
  1624. $types = [];
  1625. $pageTypes = array_keys(Pages::pageTypes());
  1626. foreach ($pageTypes as $pageType) {
  1627. if (($pageKey = array_search($pageType, $flags, true)) !== false) {
  1628. $types[] = $pageType;
  1629. unset($flags[$pageKey]);
  1630. }
  1631. }
  1632. if (count($types)) {
  1633. $collection = $collection->ofOneOfTheseTypes($types);
  1634. }
  1635. }
  1636. // Filter by page type
  1637. if ($flags) {
  1638. $accessLevels = $flags;
  1639. $collection = $collection->ofOneOfTheseAccessLevels($accessLevels);
  1640. }
  1641. }
  1642. if (!empty($queries)) {
  1643. foreach ($collection as $page) {
  1644. foreach ($queries as $query) {
  1645. $query = trim($query);
  1646. if (stripos($page->getRawContent(), $query) === false
  1647. && stripos($page->title(), $query) === false
  1648. && stripos($page->folder(), $query) === false
  1649. && stripos($page->slug(), \Grav\Plugin\Admin\Utils::slug($query)) === false
  1650. ) {
  1651. $collection->remove($page);
  1652. }
  1653. }
  1654. }
  1655. }
  1656. $results = [];
  1657. foreach ($collection as $path => $page) {
  1658. $results[] = $page->route();
  1659. }
  1660. $this->admin->json_response = [
  1661. 'status' => 'success',
  1662. 'message' => $this->admin::translate('PLUGIN_ADMIN.PAGES_FILTERED'),
  1663. 'results' => $results
  1664. ];
  1665. $this->admin->collection = $collection;
  1666. return true;
  1667. }
  1668. /**
  1669. * Process the page Markdown
  1670. *
  1671. * Preview task in the builtin editor.
  1672. *
  1673. * @return bool True if the action was performed.
  1674. */
  1675. protected function taskProcessMarkdown()
  1676. {
  1677. if (!$this->authorizeTask('process markdown', ['admin.pages', 'admin.pages.read', 'admin.super'])) {
  1678. $this->admin->json_response = [
  1679. 'status' => 'error',
  1680. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  1681. ];
  1682. return false;
  1683. }
  1684. try {
  1685. $page = $this->admin->page(true);
  1686. if (!$page) {
  1687. $this->admin->json_response = [
  1688. 'status' => 'error',
  1689. 'message' => $this->admin::translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
  1690. ];
  1691. return false;
  1692. }
  1693. $this->preparePage($page, true);
  1694. $page->header();
  1695. $page->templateFormat('html');
  1696. // Add theme template paths to Twig loader
  1697. $template_paths = $this->grav['locator']->findResources('theme://templates');
  1698. $this->grav['twig']->twig->getLoader()->addLoader(new FilesystemLoader($template_paths));
  1699. $html = $page->content();
  1700. $this->admin->json_response = ['status' => 'success', 'preview' => $html];
  1701. } catch (\Exception $e) {
  1702. /** @var Debugger $debugger */
  1703. $debugger = $this->grav['debugger'];
  1704. $debugger->addException($e);
  1705. $this->admin->json_response = ['status' => 'error', 'message' => htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_HTML5, 'UTF-8')];
  1706. return false;
  1707. }
  1708. return true;
  1709. }
  1710. // MEDIA TASKS
  1711. /**
  1712. * Determines the file types allowed to be uploaded
  1713. *
  1714. * Used by pagemedia field. Works only within legacy pages.
  1715. *
  1716. * @return bool True if the action was performed.
  1717. */
  1718. protected function taskListmedia()
  1719. {
  1720. if ($this->view !== 'media') {
  1721. return false;
  1722. }
  1723. if (!$this->authorizeTask('list media', ['admin.pages', 'admin.pages.read', 'admin.super'])) {
  1724. $this->admin->json_response = [
  1725. 'status' => 'error',
  1726. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  1727. ];
  1728. return false;
  1729. }
  1730. $media = $this->getMedia();
  1731. if (!$media) {
  1732. $this->admin->json_response = [
  1733. 'status' => 'error',
  1734. 'message' => $this->admin::translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
  1735. ];
  1736. return false;
  1737. }
  1738. $media_list = [];
  1739. /**
  1740. * @var string $name
  1741. * @var Medium|ImageMedium $medium
  1742. */
  1743. foreach ($media->all() as $name => $medium) {
  1744. $metadata = [];
  1745. $img_metadata = $medium->metadata();
  1746. if ($img_metadata) {
  1747. $metadata = $img_metadata;
  1748. }
  1749. // Get original name
  1750. /** @var ImageMedium $source */
  1751. $source = method_exists($medium, 'higherQualityAlternative') ? $medium->higherQualityAlternative() : null;
  1752. $media_list[$name] = [
  1753. 'url' => $medium->display($medium->get('extension') === 'svg' ? 'source' : 'thumbnail')->cropZoom(400, 300)->url(),
  1754. 'size' => $medium->get('size'),
  1755. 'metadata' => $metadata,
  1756. 'original' => $source ? $source->get('filename') : null
  1757. ];
  1758. }
  1759. $this->admin->json_response = ['status' => 'success', 'results' => $media_list];
  1760. return true;
  1761. }
  1762. /**
  1763. * Handles adding a media file to a page.
  1764. *
  1765. * Used by pagemedia field. Works only within legacy pages.
  1766. *
  1767. * @return bool True if the action was performed.
  1768. */
  1769. protected function taskAddmedia()
  1770. {
  1771. if ($this->view !== 'media') {
  1772. return false;
  1773. }
  1774. if (!$this->authorizeTask('add media', ['admin.pages', 'admin.pages.update', 'admin.super'])) {
  1775. $this->admin->json_response = [
  1776. 'status' => 'error',
  1777. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  1778. ];
  1779. return false;
  1780. }
  1781. /** @var Config $config */
  1782. $config = $this->grav['config'];
  1783. if (empty($_FILES)) {
  1784. $this->admin->json_response = [
  1785. 'status' => 'error',
  1786. 'message' => $this->admin::translate('PLUGIN_ADMIN.EXCEEDED_POSTMAX_LIMIT')
  1787. ];
  1788. return false;
  1789. }
  1790. if (!isset($_FILES['file']['error']) || is_array($_FILES['file']['error'])) {
  1791. $this->admin->json_response = [
  1792. 'status' => 'error',
  1793. 'message' => $this->admin::translate('PLUGIN_ADMIN.INVALID_PARAMETERS')
  1794. ];
  1795. return false;
  1796. }
  1797. // Check $_FILES['file']['error'] value.
  1798. switch ($_FILES['file']['error']) {
  1799. case UPLOAD_ERR_OK:
  1800. break;
  1801. case UPLOAD_ERR_NO_FILE:
  1802. $this->admin->json_response = [
  1803. 'status' => 'error',
  1804. 'message' => $this->admin::translate('PLUGIN_ADMIN.NO_FILES_SENT')
  1805. ];
  1806. return false;
  1807. case UPLOAD_ERR_INI_SIZE:
  1808. case UPLOAD_ERR_FORM_SIZE:
  1809. $this->admin->json_response = [
  1810. 'status' => 'error',
  1811. 'message' => $this->admin::translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT')
  1812. ];
  1813. return false;
  1814. case UPLOAD_ERR_NO_TMP_DIR:
  1815. $this->admin->json_response = [
  1816. 'status' => 'error',
  1817. 'message' => $this->admin::translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR')
  1818. ];
  1819. return false;
  1820. default:
  1821. $this->admin->json_response = [
  1822. 'status' => 'error',
  1823. 'message' => $this->admin::translate('PLUGIN_ADMIN.UNKNOWN_ERRORS')
  1824. ];
  1825. return false;
  1826. }
  1827. $filename = $_FILES['file']['name'];
  1828. // Handle bad filenames.
  1829. if (!Utils::checkFilename($filename)) {
  1830. $this->admin->json_response = [
  1831. 'status' => 'error',
  1832. 'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'),
  1833. htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'Bad filename')
  1834. ];
  1835. return false;
  1836. }
  1837. // You should also check filesize here.
  1838. $grav_limit = Utils::getUploadLimit();
  1839. if ($grav_limit > 0 && $_FILES['file']['size'] > $grav_limit) {
  1840. $this->admin->json_response = [
  1841. 'status' => 'error',
  1842. 'message' => $this->admin::translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT')
  1843. ];
  1844. return false;
  1845. }
  1846. // Check extension
  1847. $extension = strtolower(Utils::pathinfo($filename, PATHINFO_EXTENSION));
  1848. // If not a supported type, return
  1849. if (!$extension || !$config->get("media.types.{$extension}")) {
  1850. $this->admin->json_response = [
  1851. 'status' => 'error',
  1852. 'message' => $this->admin::translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension
  1853. ];
  1854. return false;
  1855. }
  1856. $page = $this->admin->page($this->route);
  1857. $media = $page ? $this->getMedia($page) : null;
  1858. if (!$media) {
  1859. $this->admin->json_response = [
  1860. 'status' => 'error',
  1861. 'message' => $this->admin::translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
  1862. ];
  1863. return false;
  1864. }
  1865. /** @var UniformResourceLocator $locator */
  1866. $locator = $this->grav['locator'];
  1867. $path = $media->getPath();
  1868. if ($locator->isStream($path)) {
  1869. $path = $locator->findResource($path, true, true);
  1870. }
  1871. // Special Sanitization for SVG
  1872. if (Utils::contains($extension, 'svg', false)) {
  1873. Security::sanitizeSVG($_FILES['file']['tmp_name']);
  1874. }
  1875. // Upload it
  1876. if (!move_uploaded_file($_FILES['file']['tmp_name'], sprintf('%s/%s', $path, $filename))) {
  1877. $this->admin->json_response = [
  1878. 'status' => 'error',
  1879. 'message' => $this->admin::translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE')
  1880. ];
  1881. return false;
  1882. }
  1883. Cache::clearCache('invalidate');
  1884. // Add metadata if needed
  1885. $include_metadata = Grav::instance()['config']->get('system.media.auto_metadata_exif', false);
  1886. $basename = str_replace(['@3x', '@2x'], '', Utils::pathinfo($filename, PATHINFO_BASENAME));
  1887. $metadata = [];
  1888. if ($include_metadata && isset($media[$basename])) {
  1889. $img_metadata = $media[$basename]->metadata();
  1890. if ($img_metadata) {
  1891. $metadata = $img_metadata;
  1892. }
  1893. }
  1894. // DEPRECATED: page
  1895. $this->grav->fireEvent('onAdminAfterAddMedia', new Event(['object' => $page, 'page' => $page]));
  1896. $this->admin->json_response = [
  1897. 'status' => 'success',
  1898. 'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
  1899. 'metadata' => $metadata,
  1900. ];
  1901. return true;
  1902. }
  1903. /**
  1904. * Request: POST .json/task:compileScss (AJAX call)
  1905. *
  1906. * @return bool
  1907. */
  1908. protected function taskCompileScss()
  1909. {
  1910. if (!$this->authorizeTask('compile scss', ['admin.plugins', 'admin.super'])) {
  1911. $this->admin->json_response = [
  1912. 'status' => 'error',
  1913. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  1914. ];
  1915. return false;
  1916. }
  1917. $default_scheme = $this->grav['config']->get('plugins.admin.whitelabel.color_scheme');
  1918. $preview = $this->post['preview'] ?? false;
  1919. $data = ['color_scheme' => $this->data['whitelabel']['color_scheme'] ?? $default_scheme];
  1920. $output_file = $preview ? 'admin-preset.css' : 'admin-preset__tmp.css';
  1921. $options = [
  1922. 'input' => 'plugin://admin/themes/grav/scss/preset.scss',
  1923. 'output' => 'asset://' .$output_file
  1924. ];
  1925. [$compile_status, $msg] = $this->grav['admin-whitelabel']->compilePresetScss($data, $options);
  1926. $json_response = [
  1927. 'status' => $compile_status ? 'success' : 'error',
  1928. 'message' => ($preview ? 'Preview ' : 'SCSS ') . $msg,
  1929. 'files' => [
  1930. 'color_scheme' => Utils::url($options['output'])
  1931. ]
  1932. ];
  1933. $this->sendJsonResponse($json_response);
  1934. }
  1935. /**
  1936. * Request: POST .json/task:exportScss (AJAX call)
  1937. *
  1938. * @return bool
  1939. */
  1940. protected function taskExportScss()
  1941. {
  1942. if (!$this->authorizeTask('export scss', ['admin.plugins', 'admin.super'])) {
  1943. $this->admin->json_response = [
  1944. 'status' => 'error',
  1945. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  1946. ];
  1947. return false;
  1948. }
  1949. $data = ['color_scheme' => $this->data['whitelabel']['color_scheme'] ?? null];
  1950. $name = empty($this->data['whitelabel']['color_scheme']['name']) ? 'admin-theme-export' : \Grav\Plugin\Admin\Utils::slug($this->data['whitelabel']['color_scheme']['name']);
  1951. $location = 'asset://' . $name . '.yaml';
  1952. [$status, $msg] = $this->grav['admin-whitelabel']->exportPresetScsss($data, $location);
  1953. $json_response = [
  1954. 'status' => $status ? 'success' : 'error',
  1955. 'message' => $msg,
  1956. 'files' => [
  1957. 'download' => Utils::url($location)
  1958. ]
  1959. ];
  1960. $this->sendJsonResponse($json_response);
  1961. }
  1962. /**
  1963. * Handles deleting a media file from a page.
  1964. *
  1965. * Used by pagemedia field.
  1966. *
  1967. * @return bool True if the action was performed.
  1968. */
  1969. protected function taskDelmedia()
  1970. {
  1971. if ($this->view !== 'media') {
  1972. return false;
  1973. }
  1974. if (!$this->authorizeTask('delete media', ['admin.pages', 'admin.pages.update', 'admin.super'])) {
  1975. $this->admin->json_response = [
  1976. 'status' => 'error',
  1977. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  1978. ];
  1979. return false;
  1980. }
  1981. $page = $this->admin->page($this->route);
  1982. $media = $page ? $this->getMedia($page) : null;
  1983. if (null === $media) {
  1984. $this->admin->json_response = [
  1985. 'status' => 'error',
  1986. 'message' => $this->admin::translate('PLUGIN_ADMIN.NO_PAGE_FOUND')
  1987. ];
  1988. return false;
  1989. }
  1990. $filename = !empty($this->post['filename']) ? Utils::basename($this->post['filename']) : null;
  1991. // Handle bad filenames.
  1992. if (!$filename || !Utils::checkFilename($filename)) {
  1993. $this->admin->json_response = [
  1994. 'status' => 'error',
  1995. 'message' => $this->admin::translate('PLUGIN_ADMIN.NO_FILE_FOUND')
  1996. ];
  1997. return false;
  1998. }
  1999. /** @var UniformResourceLocator $locator */
  2000. $locator = $this->grav['locator'];
  2001. $targetPath = $media->getPath() . '/' . $filename;
  2002. if ($locator->isStream($targetPath)) {
  2003. $targetPath = $locator->findResource($targetPath, true, true);
  2004. }
  2005. $fileParts = Utils::pathinfo($filename);
  2006. $found = false;
  2007. if (file_exists($targetPath)) {
  2008. $found = true;
  2009. $result = unlink($targetPath);
  2010. if (!$result) {
  2011. $this->admin->json_response = [
  2012. 'status' => 'error',
  2013. 'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
  2014. ];
  2015. return false;
  2016. }
  2017. }
  2018. // Remove Extra Files
  2019. foreach (scandir($media->getPath(), SCANDIR_SORT_NONE) as $file) {
  2020. if (preg_match("/{$fileParts['filename']}@\d+x\.{$fileParts['extension']}(?:\.meta\.yaml)?$|{$filename}\.meta\.yaml$/", $file)) {
  2021. $targetPath = $media->getPath() . '/' . $file;
  2022. if ($locator->isStream($targetPath)) {
  2023. $targetPath = $locator->findResource($targetPath, true, true);
  2024. }
  2025. $result = unlink($targetPath);
  2026. if (!$result) {
  2027. $this->admin->json_response = [
  2028. 'status' => 'error',
  2029. 'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
  2030. ];
  2031. return false;
  2032. }
  2033. $found = true;
  2034. }
  2035. }
  2036. Cache::clearCache('invalidate');
  2037. if (!$found) {
  2038. $this->admin->json_response = [
  2039. 'status' => 'error',
  2040. 'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_NOT_FOUND') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
  2041. ];
  2042. return false;
  2043. }
  2044. // DEPRECATED: page
  2045. $this->grav->fireEvent('onAdminAfterDelMedia', new Event(['object' => $page, 'page' => $page, 'media' => $media, 'filename' => $filename]));
  2046. $this->admin->json_response = [
  2047. 'status' => 'success',
  2048. 'message' => $this->admin::translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
  2049. ];
  2050. return true;
  2051. }
  2052. /**
  2053. * @param array $data
  2054. * @return array
  2055. */
  2056. protected function getLevelListing($data)
  2057. {
  2058. // Valid types are dir|file|link
  2059. $default_filters = ['type'=> ['root', 'dir'], 'name' => null, 'extension' => null];
  2060. $pages = $this->admin::enablePages();
  2061. $page_instances = $pages->instances();
  2062. $is_page = $data['page'] ?? true;
  2063. $route = $data['route'] ?? null;
  2064. $leaf_route = $data['leaf_route'] ?? null;
  2065. $sortby = $data['sortby'] ?? 'filename';
  2066. $order = $data['order'] ?? SORT_ASC;
  2067. $initial = $data['initial'] ?? null;
  2068. $filters = isset($data['filters']) ? $default_filters + json_decode($data['filters']) : $default_filters;
  2069. $filter_type = (array) $filters['type'];
  2070. $status = 'error';
  2071. $msg = null;
  2072. $response = [];
  2073. $children = null;
  2074. $sub_route = null;
  2075. $extra = null;
  2076. $root = false;
  2077. // Handle leaf_route
  2078. if ($leaf_route && $route !== $leaf_route) {
  2079. $nodes = explode('/', $leaf_route);
  2080. $sub_route = '/' . implode('/', array_slice($nodes, 1, $data['level']++));
  2081. $data['route'] = $sub_route;
  2082. [$status, $msg, $children, $extra] = $this->getLevelListing($data);
  2083. }
  2084. // Handle no route, assume page tree root
  2085. if (!$route) {
  2086. $is_page = false;
  2087. $route = $this->grav['locator']->findResource('page://', true);
  2088. $root = true;
  2089. }
  2090. if ($is_page) {
  2091. // Try the path
  2092. /** @var PageInterface $page */
  2093. $page = $pages->get(GRAV_ROOT . $route);
  2094. // Try a real route (like homepage)
  2095. if (is_null($page)) {
  2096. $page = $pages->find($route);
  2097. }
  2098. $path = $page ? $page->path() : null;
  2099. } else {
  2100. // Try a physical path
  2101. if (!Utils::startsWith($route, GRAV_ROOT)) {
  2102. $try_path = GRAV_ROOT . $route;
  2103. } else {
  2104. $try_path = $route;
  2105. }
  2106. $path = file_exists($try_path) ? $try_path : null;
  2107. }
  2108. $blueprintsData = $this->admin->page(true);
  2109. if (null !== $blueprintsData) {
  2110. if (method_exists($blueprintsData, 'blueprints')) {
  2111. $settings = $blueprintsData->blueprints()->schema()->getProperty($data['field']);
  2112. } elseif (method_exists($blueprintsData, 'getBlueprint')) {
  2113. $settings = $blueprintsData->getBlueprint()->schema()->getProperty($data['field']);
  2114. }
  2115. $filters = array_merge([], $filters, ($settings['filters'] ?? []));
  2116. $filter_type = $filters['type'] ?? $filter_type;
  2117. }
  2118. if ($path) {
  2119. $status = 'success';
  2120. $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_FOUND';
  2121. foreach (new \DirectoryIterator($path) as $fileInfo) {
  2122. $fileName = $fileInfo->getFilename();
  2123. $filePath = str_replace('\\', '/', $fileInfo->getPathname());
  2124. if (($fileInfo->isDot() && $fileName !== '.' && $initial) || (Utils::startsWith($fileName, '.') && strlen($fileName) > 1)) {
  2125. continue;
  2126. }
  2127. if ($fileInfo->isDot()) {
  2128. if ($root) {
  2129. $payload = [
  2130. 'name' => '<root>',
  2131. 'value' => '',
  2132. 'item-key' => '',
  2133. 'filename' => '.',
  2134. 'extension' => '',
  2135. 'type' => 'root',
  2136. 'modified' => $fileInfo->getMTime(),
  2137. 'size' => 0,
  2138. 'has-children' => false
  2139. ];
  2140. } else {
  2141. continue;
  2142. }
  2143. } else {
  2144. $file_page = $page_instances[$filePath] ?? null;
  2145. $file_path = Utils::replaceFirstOccurrence(GRAV_ROOT, '', $filePath);
  2146. $type = $fileInfo->getType();
  2147. $child_path = $file_page ? $file_page->path() : $filePath;
  2148. $count_children = Folder::countChildren($child_path);
  2149. $payload = [
  2150. 'name' => $file_page ? $file_page->title() : $fileName,
  2151. 'value' => $file_page ? $file_page->rawRoute() : $file_path,
  2152. 'item-key' => Utils::basename($file_page ? $file_page->route() : $file_path),
  2153. 'filename' => $fileName,
  2154. 'extension' => $type === 'dir' ? '' : $fileInfo->getExtension(),
  2155. 'type' => $type,
  2156. 'modified' => $fileInfo->getMTime(),
  2157. 'size' => $count_children,
  2158. 'symlink' => false,
  2159. 'has-children' => $count_children > 0
  2160. ];
  2161. }
  2162. // Fix for symlink
  2163. if ($payload['type'] === 'link') {
  2164. $payload['symlink'] = true;
  2165. $physical_path = $fileInfo->getRealPath();
  2166. $payload['type'] = is_dir($physical_path) ? 'dir' : 'file';
  2167. }
  2168. // filter types
  2169. if ($filters['type']) {
  2170. if (!in_array($payload['type'], $filter_type)) {
  2171. continue;
  2172. }
  2173. }
  2174. // Simple filter for name or extension
  2175. if (($filters['name'] && Utils::contains($payload['basename'], $filters['name'])) ||
  2176. ($filters['extension'] && Utils::contains($payload['extension'], $filters['extension']))) {
  2177. continue;
  2178. }
  2179. // Add children if any
  2180. if ($filePath === $extra && is_array($children)) {
  2181. $payload['children'] = array_values($children);
  2182. }
  2183. $response[] = $payload;
  2184. }
  2185. } else {
  2186. $msg = 'PLUGIN_ADMIN.PAGE_ROUTE_NOT_FOUND';
  2187. }
  2188. // Sorting
  2189. $response = Utils::sortArrayByKey($response, $sortby, $order);
  2190. $temp_array = [];
  2191. foreach ($response as $index => $item) {
  2192. $temp_array[$item['type']][$index] = $item;
  2193. }
  2194. $sorted = Utils::sortArrayByArray($temp_array, $filter_type);
  2195. $response = Utils::arrayFlatten($sorted);
  2196. return [$status, $this->admin::translate($msg ?? 'PLUGIN_ADMIN.NO_ROUTE_PROVIDED'), $response, $path];
  2197. }
  2198. /**
  2199. * Get page media.
  2200. *
  2201. * @param PageInterface|null $page
  2202. * @return Media|null
  2203. */
  2204. public function getMedia(PageInterface $page = null)
  2205. {
  2206. $page = $page ?? $this->admin->page($this->route);
  2207. if (!$page) {
  2208. return null;
  2209. }
  2210. $this->uri = $this->uri ?? $this->grav['uri'];
  2211. $field = (string)$this->uri->post('field', '');
  2212. $order = $this->uri->post('order') ?: null;
  2213. if ($order && is_string($order)) {
  2214. $order = array_map('trim', explode(',', $order));
  2215. }
  2216. $blueprints = $page->blueprints();
  2217. $settings = $this->getMediaFieldSettings($blueprints, $field);
  2218. $path = $settings['destination'] ?? $page->path();
  2219. return $path ? new Media($path, $order) : null;
  2220. }
  2221. /**
  2222. * @param Data\Blueprint|null $blueprint
  2223. * @param string $field
  2224. * @return array|null
  2225. */
  2226. protected function getMediaFieldSettings(?Data\Blueprint $blueprint, string $field): ?array
  2227. {
  2228. $schema = $blueprint ? $blueprint->schema() : null;
  2229. if (!$schema || $field === '') {
  2230. return null;
  2231. }
  2232. $settings = is_object($schema) ? (array)$schema->getProperty($field) : null;
  2233. if (null === $settings) {
  2234. return null;
  2235. }
  2236. if (empty($settings['destination']) || \in_array($settings['destination'], ['@self', 'self@', '@self@'], true)) {
  2237. unset($settings['destination']);
  2238. }
  2239. return $settings + ['accept' => '*', 'limit' => 1000];
  2240. }
  2241. /**
  2242. * @return string
  2243. */
  2244. protected function getDataType()
  2245. {
  2246. return trim("{$this->view}/{$this->admin->route}", '/');
  2247. }
  2248. /**
  2249. * Gets the configuration data for a given view & post
  2250. *
  2251. * @param array $data
  2252. * @return object
  2253. */
  2254. protected function prepareData(array $data)
  2255. {
  2256. $type = $this->getDataType();
  2257. return $this->admin->data($type, $data);
  2258. }
  2259. /**
  2260. * Prepare a page to be stored: update its folder, name, template, header and content
  2261. *
  2262. * @param PageInterface $page
  2263. * @param bool $clean_header
  2264. * @param string $languageCode
  2265. * @return void
  2266. */
  2267. protected function preparePage(PageInterface $page, $clean_header = false, $languageCode = ''): void
  2268. {
  2269. $input = (array)$this->data;
  2270. $this->admin::enablePages();
  2271. if (isset($input['folder']) && $input['folder'] !== $page->value('folder')) {
  2272. $order = $page->value('order');
  2273. $ordering = $order ? sprintf('%02d.', $order) : '';
  2274. $page->folder($ordering . $input['folder']);
  2275. }
  2276. if (isset($input['name']) && !empty($input['name'])) {
  2277. $type = strtolower($input['name']);
  2278. $page->template($type);
  2279. $name = preg_replace('|.*/|', '', $type);
  2280. /** @var Language $language */
  2281. $language = $this->grav['language'];
  2282. if ($language->enabled()) {
  2283. $languageCode = $languageCode ?: $language->getLanguage();
  2284. if ($languageCode) {
  2285. $isDefault = $languageCode === $language->getDefault();
  2286. $includeLang = !$isDefault || (bool)$this->grav['config']->get('system.languages.include_default_lang_file_extension', true);
  2287. if (!$includeLang) {
  2288. // Check if the language specific file exists; use it if it does.
  2289. $includeLang = file_exists("{$page->path()}/{$name}.{$languageCode}.md");
  2290. }
  2291. // Keep existing .md file if we're updating default language, otherwise always append the language.
  2292. if ($includeLang) {
  2293. $name .= '.' . $languageCode;
  2294. }
  2295. }
  2296. }
  2297. $name .= '.md';
  2298. $page->name($name);
  2299. }
  2300. // Special case for Expert mode: build the raw, unset content
  2301. if (isset($input['frontmatter'], $input['content'])) {
  2302. $page->raw("---\n" . (string)$input['frontmatter'] . "\n---\n" . (string)$input['content']);
  2303. unset($input['content']);
  2304. // Handle header normally
  2305. } elseif (isset($input['header'])) {
  2306. $header = $input['header'];
  2307. foreach ($header as $key => $value) {
  2308. if ($key === 'metadata' && is_array($header[$key])) {
  2309. foreach ($header['metadata'] as $key2 => $value2) {
  2310. if (isset($input['toggleable_header']['metadata'][$key2]) && !$input['toggleable_header']['metadata'][$key2]) {
  2311. $header['metadata'][$key2] = '';
  2312. }
  2313. }
  2314. } elseif ($key === 'taxonomy' && is_array($header[$key])) {
  2315. foreach ($header[$key] as $taxkey => $taxonomy) {
  2316. if (is_array($taxonomy) && \count($taxonomy) === 1 && trim($taxonomy[0]) === '') {
  2317. unset($header[$key][$taxkey]);
  2318. }
  2319. }
  2320. } else {
  2321. if (isset($input['toggleable_header'][$key]) && !$input['toggleable_header'][$key]) {
  2322. $header[$key] = null;
  2323. }
  2324. }
  2325. }
  2326. if ($clean_header) {
  2327. $header = Utils::arrayFilterRecursive($header, function ($k, $v) {
  2328. return !(null === $v || $v === '');
  2329. });
  2330. }
  2331. $page->header((object)$header);
  2332. $page->frontmatter(Yaml::dump((array)$page->header(), 20));
  2333. }
  2334. // Fill content last because it also renders the output.
  2335. if (isset($input['content'])) {
  2336. $page->rawMarkdown((string)$input['content']);
  2337. }
  2338. }
  2339. /**
  2340. * Find the first available $item ('slug' | 'folder') for a page
  2341. * Used when copying a page, to determine the first available slot
  2342. *
  2343. * @param string $item
  2344. * @param PageInterface $page
  2345. * @return string The first available slot
  2346. */
  2347. protected function findFirstAvailable($item, PageInterface $page)
  2348. {
  2349. $parent = $page->parent();
  2350. if (!$parent || !$parent->children()) {
  2351. return $page->{$item}();
  2352. }
  2353. $withoutPrefix = function ($string) {
  2354. $match = preg_split('/^[0-9]+\./u', $string, 2, PREG_SPLIT_DELIM_CAPTURE);
  2355. return $match[1] ?? $match[0];
  2356. };
  2357. $withoutPostfix = function ($string) {
  2358. $match = preg_split('/-(\d+)$/', $string, 2, PREG_SPLIT_DELIM_CAPTURE);
  2359. return $match[0];
  2360. };
  2361. /* $appendedNumber = function ($string) {
  2362. $match = preg_split('/-(\d+)$/', $string, 2, PREG_SPLIT_DELIM_CAPTURE);
  2363. $append = (isset($match[1]) ? (int)$match[1] + 1 : 2);
  2364. return $append;
  2365. };*/
  2366. $highest = 1;
  2367. $siblings = $parent->children();
  2368. $findCorrectAppendedNumber = function ($item, $page_item, $highest) use (
  2369. $siblings,
  2370. &$findCorrectAppendedNumber,
  2371. &$withoutPrefix
  2372. ) {
  2373. foreach ($siblings as $sibling) {
  2374. if ($withoutPrefix($sibling->{$item}()) == ($highest === 1 ? $page_item : $page_item . '-' . $highest)) {
  2375. $highest = $findCorrectAppendedNumber($item, $page_item, $highest + 1);
  2376. return $highest;
  2377. }
  2378. }
  2379. return $highest;
  2380. };
  2381. $base = $withoutPrefix($withoutPostfix($page->$item()));
  2382. $return = $base;
  2383. $highest = $findCorrectAppendedNumber($item, $base, $highest);
  2384. if ($highest > 1) {
  2385. $return .= '-' . $highest;
  2386. }
  2387. return $return;
  2388. }
  2389. /**
  2390. * @param string $frontmatter
  2391. * @return bool
  2392. */
  2393. public function checkValidFrontmatter($frontmatter)
  2394. {
  2395. try {
  2396. Yaml::parse($frontmatter);
  2397. } catch (\RuntimeException $e) {
  2398. return false;
  2399. }
  2400. return true;
  2401. }
  2402. /**
  2403. * The what should be the new filename when saving as a new language
  2404. *
  2405. * @param string $current_filename the current file name, including .md. Example: default.en.md
  2406. * @param string $language The new language it will be saved as. Example: 'it' or 'en-GB'.
  2407. * @return string The new filename. Example: 'default.it'
  2408. */
  2409. public function determineFilenameIncludingLanguage($current_filename, $language)
  2410. {
  2411. $ext = '.md';
  2412. $filename = substr($current_filename, 0, -strlen($ext));
  2413. $languages_enabled = $this->grav['config']->get('system.languages.supported', []);
  2414. $parts = explode('.', trim($filename, '.'));
  2415. $lang = array_pop($parts);
  2416. if ($lang === $language) {
  2417. return $filename . $ext;
  2418. }
  2419. if (in_array($lang, $languages_enabled, true)) {
  2420. $filename = implode('.', $parts);
  2421. }
  2422. return $filename . '.' . $language . $ext;
  2423. }
  2424. /**
  2425. * Get the next available ordering number in a folder
  2426. *
  2427. * @param string $path
  2428. * @return string the correct order string to prepend
  2429. */
  2430. public static function getNextOrderInFolder($path)
  2431. {
  2432. $files = Folder::all($path, ['recursive' => false]);
  2433. $highestOrder = 0;
  2434. foreach ($files as $file) {
  2435. preg_match(PAGE_ORDER_PREFIX_REGEX, $file, $order);
  2436. if (isset($order[0])) {
  2437. $theOrder = (int)trim($order[0], '.');
  2438. } else {
  2439. $theOrder = 0;
  2440. }
  2441. if ($theOrder >= $highestOrder) {
  2442. $highestOrder = $theOrder;
  2443. }
  2444. }
  2445. $orderOfNewFolder = $highestOrder + 1;
  2446. if ($orderOfNewFolder < 10) {
  2447. $orderOfNewFolder = '0' . $orderOfNewFolder;
  2448. }
  2449. return $orderOfNewFolder;
  2450. }
  2451. /**
  2452. * Used in 3rd party editors (e.g. next-gen).
  2453. *
  2454. * @return ResponseInterface|false
  2455. */
  2456. protected function taskConvertUrls()
  2457. {
  2458. if (!$this->authorizeTask('access page', ['admin.pages', 'admin.pages.list', 'admin.super'])) {
  2459. $this->admin->json_response = [
  2460. 'status' => 'error',
  2461. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
  2462. ];
  2463. return false;
  2464. }
  2465. $request = $this->getRequest();
  2466. $data = $request->getParsedBody();
  2467. $converted_links = [];
  2468. $converted_images = [];
  2469. $status = 'success';
  2470. $message = 'All links converted';
  2471. $data['route'] = isset($data['route']) ? base64_decode($data['route']) : null;
  2472. $data['data'] = json_decode($data['data'] ?? '{}', true);
  2473. // use the route if passed, else use current page in admin as reference
  2474. $page_route = $data['route'] ?? $this->admin->page(true);
  2475. /** @var PageInterface */
  2476. $pages = $this->admin::enablePages();
  2477. $page = $pages->find($page_route);
  2478. if (!$page) {
  2479. throw new RequestException($request,'Page Not Found', 404);
  2480. }
  2481. if (!isset($data['data'])) {
  2482. throw new RequestException($request, 'Bad Request', 400);
  2483. }
  2484. foreach ($data['data']['a'] ?? [] as $link) {
  2485. $converted_links[$link] = Excerpts::processLinkHtml($link, $page);
  2486. }
  2487. foreach ($data['data']['img'] ?? [] as $image) {
  2488. $converted_images[$image] = Excerpts::processImageHtml($image, $page);
  2489. }
  2490. $json = [
  2491. 'status' => $status,
  2492. 'message' => $message,
  2493. 'data' => ['links' => $converted_links, 'images' => $converted_images]
  2494. ];
  2495. return $this->createJsonResponse($json, 200);
  2496. }
  2497. }