adminbasecontroller.php 36 KB

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