From e385a9ac29707b33b1b483c10aab77e5c424b1c7 Mon Sep 17 00:00:00 2001 From: Diogo Peralta Cordeiro Date: Sun, 18 Apr 2021 05:47:16 +0100 Subject: [PATCH] [ATTACHMENTS] Even more further refactoring Introduce Encoder plugins Instead of abstract upload and thumb modules Ported attachment routes. In v3 thumbnail depends on existing attachment so route updated accordingly. --- .../Controller/ImageThumbnail.php | 2 +- .../ImageEncoder.php} | 58 +- .../composer.json | 0 .../Media/Controller/AttachmentDownload.php | 58 -- .../Media/Controller/AttachmentThumbnail.php | 80 -- plugins/Media/Controller/AttachmentView.php | 55 -- plugins/Media/Media.php | 48 - plugins/Media/Util/ImageFile.php | 685 ------------- plugins/Media/Util/MediaFile.php | 934 ------------------ plugins/Media/media/Attachment.php | 66 -- plugins/Media/media/AttachmentList.php | 131 --- plugins/Media/media/AttachmentListItem.php | 28 +- plugins/Media/media/InlineAttachmentList.php | 53 - .../Media/media/InlineAttachmentListItem.php | 64 -- .../README.md | 0 .../VideoEncoder.php} | 11 +- .../locale/FFmpeg.pot | 0 .../locale/en_GB/LC_MESSAGES/FFmpeg.po | 0 .../locale/pt/LC_MESSAGES/FFmpeg.po | 0 src/Controller/Attachment.php | 18 +- src/Core/GSFile.php | 24 +- src/Core/Modules/AttachmentThumbnail.php | 9 - src/Core/Modules/AttachmentValidate.php | 10 - src/Routes/Main.php | 5 +- templates/note/view.html.twig | 10 +- 25 files changed, 113 insertions(+), 2236 deletions(-) rename plugins/{ImageThumbnail => ImageEncoder}/Controller/ImageThumbnail.php (98%) rename plugins/{ImageThumbnail/ImageThumbnail.php => ImageEncoder/ImageEncoder.php} (60%) rename plugins/{ImageThumbnail => ImageEncoder}/composer.json (100%) delete mode 100644 plugins/Media/Controller/AttachmentDownload.php delete mode 100644 plugins/Media/Controller/AttachmentThumbnail.php delete mode 100644 plugins/Media/Controller/AttachmentView.php delete mode 100644 plugins/Media/Media.php delete mode 100644 plugins/Media/Util/ImageFile.php delete mode 100644 plugins/Media/Util/MediaFile.php delete mode 100644 plugins/Media/media/Attachment.php delete mode 100644 plugins/Media/media/AttachmentList.php delete mode 100644 plugins/Media/media/InlineAttachmentList.php delete mode 100644 plugins/Media/media/InlineAttachmentListItem.php rename plugins/{VideoThumbnail => VideoEncoder}/README.md (100%) rename plugins/{VideoThumbnail/VideoThumbnail.php => VideoEncoder/VideoEncoder.php} (95%) rename plugins/{VideoThumbnail => VideoEncoder}/locale/FFmpeg.pot (100%) rename plugins/{VideoThumbnail => VideoEncoder}/locale/en_GB/LC_MESSAGES/FFmpeg.po (100%) rename plugins/{VideoThumbnail => VideoEncoder}/locale/pt/LC_MESSAGES/FFmpeg.po (100%) delete mode 100644 src/Core/Modules/AttachmentThumbnail.php delete mode 100644 src/Core/Modules/AttachmentValidate.php diff --git a/plugins/ImageThumbnail/Controller/ImageThumbnail.php b/plugins/ImageEncoder/Controller/ImageThumbnail.php similarity index 98% rename from plugins/ImageThumbnail/Controller/ImageThumbnail.php rename to plugins/ImageEncoder/Controller/ImageThumbnail.php index ae656fb25d..53e0716313 100644 --- a/plugins/ImageThumbnail/Controller/ImageThumbnail.php +++ b/plugins/ImageEncoder/Controller/ImageThumbnail.php @@ -19,7 +19,7 @@ // }}} -namespace Plugin\ImageThumbnail\Controller; +namespace Plugin\ImageEncoder\Controller; use App\Core\Controller; use App\Core\DB\DB; diff --git a/plugins/ImageThumbnail/ImageThumbnail.php b/plugins/ImageEncoder/ImageEncoder.php similarity index 60% rename from plugins/ImageThumbnail/ImageThumbnail.php rename to plugins/ImageEncoder/ImageEncoder.php index 352dce86f5..906ad86e65 100644 --- a/plugins/ImageThumbnail/ImageThumbnail.php +++ b/plugins/ImageEncoder/ImageEncoder.php @@ -17,37 +17,75 @@ // along with GNU social. If not, see . // }}} -namespace Plugin\ImageThumbnail; +namespace Plugin\ImageEncoder; use App\Core\Event; use function App\Core\I18n\_m; -use App\Core\Modules\Module; -use App\Core\Router\RouteLoader; +use App\Core\Log; +use App\Core\Modules\Plugin; use App\Entity\Attachment; use App\Entity\AttachmentThumbnail; use App\Util\Common; +use Exception; use Jcupitt\Vips; +use Symfony\Component\HttpFoundation\File\File as SymfonyFile; -class ImageThumbnail extends Module +class ImageEncoder extends Plugin { - public function onAddRoute(RouteLoader $r) + /** + * Several obscure file types should be normalized to WebP on resize. + * + * Keeps only GIF (if animated) and WebP formats + * + * @return int + */ + public function preferredType(): int { - $r->connect('thumbnail', '/thumbnail/{id<\d+>}', [Controller\ImageThumbnail::class, 'thumbnail']); - return Event::next; + if ($this->type == IMAGETYPE_GIF && $this->animated) { + return $this->type; + } + + return IMAGETYPE_WEBP; } /** - * Resizes an image. It will reencode the image in the - * `self::prefferedType()` format. This only applies henceforward, + * Encodes the image to self::preferredType() format ensuring it's valid. + * + * @param SymfonyFile $sfile i/o + * @param null|string $mimetype out + * + * @return bool + */ + public function onAttachmentValidation(SymfonyFile &$sfile, ?string &$mimetype = null): bool + { + $original_mimetype = $mimetype ?? $sfile->getMimeType(); + // TODO: Encode in place + $mimetype = self::preferredType(); + return Event::stop; + } + + /** + * Resizes an image. It will encode the image in the + * `self::preferredType()` format. This only applies henceforward, * not retroactively * * Increases the 'memory_limit' to the one in the 'attachments' section in the config, to * enable the handling of bigger images, which can cause a peak of memory consumption, while * encoding * + * @param Attachment $attachment + * @param AttachmentThumbnail $thumbnail + * @param int $width + * @param int $height + * @param bool $crop + * * @throws Exception + * @throws Vips\Exception + * + * @return bool + * */ - public function onResizeImage(Attachment $attachment, AttachmentThumbnail $thumbnail, int $width, int $height, bool $crop) + public function onResizeImage(Attachment $attachment, AttachmentThumbnail $thumbnail, int $width, int $height, bool $crop): bool { $old_limit = ini_set('memory_limit', Common::config('attachments', 'memory_limit')); diff --git a/plugins/ImageThumbnail/composer.json b/plugins/ImageEncoder/composer.json similarity index 100% rename from plugins/ImageThumbnail/composer.json rename to plugins/ImageEncoder/composer.json diff --git a/plugins/Media/Controller/AttachmentDownload.php b/plugins/Media/Controller/AttachmentDownload.php deleted file mode 100644 index a993651c6a..0000000000 --- a/plugins/Media/Controller/AttachmentDownload.php +++ /dev/null @@ -1,58 +0,0 @@ -. - -namespace Plugin\Media\Controller; - -/** - * Download notice attachment - * - * @category Personal - * @package GNUsocial - * - * @author Mikael Nordfeldth - * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * - * @see https:/gnu.io/social - */ -class AttachmentDownload extends Attachment -{ - public function showPage(): void - { - // Disable errors, to not mess with the file contents (suppress errors in case access to this - // function is blocked, like in some shared hosts). Automatically reset at the end of the - // script execution, and we don't want to have any more errors until then, so don't reset it - @ini_set('display_errors', 0); - - if ($this->attachment->isLocal()) { - try { - $this->filepath = $this->attachment->getFileOrThumbnailPath(); - } catch (Exception $e) { - $this->clientError( - _m('Requested local URL for a file that is not stored locally.'), - 404 - ); - } - common_send_file( - $this->filepath, - $this->mimetype, - $this->filename, - 'attachment' - ); - } else { - common_redirect($this->attachment->getUrl(), 303); - } - } -} diff --git a/plugins/Media/Controller/AttachmentThumbnail.php b/plugins/Media/Controller/AttachmentThumbnail.php deleted file mode 100644 index 0fe5014e84..0000000000 --- a/plugins/Media/Controller/AttachmentThumbnail.php +++ /dev/null @@ -1,80 +0,0 @@ -. - -namespace Plugin\Media\Controller; - -/** - * Show notice attachments - * - * @category Personal - * @package GNUsocial - * - * @author Evan Prodromou - * @copyright 2008-2009 StatusNet, Inc. - * @license https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later - */ -class AttachmentThumbnail extends AttachmentView -{ - protected $thumb_w; // max width - protected $thumb_h; // max height - protected $thumb_c; // crop? - - protected function doPreparation() - { - parent::doPreparation(); - - $this->thumb_w = $this->int('w'); - $this->thumb_h = $this->int('h'); - $this->thumb_c = $this->boolean('c'); - } - - /** - * Show an inline representation of an attachment of the size - * requested in the GET variables (read in the constructor). Tries - * to send the most appropriate file with the correct size and - * headers or displays an error if it's not possible. - * - * @throws ClientException - * @throws ReflectionException - * @throws ServerException - */ - public function showPage(): void - { - // Returns a File_thumbnail object or throws exception if not available - $filename = $this->filename; - $filepath = $this->filepath; - try { - $thumbnail = $this->attachment->getThumbnail($this->thumb_w, $this->thumb_h, $this->thumb_c); - $file = $thumbnail->getFile(); - } catch (UseFileAsThumbnailException $e) { - // With this exception, the file exists locally $e->file; - } catch (FileNotFoundException $e) { - $this->clientError(_m('No such attachment'), 404); - } catch (Exception $e) { - if (is_null($filepath)) { - $this->clientError(_m('No such thumbnail'), 404); - } - // Remote file - } - - // Disable errors, to not mess with the file contents (suppress errors in case access to this - // function is blocked, like in some shared hosts). Automatically reset at the end of the - // script execution, and we don't want to have any more errors until then, so don't reset it - @ini_set('display_errors', 0); - - common_send_file($filepath, image_type_to_mime_type(IMAGETYPE_WEBP), $filename, 'inline'); - } -} diff --git a/plugins/Media/Controller/AttachmentView.php b/plugins/Media/Controller/AttachmentView.php deleted file mode 100644 index 0d9c12ca99..0000000000 --- a/plugins/Media/Controller/AttachmentView.php +++ /dev/null @@ -1,55 +0,0 @@ -. - -namespace Plugin\Media\Controller; - -/** - * View notice attachment - * - * @package GNUsocial - * - * @author Miguel Dantas - * @license https://www.gnu.org/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - */ -class AttachmentView extends Attachment -{ - /** Placeholder */ - public function showPage(): void - { - // Disable errors, to not mess with the file contents (suppress errors in case access to this - // function is blocked, like in some shared hosts). Automatically reset at the end of the - // script execution, and we don't want to have any more errors until then, so don't reset it - @ini_set('display_errors', 0); - - if ($this->attachment->isLocal() || $this->attachment->isFetchedRemoteFile()) { - try { - $this->filepath = $this->attachment->getFileOrThumbnailPath(); - } catch (Exception $e) { - $this->clientError( - _m('Requested local URL for a file that is not stored locally.'), - 404 - ); - } - $disposition = 'attachment'; - if (in_array(common_get_mime_media($this->mimetype), ['image', 'video'])) { - $disposition = 'inline'; - } - common_send_file($this->filepath, $this->mimetype, $this->filename, $disposition); - } else { - common_redirect($this->attachment->getUrl(), 303); - } - } -} diff --git a/plugins/Media/Media.php b/plugins/Media/Media.php deleted file mode 100644 index ce9385ff2b..0000000000 --- a/plugins/Media/Media.php +++ /dev/null @@ -1,48 +0,0 @@ -. -// }}} - -namespace Plugin\Media; - -use App\Core\Event; -use App\Core\Modules\Module; -use App\Core\Router\RouteLoader; - -class Media extends Module -{ - /** - * Map URLs to Controllers - */ - public function onAddRoute(RouteLoader $r) - { - // foreach (['' => 'attachment', - // '/view' => 'attachment_view', - // '/download' => 'attachment_download', - // '/thumbnail' => 'attachment_thumbnail'] as $postfix => $action) { - // foreach (['filehash' => '[A-Za-z0-9._-]{64}', - // 'attachment' => '[0-9]+'] as $type => $match) { - // $r->connect($action, "attachment/:{$type}{$postfix}", - // ['action' => $action], - // [$type => $match]); - // } - // } - $r->connect('attachment', '/attachment/{filehash<[A-Za-z0-9._-]{64}>}', Controller\Attachment::class); - - return Event::next; - } -} diff --git a/plugins/Media/Util/ImageFile.php b/plugins/Media/Util/ImageFile.php deleted file mode 100644 index ba3ea49a07..0000000000 --- a/plugins/Media/Util/ImageFile.php +++ /dev/null @@ -1,685 +0,0 @@ -. - -/** - * Abstraction for an image file - * - * @category Image - * @package GNUsocial - * - * @author Evan Prodromou - * @author Zach Copley - * @author Mikael Nordfeldth - * @author Miguel Dantas - * @author Diogo Cordeiro - * @copyright 2008, 2019-2020 Free Software Foundation http://fsf.org - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - */ - -namespace Plugin\Media\Util; - -use Intervention\Image\ImageManagerStatic as Image; - -/** - * A wrapper on uploaded images - * - * Makes it slightly easier to accept an image file from upload. - * - * @category Image - * @package GNUsocial - * - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * @author Evan Prodromou - * @author Zach Copley - * - * @see https://www.gnu.org/software/social/ - */ -class ImageFile extends MediaFile -{ - public $type; - public $height; - public $width; - public $rotate = 0; // degrees to rotate for properly oriented image (extrapolated from EXIF etc.) - public $animated; // Animated image? (has more than 1 frame). null means untested - public $mimetype; // The _ImageFile_ mimetype, _not_ the originating File object - - // /** - // * ImageFile constructor. - // * - // * @param int|null $id The DB id of the file. Int if known, null if not. - // * If null, it searches for it. If -1, it skips all DB - // * interactions (useful for temporary objects) - // * @param string $filepath The path of the file this media refers to. Required - // * @param string|null $filehash The hash of the file, if known. Optional - // * - // * @throws ClientException - // * @throws NoResultException - // * @throws ServerException - // * @throws UnsupportedMediaException - // */ - // public function __construct(?int $id = null, string $filepath, ?string $filehash = null) - // { - // $old_limit = ini_set('memory_limit', common_config('attachments', 'memory_limit')); - - // // These do not have to be the same as fileRecord->filename for example, - // // since we may have generated an image source file from something else! - // $this->filepath = $filepath; - // $this->filename = basename($filepath); - - // $img = Image::make($this->filepath); - // $this->mimetype = $img->mime(); - - // $cmp = function ($obj, $type) { - // if ($obj->mimetype == image_type_to_mime_type($type)) { - // $obj->type = $type; - // return true; - // } - // return false; - // }; - // if (!(($cmp($this, IMAGETYPE_GIF) && function_exists('imagecreatefromgif')) || - // ($cmp($this, IMAGETYPE_JPEG) && function_exists('imagecreatefromjpeg')) || - // ($cmp($this, IMAGETYPE_BMP) && function_exists('imagecreatefrombmp')) || - // ($cmp($this, IMAGETYPE_WBMP) && function_exists('imagecreatefromwbmp')) || - // ($cmp($this, IMAGETYPE_XBM) && function_exists('imagecreatefromxbm')) || - // ($cmp($this, IMAGETYPE_PNG) && function_exists('imagecreatefrompng')) || - // ($cmp($this, IMAGETYPE_WEBP) && function_exists('imagecreatefromwebp')) - // ) - // ) { - // common_debug("Mimetype '{$this->mimetype}' was not recognized as a supported format"); - // // TRANS: Exception thrown when trying to upload an unsupported image file format. - // throw new UnsupportedMediaException(_m('Unsupported image format.'), $this->filepath); - // } - - // $this->width = $img->width(); - // $this->height = $img->height(); - - // parent::__construct( - // $filepath, - // $this->mimetype, - // $filehash, - // $id - // ); - - // if ($this->type === IMAGETYPE_JPEG) { - // // Orientation value to rotate thumbnails properly - // $exif = @$img->exif(); - // if (is_array($exif) && isset($exif['Orientation'])) { - // switch ((int)($exif['Orientation'])) { - // case 1: // top is top - // $this->rotate = 0; - // break; - // case 3: // top is bottom - // $this->rotate = 180; - // break; - // case 6: // top is right - // $this->rotate = -90; - // break; - // case 8: // top is left - // $this->rotate = 90; - // break; - // } - // // If we ever write this back, Orientation should be set to '1' - // } - // } elseif ($this->type === IMAGETYPE_GIF) { - // $this->animated = $this->isAnimatedGif(); - // } - - // Event::handle('FillImageFileMetadata', [$this]); - - // $img->destroy(); - // ini_set('memory_limit', $old_limit); // Restore the old memory limit - // } - - /** - * Shortcut method to get an ImageFile from a File - * - * @param File $file - * - * @throws ClientException - * @throws FileNotFoundException - * @throws NoResultException - * @throws ServerException - * @throws UnsupportedMediaException - * @throws UseFileAsThumbnailException - * - * @return ImageFile - * @return ImageFile - */ - public static function fromFileObject(File $file) - { - $imgPath = null; - $media = common_get_mime_media($file->mimetype); - if (Event::handle('CreateFileImageThumbnailSource', [$file, &$imgPath, $media])) { - if (empty($file->filename) && !file_exists($imgPath)) { - throw new FileNotFoundException($imgPath); - } - - // First some mimetype specific exceptions - switch ($file->mimetype) { - case 'image/svg+xml': - throw new UseFileAsThumbnailException($file); - } - - // And we'll only consider it an image if it has such a media type - if ($media !== 'image') { - throw new UnsupportedMediaException(_m('Unsupported media format.'), $file->getPath()); - } - - $filepath = $file->getPath(); - - return new self($file->getID(), $filepath, $file->filehash); - } - } - - public function getPath() - { - if (!file_exists($this->filepath)) { - throw new FileNotFoundException($this->filepath); - } - - return $this->filepath; - } - - /** - * Process a file upload - * - * Uses MediaFile's `fromUpload` to do the majority of the work - * and ensures the uploaded file is in fact an image. - * - * @param string $param - * @param null|Profile $scoped - * - * @throws NoResultException - * @throws NoUploadedMediaException - * @throws ServerException - * @throws UnsupportedMediaException - * @throws UseFileAsThumbnailException - * @throws ClientException - * - * @return ImageFile - * - */ - public static function fromUpload(string $param = 'upload', ?Profile $scoped = null): self - { - $mediafile = parent::fromUpload($param, $scoped); - if ($mediafile instanceof self) { - return $mediafile; - } else { - $mediafile->delete(); - // We can conclude that we have failed to get the MIME type - // TRANS: Client exception thrown trying to upload an invalid image type. - // TRANS: %s is the file type that was denied - $hint = sprintf(_m('"%s" is not a supported file type on this server. ' . - 'Try using another image format.'), $mediafile->mimetype); - throw new ClientException($hint); - } - } - - /** - * Create a new ImageFile object from an url - * - * Uses MediaFile's `fromUrl` to do the majority of the work - * and ensures the uploaded file is in fact an image. - * - * @param string $url Remote image URL - * @param null|Profile $scoped - * @param null|string $name - * @param null|int $file_id same as in this class constructor - * - * @throws ClientException - * @throws HTTP_Request2_Exception - * @throws InvalidFilenameException - * @throws NoResultException - * @throws ServerException - * @throws UnsupportedMediaException - * @throws UseFileAsThumbnailException - * - * @return ImageFile - * @return ImageFile - */ - public static function fromUrl(string $url, ?Profile $scoped = null, ?string $name = null, ?int $file_id = null): self - { - $mediafile = parent::fromUrl($url, $scoped, $name, $file_id); - if ($mediafile instanceof self) { - return $mediafile; - } else { - $mediafile->delete(); - // We can conclude that we have failed to get the MIME type - // TRANS: Client exception thrown trying to upload an invalid image type. - // TRANS: %s is the file type that was denied - $hint = sprintf(_m('"%s" is not a supported file type on this server. ' . - 'Try using another image format.'), $mediafile->mimetype); - throw new ClientException($hint); - } - } - - /** - * Several obscure file types should be normalized to WebP on resize. - * - * Keeps only GIF (if animated) and WebP formats - * - * @return int - */ - public function preferredType() - { - if ($this->type == IMAGETYPE_GIF && $this->animated) { - return $this->type; - } - - return IMAGETYPE_WEBP; - } - - /** - * Copy the image file to the given destination. - * - * This function may modify the resulting file. Please use the - * returned ImageFile object to read metadata (width, height etc.) - * - * @param string $outpath - * - * @throws NoResultException - * @throws ServerException - * @throws UnsupportedMediaException - * @throws UseFileAsThumbnailException - * @throws ClientException - * - * @return ImageFile the image stored at target path - * - */ - public function copyTo($outpath) - { - return new self(null, $this->resizeTo($outpath)); - } - - /** - * Create and save a thumbnail image. - * - * @param string $outpath - * @param array $box width, height, boundary box (x,y,w,h) defaults to full image - * - * @throws UnsupportedMediaException - * @throws UseFileAsThumbnailException - * - * @return string full local filesystem filename - * @return string full local filesystem filename - * - */ - public function resizeTo($outpath, array $box = []) - { - $box['width'] = isset($box['width']) ? (int) ($box['width']) : $this->width; - $box['height'] = isset($box['height']) ? (int) ($box['height']) : $this->height; - $box['x'] = isset($box['x']) ? (int) ($box['x']) : 0; - $box['y'] = isset($box['y']) ? (int) ($box['y']) : 0; - $box['w'] = isset($box['w']) ? (int) ($box['w']) : $this->width; - $box['h'] = isset($box['h']) ? (int) ($box['h']) : $this->height; - - if (!file_exists($this->filepath)) { - // TRANS: Exception thrown during resize when image has been registered as present, - // but is no longer there. - throw new FileNotFoundException($this->filepath); - } - - // Don't rotate/crop/scale if it isn't necessary - if ($box['width'] === $this->width - && $box['height'] === $this->height - && $box['x'] === 0 - && $box['y'] === 0 - && $box['w'] === $this->width - && $box['h'] === $this->height - && $this->type === $this->preferredType()) { - if (abs($this->rotate) == 90) { - // Box is rotated 90 degrees in either direction, - // so we have to redefine x to y and vice versa. - $tmp = $box['width']; - $box['width'] = $box['height']; - $box['height'] = $tmp; - $tmp = $box['x']; - $box['x'] = $box['y']; - $box['y'] = $tmp; - $tmp = $box['w']; - $box['w'] = $box['h']; - $box['h'] = $tmp; - } - } - - $this->height = $box['h']; - $this->width = $box['w']; - - if (Event::handle('StartResizeImageFile', [$this, $outpath, $box])) { - $outpath = $this->resizeToFile($outpath, $box); - } - - if (!file_exists($outpath)) { - if ($this->fileRecord instanceof File) { - throw new UseFileAsThumbnailException($this->fileRecord); - } else { - throw new UnsupportedMediaException('No local File object exists for ImageFile.'); - } - } - - return $outpath; - } - - /** - * Resizes a file. If $box is omitted, the size is not changed, but this is still useful, - * because it will reencode the image in the `self::prefferedType()` format. This only - * applies henceforward, not retroactively - * - * Increases the 'memory_limit' to the one in the 'attachments' section in the config, to - * enable the handling of bigger images, which can cause a peak of memory consumption, while - * encoding - * - * @param $outpath - * @param array $box - * - * @throws Exception - */ - protected function resizeToFile(string $outpath, array $box): string - { - $old_limit = ini_set('memory_limit', common_config('attachments', 'memory_limit')); - - try { - $img = Image::make($this->filepath); - } catch (Exception $e) { - common_log(LOG_ERR, __METHOD__ . ' encountered exception: ' . print_r($e, true)); - // TRANS: Exception thrown when trying to resize an unknown file type. - throw new Exception(_m('Unknown file type')); - } - - if ($this->filepath === $outpath) { - @unlink($outpath); - } - - if ($this->rotate != 0) { - $img = $img->orientate(); - } - - $img->fit( - $box['width'], - $box['height'], - function ($constraint) { - if (common_config('attachments', 'upscale') !== true) { - $constraint->upsize(); // Prevent upscaling - } - } - ); - - // Ensure we save in the correct format and allow customization based on type - $type = $this->preferredType(); - switch ($type) { - case IMAGETYPE_WEBP: - $img->save($outpath, 100, 'webp'); - break; - case IMAGETYPE_GIF: - $img->save($outpath, 100, 'gif'); - break; - default: - // TRANS: Exception thrown when trying resize an unknown file type. - throw new Exception(_m('Unknown file type')); - } - - $img->destroy(); - - ini_set('memory_limit', $old_limit); // Restore the old memory limit - - return $outpath; - } - - public function scaleToFit($maxWidth = null, $maxHeight = null, $crop = null) - { - return self::getScalingValues( - $this->width, - $this->height, - $maxWidth, - $maxHeight, - $crop, - $this->rotate - ); - } - - /** - * Gets scaling values for images of various types. Cropping can be enabled. - * - * Values will scale _up_ to fit max values if cropping is enabled! - * With cropping disabled, the max value of each axis will be respected. - * - * @param $width int Original width - * @param $height int Original height - * @param $maxW int Resulting max width - * @param $maxH int Resulting max height - * @param $crop int Crop to the size (not preserving aspect ratio) - * @param int $rotate - * - * @throws ServerException - * - * @return array - * - */ - public static function getScalingValues( - $width, - $height, - $maxW = null, - $maxH = null, - $crop = null, - $rotate = 0 - ) { - $maxW = $maxW ?: common_config('thumbnail', 'width'); - $maxH = $maxH ?: common_config('thumbnail', 'height'); - - if ($maxW < 1 || ($maxH !== null && $maxH < 1)) { - throw new ServerException('Bad parameters for ImageFile::getScalingValues'); - } - if ($maxH === null) { - // if maxH is null, we set maxH to equal maxW and enable crop - $maxH = $maxW; - $crop = true; - } - - // Because GD doesn't understand EXIF orientation etc. - if (abs($rotate) == 90) { - $tmp = $width; - $width = $height; - $height = $tmp; - } - - // Cropping data (for original image size). Default values, 0 and null, - // imply no cropping and with preserved aspect ratio (per axis). - $cx = 0; // crop x - $cy = 0; // crop y - $cw = null; // crop area width - $ch = null; // crop area height - - if ($crop) { - $s_ar = $width / $height; - $t_ar = $maxW / $maxH; - - $rw = $maxW; - $rh = $maxH; - - // Source aspect ratio differs from target, recalculate crop points! - if ($s_ar > $t_ar) { - $cx = floor($width / 2 - $height * $t_ar / 2); - $cw = ceil($height * $t_ar); - } elseif ($s_ar < $t_ar) { - $cy = floor($height / 2 - $width / $t_ar / 2); - $ch = ceil($width / $t_ar); - } - } else { - $rw = $maxW; - $rh = ceil($height * $rw / $width); - - // Scaling caused too large height, decrease to max accepted value - if ($rh > $maxH) { - $rh = $maxH; - $rw = ceil($width * $rh / $height); - } - } - return [(int) $rw, (int) $rh, - (int) $cx, (int) $cy, - is_null($cw) ? $width : (int) $cw, - is_null($ch) ? $height : (int) $ch, ]; - } - - /** - * Animated GIF test, courtesy of frank at huddler dot com et al: - * http://php.net/manual/en/function.imagecreatefromgif.php#104473 - * Modified so avoid landing inside of a header (and thus not matching our regexp). - */ - protected function isAnimatedGif() - { - if (!($fh = @fopen($this->filepath, 'rb'))) { - return false; - } - - $count = 0; - //an animated gif contains multiple "frames", with each frame having a - //header made up of: - // * a static 4-byte sequence (\x00\x21\xF9\x04) - // * 4 variable bytes - // * a static 2-byte sequence (\x00\x2C) - // In total the header is maximum 10 bytes. - - // We read through the file til we reach the end of the file, or we've found - // at least 2 frame headers - while (!feof($fh) && $count < 2) { - $chunk = fread($fh, 1024 * 100); //read 100kb at a time - $count += preg_match_all('#\x00\x21\xF9\x04.{4}\x00\x2C#s', $chunk, $matches); - // rewind in case we ended up in the middle of the header, but avoid - // infinite loop (i.e. don't rewind if we're already in the end). - if (!feof($fh) && ftell($fh) >= 9) { - fseek($fh, -9, SEEK_CUR); - } - } - - fclose($fh); - return $count >= 1; // number of animated frames apart from the original image - } - - /** - * @param $width - * @param $height - * @param $crop - * @param false $upscale - * - * @throws ClientException - * @throws FileNotFoundException - * @throws FileNotStoredLocallyException - * @throws InvalidFilenameException - * @throws ServerException - * @throws UnsupportedMediaException - * @throws UseFileAsThumbnailException - * - * @return File_thumbnail - */ - public function getFileThumbnail($width = null, $height = null, $crop = null, $upscale = false) - { - if (!$this->fileRecord instanceof File) { - throw new ServerException('No File object attached to this ImageFile object.'); - } - - // Throws FileNotFoundException or FileNotStoredLocallyException - $this->filepath = $this->fileRecord->getFileOrThumbnailPath(); - $filename = basename($this->filepath); - - if ($width === null) { - $width = common_config('thumbnail', 'width'); - $height = common_config('thumbnail', 'height'); - $crop = common_config('thumbnail', 'crop'); - } - - if (!$upscale) { - if ($width > $this->width) { - $width = $this->width; - } - if (!is_null($height) && $height > $this->height) { - $height = $this->height; - } - } - - if ($height === null) { - $height = $width; - $crop = true; - } - - // Get proper aspect ratio width and height before lookup - // We have to do it through an ImageFile object because of orientation etc. - // Only other solution would've been to rotate + rewrite uploaded files - // which we don't want to do because we like original, untouched data! - list($width, $height, $x, $y, $w, $h) = $this->scaleToFit($width, $height, $crop); - - $thumb = File_thumbnail::pkeyGet([ - 'file_id' => $this->fileRecord->getID(), - 'width' => $width, - 'height' => $height, - ]); - - if ($thumb instanceof File_thumbnail) { - $this->height = $height; - $this->width = $width; - return $thumb; - } - - $type = $this->preferredType(); - $ext = image_type_to_extension($type, true); - // Decoding returns null if the file is in the old format - $filename = MediaFile::decodeFilename(basename($this->filepath)); - // Encoding null makes the file use 'untitled', and also replaces the extension - $outfilename = MediaFile::encodeFilename($filename, $this->filehash, $ext); - - // The boundary box for our resizing - $box = [ - 'width' => $width, 'height' => $height, - 'x' => $x, 'y' => $y, - 'w' => $w, 'h' => $h, - ]; - - $outpath = File_thumbnail::path( - "thumb-{$this->fileRecord->id}-{$box['width']}x{$box['height']}-{$outfilename}" - ); - - // Doublecheck that parameters are sane and integers. - if ($box['width'] < 1 || $box['width'] > common_config('thumbnail', 'maxsize') - || $box['height'] < 1 || $box['height'] > common_config('thumbnail', 'maxsize') - || $box['w'] < 1 || $box['x'] >= $this->width - || $box['h'] < 1 || $box['y'] >= $this->height) { - // Fail on bad width parameter. If this occurs, it's due to algorithm in ImageFile->scaleToFit - common_debug("Boundary box parameters for resize of {$this->filepath} : " . var_export($box, true)); - throw new ServerException('Bad thumbnail size parameters.'); - } - - common_debug(sprintf( - 'Generating a thumbnail of File id=%u of size %ux%u', - $this->fileRecord->getID(), - $width, - $height - )); - - $this->height = $box['height']; - $this->width = $box['width']; - - // Perform resize and store into file - $outpath = $this->resizeTo($outpath, $box); - $outname = basename($outpath); - - return File_thumbnail::saveThumbnail( - $this->fileRecord->getID(), - $this->fileRecord->getUrl(false), - $width, - $height, - $outname - ); - } -} diff --git a/plugins/Media/Util/MediaFile.php b/plugins/Media/Util/MediaFile.php deleted file mode 100644 index cc1377a995..0000000000 --- a/plugins/Media/Util/MediaFile.php +++ /dev/null @@ -1,934 +0,0 @@ -. - -/** - * Abstraction for media files - * - * @category Media - * @package GNUsocial - * - * @author Robin Millette - * @author Miguel Dantas - * @author Zach Copley - * @author Mikael Nordfeldth - * @author Diogo Peralta Cordeiro - * @copyright 2008-2009, 2019-2021 Free Software Foundation http://fsf.org - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - */ - -namespace Plugin\Media\Util; - -/** - * Class responsible for abstracting media files - */ -class MediaFile -{ - public $id; - public $filepath; - public $filename; - public $fileRecord; - public $fileurl; - public $short_fileurl; - public $mimetype; - - // /** - // * MediaFile constructor. - // * - // * @param string $filepath The path of the file this media refers to. Required - // * @param string $mimetype The mimetype of the file. Required - // * @param string|null $filehash The hash of the file, if known. Optional - // * @param int|null $id The DB id of the file. Int if known, null if not. - // * If null, it searches for it. If -1, it skips all DB - // * interactions (useful for temporary objects) - // * - // * @throws ClientException - // * @throws NoResultException - // * @throws ServerException - // */ - // public function __construct(string $filepath, string $mimetype, ?string $filehash = null, ?int $id = null) - // { - // $this->filepath = $filepath; - // $this->filename = basename($this->filepath); - // $this->mimetype = $mimetype; - // $this->filehash = self::getHashOfFile($this->filepath, $filehash); - // $this->id = $id; - - // // If id is -1, it means we're dealing with a temporary object and don't want to store it in the DB, - // // or add redirects - // if ($this->id !== -1) { - // if (!empty($this->id)) { - // // If we have an id, load it - // $this->fileRecord = new File(); - // $this->fileRecord->id = $this->id; - // if (!$this->fileRecord->find(true)) { - // // If we have set an ID, we need that ID to exist! - // throw new NoResultException($this->fileRecord); - // } - // } else { - // // Otherwise, store it - // $this->fileRecord = $this->storeFile(); - // } - - // $this->fileurl = common_local_url( - // 'attachment', - // ['attachment' => $this->fileRecord->id] - // ); - - // $this->short_fileurl = common_shorten_url($this->fileurl); - // } - // } - - /** - * Shortcut method to get a MediaFile from a File - * - * @param File $file - * - * @throws ClientException - * @throws FileNotFoundException - * @throws NoResultException - * @throws ServerException - * - * @return ImageFile|MediaFile - */ - public static function fromFileObject(File $file) - { - $filepath = null; - try { - $filepath = $file->getPath(); - } catch (Exception $e) { - } - return new self($filepath, common_get_mime_media($file->mimetype), $file->filehash, $file->getID()); - } - - public function attachToNotice(Notice $notice) - { - File_to_post::processNew($this->fileRecord, $notice); - } - - public function getPath() - { - return File::path($this->filename); - } - - /** - * @param null|bool $use_local true means require local, null means prefer original, false means use whatever is stored - * - * @return string - */ - public function getUrl(?bool $use_local = null): ?string - { - if ($use_local !== false) { - if (empty($this->fileurl)) { - // A locally stored file, so let's generate a URL for our instance. - return common_local_url('attachment_view', ['filehash' => $this->filehash]); - } - } - - // The original file's URL - return $this->fileurl; - } - - public function shortUrl() - { - return common_shorten_url($this->getUrl()); - } - - public function getEnclosure() - { - return $this->getFile()->getEnclosure(); - } - - public function delete($useWhere = false) - { - if (!is_null($this->fileRecord)) { - $this->fileRecord->delete($useWhere); - } - @unlink($this->filepath); - } - - /** - * Remove the file from filesystem - */ - public function unlink() - { - $this->filename = null; - // Delete the file, if it exists locally - if (!empty($this->filepath) && file_exists($this->filepath)) { - $deleted = @unlink($this->filepath); - if (!$deleted) { - common_log(LOG_ERR, sprintf('Could not unlink existing file: "%s"', $this->filepath)); - } - } - $this->fileRecord->unlink(); - } - - public function getFile() - { - if (!$this->fileRecord instanceof File) { - throw new ServerException('File record did not exist for MediaFile'); - } - - return $this->fileRecord; - } - - /** - * Calculate the hash of a file. - * - * This won't work for files >2GiB because PHP uses only 32bit. - * - * @param string $filepath - * @param null|string $filehash - * - * @throws ServerException - * - * @return string - * - */ - public static function getHashOfFile(string $filepath, $filehash = null) - { - assert(!empty($filepath), __METHOD__ . ': filepath cannot be null'); - if ($filehash === null) { - // Calculate if we have an older upload method somewhere (Qvitter) that - // doesn't do this before calling new MediaFile on its local files... - $filehash = hash_file(File::FILEHASH_ALG, $filepath); - if ($filehash === false) { - throw new ServerException('Could not read file for hashing'); - } - } - return $filehash; - } - - /** - * Retrieve or insert as a file in the DB - * - * @throws ServerException - * @throws ClientException - * - * @return object File - * - */ - protected function storeFile() - { - try { - $file = File::getByHash($this->filehash); - if (is_null($this->fileurl) && is_null($file->getUrl(false))) { - // An already existing local file is being re-added, return it - return $file; - } - } catch (NoResultException $e) { - // Well, let's just continue below. - } - - $file = new File; - - $file->filename = $this->filename; - $file->url = $this->fileurl; - $file->urlhash = is_null($file->url) ? null : File::hashurl($file->url); - $file->filehash = $this->filehash; - $file->size = filesize($this->filepath); - if ($file->size === false) { - throw new ServerException('Could not read file to get its size'); - } - $file->date = time(); - $file->mimetype = $this->mimetype; - - $file_id = $file->insert(); - - if ($file_id === false) { - common_log_db_error($file, 'INSERT', __FILE__); - // TRANS: Client exception thrown when a database error was thrown during a file upload operation. - throw new ClientException(_m('There was a database error while saving your file. Please try again.')); - } - - // Set file geometrical properties if available - try { - $image = ImageFile::fromFileObject($file); - $orig = clone $file; - $file->width = $image->width; - $file->height = $image->height; - $file->update($orig); - - // We have to cleanup after ImageFile, since it - // may have generated a temporary file from a - // video support plugin or something. - // FIXME: Do this more automagically. - // Honestly, I think this is unlikely these days, - // but better be safe than sorry, I guess - if ($image->getPath() != $file->getPath()) { - $image->unlink(); - } - } catch (ServerException $e) { - // We just couldn't make out an image from the file. This - // does not have to be UnsupportedMediaException, as we can - // also get ServerException from files not existing etc. - } - - return $file; - } - - /** - * The maximum allowed file size, as a string - */ - public static function maxFileSize() - { - $value = self::maxFileSizeInt(); - if ($value > 1024 * 1024) { - $value = $value / (1024 * 1024); - // TRANS: Number of megabytes. %d is the number. - return sprintf(_m('%dMB', '%dMB', $value), $value); - } elseif ($value > 1024) { - $value = $value / 1024; - // TRANS: Number of kilobytes. %d is the number. - return sprintf(_m('%dkB', '%dkB', $value), $value); - } else { - // TRANS: Number of bytes. %d is the number. - return sprintf(_m('%dB', '%dB', $value), $value); - } - } - - /** - * The maximum allowed file size, as an int - */ - public static function maxFileSizeInt(): int - { - return common_config('attachments', 'file_quota'); - } - - /** - * Encodes a file name and a file hash in the new file format, which is used to avoid - * having an extension in the file, removing trust in extensions, while keeping the original name - * - * @param null|string $original_name - * @param string $filehash - * @param null|bool|string $ext from File::getSafeExtension - * - * @throws ClientException - * @throws ServerException - * - * @return string - */ - public static function encodeFilename($original_name, string $filehash, $ext = null): string - { - if (empty($original_name)) { - $original_name = _m('Untitled attachment'); - } - - // If we're given an extension explicitly, use it, otherwise... - $ext = $ext ?: - // get a replacement extension if configured, returns false if it's blocked, - // null if no extension - File::getSafeExtension($original_name); - if ($ext === false) { - throw new ClientException(_m('Blacklisted file extension.')); - } - - if (!empty($ext)) { - // Remove dots if we have them (make sure they're not repeated) - $ext = preg_replace('/^\.+/', '', $ext); - $original_name = preg_replace('/\.+.+$/i', ".{$ext}", $original_name); - } - - // Avoid unnecessarily large file names - $pretty_name = substr(trim($original_name), 0, 30); // 30 seems like a sensible limit for a file name - - $enc_name = bin2hex($pretty_name); - return "{$enc_name}-{$filehash}"; - } - - /** - * Decode the new filename format - * - * @return false | null | string on failure, no match (old format) or original file name, respectively - */ - public static function decodeFilename(string $encoded_filename) - { - // Should match: - // hex-hash - // thumb-id-widthxheight-hex-hash - // And return the `hex` part - $ret = preg_match('/^(.*-)?([^-]+)-[^-]+$/', $encoded_filename, $matches); - if ($ret === false) { - return false; - } elseif ($ret === 0 || !ctype_xdigit($matches[2])) { - return null; // No match - } else { - $filename = hex2bin($matches[2]); - - // Matches extension - if (preg_match('/^(.+?)\.(.+)$/', $filename, $sub_matches) === 1) { - $ext = $sub_matches[2]; - // Previously, there was a blacklisted extension array, which could have an alternative - // extension, such as phps, to replace php. We want to turn it back (this is deprecated, - // as it no longer makes sense, since we don't trust trust files based on extension, - // but keep the feature) - $blacklist = common_config('attachments', 'extblacklist'); - if (is_array($blacklist)) { - foreach ($blacklist as $upload_ext => $safe_ext) { - if ($ext === $safe_ext) { - $ext = $upload_ext; - break; - } - } - } - return "{$sub_matches[1]}.{$ext}"; - } else { - // No extension, don't bother trying to replace it - return $filename; - } - } - } - - /** - * Create a new MediaFile or ImageFile object from an upload - * - * Tries to set the mimetype correctly, using the most secure method available and rejects the file otherwise. - * In case the upload is an image, this function returns an new ImageFile (which extends MediaFile) - * The filename has a new format: - * bin2hex("{$original_name}.{$ext}")."-{$filehash}" - * This format should be respected. Notice the dash, which is important to distinguish it from the previous - * format ("{$hash}.{$ext}") - * - * @param string $param Form name - * @param null|Profile $scoped - * - * @throws ClientException - * @throws InvalidFilenameException - * @throws NoResultException - * @throws NoUploadedMediaException - * @throws ServerException - * @throws UnsupportedMediaException - * @throws UseFileAsThumbnailException - * - * @return ImageFile|MediaFile - */ - public static function fromUpload(string $param = 'media', ?Profile $scoped = null) - { - // The existence of the "error" element means PHP has processed it properly even if it was ok. - if (!(isset($_FILES[$param], $_FILES[$param]['error']))) { - throw new NoUploadedMediaException($param); - } - - switch ($_FILES[$param]['error']) { - case UPLOAD_ERR_OK: // success, jump out - break; - case UPLOAD_ERR_INI_SIZE: - case UPLOAD_ERR_FORM_SIZE: - // TRANS: Exception thrown when too large a file is uploaded. - // TRANS: %s is the maximum file size, for example "500b", "10kB" or "2MB". - throw new ClientException(sprintf( - _m('That file is too big. The maximum file size is %s.'), - self::maxFileSize() - )); - case UPLOAD_ERR_PARTIAL: - @unlink($_FILES[$param]['tmp_name']); - // TRANS: Client exception. - throw new ClientException(_m('The uploaded file was only partially uploaded.')); - case UPLOAD_ERR_NO_FILE: - // No file; probably just a non-AJAX submission. - throw new NoUploadedMediaException($param); - case UPLOAD_ERR_NO_TMP_DIR: - // TRANS: Client exception thrown when a temporary folder is not present to store a file upload. - throw new ClientException(_m('Missing a temporary folder.')); - case UPLOAD_ERR_CANT_WRITE: - // TRANS: Client exception thrown when writing to disk is not possible during a file upload operation. - throw new ClientException(_m('Failed to write file to disk.')); - case UPLOAD_ERR_EXTENSION: - // TRANS: Client exception thrown when a file upload operation has been stopped by an extension. - throw new ClientException(_m('File upload stopped by extension.')); - default: - common_log(LOG_ERR, __METHOD__ . ': Unknown upload error ' . $_FILES[$param]['error']); - // TRANS: Client exception thrown when a file upload operation has failed with an unknown reason. - throw new ClientException(_m('System error uploading file.')); - } - - $filehash = strtolower(self::getHashOfFile($_FILES[$param]['tmp_name'])); - $fileid = null; - try { - $file = File::getByHash($filehash); - // There can be more than one file for the same filehash IF the url are different (due to different metadata). - while ($file->fetch()) { - if ($file->getUrl(false)) { - // Files uploaded by Actors of this instance won't have an url, skip. - continue; - } - try { - return ImageFile::fromFileObject($file); - } catch (UnsupportedMediaException $e) { - return self::fromFileObject($file); - } - } - // Assert: If we got to this line, then we only traversed URLs on the while loop above. - if (common_config('attachments', 'prefer_remote')) { - // Was this file imported from a remote source already? - $filepath = $file->getPath(); // This function will throw FileNotFoundException if not. - // Assert: If we got to this line, then we can use this file and just add redirections. - $mimetype = $file->mimetype; - $fileid = $file->getID(); - } else { - throw new FileNotFoundException('This isn\'t a path.'); // A bit of dadaist art. - // It's natural that a sysadmin prefers to not add redirections to the first remote link of an - // attachment, it's not a very consistent thing to do. On the other hand, lack of space can drive - // a person crazy. - - // Also note that if one configured StoreRemoteMedia to not save original images (very likely), then - // having prefer_remote enabled will never store the original attachment (sort of the idea here). - } - } catch (FileNotFoundException | NoResultException $e) { - // We have to save the upload as a new local file. This is the normal course of action. - if ($scoped instanceof Profile) { - // Throws exception if additional size does not respect quota - // This test is only needed, of course, if we're uploading something new. - File::respectsQuota($scoped, $_FILES[$param]['size']); - } - - $mimetype = self::getUploadedMimeType($_FILES[$param]['tmp_name'], $_FILES[$param]['name']); - $media = common_get_mime_media($mimetype); - - $basename = basename($_FILES[$param]['name']); - - if ($media == 'image') { - // Use -1 for the id to avoid adding this temporary file to the DB - $img = new ImageFile(-1, $_FILES[$param]['tmp_name']); - // Validate the image by re-encoding it. Additionally normalizes old formats to WebP, - // keeping GIF untouched if animated - $outpath = $img->resizeTo($img->filepath); - $ext = image_type_to_extension($img->preferredType(), false); - } - $filename = self::encodeFilename($basename, $filehash, isset($ext) ? $ext : File::getSafeExtension($basename)); - - $filepath = File::path($filename); - - if ($media == 'image') { - $result = rename($outpath, $filepath); - } else { - $result = move_uploaded_file($_FILES[$param]['tmp_name'], $filepath); - } - if (!$result) { - // TRANS: Client exception thrown when a file upload operation fails because the file could - // TRANS: not be moved from the temporary folder to the permanent file location. - // UX: too specific - throw new ClientException(_m('File could not be moved to destination directory.')); - } - - if ($media == 'image') { - return new ImageFile(null, $filepath, $filehash); - } - } - return new self($filepath, $mimetype, $filehash, $fileid); - } - - /** - * Create a new MediaFile or ImageFile object from an url - * - * Tries to set the mimetype correctly, using the most secure method available and rejects the file otherwise. - * In case the url is an image, this function returns an new ImageFile (which extends MediaFile) - * The filename has the following format: bin2hex("{$original_name}.{$ext}")."-{$filehash}" - * - * @param string $url Remote media URL - * @param null|Profile $scoped - * @param null|string $name - * @param null|int $file_id same as in this class constructor - * - * @throws ClientException - * @throws HTTP_Request2_Exception - * @throws InvalidFilenameException - * @throws NoResultException - * @throws ServerException - * @throws UnsupportedMediaException - * @throws UseFileAsThumbnailException - * - * @return ImageFile|MediaFile - * @return ImageFile|MediaFile - */ - public static function fromUrl(string $url, ?Profile $scoped = null, ?string $name = null, ?int $file_id = null) - { - if (!common_valid_http_url($url)) { - // TRANS: Server exception. %s is a URL. - throw new ServerException(sprintf('Invalid remote media URL %s.', $url)); - } - - $http = new HTTPClient(); - common_debug(sprintf('Performing HEAD request for incoming activity to avoid ' . - 'unnecessarily downloading too large files. URL: %s', - $url)); - $head = $http->head($url); - $url = $head->getEffectiveUrl(); // to avoid going through redirects again - if (empty($url)) { - throw new ServerException(sprintf('URL after redirects is somehow empty, for URL %s.', $url)); - } - $headers = $head->getHeader(); - $headers = array_change_key_case($headers, CASE_LOWER); - if (array_key_exists('content-length', $headers)) { - $fileQuota = common_config('attachments', 'file_quota'); - $fileSize = $headers['content-length']; - if ($fileSize > $fileQuota) { - // TRANS: Message used to be inserted as %2$s in the text "No file may - // TRANS: be larger than %1$d byte and the file you sent was %2$s.". - // TRANS: %1$d is the number of bytes of an uploaded file. - $fileSizeText = sprintf(_m('%1$d byte', '%1$d bytes', $fileSize), $fileSize); - - // TRANS: Message given if an upload is larger than the configured maximum. - // TRANS: %1$d (used for plural) is the byte limit for uploads, - // TRANS: %2$s is the proper form of "n bytes". This is the only ways to have - // TRANS: gettext support multiple plurals in the same message, unfortunately... - throw new ClientException( - sprintf( - _m( - 'No file may be larger than %1$d byte and the file you sent was %2$s. Try to upload a smaller version.', - 'No file may be larger than %1$d bytes and the file you sent was %2$s. Try to upload a smaller version.', - $fileQuota - ), - $fileQuota, - $fileSizeText - ) - ); - } - } else { - throw new ServerException(sprintf('Invalid remote media URL headers %s.', $url)); - } - unset($head, $headers); - - $tempfile = new TemporaryFile('gs-mediafile'); - fwrite($tempfile->getResource(), HTTPClient::quickGet($url)); - fflush($tempfile->getResource()); - - $filehash = strtolower(self::getHashOfFile($tempfile->getRealPath())); - - try { - $file = File::getByUrl($url); - /* - * If no exception is thrown the file exists locally, so we'll use - * that and just add redirections. - * But if the _actual_ locally stored file doesn't exist, getPath - * will throw FileNotFoundException. - */ - $filepath = $file->getPath(); - $mimetype = $file->mimetype; - } catch (FileNotFoundException | NoResultException $e) { - // We have to save the downloaded as a new local file. - // This is the normal course of action. - if ($scoped instanceof Profile) { - // Throws exception if additional size does not respect quota - // This test is only needed, of course, if something new is uploaded. - File::respectsQuota($scoped, filesize($tempfile->getRealPath())); - } - - $mimetype = self::getUploadedMimeType( - $tempfile->getRealPath(), - $name ?? false - ); - $media = common_get_mime_media($mimetype); - - $basename = basename($name ?? ('media' . common_timestamp())); - - if ($media === 'image') { - // Use -1 for the id to avoid adding this temporary file to the DB. - $img = new ImageFile(-1, $tempfile->getRealPath()); - // Validate the image by re-encoding it. - // Additionally normalises old formats to PNG, - // keeping JPEG and GIF untouched. - $outpath = $img->resizeTo($img->filepath); - $ext = image_type_to_extension($img->preferredType(), false); - } - $filename = self::encodeFilename( - $basename, - $filehash, - $ext ?? File::getSafeExtension($basename) - ); - - $filepath = File::path($filename); - - if ($media === 'image') { - $result = rename($outpath, $filepath); - } else { - try { - $tempfile->commit($filepath); - $result = true; - } catch (TemporaryFileException $e) { - $result = false; - } - } - if (!$result) { - // TRANS: Server exception thrown when a file upload operation fails because the file could - // TRANS: not be moved from the temporary directory to the permanent file location. - throw new ServerException(_m('File could not be moved to destination directory.')); - } - - if ($media === 'image') { - return new ImageFile($file_id, $filepath, $filehash, $url); - } - } - return new self($filepath, $mimetype, $filehash, $file_id, $url); - } - - /** - * Construct media fiile from an upload - */ - public static function fromFileInfo(SplFileInfo $finfo, Profile $scoped = null) - { - $filehash = hash_file(File::FILEHASH_ALG, $finfo->getRealPath()); - - try { - $file = File::getByHash($filehash); - $file->fetch(); - // Already have it, so let's reuse the locally stored File - // by using getPath we also check whether the file exists - // and throw a FileNotFoundException with the path if it doesn't. - $filename = basename($file->getPath()); - $mimetype = $file->mimetype; - } catch (FileNotFoundException $e) { - // This happens if the file we have uploaded has disappeared - // from the local filesystem for some reason. Since we got the - // File object from a sha256 check in fromFileInfo, it's safe - // to just copy the uploaded data to disk! - - // dump the contents of our filehandle to the path from our exception - // and report error if it failed. - if (file_put_contents($e->path, file_get_contents($finfo->getRealPath())) === false) { - // TRANS: Client exception thrown when a file upload operation fails because the file could - // TRANS: not be moved from the temporary folder to the permanent file location. - throw new ClientException(_m('File could not be moved to destination directory.')); - } - if (!chmod($e->path, 0664)) { - common_log(LOG_ERR, 'Could not chmod uploaded file: ' . _ve($e->path)); - } - - $filename = basename($file->getPath()); - $mimetype = $file->mimetype; - } catch (NoResultException $e) { - if ($scoped instanceof Profile) { - File::respectsQuota($scoped, filesize($finfo->getRealPath())); - } - - $mimetype = self::getUploadedMimeType($finfo->getRealPath()); - - $filename = strtolower($filehash) . '.' . File::guessMimeExtension($mimetype); - $filepath = File::path($filename); - - $result = copy($finfo->getRealPath(), $filepath) && chmod($filepath, 0664); - - if (!$result) { - common_log(LOG_ERR, 'File could not be moved (or chmodded) from ' . _ve($stream['uri']) . ' to ' . _ve($filepath)); - // TRANS: Client exception thrown when a file upload operation fails because the file could - // TRANS: not be moved from the temporary folder to the permanent file location. - throw new ClientException(_m('File could not be moved to destination directory.')); - } - } - - return new self($filename, $mimetype, $filehash); - } - - /** - * Attempt to identify the content type of a given file. - * - * @param string $filepath filesystem path as string (file must exist) - * @param bool $originalFilename (optional) for extension-based detection - * - * @throws ServerException - * @throws ClientException if type is known, but not supported for local uploads - * - * @return string - * - * @fixme this seems to tie a front-end error message in, kinda confusing - * - * - */ - public static function getUploadedMimeType(string $filepath, $originalFilename = false) - { - // We only accept filenames to existing files - - $mimetype = null; - - // From CodeIgniter - // We'll need this to validate the MIME info string (e.g. text/plain; charset=us-ascii) - $regexp = '/^([a-z\-]+\/[a-z0-9\-\.\+]+)(;\s[^\/]+)?$/'; - /** - * Fileinfo extension - most reliable method - * - * Apparently XAMPP, CentOS, cPanel and who knows what - * other PHP distribution channels EXPLICITLY DISABLE - * ext/fileinfo, which is otherwise enabled by default - * since PHP 5.3 ... - */ - if (function_exists('finfo_file')) { - $finfo = @finfo_open(FILEINFO_MIME); - // It is possible that a FALSE value is returned, if there is no magic MIME database - // file found on the system - if (is_resource($finfo)) { - $mime = @finfo_file($finfo, $filepath); - finfo_close($finfo); - /* According to the comments section of the PHP manual page, - * it is possible that this function returns an empty string - * for some files (e.g. if they don't exist in the magic MIME database) - */ - if (is_string($mime) && preg_match($regexp, $mime, $matches)) { - $mimetype = $matches[1]; - } - } - } - /* This is an ugly hack, but UNIX-type systems provide a "native" way to detect the file type, - * which is still more secure than depending on the value of $_FILES[$field]['type'], and as it - * was reported in issue #750 (https://github.com/EllisLab/CodeIgniter/issues/750) - it's better - * than mime_content_type() as well, hence the attempts to try calling the command line with - * three different functions. - * - * Notes: - * - the DIRECTORY_SEPARATOR comparison ensures that we're not on a Windows system - * - many system admins would disable the exec(), shell_exec(), popen() and similar functions - * due to security concerns, hence the function_usable() checks - */ - if (DIRECTORY_SEPARATOR !== '\\') { - $cmd = 'file --brief --mime ' . escapeshellarg($filepath) . ' 2>&1'; - if (empty($mimetype) && function_exists('exec')) { - /* This might look confusing, as $mime is being populated with all of the output - * when set in the second parameter. However, we only need the last line, which is - * the actual return value of exec(), and as such - it overwrites anything that could - * already be set for $mime previously. This effectively makes the second parameter a - * dummy value, which is only put to allow us to get the return status code. - */ - $mime = @exec($cmd, $mime, $return_status); - if ($return_status === 0 && is_string($mime) && preg_match($regexp, $mime, $matches)) { - $mimetype = $matches[1]; - } - } - if (empty($mimetype) && function_exists('shell_exec')) { - $mime = @shell_exec($cmd); - if (strlen($mime) > 0) { - $mime = explode("\n", trim($mime)); - if (preg_match($regexp, $mime[(count($mime) - 1)], $matches)) { - $mimetype = $matches[1]; - } - } - } - if (empty($mimetype) && function_exists('popen')) { - $proc = @popen($cmd, 'r'); - if (is_resource($proc)) { - $mime = @fread($proc, 512); - @pclose($proc); - if ($mime !== false) { - $mime = explode("\n", trim($mime)); - if (preg_match($regexp, $mime[(count($mime) - 1)], $matches)) { - $mimetype = $matches[1]; - } - } - } - } - } - // Fall back to mime_content_type(), if available (still better than $_FILES[$field]['type']) - if (empty($mimetype) && function_exists('mime_content_type')) { - $mimetype = @mime_content_type($filepath); - // It's possible that mime_content_type() returns FALSE or an empty string - if ($mimetype == false && strlen($mimetype) > 0) { - throw new ServerException(_m('Could not determine file\'s MIME type.')); - } - } - - // Unclear types are such that we can't really tell by the auto - // detect what they are (.bin, .exe etc. are just "octet-stream") - $unclearTypes = ['application/octet-stream', - 'application/vnd.ms-office', - 'application/zip', - 'text/plain', - 'text/html', // Ironically, Wikimedia Commons' SVG_logo.svg is identified as text/html - // TODO: for XML we could do better content-based sniffing too - 'text/xml', ]; - - $supported = common_config('attachments', 'supported'); - - // If we didn't match, or it is an unclear match - if ($originalFilename && (!$mimetype || in_array($mimetype, $unclearTypes))) { - try { - $type = common_supported_filename_to_mime($originalFilename); - return $type; - } catch (UnknownExtensionMimeException $e) { - // FIXME: I think we should keep the file extension here (supported should be === true here) - } catch (Exception $e) { - // Extension parsed but no connected mimetype, so $mimetype is our best guess - } - } - - // If $config['attachments']['supported'] equals boolean true, accept any mimetype - if ($supported === true || array_key_exists($mimetype, $supported)) { - // FIXME: Don't know if it always has a mimetype here because - // finfo->file CAN return false on error: http://php.net/finfo_file - // so if $supported === true, this may return something unexpected. - return $mimetype; - } - - // We can conclude that we have failed to get the MIME type - $media = common_get_mime_media($mimetype); - if ('application' !== $media) { - // TRANS: Client exception thrown trying to upload a forbidden MIME type. - // TRANS: %1$s is the file type that was denied, %2$s is the application part of - // TRANS: the MIME type that was denied. - $hint = sprintf(_m('"%1$s" is not a supported file type on this server. ' . - 'Try using another %2$s format.'), $mimetype, $media); - } else { - // TRANS: Client exception thrown trying to upload a forbidden MIME type. - // TRANS: %s is the file type that was denied. - $hint = sprintf(_m('"%s" is not a supported file type on this server.'), $mimetype); - } - throw new ClientException($hint); - } - - /** - * Title for a file, to display in the interface (if there's no better title) and - * for download filenames - * - * @param $file File object - * @returns string - */ - public static function getDisplayName(File $file): string - { - if (empty($file->filename)) { - return _m('Untitled attachment'); - } - - // New file name format is "{bin2hex(original_name.ext)}-{$hash}" - $filename = self::decodeFilename($file->filename); - - // If there was an error in the match, something's wrong with some piece - // of code (could be a file with utf8 chars in the name) - $log_error_msg = "Invalid file name for File with id={$file->id} " . - "({$file->filename}). Some plugin probably did something wrong."; - if ($filename === false) { - common_log(LOG_ERR, $log_error_msg); - } elseif ($filename === null) { - // The old file name format was "{hash}.{ext}" so we didn't have a name - // This extracts the extension - $ret = preg_match('/^.+?\.+?(.+)$/', $file->filename, $matches); - if ($ret !== 1) { - common_log(LOG_ERR, $log_error_msg); - return _m('Untitled attachment'); - } - $ext = $matches[1]; - // There's a blacklisted extension array, which could have an alternative - // extension, such as phps, to replace php. We want to turn it back - // (currently defaulted to empty, but let's keep the feature) - $blacklist = common_config('attachments', 'extblacklist'); - if (is_array($blacklist)) { - foreach ($blacklist as $upload_ext => $safe_ext) { - if ($ext === $safe_ext) { - $ext = $upload_ext; - break; - } - } - } - $filename = "untitled.{$ext}"; - } - return $filename; - } -} diff --git a/plugins/Media/media/Attachment.php b/plugins/Media/media/Attachment.php deleted file mode 100644 index b0196bf002..0000000000 --- a/plugins/Media/media/Attachment.php +++ /dev/null @@ -1,66 +0,0 @@ -. - * - * @category UI - * @package StatusNet - * - * @author Evan Prodromou - * @author Sarven Capadisli - * @copyright 2008 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * - * @see http://status.net/ - */ - -namespace Plugin\Media\media; - -/** - * used for one-off attachment action - */ -class Attachment extends AttachmentListItem -{ - public function showNoticeAttachment() - { - if (Event::handle('StartShowAttachmentLink', [$this->out, $this->attachment])) { - $this->out->elementStart('div', ['id' => 'attachment_view', - 'class' => 'h-entry', ]); - $this->out->elementStart('div', 'entry-title'); - $this->out->element('a', $this->linkAttr(), _m('Download link')); - $this->out->elementEnd('div'); - - $this->out->elementStart('article', 'e-content'); - $this->showRepresentation(); - $this->out->elementEnd('article'); - Event::handle('EndShowAttachmentLink', [$this->out, $this->attachment]); - $this->out->elementEnd('div'); - } - } - - public function show() - { - $this->showNoticeAttachment(); - } - - public function linkAttr() - { - return ['rel' => 'external', 'href' => $this->attachment->getAttachmentDownloadUrl()]; - } -} diff --git a/plugins/Media/media/AttachmentList.php b/plugins/Media/media/AttachmentList.php deleted file mode 100644 index 806122b3bd..0000000000 --- a/plugins/Media/media/AttachmentList.php +++ /dev/null @@ -1,131 +0,0 @@ -. - * - * @category UI - * @package StatusNet - * - * @author Evan Prodromou - * @author Sarven Capadisli - * @copyright 2008 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * - * @see http://status.net/ - */ - -namespace Plugin\Media\media; - -/** - * widget for displaying a list of notice attachments - * - * There are a number of actions that display a list of notices, in - * reverse chronological order. This widget abstracts out most of the - * code for UI for notice lists. It's overridden to hide some - * data for e.g. the profile page. - * - * @category UI - * @package StatusNet - * - * @author Evan Prodromou - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * - * @see http://status.net/ - * @see Notice - * @see NoticeListItem - * @see ProfileNoticeList - */ -class AttachmentList -{ - /** the current stream of notices being displayed. */ - public $notice; - - // /** - // * constructor - // * - // * @param Notice $notice stream of notices from DB_DataObject - // */ - // function __construct(Notice $notice, $out=null) - // { - // // parent::__construct($out); - // $this->notice = $notice; - // } - - /** - * show the list of attachments - * - * "Uses up" the stream by looping through it. So, probably can't - * be called twice on the same list. - * - * @return int count of items listed. - */ - public function show() - { - $attachments = $this->notice->attachments(); - foreach ($attachments as $key => $att) { - // Remove attachments which are not representable with neither a title nor thumbnail - if ($att->getTitle() === _('Untitled attachment') && !$att->hasThumbnail()) { - unset($attachments[$key]); - } - } - if (!count($attachments)) { - return 0; - } - - if ($this->notice->getProfile()->isSilenced()) { - // TRANS: Message for inline attachments list in notices when the author has been silenced. - $this->element('div', ['class' => 'error'], - _('Attachments are hidden because this profile has been silenced.')); - return 0; - } - - $this->showListStart(); - - foreach ($attachments as $att) { - $item = $this->newListItem($att); - $item->show(); - } - - $this->showListEnd(); - - return count($attachments); - } - - public function showListStart() - { - $this->out->elementStart('ol', ['class' => 'attachments']); - } - - public function showListEnd() - { - $this->out->elementEnd('ol'); - } - - /** - * returns a new list item for the current attachment - * - * @param File $attachment the current attachment - * - * @return AttachmentListItem a list item for displaying the attachment - */ - public function newListItem(File $attachment) - { - return new AttachmentListItem($attachment, $this->out); - } -} diff --git a/plugins/Media/media/AttachmentListItem.php b/plugins/Media/media/AttachmentListItem.php index b75e1a6607..0ed38a4e15 100644 --- a/plugins/Media/media/AttachmentListItem.php +++ b/plugins/Media/media/AttachmentListItem.php @@ -95,11 +95,12 @@ class AttachmentListItem $this->showEnd(); } - function linkAttr() { + public function linkAttr() + { return [ 'class' => 'u-url', 'href' => $this->attachment->getAttachmentDownloadUrl(), - 'title' => $this->linkTitle() + 'title' => $this->linkTitle(), ]; } @@ -128,7 +129,7 @@ class AttachmentListItem try { // Tell getThumbnail that we can show an animated image if it has one (4th arg, "force_still") $thumb = File_thumbnail::fromFileObject($this->attachment, null, null, false, false); - } catch (UseFileAsThumbnailException|UnsupportedMediaException|FileNotFoundException|ServerException $e) { + } catch (UseFileAsThumbnailException | UnsupportedMediaException | FileNotFoundException | ServerException $e) { common_debug("AttachmentListItem couldn't find a thumbnail for {$this->attachment->getID()} because {$e->getMessage()}"); // This remote file has no local thumbnail. $thumb = null; @@ -157,10 +158,10 @@ class AttachmentListItem try { // getUrl(true) because we don't want to hotlink, could be made configurable $this->out->element('img', ['class' => 'u-photo', - 'src' => $this->attachment->getUrl(true), - 'alt' => $this->attachment->getTitle()]); + 'src' => $this->attachment->getUrl(true), + 'alt' => $this->attachment->getTitle(), ]); } catch (FileNotStoredLocallyException $e) { - $url = $e->file->getUrl(false); + //$url = $e->file->getUrl(false); $this->out->element('a', ['href' => $url, 'rel' => 'external'], $url); } } @@ -178,12 +179,12 @@ class AttachmentListItem } $this->out->elementStart($mediatype, - array('class' => "attachment_player u-{$mediatype}", - 'poster' => $poster, - 'controls' => 'controls')); + ['class' => "attachment_player u-{$mediatype}", + 'poster' => $poster, + 'controls' => 'controls', ]); $this->out->element('source', - array('src' => $this->attachment->getUrl(), - 'type' => $this->attachment->mimetype)); + ['src' => $this->attachment->getUrl(), + 'type' => $this->attachment->mimetype, ]); $this->out->elementEnd($mediatype); break; @@ -202,12 +203,13 @@ class AttachmentListItem break; } // Fall through to default if it wasn't a _local_ text/html File object + // no break default: - Event::handle('ShowUnsupportedAttachmentRepresentation', array($this->out, $this->attachment)); + Event::handle('ShowUnsupportedAttachmentRepresentation', [$this->out, $this->attachment]); } } } else { - Event::handle('ShowUnsupportedAttachmentRepresentation', array($this->out, $this->attachment)); + Event::handle('ShowUnsupportedAttachmentRepresentation', [$this->out, $this->attachment]); } } catch (FileNotFoundException $e) { if (!$this->attachment->isLocal()) { diff --git a/plugins/Media/media/InlineAttachmentList.php b/plugins/Media/media/InlineAttachmentList.php deleted file mode 100644 index 032d189643..0000000000 --- a/plugins/Media/media/InlineAttachmentList.php +++ /dev/null @@ -1,53 +0,0 @@ -. - * - * @category UI - * @package StatusNet - * - * @author Brion Vibber - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * - * @see http://status.net/ - */ - -namespace Plugin\Media\media; - -class InlineAttachmentList extends AttachmentList -{ - public function showListStart() - { - $this->out->element('h4', 'attachments-title', _('Attachments')); - parent::showListStart(); - } - - /** - * returns a new list item for the current attachment - * - * @param File $notice the current attachment - * - * @return ListItem a list item for displaying the attachment - */ - public function newListItem(File $attachment) - { - return new InlineAttachmentListItem($attachment, $this->out); - } -} diff --git a/plugins/Media/media/InlineAttachmentListItem.php b/plugins/Media/media/InlineAttachmentListItem.php deleted file mode 100644 index f9d39b1f3d..0000000000 --- a/plugins/Media/media/InlineAttachmentListItem.php +++ /dev/null @@ -1,64 +0,0 @@ -. - * - * @category UI - * @package StatusNet - * - * @author Brion Vibber - * @copyright 2010 StatusNet, Inc. - * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 - * - * @see http://status.net/ - */ - -namespace Plugin\Media\media; - -class InlineAttachmentListItem extends AttachmentListItem -{ - /** - * start a single notice. - * - * @return void - */ - public function showStart() - { - // XXX: RDFa - // TODO: add notice_type class e.g., notice_video, notice_image - $this->out->elementStart('li', - [ - 'class' => 'inline-attachment', - 'id' => 'attachment-' . $this->attachment->getID(), - ] - ); - } - - /** - * finish the notice - * - * Close the last elements in the notice list item - * - * @return void - */ - public function showEnd() - { - $this->out->elementEnd('li'); - } -} diff --git a/plugins/VideoThumbnail/README.md b/plugins/VideoEncoder/README.md similarity index 100% rename from plugins/VideoThumbnail/README.md rename to plugins/VideoEncoder/README.md diff --git a/plugins/VideoThumbnail/VideoThumbnail.php b/plugins/VideoEncoder/VideoEncoder.php similarity index 95% rename from plugins/VideoThumbnail/VideoThumbnail.php rename to plugins/VideoEncoder/VideoEncoder.php index 1a7f725d28..ea60f164c4 100644 --- a/plugins/VideoThumbnail/VideoThumbnail.php +++ b/plugins/VideoEncoder/VideoEncoder.php @@ -26,12 +26,11 @@ * @see http://www.gnu.org/software/social/ */ -namespace Plugin\VideoThumbnail; +namespace Plugin\VideoEncoder; -use App\Core\Modules\Module; -use Plugin\Media\Util\ImageFile; +use App\Core\Modules\Plugin; -class VideoThumbnail extends Module +class VideoEncoder extends Plugin { const PLUGIN_VERSION = '0.1.0'; @@ -39,7 +38,7 @@ class VideoThumbnail extends Module * Handle resizing GIF files */ public function onStartResizeImageFile( - ImageFile $imagefile, + ImageValidate $imagefile, string $outpath, array $box ): bool { @@ -60,7 +59,7 @@ class VideoThumbnail extends Module * @see http://blog.pkh.me/p/21-high-quality-gif-with-ffmpeg.html * @see https://github.com/PHP-FFMpeg/PHP-FFMpeg/pull/592 */ - public function resizeImageFileAnimatedGif(ImageFile $imagefile, string $outpath, array $box): bool + public function resizeImageFileAnimatedGif(ImageValidate $imagefile, string $outpath, array $box): bool { // Create FFMpeg instance // Need to explictly tell the drivers location or it won't find them diff --git a/plugins/VideoThumbnail/locale/FFmpeg.pot b/plugins/VideoEncoder/locale/FFmpeg.pot similarity index 100% rename from plugins/VideoThumbnail/locale/FFmpeg.pot rename to plugins/VideoEncoder/locale/FFmpeg.pot diff --git a/plugins/VideoThumbnail/locale/en_GB/LC_MESSAGES/FFmpeg.po b/plugins/VideoEncoder/locale/en_GB/LC_MESSAGES/FFmpeg.po similarity index 100% rename from plugins/VideoThumbnail/locale/en_GB/LC_MESSAGES/FFmpeg.po rename to plugins/VideoEncoder/locale/en_GB/LC_MESSAGES/FFmpeg.po diff --git a/plugins/VideoThumbnail/locale/pt/LC_MESSAGES/FFmpeg.po b/plugins/VideoEncoder/locale/pt/LC_MESSAGES/FFmpeg.po similarity index 100% rename from plugins/VideoThumbnail/locale/pt/LC_MESSAGES/FFmpeg.po rename to plugins/VideoEncoder/locale/pt/LC_MESSAGES/FFmpeg.po diff --git a/src/Controller/Attachment.php b/src/Controller/Attachment.php index 869eba4ceb..3980e3c8c2 100644 --- a/src/Controller/Attachment.php +++ b/src/Controller/Attachment.php @@ -27,9 +27,23 @@ use Symfony\Component\HttpFoundation\Request; class Attachment extends Controller { - public function attachment_inline(Request $request, int $id) + public function attachment_show(Request $request, int $id) + { + } + + public function attachment_view(Request $request, int $id) { $res = M::getAttachmentFileInfo($id); - return M::sendFile($res['file_path'], $res['mimetype'], $res['title']); + return M::sendFile($res['file_path'], $res['mimetype'], $res['title'], 'inline'); + } + + public function attachment_download(Request $request, int $id) + { + $res = M::getAttachmentFileInfo($id); + return M::sendFile($res['file_path'], $res['mimetype'], $res['title'], 'attachment'); + } + + public function attachment_thumbnail(Request $request, int $id) + { } } diff --git a/src/Core/GSFile.php b/src/Core/GSFile.php index dd4118f972..601bd764dd 100644 --- a/src/Core/GSFile.php +++ b/src/Core/GSFile.php @@ -45,18 +45,32 @@ class GSFile { // The following properly gets the mimetype with `file` or other // available methods, so should be safe - $hash = hash_file(Attachment::FILEHASH_ALGO, $sfile->getPathname()); - $file = Attachment::create([ + $hash = hash_file(Attachment::FILEHASH_ALGO, $sfile->getPathname()); + $mimetype = $sfile->getMimeType(); + Event::handle('AttachmentValidation', [&$sfile, &$mimetype]); + $attachment = Attachment::create([ 'file_hash' => $hash, 'gsactor_id' => $actor_id, - 'mimetype' => $sfile->getMimeType(), + 'mimetype' => $mimetype, 'title' => $title ?: _m('Untitled attachment'), 'filename' => $hash, 'is_local' => $is_local, ]); $sfile->move($dest_dir, $hash); - // TODO Normalize file types - return $file; + return $attachment; + } + + /** + * Perform file validation (checks and normalization) and store the given file + */ + public static function validateAndStoreAttachmentThumbnail(SymfonyFile $sfile, + string $dest_dir, + ?string $title = null, + bool $is_local = true, + int $actor_id = null): Attachment//Thumbnail + { + $attachment = self::validateAndStoreAttachment($sfile,$dest_dir,$title,$is_local,$actor_id); + return $attachment; } /** diff --git a/src/Core/Modules/AttachmentThumbnail.php b/src/Core/Modules/AttachmentThumbnail.php deleted file mode 100644 index b72fc6419a..0000000000 --- a/src/Core/Modules/AttachmentThumbnail.php +++ /dev/null @@ -1,9 +0,0 @@ -connect('attachment_inline', '/attachment/{id<\d+>}', [C\Attachment::class, 'attachment_inline']); + $r->connect('attachment_show', '/attachment/{id<\d+>}', [C\Attachment::class, 'attachment_show']); + $r->connect('attachment_view', '/attachment/{id<\d+>}/view', [C\Attachment::class, 'attachment_view']); + $r->connect('attachment_download', '/attachment/{id<\d+>}/download', [C\Attachment::class, 'attachment_download']); + $r->connect('attachment_thumbnail', '/attachment/{id<\d+>}/thumbnail', [C\Attachment::class, 'attachment_thumbnail']); } } diff --git a/templates/note/view.html.twig b/templates/note/view.html.twig index a353391c47..47dc89976c 100644 --- a/templates/note/view.html.twig +++ b/templates/note/view.html.twig @@ -26,19 +26,19 @@ {% if attachment.mimetype starts with 'image/' %}
- {{ attachment.getTitle() }} -
{{ attachment.getTitle() }}
+ {{ attachment.getTitle() }} +
{{ attachment.getTitle() }}
{% elseif attachment.mimetype starts with 'video/' %}
-
{% else %}
- {{ attachment.getTitle() }} + {{ attachment.getTitle() }}
{% endif %} {% endfor %}