Add missing functionality to MediaUploadTrait

This commit is contained in:
Matias Griese
2020-06-04 12:47:48 +03:00
parent 62e863dec0
commit 8e9125772e
3 changed files with 194 additions and 74 deletions

View File

@@ -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);
}

View File

@@ -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
*/

View File

@@ -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