From 30107de07915e8c96e167448b82e5f1e20506d6f Mon Sep 17 00:00:00 2001 From: Hugo Sales Date: Wed, 28 Apr 2021 15:03:17 +0000 Subject: [PATCH] [Embed] Fix plugin. Only attempt to show an image, if we have one --- plugins/Embed/Embed.php | 80 ++++++++++++++---------- plugins/Embed/Entity/AttachmentEmbed.php | 48 ++++++++++++++ plugins/ImageEncoder/ImageEncoder.php | 21 ++++--- src/Entity/Attachment.php | 2 +- src/Entity/AttachmentThumbnail.php | 4 +- 5 files changed, 112 insertions(+), 43 deletions(-) diff --git a/plugins/Embed/Embed.php b/plugins/Embed/Embed.php index 1700d5dac3..1f167081df 100644 --- a/plugins/Embed/Embed.php +++ b/plugins/Embed/Embed.php @@ -51,6 +51,7 @@ use App\Entity\AttachmentThumbnail; use App\Util\Common; use App\Util\Exception\DuplicateFoundException; use App\Util\Exception\NotFoundException; +use App\Util\Formatting; use App\Util\TemporaryFile; use Embed\Embed as LibEmbed; use Symfony\Component\HttpFoundation\Request; @@ -152,28 +153,37 @@ class Embed extends Plugin /** * Replace enclosure representation of an attachment with the data from embed - * - * @param mixed $enclosure */ - public function onFileEnclosureMetadata(Attachment $attachment, &$enclosure) + public function onAttachmentFileInfo(int $attachment_id, ?array &$enclosure) { - // Never treat generic HTML links as an enclosure type! - // But if we have embed info, we'll consider it golden. try { - $embed = DB::findOneBy('attachment_embed', ['attachment_id' => $attachment->getId()]); + $embed = DB::findOneBy('attachment_embed', ['attachment_id' => $attachment_id]); } catch (NotFoundException) { return Event::next; } - foreach (['mimetype', 'url', 'title', 'modified', 'width', 'height'] as $key) { - if (isset($embed->{$key}) && !empty($embed->{$key})) { - $enclosure->{$key} = $embed->{$key}; - } + // We know about this attachment, so we 'own' it, but know + // that it doesn't have an image + if (!$embed->isImage()) { + $enclosure = null; + return Event::stop; } - return true; + + $enclosure = [ + 'filepath' => $embed->getFilepath(), + 'mimetype' => $embed->getMimetype(), + 'title' => $embed->getTitle(), + 'width' => $embed->getWidth(), + 'height' => $embed->getHeight(), + 'url' => $embed->getUrl(), + ]; + + return Event::stop; } - /** Placeholder */ + /** + * Show this attachment enhanced with the corresponing Embed data, if available + */ public function onShowAttachment(Attachment $attachment, array &$res) { try { @@ -186,18 +196,23 @@ class Embed extends Plugin return Event::next; } if (is_null($embed) && empty($embed->getAuthorName()) && empty($embed->getProvider())) { + Log::debug('Embed doesn\'t have a representation for the attachment #' . $attachment->getId()); return Event::next; } - $thumbnail = AttachmentThumbnail::getOrCreate(attachment: $attachment, width: $width, height: $height, crop: $smart_crop); - $attributes = $thumbnail->getHTMLAttributes(['class' => 'u-photo embed']); + $width = Common::config('thumbnail', 'width'); + $height = Common::config('thumbnail', 'height'); + $smart_crop = Common::config('thumbnail', 'smart_crop'); + $attributes = $embed->getImageHTMLAttributes(['class' => 'u-photo embed']); $res[] = Formatting::twigRender(<<
- + {% if attributes != false %} + + {% endif %}
- {{embed.getTitle() | escape}} + {{embed.getTitle() | escape}}
{% if embed.getAuthorName() is not null %} @@ -224,7 +239,7 @@ class Embed extends Plugin {{ embed.getHtml() | escape }}
-END, ['embed' => $embed, 'thumbnail' => $thumbnail, 'attributes' => $attributes]); +END, ['embed' => $embed, 'attributes' => $attributes]); return Event::stop; } @@ -319,23 +334,20 @@ END, ['embed' => $embed, 'thumbnail' => $thumbnail, 'attributes' => $attributes] $file = new TemporaryFile(); $file->write($imgData); - $mimetype = $headers['content-type'][0]; - Event::handle('AttachmentValidation', [&$file, &$mimetype]); - Event::handle('HashFile', [$file->getPathname(), &$hash]); - $filename = Common::config('attachments', 'dir') . "embed/{$hash}"; - $file->commit($filename); + $filepath = Common::config('storage', 'dir') . "embed/{$hash}" . Common::config('thumbnail', 'extension'); + $width = Common::config('thumbnail', 'width'); + $height = Common::config('thumbnail', 'height'); + $smart_crop = Common::config('thumbnail', 'smart_crop'); + Event::handle('ResizeImagePath', [$file->getPathname(), $filepath, $width, $height, $smart_crop, &$mimetype]); + unset($file); if (array_key_exists('content-disposition', $headers) && preg_match('/^.+; filename="(.+?)"$/', $headers['content-disposition'][0], $matches) === 1) { $original_name = $matches[1]; } - $info = getimagesize($filename); - $width = $info[0]; - $height = $info[1]; - - return [$filename, $width, $height, $original_name ?? null, $mimetype]; + return [$filepath, $width, $height, $original_name ?? null, $mimetype]; } /** @@ -382,7 +394,7 @@ END, ['embed' => $embed, 'thumbnail' => $thumbnail, 'attributes' => $attributes] try { $imgData = HTTPClient::get($url)->getContent(); if (isset($imgData)) { - [$filename, $width, $height, $original_name, $mimetype] = $this->validateAndWriteImage($imgData, $url, $headers); + [$filepath, $width, $height, $original_name, $mimetype] = $this->validateAndWriteImage($imgData, $url, $headers); } else { throw new UnsupportedMediaException(_m('HTTPClient returned an empty result')); } @@ -394,10 +406,9 @@ END, ['embed' => $embed, 'thumbnail' => $thumbnail, 'attributes' => $attributes] } DB::persist(AttachmentThumbnail::create(['attachment_id' => $attachment->getId(), 'width' => $width, 'height' => $height])); - $attachment->setFilename($filename); DB::flush(); - return [$filename, $width, $height, $original_name, $mimetype]; + return [$filepath, $width, $height, $original_name, $mimetype]; } /** @@ -435,14 +446,15 @@ END, ['embed' => $embed, 'thumbnail' => $thumbnail, 'attributes' => $attributes] if (substr($info->image, 0, 4) === 'data') { // Inline image $imgData = base64_decode(substr($info->image, stripos($info->image, 'base64,') + 7)); - [$filename, $width, $height, $original_name, $mimetype] = $this->validateAndWriteImage($imgData); + [$filepath, $width, $height, $original_name, $mimetype] = $this->validateAndWriteImage($imgData); } else { $attachment->setRemoteUrl((string) $info->image); - [$filename, $width, $height, $original_name, $mimetype] = $this->storeRemoteThumbnail($attachment); + [$filepath, $width, $height, $original_name, $mimetype] = $this->storeRemoteThumbnail($attachment); } - $metadata['width'] = $height; - $metadata['height'] = $width; + $metadata['width'] = $width; + $metadata['height'] = $height; $metadata['mimetype'] = $mimetype; + $metadata['filename'] = Formatting::removePrefix($filepath, Common::config('storage', 'dir')); } } catch (Exception $e) { Log::info("Failed to find Embed data for {$url} with 'oscarotero/Embed', got exception: " . get_class($e)); diff --git a/plugins/Embed/Entity/AttachmentEmbed.php b/plugins/Embed/Entity/AttachmentEmbed.php index 490bb04e80..8553adcf7d 100644 --- a/plugins/Embed/Entity/AttachmentEmbed.php +++ b/plugins/Embed/Entity/AttachmentEmbed.php @@ -33,6 +33,9 @@ namespace Plugin\Embed\Entity; use App\Core\Entity; +use App\Core\GSFile; +use App\Core\Router\Router; +use App\Util\Common; use DateTimeInterface; /** @@ -47,6 +50,7 @@ class AttachmentEmbed extends Entity // {{{ Autocode private int $attachment_id; private ?string $mimetype; + private ?string $filename; private ?string $provider; private ?string $provider_url; private ?int $width; @@ -80,6 +84,17 @@ class AttachmentEmbed extends Entity return $this->mimetype; } + public function setFilename(?string $filename): self + { + $this->filename = $filename; + return $this; + } + + public function getFilename(): ?string + { + return $this->filename; + } + public function setProvider(?string $provider): self { $this->provider = $provider; @@ -192,6 +207,38 @@ class AttachmentEmbed extends Entity // }}} Autocode + public function getAttachmentUrl() + { + return Router::url('attachment_show', ['id' => $this->getAttachmentId()]); + } + + public function isImage() + { + return isset($this->mimetype) && GSFile::mimetypeMajor($this->mimetype) == 'image'; + } + + /** + * Get the HTML attributes for this attachment + */ + public function getImageHTMLAttributes(array $orig = [], bool $overwrite = true) + { + if ($this->isImage()) { + $attrs = [ + 'height' => $this->getHeight(), + 'width' => $this->getWidth(), + 'src' => $this->getAttachmentUrl(), + ]; + return $overwrite ? array_merge($orig, $attrs) : array_merge($attrs, $orig); + } else { + return false; + } + } + + public function getFilepath() + { + return Common::config('storage', 'dir') . $this->filename; + } + public static function schemaDef() { return [ @@ -199,6 +246,7 @@ class AttachmentEmbed extends Entity 'fields' => [ 'attachment_id' => ['type' => 'int', 'not null' => true, 'description' => 'oEmbed for that URL/file'], 'mimetype' => ['type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'], + 'filename' => ['type' => 'varchar', 'length' => 191, 'description' => 'file name of resource when available'], 'provider' => ['type' => 'text', 'description' => 'name of this oEmbed provider'], 'provider_url' => ['type' => 'text', 'description' => 'URL of this oEmbed provider'], 'width' => ['type' => 'int', 'description' => 'width of oEmbed resource when available'], diff --git a/plugins/ImageEncoder/ImageEncoder.php b/plugins/ImageEncoder/ImageEncoder.php index bf0d2ed666..f84263992a 100644 --- a/plugins/ImageEncoder/ImageEncoder.php +++ b/plugins/ImageEncoder/ImageEncoder.php @@ -55,6 +55,11 @@ class ImageEncoder extends Plugin return Event::stop; } + public function onResizeImage(Attachment $attachment, AttachmentThumbnail $thumbnail, int $width, int $height, bool $smart_crop): bool + { + return $this->onResizeImagePath($attachment->getPath(), $thumbnail->getPath(), $width, $height, $smart_crop, $__mimetype); + } + /** * Resizes an image. It will encode the image in the * `self::preferredType()` format. This only applies henceforward, @@ -76,33 +81,35 @@ class ImageEncoder extends Plugin * @return bool * */ - public function onResizeImage(Attachment $attachment, AttachmentThumbnail $thumbnail, int $width, int $height, bool $crop): bool + public function onResizeImagePath(string $source, string $destination, int $width, int $height, bool $smart_crop, ?string &$mimetype) { $old_limit = ini_set('memory_limit', Common::config('attachments', 'memory_limit')); try { try { // -1 means load all pages, 'sequential' access means decode pixels on demand // $image = Vips\Image::newFromFile(self::getPath($attachment), ['n' => -1, 'access' => 'sequential']); - $image = Vips\Image::thumbnail($attachment->getPath(), $width, ['height' => $height]); + $image = Vips\Image::thumbnail($source, $width, ['height' => $height]); } catch (Exception $e) { Log::error(__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 ($attachment->getPath() === $thumbnail->getPath()) { - @unlink($thumbnail->getPath()); + if ($source === $destination) { + @unlink($destination); } - if ($crop) { + $type = self::preferredType(); + $mimetype = image_type_to_mime_type($type); + + if ($smart_crop) { $image = $image->smartcrop($width, $height); } - $image->writeToFile($thumbnail->getPath()); + $image->writeToFile($destination); unset($image); } finally { ini_set('memory_limit', $old_limit); // Restore the old memory limit } - return Event::next; } } diff --git a/src/Entity/Attachment.php b/src/Entity/Attachment.php index c47d742736..e08359f31f 100644 --- a/src/Entity/Attachment.php +++ b/src/Entity/Attachment.php @@ -250,7 +250,7 @@ class Attachment extends Entity 'gsactor_id' => ['type' => 'int', 'foreign key' => true, 'target' => 'GSActor.id', 'multiplicity' => 'one to one', 'description' => 'If set, used so each actor can have a version of this file (for avatars, for instance)'], 'mimetype' => ['type' => 'varchar', 'length' => 50, 'description' => 'mime type of resource'], 'title' => ['type' => 'text', 'description' => 'title of resource when available'], - 'filename' => ['type' => 'varchar', 'length' => 191, 'description' => 'title of resource when available'], + 'filename' => ['type' => 'varchar', 'length' => 191, 'description' => 'file name of resource when available'], 'is_local' => ['type' => 'bool', 'description' => 'whether the file is stored locally'], 'source' => ['type' => 'int', 'default' => null, 'description' => 'Source of the Attachment (upload, TFN, embed)'], 'scope' => ['type' => 'int', 'default' => null, 'description' => 'visibility scope for this attachment'], diff --git a/src/Entity/AttachmentThumbnail.php b/src/Entity/AttachmentThumbnail.php index 1d0aa5af69..bd98dcf227 100644 --- a/src/Entity/AttachmentThumbnail.php +++ b/src/Entity/AttachmentThumbnail.php @@ -142,7 +142,9 @@ class AttachmentThumbnail extends Entity public function getFilename() { - return $this->getAttachment()->getFileHash() . "-{$this->width}x{$this->height}.webp"; + // TODO only for images + $ext = image_type_to_extension(IMAGETYPE_WEBP, include_dot: true); + return $this->getAttachment()->getFileHash() . "-{$this->width}x{$this->height}{$ext}"; } public function getPath()