adminbasecontroller.php 33 KB

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