adminbasecontroller.php 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005
  1. <?php
  2. namespace Grav\Plugin\Admin;
  3. use Grav\Common\Config\Config;
  4. use Grav\Common\Filesystem\Folder;
  5. use Grav\Common\Grav;
  6. use Grav\Common\Page\Media;
  7. use Grav\Common\Utils;
  8. use Grav\Common\Plugin;
  9. use Grav\Common\Theme;
  10. use RocketTheme\Toolbox\Event\Event;
  11. use RocketTheme\Toolbox\File\File;
  12. /**
  13. * Class AdminController
  14. *
  15. * @package Grav\Plugin
  16. */
  17. class AdminBaseController
  18. {
  19. /**
  20. * @var Grav
  21. */
  22. public $grav;
  23. /**
  24. * @var string
  25. */
  26. public $view;
  27. /**
  28. * @var string
  29. */
  30. public $task;
  31. /**
  32. * @var string
  33. */
  34. public $route;
  35. /**
  36. * @var array
  37. */
  38. public $post;
  39. /**
  40. * @var array|null
  41. */
  42. public $data;
  43. /**
  44. * @var \Grav\Common\Uri
  45. */
  46. protected $uri;
  47. /**
  48. * @var Admin
  49. */
  50. protected $admin;
  51. /**
  52. * @var string
  53. */
  54. protected $redirect;
  55. /**
  56. * @var int
  57. */
  58. protected $redirectCode;
  59. protected $upload_errors = [
  60. 0 => "There is no error, the file uploaded with success",
  61. 1 => "The uploaded file exceeds the max upload size",
  62. 2 => "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML",
  63. 3 => "The uploaded file was only partially uploaded",
  64. 4 => "No file was uploaded",
  65. 6 => "Missing a temporary folder",
  66. 7 => "Failed to write file to disk",
  67. 8 => "A PHP extension stopped the file upload"
  68. ];
  69. /** @var array */
  70. public $blacklist_views = [];
  71. /**
  72. * Performs a task.
  73. *
  74. * @return bool True if the action was performed successfully.
  75. */
  76. public function execute()
  77. {
  78. if (in_array($this->view, $this->blacklist_views)) {
  79. return false;
  80. }
  81. if (!$this->validateNonce()) {
  82. return false;
  83. }
  84. $method = 'task' . ucfirst($this->task);
  85. if (method_exists($this, $method)) {
  86. try {
  87. $success = call_user_func([$this, $method]);
  88. } catch (\RuntimeException $e) {
  89. $success = true;
  90. $this->admin->setMessage($e->getMessage(), 'error');
  91. }
  92. } else {
  93. $success = $this->grav->fireEvent('onAdminTaskExecute',
  94. new Event(['controller' => $this, 'method' => $method]));
  95. }
  96. // Grab redirect parameter.
  97. $redirect = isset($this->post['_redirect']) ? $this->post['_redirect'] : null;
  98. unset($this->post['_redirect']);
  99. // Redirect if requested.
  100. if ($redirect) {
  101. $this->setRedirect($redirect);
  102. }
  103. return $success;
  104. }
  105. protected function validateNonce()
  106. {
  107. if (method_exists('Grav\Common\Utils', 'getNonce')) {
  108. if (strtolower($_SERVER['REQUEST_METHOD']) == 'post') {
  109. if (isset($this->post['admin-nonce'])) {
  110. $nonce = $this->post['admin-nonce'];
  111. } else {
  112. $nonce = $this->grav['uri']->param('admin-nonce');
  113. }
  114. if (!$nonce || !Utils::verifyNonce($nonce, 'admin-form')) {
  115. if ($this->task == 'addmedia') {
  116. $message = sprintf($this->admin->translate('PLUGIN_ADMIN.FILE_TOO_LARGE', null),
  117. ini_get('post_max_size'));
  118. //In this case it's more likely that the image is too big than POST can handle. Show message
  119. $this->admin->json_response = [
  120. 'status' => 'error',
  121. 'message' => $message
  122. ];
  123. return false;
  124. }
  125. $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
  126. $this->admin->json_response = [
  127. 'status' => 'error',
  128. 'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
  129. ];
  130. return false;
  131. }
  132. unset($this->post['admin-nonce']);
  133. } else {
  134. if ($this->task == 'logout') {
  135. $nonce = $this->grav['uri']->param('logout-nonce');
  136. if (!isset($nonce) || !Utils::verifyNonce($nonce, 'logout-form')) {
  137. $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'),
  138. 'error');
  139. $this->admin->json_response = [
  140. 'status' => 'error',
  141. 'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
  142. ];
  143. return false;
  144. }
  145. } else {
  146. $nonce = $this->grav['uri']->param('admin-nonce');
  147. if (!isset($nonce) || !Utils::verifyNonce($nonce, 'admin-form')) {
  148. $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'),
  149. 'error');
  150. $this->admin->json_response = [
  151. 'status' => 'error',
  152. 'message' => $this->admin->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN')
  153. ];
  154. return false;
  155. }
  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 (!$this->authorizeTask('save', $this->dataPermissions()) || !isset($_FILES)) {
  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) && substr($base,
  503. 0, 4) != substr($this->redirect, 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. unset($post['task']);
  531. // Decode JSON encoded fields and merge them to data.
  532. if (isset($post['_json'])) {
  533. $post = array_replace_recursive($post, $this->jsonDecode($post['_json']));
  534. unset($post['_json']);
  535. }
  536. $post = $this->cleanDataKeys($post);
  537. return $post;
  538. }
  539. /**
  540. * Recursively JSON decode data.
  541. *
  542. * @param array $data
  543. *
  544. * @return array
  545. */
  546. protected function jsonDecode(array $data)
  547. {
  548. foreach ($data as &$value) {
  549. if (is_array($value)) {
  550. $value = $this->jsonDecode($value);
  551. } else {
  552. $value = json_decode($value, true);
  553. }
  554. }
  555. return $data;
  556. }
  557. protected function cleanDataKeys($source = [])
  558. {
  559. $out = [];
  560. if (is_array($source)) {
  561. foreach ($source as $key => $value) {
  562. $key = str_replace('%5B', '[', str_replace('%5D', ']', $key));
  563. if (is_array($value)) {
  564. $out[$key] = $this->cleanDataKeys($value);
  565. } else {
  566. $out[$key] = $value;
  567. }
  568. }
  569. }
  570. return $out;
  571. }
  572. /**
  573. * Return true if multilang is active
  574. *
  575. * @return bool True if multilang is active
  576. */
  577. protected function isMultilang()
  578. {
  579. return count($this->grav['config']->get('system.languages.supported', [])) > 1;
  580. }
  581. /**
  582. * @param \Grav\Common\Page\Page|\Grav\Common\Data\Data $obj
  583. *
  584. * @return \Grav\Common\Page\Page|\Grav\Common\Data\Data
  585. */
  586. protected function storeFiles($obj)
  587. {
  588. // Process previously uploaded files for the current URI
  589. // and finally store them. Everything else will get discarded
  590. $queue = $this->admin->session()->getFlashObject('files-upload');
  591. $queue = $queue[base64_encode($this->grav['uri']->url())];
  592. if (is_array($queue)) {
  593. foreach ($queue as $key => $files) {
  594. foreach ($files as $destination => $file) {
  595. if (!rename($file['tmp_name'], $destination)) {
  596. throw new \RuntimeException(sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_MOVE',
  597. null), '"' . $file['tmp_name'] . '"', $destination));
  598. }
  599. unset($files[$destination]['tmp_name']);
  600. }
  601. if ($this->view == 'pages') {
  602. $keys = explode('.', preg_replace('/^header./', '', $key));
  603. $init_key = array_shift($keys);
  604. if (count($keys) > 0) {
  605. $new_data = isset($obj->header()->$init_key) ? $obj->header()->$init_key : [];
  606. Utils::setDotNotation($new_data, implode('.', $keys), $files, true);
  607. } else {
  608. $new_data = $files;
  609. }
  610. if (isset($data['header'][$init_key])) {
  611. $obj->modifyHeader($init_key,
  612. array_replace_recursive([], $data['header'][$init_key], $new_data));
  613. } else {
  614. $obj->modifyHeader($init_key, $new_data);
  615. }
  616. } else {
  617. // TODO: [this is JS handled] if it's single file, remove existing and use set, if it's multiple, use join
  618. $obj->join($key, $files); // stores
  619. }
  620. }
  621. }
  622. return $obj;
  623. }
  624. /**
  625. * Used by the filepicker field to get a list of files in a folder.
  626. */
  627. protected function taskGetFilesInFolder()
  628. {
  629. if (!$this->authorizeTask('save', $this->dataPermissions())) {
  630. return false;
  631. }
  632. $data = $this->view == 'pages' ? $this->admin->page(true) : $this->prepareData([]);
  633. $settings = $data->blueprints()->schema()->getProperty($this->post['name']);
  634. if (isset($settings['folder'])) {
  635. $folder = $settings['folder'];
  636. } else {
  637. $folder = '@self';
  638. }
  639. // Do not use self@ outside of pages
  640. if ($this->view != 'pages' && in_array($folder, ['@self', 'self@'])) {
  641. $this->admin->json_response = [
  642. 'status' => 'error',
  643. 'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null), $folder)
  644. ];
  645. return false;
  646. }
  647. // Set destination
  648. $folder = Folder::getRelativePath(rtrim($folder, '/'));
  649. $folder = $this->admin->getPagePathFromToken($folder);
  650. $media = new Media($folder);
  651. $available_files = [];
  652. $metadata = [];
  653. $thumbs = [];
  654. foreach ($media->all() as $name => $medium) {
  655. $available_files[] = $name;
  656. if (isset($settings['include_metadata'])) {
  657. $img_metadata = $medium->metadata();
  658. if ($img_metadata) {
  659. $metadata[$name] = $img_metadata;
  660. }
  661. }
  662. }
  663. // Peak in the flashObject for optimistic filepicker updates
  664. $pending_files = [];
  665. $sessionField = base64_encode($this->grav['uri']->url());
  666. $flash = $this->admin->session()->getFlashObject('files-upload');
  667. if ($flash && isset($flash[$sessionField])) {
  668. foreach ($flash[$sessionField] as $field => $data) {
  669. foreach ($data as $file) {
  670. if (dirname($file['path']) === $folder) {
  671. $pending_files[] = $file['name'];
  672. }
  673. }
  674. }
  675. }
  676. $this->admin->session()->setFlashObject('files-upload', $flash);
  677. // Handle Accepted file types
  678. // Accept can only be file extensions (.pdf|.jpg)
  679. if (isset($settings['accept'])) {
  680. $available_files = array_filter($available_files, function ($file) use ($settings) {
  681. return $this->filterAcceptedFiles($file, $settings);
  682. });
  683. $pending_files = array_filter($pending_files, function ($file) use ($settings) {
  684. return $this->filterAcceptedFiles($file, $settings);
  685. });
  686. }
  687. // Generate thumbs if needed
  688. if (isset($settings['preview_images']) && $settings['preview_images'] === true) {
  689. foreach ($available_files as $filename) {
  690. $thumbs[$filename] = $media[$filename]->zoomCrop(100,100)->url();
  691. }
  692. }
  693. $this->admin->json_response = [
  694. 'status' => 'success',
  695. 'files' => array_values($available_files),
  696. 'pending' => array_values($pending_files),
  697. 'folder' => $folder,
  698. 'metadata' => $metadata,
  699. 'thumbs' => $thumbs
  700. ];
  701. return true;
  702. }
  703. protected function filterAcceptedFiles($file, $settings)
  704. {
  705. $valid = false;
  706. foreach ((array)$settings['accept'] as $type) {
  707. $find = str_replace('*', '.*', $type);
  708. $valid |= preg_match('#' . $find . '$#', $file);
  709. }
  710. return $valid;
  711. }
  712. /**
  713. * Handle deleting a file from a blueprint
  714. *
  715. * @return bool True if the action was performed.
  716. */
  717. protected function taskRemoveFileFromBlueprint()
  718. {
  719. $uri = $this->grav['uri'];
  720. $blueprint = base64_decode($uri->param('blueprint'));
  721. $path = base64_decode($uri->param('path'));
  722. $proute = base64_decode($uri->param('proute'));
  723. $type = $uri->param('type');
  724. $field = $uri->param('field');
  725. $this->taskRemoveMedia();
  726. if ($type == 'pages') {
  727. $page = $this->admin->page(true, $proute);
  728. $keys = explode('.', preg_replace('/^header./', '', $field));
  729. $header = (array)$page->header();
  730. $data_path = implode('.', $keys);
  731. $data = Utils::getDotNotation($header, $data_path);
  732. if (isset($data[$path])) {
  733. unset($data[$path]);
  734. Utils::setDotNotation($header, $data_path, $data);
  735. $page->header($header);
  736. }
  737. $page->save();
  738. } else {
  739. $blueprint_prefix = $type == 'config' ? '' : $type . '.';
  740. $blueprint_name = str_replace('/blueprints', '', str_replace('config/', '', $blueprint));
  741. $blueprint_field = $blueprint_prefix . $blueprint_name . '.' . $field;
  742. $files = $this->grav['config']->get($blueprint_field);
  743. if ($files) {
  744. foreach ($files as $key => $value) {
  745. if ($key == $path) {
  746. unset($files[$key]);
  747. }
  748. }
  749. }
  750. $this->grav['config']->set($blueprint_field, $files);
  751. switch ($type) {
  752. case 'config':
  753. $data = $this->grav['config']->get($blueprint_name);
  754. $config = $this->admin->data($blueprint, $data);
  755. $config->save();
  756. break;
  757. case 'themes':
  758. Theme::saveConfig($blueprint_name);
  759. break;
  760. case 'plugins':
  761. Plugin::saveConfig($blueprint_name);
  762. break;
  763. }
  764. }
  765. $this->admin->json_response = [
  766. 'status' => 'success',
  767. 'message' => $this->admin->translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
  768. ];
  769. return true;
  770. }
  771. /**
  772. * Handles removing a media file
  773. *
  774. * @return bool True if the action was performed
  775. */
  776. public function taskRemoveMedia()
  777. {
  778. if (!$this->canEditMedia()) {
  779. return false;
  780. }
  781. $filename = base64_decode($this->grav['uri']->param('route'));
  782. if (!$filename) {
  783. $filename = base64_decode($this->route);
  784. }
  785. $file = File::instance($filename);
  786. $resultRemoveMedia = false;
  787. if ($file->exists()) {
  788. $resultRemoveMedia = $file->delete();
  789. $fileParts = pathinfo($filename);
  790. foreach (scandir($fileParts['dirname']) as $file) {
  791. $regex_pattern = "/" . preg_quote($fileParts['filename']) . "@\d+x\." . $fileParts['extension'] . "(?:\.meta\.yaml)?$|" . preg_quote($fileParts['basename']) . "\.meta\.yaml$/";
  792. if (preg_match($regex_pattern, $file)) {
  793. $path = $fileParts['dirname'] . '/' . $file;
  794. @unlink($path);
  795. }
  796. }
  797. }
  798. if ($resultRemoveMedia) {
  799. if ($this->grav['uri']->extension() === 'json') {
  800. $this->admin->json_response = [
  801. 'status' => 'success',
  802. 'message' => $this->admin->translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
  803. ];
  804. } else {
  805. $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL'), 'info');
  806. $this->clearMediaCache();
  807. $this->setRedirect('/media-manager');
  808. }
  809. return true;
  810. } else {
  811. if ($this->grav['uri']->extension() === 'json') {
  812. $this->admin->json_response = [
  813. 'status' => 'success',
  814. 'message' => $this->admin->translate('PLUGIN_ADMIN.REMOVE_FAILED')
  815. ];
  816. } else {
  817. $this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.REMOVE_FAILED'), 'error');
  818. }
  819. return false;
  820. }
  821. }
  822. /**
  823. * Handles clearing the media cache
  824. *
  825. * @return bool True if the action was performed
  826. */
  827. protected function clearMediaCache()
  828. {
  829. $key = 'media-manager-files';
  830. $cache = $this->grav['cache'];
  831. $cache->delete(md5($key));
  832. return true;
  833. }
  834. /**
  835. * Determine if the user can edit media
  836. *
  837. * @param string $type
  838. *
  839. * @return bool True if the media action is allowed
  840. */
  841. protected function canEditMedia($type = 'media')
  842. {
  843. if (!$this->authorizeTask('edit media', ['admin.' . $type, 'admin.super'])) {
  844. return false;
  845. }
  846. return true;
  847. }
  848. }