mirror of
https://github.com/getgrav/grav.git
synced 2026-02-23 15:11:27 +01:00
Add missing functionality to MediaUploadTrait
This commit is contained in:
@@ -662,8 +662,10 @@ class UserObject extends FlexObject implements UserInterface, MediaManipulationI
|
||||
*/
|
||||
protected function setUpdatedMedia(array $files): void
|
||||
{
|
||||
// For shared media folder we need to keep path for backwards compatibility.
|
||||
$folder = $this->getMediaFolder();
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
|
||||
$media = $this->getMedia();
|
||||
|
||||
$list = [];
|
||||
$list_original = [];
|
||||
@@ -679,32 +681,35 @@ class UserObject extends FlexObject implements UserInterface, MediaManipulationI
|
||||
$data = null;
|
||||
}
|
||||
|
||||
$settings = $this->getBlueprint()->schema()->getProperty($field);
|
||||
// Load configuration for the field.
|
||||
$schema = $this->getBlueprint()->schema();
|
||||
$settings = $schema ? (array)$schema->getProperty($field) : [];
|
||||
|
||||
// Generate random name if required
|
||||
if ($settings['random_name'] ?? false) {
|
||||
$extension = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
$data['name'] = $filename = Utils::generateRandomString(15) . '.' . $extension;
|
||||
// Set destination folder.
|
||||
$self = false;
|
||||
if (empty($settings['destination']) || in_array($settings['destination'], ['@self', 'self@', '@self@'], true)) {
|
||||
$settings['destination'] = $this->getMediaFolder();
|
||||
$self = true;
|
||||
}
|
||||
$folder = $settings['destination'];
|
||||
|
||||
if ($this->_loadMedia) {
|
||||
// Check file upload against media limits.
|
||||
$filename = $media->checkUploadedFile($file, $filename, $settings);
|
||||
|
||||
if ($this->_loadMedia && $self) {
|
||||
$filepath = $filename;
|
||||
} else {
|
||||
if (!$folder) {
|
||||
throw new \RuntimeException('No media folder support');
|
||||
}
|
||||
$filepath = "{$folder}/{$filename}";
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = Grav::instance()['locator'];
|
||||
$filepath = $locator->findResource($folder, false, true) . '/' . $filename;
|
||||
if ($data) {
|
||||
$data['path'] = $filepath;
|
||||
// For backwards compatibility we are always using relative path from the installation root.
|
||||
if ($locator->isStream($folder)) {
|
||||
$filepath = $locator->findResource($filepath, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for original images.
|
||||
if (strpos($field, '/original')) {
|
||||
if ($this->_loadMedia) {
|
||||
if ($this->_loadMedia && $self) {
|
||||
$list_original[$filename] = $file;
|
||||
}
|
||||
continue;
|
||||
@@ -712,7 +717,10 @@ class UserObject extends FlexObject implements UserInterface, MediaManipulationI
|
||||
|
||||
$list[$filename] = $file;
|
||||
|
||||
if ($data) {
|
||||
if (null !== $data) {
|
||||
$data['name'] = $filename;
|
||||
$data['path'] = $filepath;
|
||||
|
||||
$this->setNestedProperty("{$field}\n{$filepath}", $data, "\n");
|
||||
} else {
|
||||
$this->unsetNestedProperty("{$field}\n{$filepath}", "\n");
|
||||
@@ -736,7 +744,7 @@ class UserObject extends FlexObject implements UserInterface, MediaManipulationI
|
||||
foreach ($this->_uploads_original ?? [] as $name => $file) {
|
||||
$name = 'original/' . $name;
|
||||
if ($file) {
|
||||
$media->uploadFile($file, $name);
|
||||
$media->copyUploadedFile($file, $name);
|
||||
} else {
|
||||
$media->deleteFile($name);
|
||||
}
|
||||
@@ -748,7 +756,7 @@ class UserObject extends FlexObject implements UserInterface, MediaManipulationI
|
||||
*/
|
||||
foreach ($this->getUpdatedMedia() as $filename => $file) {
|
||||
if ($file) {
|
||||
$media->uploadFile($file, $filename);
|
||||
$media->copyUploadedFile($file, $filename);
|
||||
} else {
|
||||
$media->deleteFile($filename);
|
||||
}
|
||||
|
||||
@@ -18,23 +18,43 @@ use RuntimeException;
|
||||
interface MediaUploadInterface
|
||||
{
|
||||
/**
|
||||
* Checks that uploaded file meets the requirements. Returns new filename.
|
||||
*
|
||||
* @example
|
||||
* $filename = null; // Override filename if needed (ignored if randomizing filenames).
|
||||
* $settings = ['destination' => 'user://pages/media']; // Settings from the form field.
|
||||
* $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
|
||||
* $media->copyUploadedFile($uploadedFile, $filename);
|
||||
|
||||
* @param UploadedFileInterface $uploadedFile
|
||||
* @param string|null $filename
|
||||
* @param array|null $settings
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function checkUploadedFile(UploadedFileInterface $uploadedFile, string $filename = null): string;
|
||||
public function checkUploadedFile(UploadedFileInterface $uploadedFile, string $filename = null, array $settings = null): string;
|
||||
|
||||
/**
|
||||
* Upload file to the media collection.
|
||||
* Copy uploaded file to the media collection.
|
||||
*
|
||||
* WARNING: Always check uploaded file before copying it!
|
||||
*
|
||||
* @example
|
||||
* $filename = null; // Override filename if needed (ignored if randomizing filenames).
|
||||
* $settings = ['destination' => 'user://pages/media']; // Settings from the form field.
|
||||
* $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
|
||||
* $media->copyUploadedFile($uploadedFile, $filename);
|
||||
*
|
||||
* @param UploadedFileInterface $uploadedFile
|
||||
* @param string|null $filename
|
||||
* @param string $filename
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function uploadFile(UploadedFileInterface $uploadedFile, string $filename = null): void;
|
||||
public function copyUploadedFile(UploadedFileInterface $uploadedFile, string $filename): void;
|
||||
|
||||
/**
|
||||
* Delete real file from the media collection.
|
||||
*
|
||||
* @param string $filename
|
||||
* @return void
|
||||
*/
|
||||
|
||||
@@ -13,6 +13,7 @@ use Grav\Common\Config\Config;
|
||||
use Grav\Common\Filesystem\Folder;
|
||||
use Grav\Common\Grav;
|
||||
use Grav\Common\Language\Language;
|
||||
use Grav\Common\Security;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Filesystem\Filesystem;
|
||||
use Grav\Framework\Form\FormFlashFile;
|
||||
@@ -26,35 +27,84 @@ use RuntimeException;
|
||||
*/
|
||||
trait MediaUploadTrait
|
||||
{
|
||||
/** @var array */
|
||||
private $_upload_defaults = [
|
||||
'avoid_overwriting' => false, // Do not override existing files (adds datetime postfix if conflict).
|
||||
'random_name' => false, // True if name needs to be randomized.
|
||||
'accept' => ['image/*'], // Accepted mime types or file extensions.
|
||||
'limit' => 10, // Maximum number of files.
|
||||
'filesize' => null, // Maximum filesize in MB.
|
||||
'destination' => null // Destination path, if empty, exception is thrown.
|
||||
];
|
||||
|
||||
/**
|
||||
* Checks that uploaded file meets the requirements. Returns new filename.
|
||||
*
|
||||
* @example
|
||||
* $filename = null; // Override filename if needed (ignored if randomizing filenames).
|
||||
* $settings = ['destination' => 'user://pages/media']; // Settings from the form field.
|
||||
* $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
|
||||
* $media->copyUploadedFile($uploadedFile, $filename);
|
||||
*
|
||||
* @param UploadedFileInterface $uploadedFile
|
||||
* @param string|null $filename
|
||||
* @param array|null $settings
|
||||
* @return string
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function checkUploadedFile(UploadedFileInterface $uploadedFile, string $filename = null): string
|
||||
public function checkUploadedFile(UploadedFileInterface $uploadedFile, string $filename = null, array $settings = null): string
|
||||
{
|
||||
// Add the defaults to the settings.
|
||||
if (!$settings) {
|
||||
$settings = $this->_upload_defaults;
|
||||
} else {
|
||||
$settings += $this->_upload_defaults;
|
||||
}
|
||||
|
||||
// Destination is always needed (but it can be set in defaults).
|
||||
if (!isset($settings['destination'])) {
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.DESTINATION_NOT_SPECIFIED'), 400);
|
||||
}
|
||||
|
||||
// Check if there is an upload error.
|
||||
switch ($uploadedFile->getError()) {
|
||||
case UPLOAD_ERR_OK:
|
||||
break;
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 400);
|
||||
case UPLOAD_ERR_PARTIAL:
|
||||
case UPLOAD_ERR_NO_FILE:
|
||||
if (!$uploadedFile instanceof FormFlashFile) {
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILES_SENT'), 400);
|
||||
}
|
||||
break;
|
||||
case UPLOAD_ERR_INI_SIZE:
|
||||
case UPLOAD_ERR_FORM_SIZE:
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 400);
|
||||
case UPLOAD_ERR_NO_TMP_DIR:
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 400);
|
||||
case UPLOAD_ERR_CANT_WRITE:
|
||||
case UPLOAD_ERR_EXTENSION:
|
||||
default:
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 400);
|
||||
}
|
||||
|
||||
if (null === $filename) {
|
||||
// Decide which filename to use.
|
||||
if ($settings['random_name']) {
|
||||
// Generate random filename if asked for.
|
||||
$extension = pathinfo($filename, PATHINFO_EXTENSION);
|
||||
$filename = Utils::generateRandomString(15) . '.' . $extension;
|
||||
} elseif (null === $filename) {
|
||||
// If no filename is given, use the filename from the uploaded file.
|
||||
$filename = $uploadedFile->getClientFilename() ?? '';
|
||||
}
|
||||
|
||||
// Handle conflicting filename if needed.
|
||||
if ($settings['avoid_overwriting']) {
|
||||
$destination = $settings['destination'];
|
||||
if (file_exists("{$destination}/{$filename}")) {
|
||||
$filename = date('YmdHis') . '-' . $filename;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the filename is allowed.
|
||||
if (!Utils::checkFilename($filename)) {
|
||||
throw new RuntimeException(
|
||||
@@ -63,41 +113,101 @@ trait MediaUploadTrait
|
||||
);
|
||||
}
|
||||
|
||||
// Check size against the upload limit.
|
||||
// Check if the file extension is allowed.
|
||||
$extension = mb_strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
if (!$extension || !$this->getConfig()->get("media.types.{$extension}")) {
|
||||
// Not a supported type.
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400);
|
||||
}
|
||||
|
||||
// Calculate maximum file size (from MB).
|
||||
if ($settings['filesize']) {
|
||||
$max_filesize = $settings['filesize'] * 1048576;
|
||||
if ($uploadedFile->getSize() > $max_filesize) {
|
||||
// TODO: use own language string
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Check size against the Grav upload limit.
|
||||
$grav_limit = Utils::getUploadLimit();
|
||||
if ($grav_limit > 0 && $uploadedFile->getSize() > $grav_limit) {
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.EXCEEDED_GRAV_FILESIZE_LIMIT'), 400);
|
||||
}
|
||||
|
||||
$this->checkFileExtension($filename);
|
||||
// Handle Accepted file types. Accept can only be mime types (image/png | image/*) or file extensions (.pdf | .jpg)
|
||||
$accepted = false;
|
||||
$errors = [];
|
||||
// Do not trust mime type sent by the browser.
|
||||
$mime = Utils::getMimeByFilename($filename);
|
||||
foreach ((array)$settings['accept'] as $type) {
|
||||
// Force acceptance of any file when star notation
|
||||
if ($type === '*') {
|
||||
$accepted = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$isMime = strstr($type, '/');
|
||||
$find = str_replace(['.', '*', '+'], ['\.', '.*', '\+'], $type);
|
||||
|
||||
if ($isMime) {
|
||||
$match = preg_match('#' . $find . '$#', $mime);
|
||||
if (!$match) {
|
||||
// TODO: translate
|
||||
$errors[] = 'The MIME type "' . $mime . '" for the file "' . $filename . '" is not an accepted.';
|
||||
} else {
|
||||
$accepted = true;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$match = preg_match('#' . $find . '$#', $filename);
|
||||
if (!$match) {
|
||||
// TODO: translate
|
||||
$errors[] = 'The File Extension for the file "' . $filename . '" is not an accepted.';
|
||||
} else {
|
||||
$accepted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$accepted) {
|
||||
throw new RuntimeException(implode('<br />', $errors), 400);
|
||||
}
|
||||
|
||||
return $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file to the media collection.
|
||||
* Copy uploaded file to the media collection.
|
||||
*
|
||||
* WARNING: Always check uploaded file before copying it!
|
||||
*
|
||||
* @example
|
||||
* $filename = null; // Override filename if needed (ignored if randomizing filenames).
|
||||
* $settings = ['destination' => 'user://pages/media']; // Settings from the form field.
|
||||
* $filename = $media->checkUploadedFile($uploadedFile, $filename, $settings);
|
||||
* $media->copyUploadedFile($uploadedFile, $filename);
|
||||
*
|
||||
* @param UploadedFileInterface $uploadedFile
|
||||
* @param string|null $filename
|
||||
* @param string $filename
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
public function uploadFile(UploadedFileInterface $uploadedFile, string $filename = null): void
|
||||
public function copyUploadedFile(UploadedFileInterface $uploadedFile, string $filename): void
|
||||
{
|
||||
// First check if the file is a valid upload (throws error if not).
|
||||
$filename = $this->checkUploadedFile($uploadedFile, $filename);
|
||||
|
||||
$path = $this->getPath();
|
||||
if (!$path) {
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
|
||||
}
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $this->getGrav()['locator'];
|
||||
|
||||
try {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $this->getGrav()['locator'];
|
||||
// Do not use streams internally.
|
||||
if ($locator->isStream($path)) {
|
||||
$path = (string)$locator->findResource($path, true, true);
|
||||
$locator->clearCache($path);
|
||||
$locator->clearCache();
|
||||
}
|
||||
|
||||
$filepath = sprintf('%s/%s', $path, $filename);
|
||||
@@ -127,15 +237,24 @@ trait MediaUploadTrait
|
||||
} else {
|
||||
$uploadedFile->moveTo($filepath);
|
||||
}
|
||||
|
||||
// Special content sanitization for SVG.
|
||||
$mime = Utils::getMimeByFilename($filename);
|
||||
if (Utils::contains($mime, 'svg', false)) {
|
||||
Security::sanitizeSVG($filepath);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.FAILED_TO_MOVE_UPLOADED_FILE'), 400);
|
||||
}
|
||||
|
||||
// Finally clear media cache.
|
||||
$locator->clearCache();
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete real file from the media collection.
|
||||
*
|
||||
* @param string $filename
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
@@ -164,19 +283,21 @@ trait MediaUploadTrait
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
|
||||
if ($locator->isStream($targetFile)) {
|
||||
$targetPath = (string)$locator->findResource($targetPath, true, true);
|
||||
$targetFile = (string)$locator->findResource($targetFile, true, true);
|
||||
$locator->clearCache($targetPath);
|
||||
$locator->clearCache($targetFile);
|
||||
$locator->clearCache();
|
||||
}
|
||||
|
||||
$fileParts = (array)$filesystem->pathinfo($basename);
|
||||
|
||||
// If path doesn't exist, there's nothing to do.
|
||||
if (!file_exists($targetPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove media file.
|
||||
if (file_exists($targetFile)) {
|
||||
$result = unlink($targetFile);
|
||||
if (!$result) {
|
||||
@@ -184,12 +305,11 @@ trait MediaUploadTrait
|
||||
}
|
||||
}
|
||||
|
||||
// Remove associated .meta.yaml files.
|
||||
$dir = scandir($targetPath, SCANDIR_SORT_NONE);
|
||||
if (false === $dir) {
|
||||
throw new RuntimeException('Internal error');
|
||||
throw new RuntimeException('Internal error (M102)');
|
||||
}
|
||||
|
||||
// Remove associated .meta.yaml files.
|
||||
foreach ($dir as $file) {
|
||||
$preg_name = preg_quote($fileParts['filename'], '`');
|
||||
$preg_ext = preg_quote($fileParts['extension'] ?? '.', '`');
|
||||
@@ -211,38 +331,10 @@ trait MediaUploadTrait
|
||||
}
|
||||
}
|
||||
|
||||
// Finally clear media cache.
|
||||
$this->clearCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the file extension.
|
||||
*
|
||||
* @param string $filename
|
||||
* @return void
|
||||
* @throws RuntimeException
|
||||
*/
|
||||
protected function checkFileExtension(string $filename)
|
||||
{
|
||||
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
if (!$extension || !$this->getConfig()->get("media.types.{$extension}")) {
|
||||
// Not a supported type.
|
||||
throw new RuntimeException($this->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension, 400);
|
||||
}
|
||||
}
|
||||
|
||||
protected function clearMediaCache(): void
|
||||
{
|
||||
$grav = $this->getGrav();
|
||||
$path = $this->getPath();
|
||||
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $grav['locator'];
|
||||
if ($path && $locator->isStream($path)) {
|
||||
$path = (string)$locator->findResource($path, true, true);
|
||||
$locator->clearCache($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $string
|
||||
* @return string
|
||||
|
||||
Reference in New Issue
Block a user