Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.
 
 
 
 
 
 

676 lines
22 KiB

  1. <?php
  2. declare(strict_types=1);
  3. namespace Grav\Plugin\FlexObjects\Controllers;
  4. use Exception;
  5. use Grav\Common\Debugger;
  6. use Grav\Common\Page\Interfaces\PageInterface;
  7. use Grav\Common\Page\Medium\Medium;
  8. use Grav\Common\Page\Medium\MediumFactory;
  9. use Grav\Common\Utils;
  10. use Grav\Framework\Flex\FlexObject;
  11. use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
  12. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  13. use Grav\Framework\Media\Interfaces\MediaInterface;
  14. use LogicException;
  15. use Psr\Http\Message\ResponseInterface;
  16. use Psr\Http\Message\UploadedFileInterface;
  17. use RocketTheme\Toolbox\Event\Event;
  18. use RuntimeException;
  19. use function is_array;
  20. use function is_string;
  21. /**
  22. * Class MediaController
  23. * @package Grav\Plugin\FlexObjects\Controllers
  24. */
  25. class MediaController extends AbstractController
  26. {
  27. /**
  28. * @return ResponseInterface
  29. */
  30. public function taskMediaUpload(): ResponseInterface
  31. {
  32. $this->checkAuthorization('media.create');
  33. $object = $this->getObject();
  34. if (null === $object) {
  35. throw new RuntimeException('Not Found', 404);
  36. }
  37. if (!method_exists($object, 'checkUploadedMediaFile')) {
  38. throw new RuntimeException('Not Found', 404);
  39. }
  40. // Get updated object from Form Flash.
  41. $flash = $this->getFormFlash($object);
  42. if ($flash->exists()) {
  43. $object = $flash->getObject() ?? $object;
  44. $object->update([], $flash->getFilesByFields());
  45. }
  46. // Get field for the uploaded media.
  47. $field = $this->getPost('name', 'undefined');
  48. if ($field === 'undefined') {
  49. $field = null;
  50. }
  51. $request = $this->getRequest();
  52. $files = $request->getUploadedFiles();
  53. if ($field && isset($files['data'])) {
  54. $files = $files['data'];
  55. $parts = explode('.', $field);
  56. $last = array_pop($parts);
  57. foreach ($parts as $name) {
  58. if (!is_array($files[$name])) {
  59. throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
  60. }
  61. $files = $files[$name];
  62. }
  63. $file = $files[$last] ?? null;
  64. } else {
  65. // Legacy call with name being the filename instead of field name.
  66. $file = $files['file'] ?? null;
  67. $field = null;
  68. }
  69. /** @var UploadedFileInterface $file */
  70. if (is_array($file)) {
  71. $file = reset($file);
  72. }
  73. if (!$file instanceof UploadedFileInterface) {
  74. throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
  75. }
  76. $filename = $file->getClientFilename();
  77. $object->checkUploadedMediaFile($file, $filename, $field);
  78. try {
  79. // TODO: This only merges main level data, but is good for ordering (for now).
  80. $data = $flash->getData() ?? [];
  81. $data = array_replace($data, (array)$this->getPost('data'));
  82. $crop = $this->getPost('crop');
  83. if (is_string($crop)) {
  84. $crop = json_decode($crop, true, 512, JSON_THROW_ON_ERROR);
  85. }
  86. $flash->setData($data);
  87. $flash->addUploadedFile($file, $field, $crop);
  88. $flash->save();
  89. } catch (Exception $e) {
  90. throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
  91. }
  92. // Include exif metadata into the response if configured to do so
  93. $metadata = [];
  94. $include_metadata = $this->grav['config']->get('system.media.auto_metadata_exif', false);
  95. if ($include_metadata) {
  96. $medium = MediumFactory::fromUploadedFile($file);
  97. $media = $object->getMedia();
  98. $media->add($filename, $medium);
  99. $basename = str_replace(['@3x', '@2x'], '', Utils::pathinfo($filename, PATHINFO_BASENAME));
  100. if (isset($media[$basename])) {
  101. $metadata = $media[$basename]->metadata() ?: [];
  102. }
  103. }
  104. $response = [
  105. 'code' => 200,
  106. 'status' => 'success',
  107. 'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
  108. 'filename' => htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
  109. 'metadata' => $metadata
  110. ];
  111. return $this->createJsonResponse($response);
  112. }
  113. /**
  114. * @return ResponseInterface
  115. */
  116. public function taskMediaUploadMeta(): ResponseInterface
  117. {
  118. try {
  119. $this->checkAuthorization('media.create');
  120. $object = $this->getObject();
  121. if (null === $object) {
  122. throw new RuntimeException('Not Found', 404);
  123. }
  124. if (!method_exists($object, 'getMediaField')) {
  125. throw new RuntimeException('Not Found', 404);
  126. }
  127. $object->refresh();
  128. // Get updated object from Form Flash.
  129. $flash = $this->getFormFlash($object);
  130. if ($flash->exists()) {
  131. $object = $flash->getObject() ?? $object;
  132. $object->update([], $flash->getFilesByFields());
  133. }
  134. // Get field and data for the uploaded media.
  135. $field = (string)$this->getPost('field');
  136. $media = $object->getMediaField($field);
  137. if (!$media) {
  138. throw new RuntimeException('Media field not found: ' . $field, 404);
  139. }
  140. $data = $this->getPost('data');
  141. if (is_string($data)) {
  142. $data = json_decode($data, true);
  143. }
  144. $filename = Utils::basename($data['name'] ?? '');
  145. // Update field.
  146. $files = $object->getNestedProperty($field, []);
  147. // FIXME: Do we want to save something into the field as well?
  148. $files[$filename] = [];
  149. $object->setNestedProperty($field, $files);
  150. $info = [
  151. 'modified' => $data['modified'] ?? null,
  152. 'size' => $data['size'] ?? null,
  153. 'mime' => $data['mime'] ?? null,
  154. 'width' => $data['width'] ?? null,
  155. 'height' => $data['height'] ?? null,
  156. 'duration' => $data['duration'] ?? null,
  157. 'orientation' => $data['orientation'] ?? null,
  158. 'meta' => array_filter($data, static function ($val) { return $val !== null; })
  159. ];
  160. $info = array_filter($info, static function ($val) { return $val !== null; });
  161. // As the file may not be saved locally, we need to update the index.
  162. $media->updateIndex([$filename => $info]);
  163. $object->save();
  164. $flash->save();
  165. $response = [
  166. 'code' => 200,
  167. 'status' => 'success',
  168. 'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
  169. 'field' => $field,
  170. 'filename' => $filename,
  171. 'metadata' => $data
  172. ];
  173. } catch (\Exception $e) {
  174. /** @var Debugger $debugger */
  175. $debugger = $this->grav['debugger'];
  176. $debugger->addException($e);
  177. return $this->createJsonErrorResponse($e);
  178. }
  179. return $this->createJsonResponse($response);
  180. }
  181. /**
  182. * @return ResponseInterface
  183. */
  184. public function taskMediaReorder(): ResponseInterface
  185. {
  186. try {
  187. $this->checkAuthorization('media.update');
  188. $object = $this->getObject();
  189. if (null === $object) {
  190. throw new RuntimeException('Not Found', 404);
  191. }
  192. if (!method_exists($object, 'getMediaField')) {
  193. throw new RuntimeException('Not Found', 404);
  194. }
  195. $object->refresh();
  196. // Get updated object from Form Flash.
  197. $flash = $this->getFormFlash($object);
  198. if ($flash->exists()) {
  199. $object = $flash->getObject() ?? $object;
  200. $object->update([], $flash->getFilesByFields());
  201. }
  202. // Get field and data for the uploaded media.
  203. $field = (string)$this->getPost('field');
  204. $media = $object->getMediaField($field);
  205. if (!$media) {
  206. throw new RuntimeException('Media field not found: ' . $field, 404);
  207. }
  208. // Create id => filename map from all files in the media.
  209. $map = [];
  210. foreach ($media as $name => $medium) {
  211. $id = $medium->get('meta.id');
  212. if ($id) {
  213. $map[$id] = $name;
  214. }
  215. }
  216. // Get reorder list and reorder the map.
  217. $data = $this->getPost('data');
  218. if (is_string($data)) {
  219. $data = json_decode($data, true);
  220. }
  221. $data = array_fill_keys($data, null);
  222. $map = array_filter(array_merge($data, $map), static function($val) { return $val !== null; });
  223. // Reorder the files.
  224. $files = $object->getNestedProperty($field, []);
  225. $map = array_fill_keys($map, null);
  226. $files = array_filter(array_merge($map, $files), static function($val) { return $val !== null; });
  227. // Update field.
  228. $object->setNestedProperty($field, $files);
  229. $object->save();
  230. $flash->save();
  231. $response = [
  232. 'code' => 200,
  233. 'status' => 'success',
  234. 'message' => $this->translate('PLUGIN_ADMIN.FIELD_REORDER_SUCCESSFUL'),
  235. 'field' => $field,
  236. 'ordering' => array_keys($files)
  237. ];
  238. } catch (\Exception $e) {
  239. /** @var Debugger $debugger */
  240. $debugger = $this->grav['debugger'];
  241. $debugger->addException($e);
  242. $ex = new RuntimeException($this->translate('PLUGIN_ADMIN.FIELD_REORDER_FAILED', $field), $e->getCode(), $e);
  243. return $this->createJsonErrorResponse($ex);
  244. }
  245. return $this->createJsonResponse($response);
  246. }
  247. /**
  248. * @return ResponseInterface
  249. */
  250. public function taskMediaDelete(): ResponseInterface
  251. {
  252. $this->checkAuthorization('media.delete');
  253. /** @var FlexObjectInterface|null $object */
  254. $object = $this->getObject();
  255. if (!$object) {
  256. throw new RuntimeException('Not Found', 404);
  257. }
  258. $filename = $this->getPost('filename');
  259. // Handle bad filenames.
  260. if (!Utils::checkFilename($filename)) {
  261. throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILE_FOUND'), 400);
  262. }
  263. try {
  264. $field = $this->getPost('name');
  265. $flash = $this->getFormFlash($object);
  266. $flash->removeFile($filename, $field);
  267. $flash->save();
  268. } catch (Exception $e) {
  269. throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
  270. }
  271. $response = [
  272. 'code' => 200,
  273. 'status' => 'success',
  274. 'message' => $this->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
  275. ];
  276. return $this->createJsonResponse($response);
  277. }
  278. /**
  279. * Used in pagemedia field.
  280. *
  281. * @return ResponseInterface
  282. */
  283. public function taskMediaCopy(): ResponseInterface
  284. {
  285. $this->checkAuthorization('media.create');
  286. /** @var FlexObjectInterface|null $object */
  287. $object = $this->getObject();
  288. if (!$object) {
  289. throw new RuntimeException('Not Found', 404);
  290. }
  291. if (!method_exists($object, 'uploadMediaFile')) {
  292. throw new RuntimeException('Not Found', 404);
  293. }
  294. $request = $this->getRequest();
  295. $files = $request->getUploadedFiles();
  296. $file = $files['file'] ?? null;
  297. if (!$file instanceof UploadedFileInterface) {
  298. throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
  299. }
  300. $post = $request->getParsedBody();
  301. $filename = $post['name'] ?? $file->getClientFilename();
  302. // Upload media right away.
  303. $object->uploadMediaFile($file, $filename);
  304. // Include exif metadata into the response if configured to do so
  305. $metadata = [];
  306. $include_metadata = $this->grav['config']->get('system.media.auto_metadata_exif', false);
  307. if ($include_metadata) {
  308. $basename = str_replace(['@3x', '@2x'], '', Utils::pathinfo($filename, PATHINFO_BASENAME));
  309. $media = $object->getMedia();
  310. if (isset($media[$basename])) {
  311. $metadata = $media[$basename]->metadata() ?: [];
  312. }
  313. }
  314. if ($object instanceof PageInterface) {
  315. // Backwards compatibility to existing plugins.
  316. // DEPRECATED: page
  317. $this->grav->fireEvent('onAdminAfterAddMedia', new Event(['object' => $object, 'page' => $object]));
  318. }
  319. $response = [
  320. 'code' => 200,
  321. 'status' => 'success',
  322. 'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
  323. 'filename' => htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
  324. 'metadata' => $metadata
  325. ];
  326. return $this->createJsonResponse($response);
  327. }
  328. /**
  329. * Used in pagemedia field.
  330. *
  331. * @return ResponseInterface
  332. */
  333. public function taskMediaRemove(): ResponseInterface
  334. {
  335. $this->checkAuthorization('media.delete');
  336. /** @var FlexObjectInterface|null $object */
  337. $object = $this->getObject();
  338. if (!$object) {
  339. throw new RuntimeException('Not Found', 404);
  340. }
  341. if (!method_exists($object, 'deleteMediaFile')) {
  342. throw new RuntimeException('Not Found', 404);
  343. }
  344. $field = $this->getPost('field');
  345. $filename = $this->getPost('filename');
  346. // Handle bad filenames.
  347. if (!Utils::checkFilename($filename)) {
  348. throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILE_FOUND'), 400);
  349. }
  350. $object->deleteMediaFile($filename, $field);
  351. if ($field) {
  352. $order = $object->getNestedProperty($field);
  353. unset($order[$filename]);
  354. $object->setNestedProperty($field, $order);
  355. $object->save();
  356. }
  357. if ($object instanceof PageInterface) {
  358. // Backwards compatibility to existing plugins.
  359. // DEPRECATED: page
  360. $this->grav->fireEvent('onAdminAfterDelMedia', new Event(['object' => $object, 'page' => $object, 'media' => $object->getMedia(), 'filename' => $filename]));
  361. }
  362. $response = [
  363. 'code' => 200,
  364. 'status' => 'success',
  365. 'message' => $this->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . htmlspecialchars($filename, ENT_QUOTES | ENT_HTML5, 'UTF-8')
  366. ];
  367. return $this->createJsonResponse($response);
  368. }
  369. /**
  370. * @return ResponseInterface
  371. */
  372. public function actionMediaList(): ResponseInterface
  373. {
  374. $this->checkAuthorization('media.list');
  375. /** @var MediaInterface|FlexObjectInterface $object */
  376. $object = $this->getObject();
  377. if (!$object) {
  378. throw new RuntimeException('Not Found', 404);
  379. }
  380. // Get updated object from Form Flash.
  381. $flash = $this->getFormFlash($object);
  382. if ($flash->exists()) {
  383. $object = $flash->getObject() ?? $object;
  384. $object->update([], $flash->getFilesByFields());
  385. }
  386. $media = $object->getMedia();
  387. $media_list = [];
  388. /**
  389. * @var string $name
  390. * @var Medium $medium
  391. */
  392. foreach ($media->all() as $name => $medium) {
  393. $media_list[$name] = [
  394. 'url' => $medium->display($medium->get('extension') === 'svg' ? 'source' : 'thumbnail')->cropZoom(400, 300)->url(),
  395. 'size' => $medium->get('size'),
  396. 'metadata' => $medium->metadata() ?: [],
  397. 'original' => $medium->higherQualityAlternative()->get('filename')
  398. ];
  399. }
  400. $response = [
  401. 'code' => 200,
  402. 'status' => 'success',
  403. 'results' => $media_list
  404. ];
  405. return $this->createJsonResponse($response);
  406. }
  407. /**
  408. * Used by the filepicker field to get a list of files in a folder.
  409. *
  410. * @return ResponseInterface
  411. */
  412. protected function actionMediaPicker(): ResponseInterface
  413. {
  414. $this->checkAuthorization('media.list');
  415. /** @var FlexObject $object */
  416. $object = $this->getObject();
  417. if (!$object || !\is_callable([$object, 'getFieldSettings'])) {
  418. throw new RuntimeException('Not Found', 404);
  419. }
  420. // Get updated object from Form Flash.
  421. $flash = $this->getFormFlash($object);
  422. if ($flash->exists()) {
  423. $object = $flash->getObject() ?? $object;
  424. $object->update([], $flash->getFilesByFields());
  425. }
  426. $name = $this->getPost('name');
  427. $settings = $name ? $object->getFieldSettings($name) : null;
  428. if (empty($settings['media_picker_field'])) {
  429. throw new RuntimeException('Not Found', 404);
  430. }
  431. $media = $object->getMediaField($name);
  432. $available_files = [];
  433. $metadata = [];
  434. $thumbs = [];
  435. /**
  436. * @var string $name
  437. * @var Medium $medium
  438. */
  439. foreach ($media->all() as $name => $medium) {
  440. $available_files[] = $name;
  441. if (isset($settings['include_metadata'])) {
  442. $img_metadata = $medium->metadata();
  443. if ($img_metadata) {
  444. $metadata[$name] = $img_metadata;
  445. }
  446. }
  447. }
  448. // Peak in the flashObject for optimistic filepicker updates
  449. $pending_files = [];
  450. $sessionField = base64_encode($this->grav['uri']->url());
  451. $flash = $this->getSession()->getFlashObject('files-upload');
  452. $folder = $media->getPath() ?: null;
  453. if ($flash && isset($flash[$sessionField])) {
  454. foreach ($flash[$sessionField] as $field => $data) {
  455. foreach ($data as $file) {
  456. $test = \dirname($file['path']);
  457. if ($test === $folder) {
  458. $pending_files[] = $file['name'];
  459. }
  460. }
  461. }
  462. }
  463. $this->getSession()->setFlashObject('files-upload', $flash);
  464. // Handle Accepted file types
  465. // Accept can only be file extensions (.pdf|.jpg)
  466. if (isset($settings['accept'])) {
  467. $available_files = array_filter($available_files, function ($file) use ($settings) {
  468. return $this->filterAcceptedFiles($file, $settings);
  469. });
  470. $pending_files = array_filter($pending_files, function ($file) use ($settings) {
  471. return $this->filterAcceptedFiles($file, $settings);
  472. });
  473. }
  474. if (isset($settings['deny'])) {
  475. $available_files = array_filter($available_files, function ($file) use ($settings) {
  476. return $this->filterDeniedFiles($file, $settings);
  477. });
  478. $pending_files = array_filter($pending_files, function ($file) use ($settings) {
  479. return $this->filterDeniedFiles($file, $settings);
  480. });
  481. }
  482. // Generate thumbs if needed
  483. if (isset($settings['preview_images']) && $settings['preview_images'] === true) {
  484. foreach ($available_files as $filename) {
  485. $thumbs[$filename] = $media[$filename]->zoomCrop(100,100)->url();
  486. }
  487. }
  488. $response = [
  489. 'code' => 200,
  490. 'status' => 'success',
  491. 'files' => array_values($available_files),
  492. 'pending' => array_values($pending_files),
  493. 'folder' => $folder,
  494. 'metadata' => $metadata,
  495. 'thumbs' => $thumbs
  496. ];
  497. return $this->createJsonResponse($response);
  498. }
  499. /**
  500. * @param string $file
  501. * @param array $settings
  502. * @return false|int
  503. */
  504. protected function filterAcceptedFiles(string $file, array $settings)
  505. {
  506. $valid = false;
  507. foreach ((array)$settings['accept'] as $type) {
  508. $find = str_replace('*', '.*', $type);
  509. $valid |= preg_match('#' . $find . '$#i', $file);
  510. }
  511. return $valid;
  512. }
  513. /**
  514. * @param string $file
  515. * @param array $settings
  516. * @return false|int
  517. */
  518. protected function filterDeniedFiles(string $file, array $settings)
  519. {
  520. $valid = true;
  521. foreach ((array)$settings['deny'] as $type) {
  522. $find = str_replace('*', '.*', $type);
  523. $valid = !preg_match('#' . $find . '$#i', $file);
  524. }
  525. return $valid;
  526. }
  527. /**
  528. * @param string $action
  529. * @return void
  530. * @throws LogicException
  531. * @throws RuntimeException
  532. */
  533. protected function checkAuthorization(string $action): void
  534. {
  535. $object = $this->getObject();
  536. if (!$object) {
  537. throw new RuntimeException('Not Found', 404);
  538. }
  539. // If object does not have ACL support ignore ACL checks.
  540. if (!$object instanceof FlexAuthorizeInterface) {
  541. return;
  542. }
  543. switch ($action) {
  544. case 'media.list':
  545. $action = 'read';
  546. break;
  547. case 'media.create':
  548. case 'media.update':
  549. case 'media.delete':
  550. $action = $object->exists() ? 'update' : 'create';
  551. break;
  552. default:
  553. throw new LogicException(sprintf('Unsupported authorize action %s', $action), 500);
  554. }
  555. if (!$object->isAuthorized($action, null, $this->user)) {
  556. throw new RuntimeException('Forbidden', 403);
  557. }
  558. }
  559. }