499 lines
15 KiB
PHP
499 lines
15 KiB
PHP
|
<?php
|
||
|
|
||
|
declare(strict_types=1);
|
||
|
|
||
|
namespace Grav\Plugin\FlexObjects\Controllers;
|
||
|
|
||
|
use Exception;
|
||
|
use Grav\Common\Page\Interfaces\PageInterface;
|
||
|
use Grav\Common\Page\Medium\Medium;
|
||
|
use Grav\Common\Page\Medium\MediumFactory;
|
||
|
use Grav\Common\Utils;
|
||
|
use Grav\Framework\Flex\FlexObject;
|
||
|
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
|
||
|
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
||
|
use Grav\Framework\Media\Interfaces\MediaInterface;
|
||
|
use LogicException;
|
||
|
use Psr\Http\Message\ResponseInterface;
|
||
|
use Psr\Http\Message\UploadedFileInterface;
|
||
|
use RocketTheme\Toolbox\Event\Event;
|
||
|
use RuntimeException;
|
||
|
use function is_array;
|
||
|
use function is_string;
|
||
|
|
||
|
/**
|
||
|
* Class MediaController
|
||
|
* @package Grav\Plugin\FlexObjects\Controllers
|
||
|
*/
|
||
|
class MediaController extends AbstractController
|
||
|
{
|
||
|
/**
|
||
|
* @return ResponseInterface
|
||
|
*/
|
||
|
public function taskMediaUpload(): ResponseInterface
|
||
|
{
|
||
|
$this->checkAuthorization('media.create');
|
||
|
|
||
|
$object = $this->getObject();
|
||
|
if (null === $object) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
if (!method_exists($object, 'checkUploadedMediaFile')) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
// Get updated object from Form Flash.
|
||
|
$flash = $this->getFormFlash($object);
|
||
|
if ($flash->exists()) {
|
||
|
$object = $flash->getObject() ?? $object;
|
||
|
$object->update([], $flash->getFilesByFields());
|
||
|
}
|
||
|
|
||
|
// Get field for the uploaded media.
|
||
|
$field = $this->getPost('name', 'undefined');
|
||
|
if ($field === 'undefined') {
|
||
|
$field = null;
|
||
|
}
|
||
|
|
||
|
$files = $this->getRequest()->getUploadedFiles();
|
||
|
if ($field && isset($files['data'])) {
|
||
|
$files = $files['data'];
|
||
|
$parts = explode('.', $field);
|
||
|
$last = array_pop($parts);
|
||
|
foreach ($parts as $name) {
|
||
|
if (!is_array($files[$name])) {
|
||
|
throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
|
||
|
}
|
||
|
$files = $files[$name];
|
||
|
}
|
||
|
$file = $files[$last] ?? null;
|
||
|
|
||
|
} else {
|
||
|
// Legacy call with name being the filename instead of field name.
|
||
|
$file = $files['file'] ?? null;
|
||
|
$field = null;
|
||
|
}
|
||
|
|
||
|
/** @var UploadedFileInterface $file */
|
||
|
if (is_array($file)) {
|
||
|
$file = reset($file);
|
||
|
}
|
||
|
|
||
|
if (!$file instanceof UploadedFileInterface) {
|
||
|
throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
|
||
|
}
|
||
|
|
||
|
$filename = $file->getClientFilename();
|
||
|
|
||
|
$object->checkUploadedMediaFile($file, $filename, $field);
|
||
|
|
||
|
try {
|
||
|
$crop = $this->getPost('crop');
|
||
|
if (is_string($crop)) {
|
||
|
$crop = json_decode($crop, true, 512, JSON_THROW_ON_ERROR);
|
||
|
}
|
||
|
|
||
|
$flash->addUploadedFile($file, $field, $crop);
|
||
|
$flash->save();
|
||
|
} catch (Exception $e) {
|
||
|
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
|
||
|
}
|
||
|
|
||
|
// Include exif metadata into the response if configured to do so
|
||
|
$metadata = [];
|
||
|
$include_metadata = $this->grav['config']->get('system.media.auto_metadata_exif', false);
|
||
|
if ($include_metadata) {
|
||
|
$medium = MediumFactory::fromUploadedFile($file);
|
||
|
|
||
|
$media = $object->getMedia();
|
||
|
$media->add($filename, $medium);
|
||
|
|
||
|
$basename = str_replace(['@3x', '@2x'], '', pathinfo($filename, PATHINFO_BASENAME));
|
||
|
if (isset($media[$basename])) {
|
||
|
$metadata = $media[$basename]->metadata() ?: [];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$response = [
|
||
|
'code' => 200,
|
||
|
'status' => 'success',
|
||
|
'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
|
||
|
'filename' => $filename,
|
||
|
'metadata' => $metadata
|
||
|
];
|
||
|
|
||
|
return $this->createJsonResponse($response);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return ResponseInterface
|
||
|
*/
|
||
|
public function taskMediaDelete(): ResponseInterface
|
||
|
{
|
||
|
$this->checkAuthorization('media.delete');
|
||
|
|
||
|
/** @var FlexObjectInterface|null $object */
|
||
|
$object = $this->getObject();
|
||
|
if (!$object) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
$filename = $this->getPost('filename');
|
||
|
|
||
|
// Handle bad filenames.
|
||
|
if (!Utils::checkFilename($filename)) {
|
||
|
throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILE_FOUND'), 400);
|
||
|
}
|
||
|
|
||
|
try {
|
||
|
$field = $this->getPost('name');
|
||
|
$flash = $this->getFormFlash($object);
|
||
|
$flash->removeFile($filename, $field);
|
||
|
$flash->save();
|
||
|
} catch (Exception $e) {
|
||
|
throw new RuntimeException($e->getMessage(), $e->getCode(), $e);
|
||
|
}
|
||
|
|
||
|
$response = [
|
||
|
'code' => 200,
|
||
|
'status' => 'success',
|
||
|
'message' => $this->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . $filename
|
||
|
];
|
||
|
|
||
|
return $this->createJsonResponse($response);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Used in pagemedia field.
|
||
|
*
|
||
|
* @return ResponseInterface
|
||
|
*/
|
||
|
public function taskMediaCopy(): ResponseInterface
|
||
|
{
|
||
|
$this->checkAuthorization('media.create');
|
||
|
|
||
|
/** @var FlexObjectInterface|null $object */
|
||
|
$object = $this->getObject();
|
||
|
if (!$object) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
if (!method_exists($object, 'uploadMediaFile')) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
$request = $this->getRequest();
|
||
|
$files = $request->getUploadedFiles();
|
||
|
|
||
|
$file = $files['file'] ?? null;
|
||
|
if (!$file instanceof UploadedFileInterface) {
|
||
|
throw new RuntimeException($this->translate('PLUGIN_ADMIN.INVALID_PARAMETERS'), 400);
|
||
|
}
|
||
|
|
||
|
$post = $request->getParsedBody();
|
||
|
$filename = $post['name'] ?? $file->getClientFilename();
|
||
|
|
||
|
// Upload media right away.
|
||
|
$object->uploadMediaFile($file, $filename);
|
||
|
|
||
|
// Include exif metadata into the response if configured to do so
|
||
|
$metadata = [];
|
||
|
$include_metadata = $this->grav['config']->get('system.media.auto_metadata_exif', false);
|
||
|
if ($include_metadata) {
|
||
|
$basename = str_replace(['@3x', '@2x'], '', pathinfo($filename, PATHINFO_BASENAME));
|
||
|
$media = $object->getMedia();
|
||
|
if (isset($media[$basename])) {
|
||
|
$metadata = $media[$basename]->metadata() ?: [];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ($object instanceof PageInterface) {
|
||
|
// Backwards compatibility to existing plugins.
|
||
|
// DEPRECATED: page
|
||
|
$this->grav->fireEvent('onAdminAfterAddMedia', new Event(['object' => $object, 'page' => $object]));
|
||
|
}
|
||
|
|
||
|
$response = [
|
||
|
'code' => 200,
|
||
|
'status' => 'success',
|
||
|
'message' => $this->translate('PLUGIN_ADMIN.FILE_UPLOADED_SUCCESSFULLY'),
|
||
|
'filename' => $filename,
|
||
|
'metadata' => $metadata
|
||
|
];
|
||
|
|
||
|
return $this->createJsonResponse($response);
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* Used in pagemedia field.
|
||
|
*
|
||
|
* @return ResponseInterface
|
||
|
*/
|
||
|
public function taskMediaRemove(): ResponseInterface
|
||
|
{
|
||
|
$this->checkAuthorization('media.delete');
|
||
|
|
||
|
/** @var FlexObjectInterface|null $object */
|
||
|
$object = $this->getObject();
|
||
|
if (!$object) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
if (!method_exists($object, 'deleteMediaFile')) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
$filename = $this->getPost('filename');
|
||
|
|
||
|
// Handle bad filenames.
|
||
|
if (!Utils::checkFilename($filename)) {
|
||
|
throw new RuntimeException($this->translate('PLUGIN_ADMIN.NO_FILE_FOUND'), 400);
|
||
|
}
|
||
|
|
||
|
$object->deleteMediaFile($filename);
|
||
|
|
||
|
if ($object instanceof PageInterface) {
|
||
|
// Backwards compatibility to existing plugins.
|
||
|
// DEPRECATED: page
|
||
|
$this->grav->fireEvent('onAdminAfterDelMedia', new Event(['object' => $object, 'page' => $object, 'media' => $object->getMedia(), 'filename' => $filename]));
|
||
|
}
|
||
|
|
||
|
$response = [
|
||
|
'code' => 200,
|
||
|
'status' => 'success',
|
||
|
'message' => $this->translate('PLUGIN_ADMIN.FILE_DELETED') . ': ' . $filename
|
||
|
];
|
||
|
|
||
|
return $this->createJsonResponse($response);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @return ResponseInterface
|
||
|
*/
|
||
|
public function actionMediaList(): ResponseInterface
|
||
|
{
|
||
|
$this->checkAuthorization('media.list');
|
||
|
|
||
|
/** @var MediaInterface|FlexObjectInterface $object */
|
||
|
$object = $this->getObject();
|
||
|
if (!$object) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
// Get updated object from Form Flash.
|
||
|
$flash = $this->getFormFlash($object);
|
||
|
if ($flash->exists()) {
|
||
|
$object = $flash->getObject() ?? $object;
|
||
|
$object->update([], $flash->getFilesByFields());
|
||
|
}
|
||
|
|
||
|
$media = $object->getMedia();
|
||
|
$media_list = [];
|
||
|
|
||
|
/**
|
||
|
* @var string $name
|
||
|
* @var Medium $medium
|
||
|
*/
|
||
|
foreach ($media->all() as $name => $medium) {
|
||
|
$media_list[$name] = [
|
||
|
'url' => $medium->display($medium->get('extension') === 'svg' ? 'source' : 'thumbnail')->cropZoom(400, 300)->url(),
|
||
|
'size' => $medium->get('size'),
|
||
|
'metadata' => $medium->metadata() ?: [],
|
||
|
'original' => $medium->higherQualityAlternative()->get('filename')
|
||
|
];
|
||
|
}
|
||
|
|
||
|
$response = [
|
||
|
'code' => 200,
|
||
|
'status' => 'success',
|
||
|
'results' => $media_list
|
||
|
];
|
||
|
|
||
|
return $this->createJsonResponse($response);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Used by the filepicker field to get a list of files in a folder.
|
||
|
*
|
||
|
* @return ResponseInterface
|
||
|
*/
|
||
|
protected function actionMediaPicker(): ResponseInterface
|
||
|
{
|
||
|
$this->checkAuthorization('media.list');
|
||
|
|
||
|
/** @var FlexObject $object */
|
||
|
$object = $this->getObject();
|
||
|
if (!$object || !\is_callable([$object, 'getFieldSettings'])) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
// Get updated object from Form Flash.
|
||
|
$flash = $this->getFormFlash($object);
|
||
|
if ($flash->exists()) {
|
||
|
$object = $flash->getObject() ?? $object;
|
||
|
$object->update([], $flash->getFilesByFields());
|
||
|
}
|
||
|
|
||
|
$name = $this->getPost('name');
|
||
|
$settings = $name ? $object->getFieldSettings($name) : null;
|
||
|
if (empty($settings['media_picker_field'])) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
$media = $object->getMediaField($name);
|
||
|
|
||
|
$available_files = [];
|
||
|
$metadata = [];
|
||
|
$thumbs = [];
|
||
|
|
||
|
/**
|
||
|
* @var string $name
|
||
|
* @var Medium $medium
|
||
|
*/
|
||
|
foreach ($media->all() as $name => $medium) {
|
||
|
$available_files[] = $name;
|
||
|
|
||
|
if (isset($settings['include_metadata'])) {
|
||
|
$img_metadata = $medium->metadata();
|
||
|
if ($img_metadata) {
|
||
|
$metadata[$name] = $img_metadata;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
// Peak in the flashObject for optimistic filepicker updates
|
||
|
$pending_files = [];
|
||
|
$sessionField = base64_encode($this->grav['uri']->url());
|
||
|
$flash = $this->getSession()->getFlashObject('files-upload');
|
||
|
$folder = $media->getPath() ?: null;
|
||
|
|
||
|
if ($flash && isset($flash[$sessionField])) {
|
||
|
foreach ($flash[$sessionField] as $field => $data) {
|
||
|
foreach ($data as $file) {
|
||
|
$test = \dirname($file['path']);
|
||
|
if ($test === $folder) {
|
||
|
$pending_files[] = $file['name'];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$this->getSession()->setFlashObject('files-upload', $flash);
|
||
|
|
||
|
// Handle Accepted file types
|
||
|
// Accept can only be file extensions (.pdf|.jpg)
|
||
|
if (isset($settings['accept'])) {
|
||
|
$available_files = array_filter($available_files, function ($file) use ($settings) {
|
||
|
return $this->filterAcceptedFiles($file, $settings);
|
||
|
});
|
||
|
|
||
|
$pending_files = array_filter($pending_files, function ($file) use ($settings) {
|
||
|
return $this->filterAcceptedFiles($file, $settings);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (isset($settings['deny'])) {
|
||
|
$available_files = array_filter($available_files, function ($file) use ($settings) {
|
||
|
return $this->filterDeniedFiles($file, $settings);
|
||
|
});
|
||
|
|
||
|
$pending_files = array_filter($pending_files, function ($file) use ($settings) {
|
||
|
return $this->filterDeniedFiles($file, $settings);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Generate thumbs if needed
|
||
|
if (isset($settings['preview_images']) && $settings['preview_images'] === true) {
|
||
|
foreach ($available_files as $filename) {
|
||
|
$thumbs[$filename] = $media[$filename]->zoomCrop(100,100)->url();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$response = [
|
||
|
'code' => 200,
|
||
|
'status' => 'success',
|
||
|
'files' => array_values($available_files),
|
||
|
'pending' => array_values($pending_files),
|
||
|
'folder' => $folder,
|
||
|
'metadata' => $metadata,
|
||
|
'thumbs' => $thumbs
|
||
|
];
|
||
|
|
||
|
return $this->createJsonResponse($response);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $file
|
||
|
* @param array $settings
|
||
|
* @return false|int
|
||
|
*/
|
||
|
protected function filterAcceptedFiles(string $file, array $settings)
|
||
|
{
|
||
|
$valid = false;
|
||
|
|
||
|
foreach ((array)$settings['accept'] as $type) {
|
||
|
$find = str_replace('*', '.*', $type);
|
||
|
$valid |= preg_match('#' . $find . '$#i', $file);
|
||
|
}
|
||
|
|
||
|
return $valid;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $file
|
||
|
* @param array $settings
|
||
|
* @return false|int
|
||
|
*/
|
||
|
protected function filterDeniedFiles(string $file, array $settings)
|
||
|
{
|
||
|
$valid = true;
|
||
|
|
||
|
foreach ((array)$settings['deny'] as $type) {
|
||
|
$find = str_replace('*', '.*', $type);
|
||
|
$valid = !preg_match('#' . $find . '$#i', $file);
|
||
|
}
|
||
|
|
||
|
return $valid;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @param string $action
|
||
|
* @return void
|
||
|
* @throws LogicException
|
||
|
* @throws RuntimeException
|
||
|
*/
|
||
|
protected function checkAuthorization(string $action): void
|
||
|
{
|
||
|
$object = $this->getObject();
|
||
|
if (!$object) {
|
||
|
throw new RuntimeException('Not Found', 404);
|
||
|
}
|
||
|
|
||
|
// If object does not have ACL support ignore ACL checks.
|
||
|
if (!$object instanceof FlexAuthorizeInterface) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
switch ($action) {
|
||
|
case 'media.list':
|
||
|
$action = 'read';
|
||
|
break;
|
||
|
|
||
|
case 'media.create':
|
||
|
case 'media.delete':
|
||
|
$action = $object->exists() ? 'update' : 'create';
|
||
|
break;
|
||
|
|
||
|
default:
|
||
|
throw new LogicException(sprintf('Unsupported authorize action %s', $action), 500);
|
||
|
}
|
||
|
|
||
|
if (!$object->isAuthorized($action, null, $this->user)) {
|
||
|
throw new RuntimeException('Forbidden', 403);
|
||
|
}
|
||
|
}
|
||
|
}
|