Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 
 
 
 
 

1175 lignes
38 KiB

  1. <?php
  2. /**
  3. * @package Grav\Plugin\Admin
  4. *
  5. * @copyright Copyright (c) 2015 - 2024 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\Cache;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Data\Data;
  12. use Grav\Common\Debugger;
  13. use Grav\Common\Filesystem\Folder;
  14. use Grav\Common\Grav;
  15. use Grav\Common\Media\Interfaces\MediaInterface;
  16. use Grav\Common\Page\Interfaces\PageInterface;
  17. use Grav\Common\Page\Media;
  18. use Grav\Common\Security;
  19. use Grav\Common\Uri;
  20. use Grav\Common\User\Interfaces\UserInterface;
  21. use Grav\Common\Utils;
  22. use Grav\Common\Plugin;
  23. use Grav\Common\Theme;
  24. use Grav\Framework\Controller\Traits\ControllerResponseTrait;
  25. use Grav\Framework\RequestHandler\Exception\RequestException;
  26. use JsonException;
  27. use Psr\Http\Message\ResponseInterface;
  28. use Psr\Http\Message\ServerRequestInterface;
  29. use RocketTheme\Toolbox\Event\Event;
  30. use RocketTheme\Toolbox\File\File;
  31. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  32. /**
  33. * Class AdminController
  34. *
  35. * @package Grav\Plugin
  36. */
  37. class AdminBaseController
  38. {
  39. use ControllerResponseTrait;
  40. /** @var Grav */
  41. public $grav;
  42. /** @var string */
  43. public $view;
  44. /** @var string */
  45. public $task;
  46. /** @var string */
  47. public $route;
  48. /** @var array */
  49. public $post;
  50. /** @var array|null */
  51. public $data;
  52. /** @var array */
  53. public $blacklist_views = [];
  54. /** @var Uri */
  55. protected $uri;
  56. /** @var Admin */
  57. protected $admin;
  58. /** @var string */
  59. protected $redirect;
  60. /** @var int */
  61. protected $redirectCode;
  62. /** @var string[] */
  63. protected $upload_errors = [
  64. 0 => 'There is no error, the file uploaded with success',
  65. 1 => 'The uploaded file exceeds the max upload size',
  66. 2 => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML',
  67. 3 => 'The uploaded file was only partially uploaded',
  68. 4 => 'No file was uploaded',
  69. 6 => 'Missing a temporary folder',
  70. 7 => 'Failed to write file to disk',
  71. 8 => 'A PHP extension stopped the file upload'
  72. ];
  73. /**
  74. * Performs a task.
  75. *
  76. * @return bool True if the action was performed successfully.
  77. */
  78. public function execute()
  79. {
  80. if (null === $this->admin) {
  81. $this->admin = $this->grav['admin'];
  82. }
  83. // Ignore blacklisted views.
  84. if (in_array($this->view, $this->blacklist_views, true)) {
  85. return false;
  86. }
  87. // Make sure that user is logged into admin.
  88. if (!$this->admin->authorize()) {
  89. return false;
  90. }
  91. // Always validate nonce.
  92. if (!$this->validateNonce()) {
  93. return false;
  94. }
  95. $method = 'task' . ucfirst($this->task);
  96. if (method_exists($this, $method)) {
  97. try {
  98. $response = $this->{$method}();
  99. } catch (RequestException $e) {
  100. /** @var Debugger $debugger */
  101. $debugger = $this->grav['debugger'];
  102. $debugger->addException($e);
  103. $response = $this->createErrorResponse($e);
  104. } catch (\RuntimeException $e) {
  105. /** @var Debugger $debugger */
  106. $debugger = $this->grav['debugger'];
  107. $debugger->addException($e);
  108. $response = true;
  109. $this->admin->setMessage($e->getMessage(), 'error');
  110. }
  111. } else {
  112. $response = $this->grav->fireEvent('onAdminTaskExecute',
  113. new Event(['controller' => $this, 'method' => $method]));
  114. }
  115. if ($response instanceof ResponseInterface) {
  116. $this->close($response);
  117. }
  118. // Grab redirect parameter.
  119. $redirect = $this->post['_redirect'] ?? null;
  120. unset($this->post['_redirect']);
  121. // Redirect if requested.
  122. if ($redirect) {
  123. $this->setRedirect($redirect);
  124. }
  125. return $response;
  126. }
  127. protected function validateNonce()
  128. {
  129. if (strtolower($_SERVER['REQUEST_METHOD']) === 'post') {
  130. if (isset($this->post['admin-nonce'])) {
  131. $nonce = $this->post['admin-nonce'];
  132. } else {
  133. $nonce = $this->grav['uri']->param('admin-nonce');
  134. }
  135. if (!$nonce || !Utils::verifyNonce($nonce, 'admin-form')) {
  136. if ($this->task === 'addmedia') {
  137. $message = sprintf($this->admin::translate('PLUGIN_ADMIN.FILE_TOO_LARGE', null),
  138. ini_get('post_max_size'));
  139. //In this case it's more likely that the image is too big than POST can handle. Show message
  140. $this->admin->json_response = [
  141. 'status' => 'error',
  142. 'message' => $message
  143. ];
  144. return false;
  145. }
  146. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
  147. $this->admin->json_response = [
  148. 'status' => 'error',
  149. 'message' => $this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
  150. ];
  151. return false;
  152. }
  153. unset($this->post['admin-nonce']);
  154. } else {
  155. if ($this->task === 'logout') {
  156. $nonce = $this->grav['uri']->param('logout-nonce');
  157. if (null === $nonce || !Utils::verifyNonce($nonce, 'logout-form')) {
  158. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'),
  159. 'error');
  160. $this->admin->json_response = [
  161. 'status' => 'error',
  162. 'message' => $this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
  163. ];
  164. return false;
  165. }
  166. } else {
  167. $nonce = $this->grav['uri']->param('admin-nonce');
  168. if (null === $nonce || !Utils::verifyNonce($nonce, 'admin-form')) {
  169. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'),
  170. 'error');
  171. $this->admin->json_response = [
  172. 'status' => 'error',
  173. 'message' => $this->admin::translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
  174. ];
  175. return false;
  176. }
  177. }
  178. }
  179. return true;
  180. }
  181. /**
  182. * Sets the page redirect.
  183. *
  184. * @param string $path The path to redirect to
  185. * @param int $code The HTTP redirect code
  186. * @return void
  187. */
  188. public function setRedirect($path, $code = 303)
  189. {
  190. $this->redirect = $path;
  191. $this->redirectCode = $code;
  192. }
  193. /**
  194. * Sends JSON response and terminates the call.
  195. *
  196. * @param array $json
  197. * @param int $code
  198. * @return never-return
  199. */
  200. protected function sendJsonResponse(array $json, $code = 200): void
  201. {
  202. // JSON response.
  203. $response = $this->createJsonResponse($json, $code);
  204. $this->close($response);
  205. }
  206. /**
  207. * @param ResponseInterface $response
  208. * @return never-return
  209. */
  210. protected function close(ResponseInterface $response): void
  211. {
  212. $this->grav->close($response);
  213. }
  214. /**
  215. * Handles ajax upload for files.
  216. * Stores in a flash object the temporary file and deals with potential file errors.
  217. *
  218. * @return bool True if the action was performed.
  219. */
  220. public function taskFilesUpload()
  221. {
  222. if (null === $_FILES || !$this->authorizeTask('upload file', $this->dataPermissions())) {
  223. return false;
  224. }
  225. /** @var Config $config */
  226. $config = $this->grav['config'];
  227. $data = $this->view === 'pages' ? $this->admin->page(true) : $this->prepareData([]);
  228. $settings = $data->blueprints()->schema()->getProperty($this->post['name']);
  229. $settings = (object)array_merge([
  230. 'avoid_overwriting' => false,
  231. 'random_name' => false,
  232. 'accept' => ['image/*'],
  233. 'limit' => 10,
  234. 'filesize' => Utils::getUploadLimit()
  235. ], (array)$settings, ['name' => $this->post['name']]);
  236. $upload = $this->normalizeFiles($_FILES['data'], $settings->name);
  237. $filename = $upload->file->name;
  238. // Handle bad filenames.
  239. if (!Utils::checkFilename($filename)) {
  240. $this->admin->json_response = [
  241. 'status' => 'error',
  242. 'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD', null),
  243. htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'Bad filename')
  244. ];
  245. return false;
  246. }
  247. if (!isset($settings->destination)) {
  248. $this->admin->json_response = [
  249. 'status' => 'error',
  250. 'message' => $this->admin::translate('PLUGIN_ADMIN.DESTINATION_NOT_SPECIFIED', null)
  251. ];
  252. return false;
  253. }
  254. // Do not use self@ outside of pages
  255. if ($this->view !== 'pages' && in_array($settings->destination, ['@self', 'self@', '@self@'])) {
  256. $this->admin->json_response = [
  257. 'status' => 'error',
  258. 'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null),
  259. htmlspecialchars($settings->destination, ENT_QUOTES | ENT_HTML5, 'UTF-8'))
  260. ];
  261. return false;
  262. }
  263. // Handle errors and breaks without proceeding further
  264. if ($upload->file->error !== UPLOAD_ERR_OK) {
  265. $this->admin->json_response = [
  266. 'status' => 'error',
  267. 'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD', null),
  268. htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
  269. $this->upload_errors[$upload->file->error])
  270. ];
  271. return false;
  272. }
  273. // Handle file size limits
  274. $settings->filesize *= 1048576; // 2^20 [MB in Bytes]
  275. if ($settings->filesize > 0 && $upload->file->size > $settings->filesize) {
  276. $this->admin->json_response = [
  277. 'status' => 'error',
  278. 'message' => $this->admin::translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT')
  279. ];
  280. return false;
  281. }
  282. // Handle Accepted file types
  283. // Accept can only be mime types (image/png | image/*) or file extensions (.pdf|.jpg)
  284. $accepted = false;
  285. $errors = [];
  286. // Do not trust mimetype sent by the browser
  287. $mime = Utils::getMimeByFilename($filename);
  288. foreach ((array)$settings->accept as $type) {
  289. // Force acceptance of any file when star notation
  290. if ($type === '*') {
  291. $accepted = true;
  292. break;
  293. }
  294. $isMime = strstr($type, '/');
  295. $find = str_replace(['.', '*', '+'], ['\.', '.*', '\+'], $type);
  296. if ($isMime) {
  297. $match = preg_match('#' . $find . '$#', $mime);
  298. if (!$match) {
  299. $errors[] = htmlspecialchars('The MIME type "' . $mime . '" for the file "' . $filename . '" is not an accepted.', ENT_QUOTES | ENT_HTML5, 'UTF-8');
  300. } else {
  301. $accepted = true;
  302. break;
  303. }
  304. } else {
  305. $match = preg_match('#' . $find . '$#', $filename);
  306. if (!$match) {
  307. $errors[] = htmlspecialchars('The File Extension for the file "' . $filename . '" is not an accepted.', ENT_QUOTES | ENT_HTML5, 'UTF-8');
  308. } else {
  309. $accepted = true;
  310. break;
  311. }
  312. }
  313. }
  314. if (!$accepted) {
  315. $this->admin->json_response = [
  316. 'status' => 'error',
  317. 'message' => implode('<br />', $errors)
  318. ];
  319. return false;
  320. }
  321. // Remove the error object to avoid storing it
  322. unset($upload->file->error);
  323. // we need to move the file at this stage or else
  324. // it won't be available upon save later on
  325. // since php removes it from the upload location
  326. $tmp_dir = Admin::getTempDir();
  327. $tmp_file = $upload->file->tmp_name;
  328. $tmp = $tmp_dir . '/uploaded-files/' . Utils::basename($tmp_file);
  329. Folder::create(dirname($tmp));
  330. if (!move_uploaded_file($tmp_file, $tmp)) {
  331. $this->admin->json_response = [
  332. 'status' => 'error',
  333. 'message' => sprintf(
  334. $this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE', null),
  335. '',
  336. htmlspecialchars($tmp, ENT_QUOTES | ENT_HTML5, 'UTF-8')
  337. )
  338. ];
  339. return false;
  340. }
  341. // Special Sanitization for SVG
  342. if (Utils::contains($mime, 'svg', false)) {
  343. Security::sanitizeSVG($tmp);
  344. }
  345. $upload->file->tmp_name = $tmp;
  346. // Retrieve the current session of the uploaded files for the field
  347. // and initialize it if it doesn't exist
  348. $sessionField = base64_encode($this->grav['uri']->url());
  349. $flash = $this->admin->session()->getFlashObject('files-upload') ?? [];
  350. if (!isset($flash[$sessionField])) {
  351. $flash[$sessionField] = [];
  352. }
  353. if (!isset($flash[$sessionField][$upload->field])) {
  354. $flash[$sessionField][$upload->field] = [];
  355. }
  356. // Set destination
  357. if ($this->grav['locator']->isStream($settings->destination)) {
  358. $destination = $this->grav['locator']->findResource($settings->destination, false, true);
  359. } else {
  360. $destination = Folder::getRelativePath(rtrim($settings->destination, '/'));
  361. $destination = $this->admin->getPagePathFromToken($destination);
  362. }
  363. // Create destination if needed
  364. if (!is_dir($destination)) {
  365. Folder::mkdir($destination);
  366. }
  367. // Generate random name if required
  368. if ($settings->random_name) { // TODO: document
  369. $extension = Utils::pathinfo($upload->file->name, PATHINFO_EXTENSION);
  370. $upload->file->name = Utils::generateRandomString(15) . '.' . $extension;
  371. }
  372. // Handle conflicting name if needed
  373. if ($settings->avoid_overwriting) { // TODO: document
  374. if (file_exists($destination . '/' . $upload->file->name)) {
  375. $upload->file->name = date('YmdHis') . '-' . $upload->file->name;
  376. }
  377. }
  378. // Prepare object for later save
  379. $path = $destination . '/' . $upload->file->name;
  380. $upload->file->path = $path;
  381. // $upload->file->route = $page ? $path : null;
  382. // Prepare data to be saved later
  383. $flash[$sessionField][$upload->field][$path] = (array)$upload->file;
  384. // Finally store the new uploaded file in the field session
  385. $this->admin->session()->setFlashObject('files-upload', $flash);
  386. $this->admin->json_response = [
  387. 'status' => 'success',
  388. 'session' => \json_encode([
  389. 'sessionField' => base64_encode($this->grav['uri']->url()),
  390. 'path' => $upload->file->path,
  391. 'field' => $settings->name
  392. ])
  393. ];
  394. return true;
  395. }
  396. /**
  397. * Checks if the user is allowed to perform the given task with its associated permissions
  398. *
  399. * @param string $task The task to execute
  400. * @param array $permissions The permissions given
  401. *
  402. * @return bool True if authorized. False if not.
  403. */
  404. public function authorizeTask($task = '', $permissions = [])
  405. {
  406. if (!$this->admin->authorize($permissions)) {
  407. if ($this->grav['uri']->extension() === 'json') {
  408. $this->admin->json_response = [
  409. 'status' => 'unauthorized',
  410. 'message' => $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' ' . $task . '.'
  411. ];
  412. } else {
  413. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' ' . $task . '.',
  414. 'error');
  415. }
  416. return false;
  417. }
  418. return true;
  419. }
  420. /**
  421. * Checks if the user is allowed to perform the given task with its associated permissions.
  422. * Throws exception if the check fails.
  423. *
  424. * @param string $task The task to execute
  425. * @param array $permissions The permissions given
  426. * @throws RequestException
  427. */
  428. public function checkTaskAuthorization($task = '', $permissions = [])
  429. {
  430. if (!$this->admin->authorize($permissions)) {
  431. throw new RequestException($this->getRequest(), $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' ' . $task . '.', 403);
  432. }
  433. }
  434. /**
  435. * Gets the permissions needed to access a given view
  436. *
  437. * @return array An array of permissions
  438. */
  439. protected function dataPermissions()
  440. {
  441. $type = $this->view;
  442. $permissions = ['admin.super'];
  443. switch ($type) {
  444. case 'config':
  445. $type = $this->route ?: 'system';
  446. $permissions[] = 'admin.configuration.' . $type;
  447. break;
  448. case 'plugins':
  449. $permissions[] = 'admin.plugins';
  450. break;
  451. case 'themes':
  452. $permissions[] = 'admin.themes';
  453. break;
  454. case 'users':
  455. $permissions[] = 'admin.users';
  456. break;
  457. case 'user':
  458. $permissions[] = 'admin.login';
  459. $permissions[] = 'admin.users';
  460. break;
  461. case 'pages':
  462. $permissions[] = 'admin.pages';
  463. break;
  464. default:
  465. $permissions[] = 'admin.configuration.' . $type;
  466. $permissions[] = 'admin.configuration_' . $type;
  467. }
  468. return $permissions;
  469. }
  470. /**
  471. * Gets the configuration data for a given view & post
  472. *
  473. * @param array $data
  474. *
  475. * @return array
  476. */
  477. protected function prepareData(array $data)
  478. {
  479. return $data;
  480. }
  481. /**
  482. * Internal method to normalize the $_FILES array
  483. *
  484. * @param array $data $_FILES starting point data
  485. * @param string $key
  486. *
  487. * @return object a new Object with a normalized list of files
  488. */
  489. protected function normalizeFiles($data, $key = '')
  490. {
  491. $files = new \stdClass();
  492. $files->field = $key;
  493. $files->file = new \stdClass();
  494. foreach ($data as $fieldName => $fieldValue) {
  495. // Since Files Upload are always happening via Ajax
  496. // we are not interested in handling `multiple="true"`
  497. // because they are always handled one at a time.
  498. // For this reason we normalize the value to string,
  499. // in case it is arriving as an array.
  500. $value = (array)Utils::getDotNotation($fieldValue, $key);
  501. $files->file->{$fieldName} = array_shift($value);
  502. }
  503. return $files;
  504. }
  505. /**
  506. * Removes a file from the flash object session, before it gets saved
  507. *
  508. * @return bool True if the action was performed.
  509. */
  510. public function taskFilesSessionRemove()
  511. {
  512. if (!$this->authorizeTask('delete file', $this->dataPermissions())) {
  513. return false;
  514. }
  515. // Retrieve the current session of the uploaded files for the field
  516. // and initialize it if it doesn't exist
  517. $sessionField = base64_encode($this->grav['uri']->url());
  518. $request = \json_decode($this->post['session']);
  519. // Ensure the URI requested matches the current one, otherwise fail
  520. if ($request->sessionField !== $sessionField) {
  521. return false;
  522. }
  523. // Retrieve the flash object and remove the requested file from it
  524. $flash = $this->admin->session()->getFlashObject('files-upload') ?? [];
  525. $endpoint = $flash[$request->sessionField][$request->field][$request->path] ?? null;
  526. if (isset($endpoint)) {
  527. if (file_exists($endpoint['tmp_name'])) {
  528. unlink($endpoint['tmp_name']);
  529. }
  530. unset($endpoint);
  531. }
  532. // Walk backward to cleanup any empty field that's left
  533. // Field
  534. if (isset($flash[$request->sessionField][$request->field][$request->path])) {
  535. unset($flash[$request->sessionField][$request->field][$request->path]);
  536. }
  537. // Field
  538. if (isset($flash[$request->sessionField][$request->field]) && empty($flash[$request->sessionField][$request->field])) {
  539. unset($flash[$request->sessionField][$request->field]);
  540. }
  541. // Session Field
  542. if (isset($flash[$request->sessionField]) && empty($flash[$request->sessionField])) {
  543. unset($flash[$request->sessionField]);
  544. }
  545. // If there's anything left to restore in the flash object, do so
  546. if (count($flash)) {
  547. $this->admin->session()->setFlashObject('files-upload', $flash);
  548. }
  549. $this->admin->json_response = ['status' => 'success'];
  550. return true;
  551. }
  552. /**
  553. * Redirect to the route stored in $this->redirect
  554. *
  555. * Route may or may not be prefixed by /en or /admin or /en/admin.
  556. *
  557. * @return void
  558. */
  559. public function redirect()
  560. {
  561. $this->admin->redirect($this->redirect, $this->redirectCode);
  562. }
  563. /**
  564. * Prepare and return POST data.
  565. *
  566. * @param array $post
  567. * @return array
  568. */
  569. protected function getPost($post)
  570. {
  571. if (!is_array($post)) {
  572. return [];
  573. }
  574. unset($post['task']);
  575. // Decode JSON encoded fields and merge them to data.
  576. if (isset($post['_json'])) {
  577. $post = array_replace_recursive($post, $this->jsonDecode($post['_json']));
  578. unset($post['_json']);
  579. }
  580. return $this->cleanDataKeys($post);
  581. }
  582. /**
  583. * Recursively JSON decode data.
  584. *
  585. * @param array $data
  586. * @return array
  587. * @throws JsonException
  588. * @internal Do not use directly!
  589. */
  590. protected function jsonDecode(array $data): array
  591. {
  592. foreach ($data as &$value) {
  593. if (is_array($value)) {
  594. $value = $this->jsonDecode($value);
  595. } else {
  596. $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
  597. }
  598. }
  599. return $data;
  600. }
  601. /**
  602. * @param array $source
  603. * @return array
  604. * @internal Do not use directly!
  605. */
  606. protected function cleanDataKeys(array $source): array
  607. {
  608. $out = [];
  609. foreach ($source as $key => $value) {
  610. $key = str_replace(['%5B', '%5D'], ['[', ']'], $key);
  611. if (is_array($value)) {
  612. $out[$key] = $this->cleanDataKeys($value);
  613. } else {
  614. $out[$key] = $value;
  615. }
  616. }
  617. return $out;
  618. }
  619. /**
  620. * Return true if multilang is active
  621. *
  622. * @return bool True if multilang is active
  623. */
  624. protected function isMultilang()
  625. {
  626. return count($this->grav['config']->get('system.languages.supported', [])) > 1;
  627. }
  628. /**
  629. * @param PageInterface|UserInterface|Data $obj
  630. *
  631. * @return PageInterface|UserInterface|Data
  632. */
  633. protected function storeFiles($obj)
  634. {
  635. // Process previously uploaded files for the current URI
  636. // and finally store them. Everything else will get discarded
  637. $queue = $this->admin->session()->getFlashObject('files-upload');
  638. if (is_array($queue)) {
  639. $queue = $queue[base64_encode($this->grav['uri']->url())];
  640. foreach ($queue as $key => $files) {
  641. foreach ($files as $destination => $file) {
  642. if (!rename($file['tmp_name'], $destination)) {
  643. throw new \RuntimeException(sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE',
  644. null), '"' . $file['tmp_name'] . '"', $destination));
  645. }
  646. unset($files[$destination]['tmp_name']);
  647. }
  648. if ($this->view === 'pages') {
  649. $keys = explode('.', preg_replace('/^header./', '', $key));
  650. $init_key = array_shift($keys);
  651. if (count($keys) > 0) {
  652. $new_data = $obj->header()->{$init_key} ?? [];
  653. Utils::setDotNotation($new_data, implode('.', $keys), $files, true);
  654. } else {
  655. $new_data = $files;
  656. }
  657. if (isset($obj->header()->{$init_key})) {
  658. $obj->modifyHeader($init_key,
  659. array_replace_recursive([], $obj->header()->{$init_key}, $new_data));
  660. } else {
  661. $obj->modifyHeader($init_key, $new_data);
  662. }
  663. } elseif ($obj instanceof UserInterface and $key === 'avatar') {
  664. $obj->set($key, $files);
  665. } else {
  666. // TODO: [this is JS handled] if it's single file, remove existing and use set, if it's multiple, use join
  667. $obj->join($key, $files); // stores
  668. }
  669. }
  670. }
  671. return $obj;
  672. }
  673. /**
  674. * Used by the filepicker field to get a list of files in a folder.
  675. *
  676. * @return bool
  677. */
  678. protected function taskGetFilesInFolder()
  679. {
  680. if (!$this->authorizeTask('get files', $this->dataPermissions())) {
  681. return false;
  682. }
  683. $data = $this->view === 'pages' ? $this->admin->page(true) : $this->prepareData([]);
  684. if (null === $data) {
  685. return false;
  686. }
  687. if (method_exists($data, 'blueprints')) {
  688. $settings = $data->blueprints()->schema()->getProperty($this->post['name']);
  689. } elseif (method_exists($data, 'getBlueprint')) {
  690. $settings = $data->getBlueprint()->schema()->getProperty($this->post['name']);
  691. }
  692. if (isset($settings['folder'])) {
  693. $folder = $settings['folder'];
  694. } else {
  695. $folder = 'self@';
  696. }
  697. // Do not use self@ outside of pages
  698. if ($this->view !== 'pages' && in_array($folder, ['@self', 'self@', '@self@'])) {
  699. if (!$data instanceof MediaInterface) {
  700. $this->admin->json_response = [
  701. 'status' => 'error',
  702. 'message' => sprintf($this->admin::translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null), $folder)
  703. ];
  704. return false;
  705. }
  706. $media = $data->getMedia();
  707. } else {
  708. /** @var UniformResourceLocator $locator */
  709. $locator = $this->grav['locator'];
  710. if ($locator->isStream($folder)) {
  711. $folder = $locator->findResource($folder);
  712. }
  713. // Set destination
  714. $folder = Folder::getRelativePath(rtrim($folder, '/'));
  715. $folder = $this->admin->getPagePathFromToken($folder);
  716. $media = new Media($folder);
  717. }
  718. $available_files = [];
  719. $metadata = [];
  720. $thumbs = [];
  721. foreach ($media->all() as $name => $medium) {
  722. $available_files[] = $name;
  723. if (isset($settings['include_metadata'])) {
  724. $img_metadata = $medium->metadata();
  725. if ($img_metadata) {
  726. $metadata[$name] = $img_metadata;
  727. }
  728. }
  729. }
  730. // Peak in the flashObject for optimistic filepicker updates
  731. $pending_files = [];
  732. $sessionField = base64_encode($this->grav['uri']->url());
  733. $flash = $this->admin->session()->getFlashObject('files-upload');
  734. if ($flash && isset($flash[$sessionField])) {
  735. foreach ($flash[$sessionField] as $field => $data) {
  736. foreach ($data as $file) {
  737. if (dirname($file['path']) === $folder) {
  738. $pending_files[] = $file['name'];
  739. }
  740. }
  741. }
  742. }
  743. $this->admin->session()->setFlashObject('files-upload', $flash);
  744. // Handle Accepted file types
  745. // Accept can only be file extensions (.pdf|.jpg)
  746. if (isset($settings['accept'])) {
  747. $available_files = array_filter($available_files, function ($file) use ($settings) {
  748. return $this->filterAcceptedFiles($file, $settings);
  749. });
  750. $pending_files = array_filter($pending_files, function ($file) use ($settings) {
  751. return $this->filterAcceptedFiles($file, $settings);
  752. });
  753. }
  754. // Generate thumbs if needed
  755. if (isset($settings['preview_images']) && $settings['preview_images'] === true) {
  756. foreach ($available_files as $filename) {
  757. $thumbs[$filename] = $media[$filename]->zoomCrop(100,100)->url();
  758. }
  759. }
  760. $this->admin->json_response = [
  761. 'status' => 'success',
  762. 'files' => array_values($available_files),
  763. 'pending' => array_values($pending_files),
  764. 'folder' => $folder,
  765. 'metadata' => $metadata,
  766. 'thumbs' => $thumbs
  767. ];
  768. return true;
  769. }
  770. /**
  771. * @param string $file
  772. * @param array $settings
  773. * @return false
  774. */
  775. protected function filterAcceptedFiles($file, $settings)
  776. {
  777. $valid = false;
  778. foreach ((array)$settings['accept'] as $type) {
  779. $find = str_replace('*', '.*', $type);
  780. $valid |= preg_match('#' . $find . '$#i', $file);
  781. }
  782. return $valid;
  783. }
  784. /**
  785. * Handle deleting a file from a blueprint
  786. *
  787. * @return bool True if the action was performed.
  788. */
  789. protected function taskRemoveFileFromBlueprint()
  790. {
  791. if (!$this->authorizeTask('remove file', $this->dataPermissions())) {
  792. return false;
  793. }
  794. /** @var Uri $uri */
  795. $uri = $this->grav['uri'];
  796. $blueprint = base64_decode($uri->param('blueprint'));
  797. $path = base64_decode($uri->param('path'));
  798. $route = base64_decode($uri->param('proute'));
  799. $type = $uri->param('type');
  800. $field = $uri->param('field');
  801. $filename = Utils::basename($this->post['filename'] ?? '');
  802. if ($filename === '') {
  803. $this->admin->json_response = [
  804. 'status' => 'error',
  805. 'message' => 'Filename is empty'
  806. ];
  807. return false;
  808. }
  809. // Get Blueprint
  810. if ($type === 'pages' || strpos($blueprint, 'pages/') === 0) {
  811. $page = $this->admin->page(true, $route);
  812. if (!$page) {
  813. $this->admin->json_response = [
  814. 'status' => 'error',
  815. 'message' => 'Page not found'
  816. ];
  817. return false;
  818. }
  819. $blueprints = $page->blueprints();
  820. $path = Folder::getRelativePath($page->path());
  821. $settings = (object)$blueprints->schema()->getProperty($field);
  822. } else {
  823. $page = null;
  824. if ($type === 'themes' || $type === 'plugins') {
  825. $obj = $this->grav[$type]->get(Utils::substrToString($blueprint, '/')); //here
  826. $settings = (object) $obj->blueprints()->schema()->getProperty($field);
  827. } else {
  828. $settings = (object)$this->admin->blueprints($blueprint)->schema()->getProperty($field);
  829. }
  830. }
  831. // Get destination
  832. if ($this->grav['locator']->isStream($settings->destination)) {
  833. $destination = $this->grav['locator']->findResource($settings->destination, false, true);
  834. } else {
  835. $destination = Folder::getRelativePath(rtrim($settings->destination, '/'));
  836. $destination = $this->admin->getPagePathFromToken($destination, $page);
  837. }
  838. // Not in path
  839. if (!Utils::startsWith($path, $destination)) {
  840. $this->admin->json_response = [
  841. 'status' => 'error',
  842. 'message' => 'Path not valid for this data type'
  843. ];
  844. return false;
  845. }
  846. // Only remove files from correct destination...
  847. $this->taskRemoveMedia($destination . '/' . $filename);
  848. if ($page) {
  849. $keys = explode('.', preg_replace('/^header./', '', $field));
  850. $header = (array)$page->header();
  851. $data_path = implode('.', $keys);
  852. $data = Utils::getDotNotation($header, $data_path);
  853. if (isset($data[$path])) {
  854. unset($data[$path]);
  855. Utils::setDotNotation($header, $data_path, $data);
  856. $page->header($header);
  857. }
  858. $page->save();
  859. } elseif ($type === 'user') {
  860. $user = Grav::instance()['user'];
  861. unset($user->avatar);
  862. $user->save();
  863. } else {
  864. $blueprint_prefix = $type === 'config' ? '' : $type . '.';
  865. $blueprint_name = str_replace(['config/', '/blueprints'], '', $blueprint);
  866. $blueprint_field = $blueprint_prefix . $blueprint_name . '.' . $field;
  867. $files = $this->grav['config']->get($blueprint_field);
  868. if ($files) {
  869. foreach ($files as $key => $value) {
  870. if ($key == $path) {
  871. unset($files[$key]);
  872. }
  873. }
  874. }
  875. $this->grav['config']->set($blueprint_field, $files);
  876. switch ($type) {
  877. case 'config':
  878. $data = $this->grav['config']->get($blueprint_name);
  879. $config = $this->admin->data($blueprint, $data);
  880. $config->save();
  881. break;
  882. case 'themes':
  883. Theme::saveConfig($blueprint_name);
  884. break;
  885. case 'plugins':
  886. Plugin::saveConfig($blueprint_name);
  887. break;
  888. }
  889. }
  890. Cache::clearCache('invalidate');
  891. $this->admin->json_response = [
  892. 'status' => 'success',
  893. 'message' => $this->admin::translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
  894. ];
  895. return true;
  896. }
  897. /**
  898. * Handles removing a media file
  899. *
  900. * @note This task cannot be used anymore.
  901. *
  902. * @return bool True if the action was performed
  903. */
  904. public function taskRemoveMedia($filename = null)
  905. {
  906. if (!$this->canEditMedia()) {
  907. return false;
  908. }
  909. if (null === $filename) {
  910. throw new \RuntimeException('Admin task RemoveMedia has been disabled.');
  911. }
  912. $file = File::instance($filename);
  913. $resultRemoveMedia = false;
  914. if ($file->exists()) {
  915. $resultRemoveMedia = $file->delete();
  916. $fileParts = Utils::pathinfo($filename);
  917. foreach (scandir($fileParts['dirname']) as $file) {
  918. $regex_pattern = '/' . preg_quote($fileParts['filename'], '/') . "@\d+x\." . $fileParts['extension'] . "(?:\.meta\.yaml)?$|" . preg_quote($fileParts['basename'], '/') . "\.meta\.yaml$/";
  919. if (preg_match($regex_pattern, $file)) {
  920. $path = $fileParts['dirname'] . '/' . $file;
  921. @unlink($path);
  922. }
  923. }
  924. }
  925. if ($resultRemoveMedia) {
  926. if ($this->grav['uri']->extension() === 'json') {
  927. $this->admin->json_response = [
  928. 'status' => 'success',
  929. 'message' => $this->admin::translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
  930. ];
  931. } else {
  932. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL'), 'info');
  933. $this->clearMediaCache();
  934. $this->setRedirect('/media-manager');
  935. }
  936. return true;
  937. }
  938. if ($this->grav['uri']->extension() === 'json') {
  939. $this->admin->json_response = [
  940. 'status' => 'success',
  941. 'message' => $this->admin::translate('PLUGIN_ADMIN.REMOVE_FAILED')
  942. ];
  943. } else {
  944. $this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.REMOVE_FAILED'), 'error');
  945. }
  946. return false;
  947. }
  948. /**
  949. * Handles clearing the media cache
  950. *
  951. * @return bool True if the action was performed
  952. */
  953. protected function clearMediaCache()
  954. {
  955. $key = 'media-manager-files';
  956. $cache = $this->grav['cache'];
  957. $cache->delete(md5($key));
  958. return true;
  959. }
  960. /**
  961. * Determine if the user can edit media
  962. *
  963. * @param string $type
  964. *
  965. * @return bool True if the media action is allowed
  966. */
  967. protected function canEditMedia($type = 'media')
  968. {
  969. if (!$this->authorizeTask('edit media', ['admin.' . $type, 'admin.super'])) {
  970. return false;
  971. }
  972. return true;
  973. }
  974. /**
  975. * @param string $message
  976. * @param string $type
  977. * @return $this
  978. */
  979. protected function setMessage($message, $type = 'info')
  980. {
  981. $this->admin->setMessage($message, $type);
  982. return $this;
  983. }
  984. /**
  985. * @return Config
  986. */
  987. protected function getConfig(): Config
  988. {
  989. return $this->grav['config'];
  990. }
  991. /**
  992. * @return ServerRequestInterface
  993. */
  994. protected function getRequest(): ServerRequestInterface
  995. {
  996. return $this->grav['request'];
  997. }
  998. }