FlexMediaTrait.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  1. <?php
  2. namespace Grav\Framework\Flex\Traits;
  3. /**
  4. * @package Grav\Framework\Flex
  5. *
  6. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  7. * @license MIT License; see LICENSE file for details.
  8. */
  9. use Grav\Common\Cache;
  10. use Grav\Common\Config\Config;
  11. use Grav\Common\Filesystem\Folder;
  12. use Grav\Common\Grav;
  13. use Grav\Common\Media\Interfaces\MediaCollectionInterface;
  14. use Grav\Common\Media\Traits\MediaTrait;
  15. use Grav\Common\Page\Medium\AbstractMedia;
  16. use Grav\Common\Page\Medium\Medium;
  17. use Grav\Common\Page\Medium\MediumFactory;
  18. use Grav\Common\Utils;
  19. use Grav\Framework\Flex\FlexDirectory;
  20. use Grav\Framework\Form\FormFlashFile;
  21. use Psr\Http\Message\UploadedFileInterface;
  22. use RocketTheme\Toolbox\File\YamlFile;
  23. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  24. use RuntimeException;
  25. /**
  26. * Implements Grav Page content and header manipulation methods.
  27. */
  28. trait FlexMediaTrait
  29. {
  30. use MediaTrait {
  31. MediaTrait::getMedia as protected getExistingMedia;
  32. }
  33. protected $_uploads;
  34. public function __debugInfo()
  35. {
  36. return parent::__debugInfo() + [
  37. 'uploads:private' => $this->getUpdatedMedia()
  38. ];
  39. }
  40. /**
  41. * @return string
  42. */
  43. public function getStorageFolder()
  44. {
  45. return $this->exists() ? $this->getFlexDirectory()->getStorageFolder($this->getStorageKey()) : '';
  46. }
  47. /**
  48. * @return string
  49. */
  50. public function getMediaFolder()
  51. {
  52. return $this->exists() ? $this->getFlexDirectory()->getMediaFolder($this->getStorageKey()) : '';
  53. }
  54. /**
  55. * @return MediaCollectionInterface
  56. */
  57. public function getMedia()
  58. {
  59. if ($this->media === null) {
  60. /** @var AbstractMedia $media */
  61. $media = $this->getExistingMedia();
  62. // Include uploaded media to the object media.
  63. /** @var FormFlashFile $upload */
  64. foreach ($this->getUpdatedMedia() as $filename => $upload) {
  65. // Just make sure we do not include removed or moved media.
  66. if ($upload && $upload->getError() === \UPLOAD_ERR_OK && !$upload->isMoved()) {
  67. $media->add($filename, MediumFactory::fromUploadedFile($upload));
  68. }
  69. }
  70. $media->setTimestamps();
  71. }
  72. return $this->media;
  73. }
  74. public function checkUploadedMediaFile(UploadedFileInterface $uploadedFile)
  75. {
  76. $grav = Grav::instance();
  77. $language = $grav['language'];
  78. switch ($uploadedFile->getError()) {
  79. case UPLOAD_ERR_OK:
  80. break;
  81. case UPLOAD_ERR_NO_FILE:
  82. if ($uploadedFile instanceof FormFlashFile) {
  83. break;
  84. }
  85. throw new RuntimeException($language->translate('PLUGIN_ADMIN.NO_FILES_SENT'), 400);
  86. case UPLOAD_ERR_INI_SIZE:
  87. case UPLOAD_ERR_FORM_SIZE:
  88. throw new RuntimeException($language->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 400);
  89. case UPLOAD_ERR_NO_TMP_DIR:
  90. throw new RuntimeException($language->translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 400);
  91. default:
  92. throw new RuntimeException($language->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 400);
  93. }
  94. $filename = $uploadedFile->getClientFilename();
  95. if (!Utils::checkFilename($filename)) {
  96. throw new RuntimeException(sprintf($language->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'), $filename, 'Bad filename'), 400);
  97. }
  98. $grav_limit = Utils::getUploadLimit();
  99. if ($grav_limit > 0 && $uploadedFile->getSize() > $grav_limit) {
  100. throw new RuntimeException($language->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400);
  101. }
  102. $this->checkMediaFilename($filename);
  103. }
  104. public function checkMediaFilename(string $filename)
  105. {
  106. // Check the file extension.
  107. $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
  108. $grav = Grav::instance();
  109. /** @var Config $config */
  110. $config = $grav['config'];
  111. // If not a supported type, return
  112. if (!$extension || !$config->get("media.types.{$extension}")) {
  113. $language = $grav['language'];
  114. throw new RuntimeException($language->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400);
  115. }
  116. }
  117. public function uploadMediaFile(UploadedFileInterface $uploadedFile, string $filename = null): void
  118. {
  119. $this->checkUploadedMediaFile($uploadedFile);
  120. if ($filename) {
  121. $this->checkMediaFilename(basename($filename));
  122. } else {
  123. $filename = $uploadedFile->getClientFilename();
  124. }
  125. $media = $this->getMedia();
  126. $grav = Grav::instance();
  127. /** @var UniformResourceLocator $locator */
  128. $locator = $grav['locator'];
  129. $path = $media->getPath();
  130. if (!$path) {
  131. $language = $grav['language'];
  132. throw new RuntimeException($language->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
  133. }
  134. if ($locator->isStream($path)) {
  135. $path = $locator->findResource($path, true, true);
  136. $locator->clearCache($path);
  137. }
  138. try {
  139. // Upload it
  140. $filepath = sprintf('%s/%s', $path, $filename);
  141. Folder::create(\dirname($filepath));
  142. if ($uploadedFile instanceof FormFlashFile) {
  143. $metadata = $uploadedFile->getMetaData();
  144. if ($metadata) {
  145. $file = YamlFile::instance($filepath . '.meta.yaml');
  146. $file->save(['upload' => $metadata]);
  147. }
  148. if ($uploadedFile->getError() === \UPLOAD_ERR_OK) {
  149. $uploadedFile->moveTo($filepath);
  150. } elseif (!file_exists($filepath) && $pos = strpos($filename, '/')) {
  151. $origpath = sprintf('%s/%s', $path, substr($filename, $pos));
  152. if (file_exists($origpath)) {
  153. copy($origpath, $filepath);
  154. }
  155. }
  156. } else {
  157. $uploadedFile->moveTo($filepath);
  158. }
  159. } catch (\Exception $e) {
  160. $language = $grav['language'];
  161. throw new RuntimeException($language->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
  162. }
  163. $this->clearMediaCache();
  164. }
  165. public function deleteMediaFile(string $filename): void
  166. {
  167. $grav = Grav::instance();
  168. $language = $grav['language'];
  169. $basename = basename($filename);
  170. $dirname = dirname($filename);
  171. $dirname = $dirname === '.' ? '' : '/' . $dirname;
  172. if (!Utils::checkFilename($basename)) {
  173. throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': Bad filename: ' . $filename, 400);
  174. }
  175. $media = $this->getMedia();
  176. $path = $media->getPath();
  177. if (!$path) {
  178. return;
  179. }
  180. /** @var UniformResourceLocator $locator */
  181. $locator = $grav['locator'];
  182. $targetPath = $path . '/' . $dirname;
  183. $targetFile = $path . '/' . $filename;
  184. if ($locator->isStream($targetFile)) {
  185. $targetPath = $locator->findResource($targetPath, true, true);
  186. $targetFile = $locator->findResource($targetFile, true, true);
  187. $locator->clearCache($targetPath);
  188. $locator->clearCache($targetFile);
  189. }
  190. $fileParts = pathinfo($basename);
  191. if (!file_exists($targetPath)) {
  192. return;
  193. }
  194. if (file_exists($targetFile)) {
  195. $result = unlink($targetFile);
  196. if (!$result) {
  197. throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
  198. }
  199. }
  200. // Remove Extra Files
  201. foreach (scandir($targetPath, SCANDIR_SORT_NONE) as $file) {
  202. $preg_name = preg_quote($fileParts['filename'], '`');
  203. $preg_ext =preg_quote($fileParts['extension'], '`');
  204. $preg_filename = preg_quote($basename, '`');
  205. if (preg_match("`({$preg_name}@\d+x\.{$preg_ext}(?:\.meta\.yaml)?$|{$preg_filename}\.meta\.yaml)$`", $file)) {
  206. $testPath = $targetPath . '/' . $file;
  207. if ($locator->isStream($testPath)) {
  208. $testPath = $locator->findResource($testPath, true, true);
  209. $locator->clearCache($testPath);
  210. }
  211. if (is_file($testPath)) {
  212. $result = unlink($testPath);
  213. if (!$result) {
  214. throw new RuntimeException($language->translate('PLUGIN_ADMIN.FILE_COULD_NOT_BE_DELETED') . ': ' . $filename, 500);
  215. }
  216. }
  217. }
  218. }
  219. $this->clearMediaCache();
  220. }
  221. /**
  222. * @param array $files
  223. */
  224. protected function setUpdatedMedia(array $files): void
  225. {
  226. $list = [];
  227. foreach ($files as $field => $group) {
  228. if ($field === '' || \strpos($field, '/', true)) {
  229. continue;
  230. }
  231. foreach ($group as $filename => $file) {
  232. $list[$filename] = $file;
  233. }
  234. }
  235. $this->_uploads = $list;
  236. }
  237. /**
  238. * @return array
  239. */
  240. protected function getUpdatedMedia(): array
  241. {
  242. return $this->_uploads ?? [];
  243. }
  244. protected function saveUpdatedMedia(): void
  245. {
  246. /**
  247. * @var string $filename
  248. * @var UploadedFileInterface $file
  249. */
  250. foreach ($this->getUpdatedMedia() as $filename => $file) {
  251. if ($file) {
  252. $this->uploadMediaFile($file, $filename);
  253. } else {
  254. $this->deleteMediaFile($filename);
  255. }
  256. }
  257. $this->setUpdatedMedia([]);
  258. }
  259. /**
  260. * @param string $uri
  261. * @return Medium|null
  262. */
  263. protected function createMedium($uri)
  264. {
  265. $grav = Grav::instance();
  266. /** @var UniformResourceLocator $locator */
  267. $locator = $grav['locator'];
  268. $file = $uri && $locator->isStream($uri) ? $locator->findResource($uri) : $uri;
  269. return $file && file_exists($file) ? MediumFactory::fromFile($file) : null;
  270. }
  271. /**
  272. * @return Cache
  273. */
  274. protected function getMediaCache()
  275. {
  276. return $this->getCache('object');
  277. }
  278. protected function offsetLoad_media()
  279. {
  280. return $this->getMedia();
  281. }
  282. protected function offsetSerialize_media()
  283. {
  284. return null;
  285. }
  286. abstract public function getFlexDirectory(): FlexDirectory;
  287. abstract public function getStorageKey();
  288. }