From a2426c23d2ac50a72db3dc5d44f5cb56509f3c99 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Sun, 23 Aug 2026 13:18:38 +0200 Subject: [PATCH 1/3] Add data classes for unfurl url images --- .../templates/shared_unfurlUrl.tpl | 2 +- phpstan-baseline.neon | 12 --- .../lib/data/unfurl/url/UnfurlUrl.class.php | 99 +++++-------------- .../unfurl/url/UnfurlUrlCollection.class.php | 56 +++++++++++ .../data/unfurl/url/UnfurlUrlList.class.php | 32 +----- .../unfurl/url/image/UnfurlUrlImage.class.php | 91 +++++++++++++++++ .../url/image/UnfurlUrlImageBuilder.class.php | 87 ++++++++++++++++ .../image/UnfurlUrlImageCollection.class.php | 51 ++++++++++ .../url/image/UnfurlUrlImageList.class.php | 17 ++++ .../UnfurlUrlRebuildDataWorker.class.php | 16 +-- 10 files changed, 338 insertions(+), 125 deletions(-) create mode 100644 wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlCollection.class.php create mode 100644 wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImage.class.php create mode 100644 wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageBuilder.class.php create mode 100644 wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageCollection.class.php create mode 100644 wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageList.class.php diff --git a/com.woltlab.wcf/templates/shared_unfurlUrl.tpl b/com.woltlab.wcf/templates/shared_unfurlUrl.tpl index 9578393b07c..3deb0ec9ece 100644 --- a/com.woltlab.wcf/templates/shared_unfurlUrl.tpl +++ b/com.woltlab.wcf/templates/shared_unfurlUrl.tpl @@ -8,7 +8,7 @@ *}{if $object->hasSquaredImage()} unfurlUrlCardSquaredImage{/if}{* *}"> {if $object->hasImageUrl()} - + {/if}
url appendClassname=false isUgc=$enableUgc}>{$object->title} diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 02beb3d8280..2b043b26571 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -636,24 +636,12 @@ parameters: count: 1 path: wcfsetup/install/files/lib/data/trophy/TrophyAction.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 4 - path: wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrl.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed count: 3 path: wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlAction.class.php - - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' - identifier: empty.notAllowed - count: 1 - path: wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlList.class.php - - message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#' identifier: empty.notAllowed diff --git a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrl.class.php b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrl.class.php index 34a3aa4d06d..c79a83514aa 100644 --- a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrl.class.php +++ b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrl.class.php @@ -2,12 +2,9 @@ namespace wcf\data\unfurl\url; -use wcf\action\ImageProxyAction; -use wcf\data\DatabaseObject; -use wcf\system\cache\runtime\FileRuntimeCache; -use wcf\system\request\LinkHandler; +use wcf\data\CollectionDatabaseObject; +use wcf\data\unfurl\url\image\UnfurlUrlImage; use wcf\system\WCF; -use wcf\util\CryptoUtil; use wcf\util\Url; /** @@ -23,19 +20,13 @@ * @property-read string $urlHash * @property-read string $title * @property-read ?string $description - * @property-read string $imageHash - * @property-read string $imageUrl - * @property-read ?string $imageUrlHash - * @property-read ?string $imageExtension - * @property-read int $width - * @property-read int $height - * @property-read int $lastFetch * @property-read ?int $imageID - * @property-read int $isStored * @property-read string $status - * @property-read ?int $fileID + * @property-read int $lastFetch + * + * @extends CollectionDatabaseObject */ -class UnfurlUrl extends DatabaseObject +class UnfurlUrl extends CollectionDatabaseObject { private const IMAGE_SQUARED = "SQUARED"; @@ -56,27 +47,6 @@ class UnfurlUrl extends DatabaseObject public const THUMBNAIL_WIDTH = 800; public const THUMBNAIL_HEIGHT = 400; - public function __construct(null|string|int $id, ?array $row = null, ?DatabaseObject $object = null) - { - if ($id !== null) { - $sql = "SELECT unfurl_url.*, unfurl_url_image.* - FROM wcf1_unfurl_url unfurl_url - LEFT JOIN wcf1_unfurl_url_image unfurl_url_image - ON unfurl_url_image.imageID = unfurl_url.imageID - WHERE unfurl_url.urlID = ?"; - $statement = WCF::getDB()->prepare($sql); - $statement->execute([$id]); - $row = $statement->fetchArray(); - - // enforce data type 'array' - if ($row === false) { - $row = []; - } - } - - parent::__construct(null, $row, $id !== null ? null : $object); - } - /** * Renders the unfurl url card and returns the template. */ @@ -98,41 +68,22 @@ public function getHost(): string return $url['host']; } - /** - * Returns the image url for the url. - * - * @throws \wcf\system\exception\SystemException - */ public function getImageUrl(): ?string { - if (\URL_UNFURLING_SAVE_IMAGES !== 0 && $this->isStored !== 0 && $this->fileID !== null) { - $file = FileRuntimeCache::getInstance()->getObject($this->fileID); - - return 'data:image/webp;base64, ' . \file_get_contents($file->getPathname()); - } elseif (!empty($this->imageUrl)) { - if (\MODULE_IMAGE_PROXY !== 0) { - $key = CryptoUtil::createSignedString($this->imageUrl); - - return LinkHandler::getInstance()->getControllerLink(ImageProxyAction::class, [ - 'key' => $key, - ]); - } elseif (\IMAGE_ALLOW_EXTERNAL_SOURCE !== 0) { - return $this->imageUrl; - } + if ($this->imageID === null) { + return null; } - return null; + return $this->getImage()->getImageUrl(); } public function hasImageUrl(): bool { - if (\URL_UNFURLING_SAVE_IMAGES !== 0 && $this->isStored !== 0 && $this->fileID !== null) { - return true; - } elseif (!empty($this->imageUrl) && (\MODULE_IMAGE_PROXY !== 0 || \IMAGE_ALLOW_EXTERNAL_SOURCE !== 0)) { - return true; + if ($this->imageID === null) { + return false; } - return false; + return $this->getImage()->hasImageUrl(); } public function hasCoverImage(): bool @@ -147,7 +98,7 @@ public function hasSquaredImage(): bool public function isPlainUrl(): bool { - return empty($this->description) && empty($this->imageID); + return ($this->description ?? '') === '' && $this->imageID === null; } private function getImageType(): string @@ -156,7 +107,7 @@ private function getImageType(): string return self::IMAGE_NO_IMAGE; } - if ($this->width === $this->height) { + if ($this->getImage()->width === $this->getImage()->height) { return self::IMAGE_SQUARED; } @@ -171,6 +122,14 @@ public function hasFetchedContent(): bool return $this->status === self::STATUS_SUCCESSFUL; } + /** + * @since 6.3 + */ + public function getImage(): ?UnfurlUrlImage + { + return $this->getCollection()->getImage($this); + } + /** * Returns the unfurl url object for a given url. * @@ -182,18 +141,12 @@ public static function getByUrl(string $url): ?self throw new \InvalidArgumentException("Given URL is not a valid URL."); } - $sql = "SELECT unfurl_url.*, unfurl_url_image.* - FROM wcf1_unfurl_url unfurl_url - LEFT JOIN wcf1_unfurl_url_image unfurl_url_image - ON unfurl_url_image.imageID = unfurl_url.imageID - WHERE unfurl_url.urlHash = ?"; + $sql = "SELECT * + FROM wcf1_unfurl_url + WHERE urlHash = ?"; $statement = WCF::getDB()->prepare($sql); $statement->execute([\sha1($url)]); - $row = $statement->fetchArray(); - if ($row === false) { - return null; - } - return new self(null, $row); + return $statement->fetchSingleObject(self::class); } } diff --git a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlCollection.class.php b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlCollection.class.php new file mode 100644 index 00000000000..5640603708c --- /dev/null +++ b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlCollection.class.php @@ -0,0 +1,56 @@ + + * @since 6.3 + * + * @extends DatabaseObjectCollection + */ +class UnfurlUrlCollection extends DatabaseObjectCollection +{ + /** + * @var array + */ + private array $images; + + public function getImage(UnfurlUrl $object): ?UnfurlUrlImage + { + $this->loadImages(); + + return $this->images[$object->imageID] ?? null; + } + + private function loadImages(): void + { + if (isset($this->images)) { + return; + } + $this->images = []; + + $imageIDs = \array_unique(\array_map( + fn($object) => $object->imageID, + \array_filter( + $this->getObjects(), + fn($object) => $object->imageID !== null + ) + )); + if ($imageIDs === []) { + return; + } + + $list = new UnfurlUrlImageList(); + $list->setObjectIDs($imageIDs); + $list->readObjects(); + $this->images = $list->getObjects(); + } +} diff --git a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlList.class.php b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlList.class.php index f50ee5edb64..5c214ff8709 100644 --- a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlList.class.php +++ b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlList.class.php @@ -3,7 +3,6 @@ namespace wcf\data\unfurl\url; use wcf\data\DatabaseObjectList; -use wcf\system\cache\runtime\FileRuntimeCache; /** * Represents a list of unfurled urls. @@ -15,33 +14,4 @@ * * @extends DatabaseObjectList */ -class UnfurlUrlList extends DatabaseObjectList -{ - public function __construct() - { - parent::__construct(); - - if (!empty($this->sqlSelects)) { - $this->sqlSelects .= ','; - } - $this->sqlSelects .= "unfurl_url_image.*"; - $this->sqlJoins .= " - LEFT JOIN wcf1_unfurl_url_image unfurl_url_image - ON unfurl_url_image.imageID = unfurl_url.imageID"; - } - - #[\Override] - public function readObjects() - { - parent::readObjects(); - - $fileIDs = []; - foreach ($this->objects as $object) { - if ($object->fileID !== null) { - $fileIDs[] = $object->fileID; - } - } - - FileRuntimeCache::getInstance()->cacheObjectIDs($fileIDs); - } -} +class UnfurlUrlList extends DatabaseObjectList {} diff --git a/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImage.class.php b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImage.class.php new file mode 100644 index 00000000000..47559cf4c50 --- /dev/null +++ b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImage.class.php @@ -0,0 +1,91 @@ + + * @since 6.3 + * + * @property-read int $imageID + * @property-read string $imageUrl + * @property-read ?string $imageUrlHash + * @property-read int $width + * @property-read int $height + * @property-read ?string $imageExtension + * @property-read int $isStored + * @property-read ?int $fileID + * + * @extends CollectionDatabaseObject + */ +class UnfurlUrlImage extends CollectionDatabaseObject +{ + /** + * Returns the stored file of this image or `null` if the image has not + * been stored locally. + */ + public function getFile(): ?File + { + if ($this->isStored === 0 || $this->fileID === null) { + return null; + } + + return $this->getCollection()->getFile($this); + } + + /** + * Returns the image for the given image url or `null` if there is no such image. + */ + public static function getByImageUrl(string $imageUrl): ?self + { + $sql = "SELECT * + FROM wcf1_unfurl_url_image + WHERE imageUrlHash = ?"; + $statement = WCF::getDB()->prepare($sql); + $statement->execute([\sha1($imageUrl)]); + + return $statement->fetchSingleObject(self::class); + } + + public function getImageUrl(): ?string + { + if (\URL_UNFURLING_SAVE_IMAGES !== 0 && $this->isStored !== 0 && $this->fileID !== null) { + $file = $this->getCollection()->getFile($this); + + return 'data:image/webp;base64, ' . \file_get_contents($file->getPathname()); + } elseif ($this->imageUrl !== '') { + if (\MODULE_IMAGE_PROXY !== 0) { + $key = CryptoUtil::createSignedString($this->imageUrl); + + return LinkHandler::getInstance()->getControllerLink(ImageProxyAction::class, [ + 'key' => $key, + ]); + } elseif (\IMAGE_ALLOW_EXTERNAL_SOURCE !== 0) { + return $this->imageUrl; + } + } + + return null; + } + + public function hasImageUrl(): bool + { + if (\URL_UNFURLING_SAVE_IMAGES !== 0 && $this->isStored !== 0 && $this->fileID !== null) { + return true; + } elseif ($this->imageUrl !== '' && (\MODULE_IMAGE_PROXY !== 0 || \IMAGE_ALLOW_EXTERNAL_SOURCE !== 0)) { + return true; + } + + return false; + } +} diff --git a/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageBuilder.class.php b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageBuilder.class.php new file mode 100644 index 00000000000..b1b4fda45e1 --- /dev/null +++ b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageBuilder.class.php @@ -0,0 +1,87 @@ + + * @since 6.3 + * + * @extends DatabaseObjectBuilder + */ +final class UnfurlUrlImageBuilder extends DatabaseObjectBuilder +{ + /** + * Sets the url of the image and derives the hash used to identify it. + */ + public function setImageUrl(string $imageUrl): static + { + $this->properties['imageUrl'] = $imageUrl; + $this->properties['imageUrlHash'] = \sha1($imageUrl); + + return $this; + } + + public function setWidth(int $width): static + { + $this->properties['width'] = $width; + + return $this; + } + + public function setHeight(int $height): static + { + $this->properties['height'] = $height; + + return $this; + } + + public function setImageExtension(?string $imageExtension): static + { + $this->properties['imageExtension'] = $imageExtension; + + return $this; + } + + public function setFile(?File $file): static + { + $this->properties['fileID'] = $file?->fileID; + $this->properties['isStored'] = $file !== null ? 1 : 0; + + return $this; + } + + #[\Override] + protected function getRequiredProperties(): array + { + return ['imageUrl', 'width', 'height']; + } + + #[\Override] + protected static function beforeDeleteAll(array $objectIDs): void + { + $conditionBuilder = new PreparedStatementConditionBuilder(); + $conditionBuilder->add('imageID IN (?)', [$objectIDs]); + $conditionBuilder->add('fileID IS NOT NULL'); + + $sql = "SELECT fileID + FROM wcf1_unfurl_url_image + " . $conditionBuilder; + $statement = WCF::getDB()->prepare($sql); + $statement->execute($conditionBuilder->getParameters()); + $fileIDs = $statement->fetchAll(\PDO::FETCH_COLUMN); + + if ($fileIDs !== []) { + FileEditor::deleteAll($fileIDs); + } + } +} diff --git a/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageCollection.class.php b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageCollection.class.php new file mode 100644 index 00000000000..0f2906008e0 --- /dev/null +++ b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageCollection.class.php @@ -0,0 +1,51 @@ + + * @since 6.3 + * + * @extends DatabaseObjectCollection + */ +class UnfurlUrlImageCollection extends DatabaseObjectCollection +{ + private bool $filesCached = false; + + public function getFile(UnfurlUrlImage $object): ?File + { + $this->cacheFiles(); + + return FileRuntimeCache::getInstance()->getObject($object->fileID); + } + + private function cacheFiles(): void + { + if ($this->filesCached) { + return; + } + + $this->filesCached = true; + + $fileIDs = \array_unique(\array_map( + fn($object) => $object->fileID, + \array_filter( + $this->getObjects(), + fn($object) => $object->fileID !== null + ) + )); + if ($fileIDs === []) { + return; + } + + FileRuntimeCache::getInstance()->cacheObjectIDs($fileIDs); + } +} diff --git a/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageList.class.php b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageList.class.php new file mode 100644 index 00000000000..9554b3600f1 --- /dev/null +++ b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageList.class.php @@ -0,0 +1,17 @@ + + * @since 6.3 + * + * @extends DatabaseObjectList + */ +class UnfurlUrlImageList extends DatabaseObjectList {} diff --git a/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php index e277e88c13f..cacf4218b0d 100644 --- a/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php +++ b/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php @@ -49,26 +49,26 @@ public function execute() $deleteFileIDs = []; $cleanUpImageIDs = []; foreach ($this->getObjectList()->getObjects() as $unfurlUrl) { - if ($unfurlUrl->isStored === 0 || $unfurlUrl->imageID === null) { + if ($unfurlUrl->imageID === null || $unfurlUrl->getImage()->isStored === 0) { continue; } if (\URL_UNFURLING_SAVE_IMAGES === 0) { // delete stored images - if ($unfurlUrl->fileID !== null) { - $deleteFileIDs[] = $unfurlUrl->fileID; + if ($unfurlUrl->getImage()->fileID !== null) { + $deleteFileIDs[] = $unfurlUrl->getImage()->fileID; } else { $fileLocation = $this->getOldFileLocation($unfurlUrl); @\unlink($fileLocation); } $cleanUpImageIDs[] = $unfurlUrl->imageID; - } elseif ($unfurlUrl->fileID === null) { + } elseif ($unfurlUrl->getImage()->fileID === null) { $fileLocation = $this->getOldFileLocation($unfurlUrl); $file = UnfurlUrlEditor::saveUnfurlImage( $fileLocation, - \pathinfo($unfurlUrl->imageUrl, \PATHINFO_FILENAME) + \pathinfo($unfurlUrl->getImage()->imageUrl, \PATHINFO_FILENAME) ); @\unlink($fileLocation); @@ -101,9 +101,9 @@ private function getOldFileLocation(UnfurlUrl $unfurlUrl): string '%s%s%s/%s.%s', \WCF_DIR, UnfurlUrl::IMAGE_DIR, - \substr($unfurlUrl->imageUrlHash, 0, 2), - $unfurlUrl->imageUrlHash, - $unfurlUrl->imageExtension + \substr($unfurlUrl->getImage()->imageUrlHash, 0, 2), + $unfurlUrl->getImage()->imageUrlHash, + $unfurlUrl->getImage()->imageExtension ); } } From 55ac5a14af90e65749318749c174a062af7235db Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Mon, 24 Aug 2026 00:00:01 +0200 Subject: [PATCH 2/3] Add builder-based creation and update of unfurl urls --- .../unfurl/url/CreateUnfurlUrl.class.php | 32 ++++++ .../url/FindOrCreateUnfurlUrl.class.php | 13 +-- .../data/unfurl/url/UnfurlUrlAction.class.php | 1 + .../unfurl/url/UnfurlUrlBuilder.class.php | 103 ++++++++++++++++++ .../unfurl/url/image/UnfurlUrlImage.class.php | 2 +- .../url/image/UnfurlUrlImageBuilder.class.php | 7 +- .../unfurl/url/UnfurlUrlCreated.class.php | 23 ++++ .../job/UnfurlUrlBackgroundJob.class.php | 36 +++--- 8 files changed, 185 insertions(+), 32 deletions(-) create mode 100644 wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrl.class.php create mode 100644 wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlBuilder.class.php create mode 100644 wcfsetup/install/files/lib/event/unfurl/url/UnfurlUrlCreated.class.php diff --git a/wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrl.class.php b/wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrl.class.php new file mode 100644 index 00000000000..50f45311e26 --- /dev/null +++ b/wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrl.class.php @@ -0,0 +1,32 @@ + + * @since 6.3 + */ +final class CreateUnfurlUrl +{ + public function __construct( + private readonly UnfurlUrlBuilder $builder, + ) {} + + public function __invoke(): UnfurlUrl + { + $unfurlUrl = $this->builder->create(); + + EventHandler::getInstance()->fire(new UnfurlUrlCreated($unfurlUrl, $this->builder)); + + return $unfurlUrl; + } +} diff --git a/wcfsetup/install/files/lib/command/unfurl/url/FindOrCreateUnfurlUrl.class.php b/wcfsetup/install/files/lib/command/unfurl/url/FindOrCreateUnfurlUrl.class.php index d63fb1afda6..ab44b4ededc 100644 --- a/wcfsetup/install/files/lib/command/unfurl/url/FindOrCreateUnfurlUrl.class.php +++ b/wcfsetup/install/files/lib/command/unfurl/url/FindOrCreateUnfurlUrl.class.php @@ -3,7 +3,7 @@ namespace wcf\command\unfurl\url; use wcf\data\unfurl\url\UnfurlUrl; -use wcf\data\unfurl\url\UnfurlUrlAction; +use wcf\data\unfurl\url\UnfurlUrlBuilder; use wcf\system\background\BackgroundQueueHandler; use wcf\system\background\job\UnfurlUrlBackgroundJob; @@ -28,15 +28,8 @@ public function __invoke(): UnfurlUrl $object = UnfurlUrl::getByUrl($this->url); if ($object === null) { - $returnValues = (new UnfurlUrlAction([], 'create', [ - 'data' => [ - 'url' => $this->url, - 'urlHash' => \sha1($this->url), - ], - ]))->executeAction(); - - $object = $returnValues['returnValues']; - \assert($object instanceof UnfurlUrl); + $object = new CreateUnfurlUrl(UnfurlUrlBuilder::forCreate() + ->setUrl($this->url))(); } if ($object->status !== UnfurlUrl::STATUS_PENDING && $object->lastFetch < \TIME_NOW - self::REFETCH_UNFURL_URL) { diff --git a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlAction.class.php b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlAction.class.php index 6334f6c920a..2d98c9ea325 100644 --- a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlAction.class.php +++ b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlAction.class.php @@ -15,6 +15,7 @@ * @copyright 2001-2021 WoltLab GmbH * @license GNU Lesser General Public License * @since 5.4 + * @deprecated 6.3 * * @extends AbstractDatabaseObjectAction */ diff --git a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlBuilder.class.php b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlBuilder.class.php new file mode 100644 index 00000000000..4383b5af490 --- /dev/null +++ b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlBuilder.class.php @@ -0,0 +1,103 @@ + + * @since 6.3 + * + * @extends DatabaseObjectBuilder + */ +final class UnfurlUrlBuilder extends DatabaseObjectBuilder +{ + /** + * Sets the url and derives the hash used to identify it. + * + * @throws \InvalidArgumentException if the given url is invalid + */ + public function setUrl(string $url): static + { + if (!Url::is($url)) { + throw new \InvalidArgumentException("Given URL is not a valid URL."); + } + + $this->properties['url'] = $url; + $this->properties['urlHash'] = \sha1($url); + + return $this; + } + + public function setTitle(string $title): static + { + $this->properties['title'] = $title; + + return $this; + } + + public function setDescription(?string $description): static + { + $this->properties['description'] = $description; + + return $this; + } + + public function setImageID(?int $imageID): static + { + $this->properties['imageID'] = $imageID; + + return $this; + } + + /** + * @throws \InvalidArgumentException if the given status is unknown + */ + public function setStatus(string $status): static + { + switch ($status) { + case UnfurlUrl::STATUS_PENDING: + case UnfurlUrl::STATUS_REJECTED: + case UnfurlUrl::STATUS_SUCCESSFUL: + break; + + default: + throw new \InvalidArgumentException("Invalid status '{$status}' given."); + } + + $this->properties['status'] = $status; + + return $this; + } + + public function setLastFetch(int $lastFetch): static + { + $this->properties['lastFetch'] = $lastFetch; + + return $this; + } + + #[\Override] + protected function afterCreate(DatabaseObject $object): void + { + BackgroundQueueHandler::getInstance()->enqueueIn([ + new UnfurlUrlBackgroundJob($object), + ]); + + BackgroundQueueHandler::getInstance()->forceCheck(); + } + + #[\Override] + protected function getRequiredProperties(): array + { + return ['url', 'urlHash']; + } +} diff --git a/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImage.class.php b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImage.class.php index 47559cf4c50..6f8e24c3b05 100644 --- a/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImage.class.php +++ b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImage.class.php @@ -22,7 +22,7 @@ * @property-read ?string $imageUrlHash * @property-read int $width * @property-read int $height - * @property-read ?string $imageExtension + * @property-read ?string $imageExtension deprecated, only in use to migrate old files * @property-read int $isStored * @property-read ?int $fileID * diff --git a/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageBuilder.class.php b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageBuilder.class.php index b1b4fda45e1..2f623247ae7 100644 --- a/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageBuilder.class.php +++ b/wcfsetup/install/files/lib/data/unfurl/url/image/UnfurlUrlImageBuilder.class.php @@ -3,7 +3,6 @@ namespace wcf\data\unfurl\url\image; use wcf\data\DatabaseObjectBuilder; -use wcf\data\file\File; use wcf\data\file\FileEditor; use wcf\system\database\util\PreparedStatementConditionBuilder; use wcf\system\WCF; @@ -52,10 +51,10 @@ public function setImageExtension(?string $imageExtension): static return $this; } - public function setFile(?File $file): static + public function setFileID(?int $fileID): static { - $this->properties['fileID'] = $file?->fileID; - $this->properties['isStored'] = $file !== null ? 1 : 0; + $this->properties['fileID'] = $fileID; + $this->properties['isStored'] = $fileID !== null ? 1 : 0; return $this; } diff --git a/wcfsetup/install/files/lib/event/unfurl/url/UnfurlUrlCreated.class.php b/wcfsetup/install/files/lib/event/unfurl/url/UnfurlUrlCreated.class.php new file mode 100644 index 00000000000..80c928eab72 --- /dev/null +++ b/wcfsetup/install/files/lib/event/unfurl/url/UnfurlUrlCreated.class.php @@ -0,0 +1,23 @@ + + * @since 6.3 + */ +final class UnfurlUrlCreated implements IPsr14Event +{ + public function __construct( + public readonly UnfurlUrl $unfurlUrl, + public readonly UnfurlUrlBuilder $builder, + ) {} +} diff --git a/wcfsetup/install/files/lib/system/background/job/UnfurlUrlBackgroundJob.class.php b/wcfsetup/install/files/lib/system/background/job/UnfurlUrlBackgroundJob.class.php index 6966de48e13..b1fa791bc60 100644 --- a/wcfsetup/install/files/lib/system/background/job/UnfurlUrlBackgroundJob.class.php +++ b/wcfsetup/install/files/lib/system/background/job/UnfurlUrlBackgroundJob.class.php @@ -4,8 +4,9 @@ use Psr\Http\Message\ResponseInterface; use wcf\data\file\File; +use wcf\data\unfurl\url\image\UnfurlUrlImageBuilder; use wcf\data\unfurl\url\UnfurlUrl; -use wcf\data\unfurl\url\UnfurlUrlAction; +use wcf\data\unfurl\url\UnfurlUrlBuilder; use wcf\data\unfurl\url\UnfurlUrlEditor; use wcf\system\message\unfurl\exception\DownloadFailed; use wcf\system\message\unfurl\exception\ParsingFailed; @@ -122,9 +123,7 @@ public function perform(): void /** * @return array{ * imageUrl: string, - * imageUrlHash: string, * fileID: ?int, - * isStored: 1|0, * width: int, * height: int, * }|array{} @@ -171,9 +170,7 @@ private function getImageData(UnfurlResponse $unfurlResponse): array return [ 'imageUrl' => $unfurlResponse->getImageUrl(), - 'imageUrlHash' => \sha1($unfurlResponse->getImageUrl()), 'fileID' => $file?->fileID, - 'isStored' => $file !== null ? 1 : 0, 'width' => $width, 'height' => $height, ]; @@ -280,7 +277,7 @@ private function getImageExtension(array $imageData): ?string } /** - * @param array{imageUrl: string, imageUrlHash: string, fileID: ?int, isStored: 1|0, width: int, height: int}|array{} $imageData + * @param array{imageUrl: string, fileID: ?int, width: int, height: int}|array{} $imageData */ private function save( string $status, @@ -303,17 +300,22 @@ private function save( throw new \BadMethodCallException("You cannot pass an imageID and imageData at the same time."); } - $urlAction = new UnfurlUrlAction([$this->urlID], 'update', [ - 'data' => [ - 'status' => $status, - 'title' => $title, - 'description' => $description, - 'imageID' => $imageID, - 'lastFetch' => \TIME_NOW, - ], - 'imageData' => $imageData, - ]); - $urlAction->executeAction(); + if ($imageData !== []) { + $imageID = UnfurlUrlImageBuilder::forCreate() + ->setImageUrl($imageData['imageUrl']) + ->setFileID($imageData['fileID']) + ->setWidth($imageData['width']) + ->setHeight($imageData['height']) + ->create()->imageID; + } + + UnfurlUrlBuilder::forUpdate(new UnfurlUrl($this->urlID)) + ->setStatus($status) + ->setTitle($title) + ->setDescription($description) + ->setImageID($imageID) + ->setLastFetch(\TIME_NOW) + ->update(); } #[\Override] From 040768b5698342a6bae2f6d70dfe0ece02c864e9 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Mon, 24 Aug 2026 18:04:28 +0200 Subject: [PATCH 3/3] Move unfurl image creation into `CreateUnfurlUrlImageFile` command --- .../url/CreateUnfurlUrlImageFile.class.php | 89 +++++++++++++++++++ .../data/unfurl/url/UnfurlUrlAction.class.php | 2 +- .../data/unfurl/url/UnfurlUrlEditor.class.php | 60 ++----------- .../job/UnfurlUrlBackgroundJob.class.php | 8 +- .../UnfurlUrlRebuildDataWorker.class.php | 6 +- 5 files changed, 102 insertions(+), 63 deletions(-) create mode 100644 wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrlImageFile.class.php diff --git a/wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrlImageFile.class.php b/wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrlImageFile.class.php new file mode 100644 index 00000000000..911d35fb404 --- /dev/null +++ b/wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrlImageFile.class.php @@ -0,0 +1,89 @@ + + * @since 6.3 + */ +final class CreateUnfurlUrlImageFile +{ + public function __construct( + private readonly string $pathname, + private readonly string $originalFilename, + ) {} + + public function __invoke(): ?File + { + $imageData = @\getimagesize($this->pathname); + if ($imageData === false) { + return null; + } + + $imageAdapter = ImageHandler::getInstance()->getAdapter(); + if (!$imageAdapter->checkMemoryLimit($imageData[0], $imageData[1], $imageData['mime'])) { + return null; + } + + $webpFile = FileUtil::getTemporaryFilename(extension: 'webp'); + $binFile = FileUtil::getTemporaryFilename(extension: 'bin'); + + try { + $imageAdapter->loadFile($this->pathname); + $thumbnail = $imageAdapter->createThumbnail(UnfurlUrl::THUMBNAIL_WIDTH, UnfurlUrl::THUMBNAIL_HEIGHT); + $imageAdapter->saveImageAs($thumbnail, $webpFile, 'webp', 80); + + // Clean up the thumbnail + $thumbnail = null; + + $webpContent = \file_get_contents($webpFile); + if ($webpContent === false) { + return null; + } + + // Save the webp file as a base64 encoded binary file + \file_put_contents($binFile, \base64_encode($webpContent)); + + return FileEditor::createFromExistingFile( + $binFile, + \pathinfo($this->originalFilename, \PATHINFO_BASENAME) . ".bin", + 'com.woltlab.wcf.unfurl' + ); + } catch (SystemException | ImageNotReadable $e) { + return null; + } catch (ImageNotProcessable $e) { + logThrowable($e); + + return null; + } catch (\Throwable $e) { + logThrowable($e); + // Ignore any errors trying to save the file unless in debug mode. + if (\ENABLE_DEBUG_MODE !== 0) { + throw $e; + } + + return null; + } finally { + // Clean up temporary files + @\unlink($webpFile); + @\unlink($binFile); + } + } +} diff --git a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlAction.class.php b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlAction.class.php index 2d98c9ea325..d710dd24e0e 100644 --- a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlAction.class.php +++ b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlAction.class.php @@ -15,7 +15,7 @@ * @copyright 2001-2021 WoltLab GmbH * @license GNU Lesser General Public License * @since 5.4 - * @deprecated 6.3 + * @deprecated 6.3 Use `UnfurlUrlBuilder` instead. * * @extends AbstractDatabaseObjectAction */ diff --git a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlEditor.class.php b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlEditor.class.php index 5c993e4cc19..88c983d54be 100644 --- a/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlEditor.class.php +++ b/wcfsetup/install/files/lib/data/unfurl/url/UnfurlUrlEditor.class.php @@ -2,16 +2,9 @@ namespace wcf\data\unfurl\url; +use wcf\command\unfurl\url\CreateUnfurlUrlImageFile; use wcf\data\DatabaseObjectEditor; use wcf\data\file\File; -use wcf\data\file\FileEditor; -use wcf\system\exception\SystemException; -use wcf\system\image\adapter\exception\ImageNotProcessable; -use wcf\system\image\adapter\exception\ImageNotReadable; -use wcf\system\image\ImageHandler; -use wcf\util\FileUtil; - -use function wcf\functions\exception\logThrowable; /** * Provide functions to edit an unfurl url. @@ -20,6 +13,7 @@ * @copyright 2001-2021 WoltLab GmbH * @license GNU Lesser General Public License * @since 5.4 + * @deprecated 6.3 Use `UnfurlUrlBuilder` instead. * * @mixin UnfurlUrl * @extends DatabaseObjectEditor @@ -33,55 +27,11 @@ class UnfurlUrlEditor extends DatabaseObjectEditor /** * Creates a webp thumbnail for the given file and saves it base64 encoded in a new `.bin` file. + * + * @deprecated 6.3 Use `CreateUnfurlUrlImageFile` command instead. */ public static function saveUnfurlImage(string $file, string $originalFile): ?File { - $imageData = @\getimagesize($file); - if ($imageData === false) { - return null; - } - - $imageAdapter = ImageHandler::getInstance()->getAdapter(); - if (!$imageAdapter->checkMemoryLimit($imageData[0], $imageData[1], $imageData['mime'])) { - return null; - } - $webpFile = FileUtil::getTemporaryFilename(extension: 'webp'); - $binFile = FileUtil::getTemporaryFilename(extension: 'bin'); - - try { - $imageAdapter->loadFile($file); - $thumbnail = $imageAdapter->createThumbnail(UnfurlUrl::THUMBNAIL_WIDTH, UnfurlUrl::THUMBNAIL_HEIGHT); - $imageAdapter->saveImageAs($thumbnail, $webpFile, 'webp', 80); - - // Clean up the thumbnail - $thumbnail = null; - - // Save the webp file as a base64 encoded binary file - \file_put_contents($binFile, \base64_encode(\file_get_contents($webpFile))); - - return FileEditor::createFromExistingFile( - $binFile, - \pathinfo($originalFile, \PATHINFO_BASENAME) . ".bin", - 'com.woltlab.wcf.unfurl' - ); - } catch (SystemException | ImageNotReadable $e) { - return null; - } catch (ImageNotProcessable $e) { - logThrowable($e); - - return null; - } catch (\Throwable $e) { - logThrowable($e); - // Ignore any errors trying to save the file unless in debug mode. - if (\ENABLE_DEBUG_MODE !== 0) { - throw $e; - } - - return null; - } finally { - // Clean up temporary files - @\unlink($webpFile); - @\unlink($binFile); - } + return new CreateUnfurlUrlImageFile($file, $originalFile)(); } } diff --git a/wcfsetup/install/files/lib/system/background/job/UnfurlUrlBackgroundJob.class.php b/wcfsetup/install/files/lib/system/background/job/UnfurlUrlBackgroundJob.class.php index b1fa791bc60..6120dbb6487 100644 --- a/wcfsetup/install/files/lib/system/background/job/UnfurlUrlBackgroundJob.class.php +++ b/wcfsetup/install/files/lib/system/background/job/UnfurlUrlBackgroundJob.class.php @@ -3,11 +3,11 @@ namespace wcf\system\background\job; use Psr\Http\Message\ResponseInterface; +use wcf\command\unfurl\url\CreateUnfurlUrlImageFile; use wcf\data\file\File; use wcf\data\unfurl\url\image\UnfurlUrlImageBuilder; use wcf\data\unfurl\url\UnfurlUrl; use wcf\data\unfurl\url\UnfurlUrlBuilder; -use wcf\data\unfurl\url\UnfurlUrlEditor; use wcf\system\message\unfurl\exception\DownloadFailed; use wcf\system\message\unfurl\exception\ParsingFailed; use wcf\system\message\unfurl\exception\UrlInaccessible; @@ -154,7 +154,7 @@ private function getImageData(UnfurlResponse $unfurlResponse): array $width = $height = 0; if ($file !== null) { - // The stored file contains the base64 encoded image, see UnfurlUrlEditor::saveUnfurlImage(). + // The stored file contains the base64 encoded image, see CreateUnfurlUrlImageFile. $decodedImage = \base64_decode( \file_get_contents($file->getPathname()), true @@ -243,14 +243,14 @@ private function createFile(array $imageData, string $originalFile, string $imag $tmp = FileUtil::getTemporaryFilename(extension: $extension); \file_put_contents($tmp, $image); - $file = UnfurlUrlEditor::saveUnfurlImage( + $file = new CreateUnfurlUrlImageFile( $tmp, \sprintf( "%s.%s", $originalFile, $extension ) - ); + )(); // Clean up temporary files @\unlink($tmp); diff --git a/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php index cacf4218b0d..7200360f74e 100644 --- a/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php +++ b/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php @@ -2,9 +2,9 @@ namespace wcf\system\worker; +use wcf\command\unfurl\url\CreateUnfurlUrlImageFile; use wcf\data\file\FileEditor; use wcf\data\unfurl\url\UnfurlUrl; -use wcf\data\unfurl\url\UnfurlUrlEditor; use wcf\data\unfurl\url\UnfurlUrlList; use wcf\system\database\util\PreparedStatementConditionBuilder; use wcf\system\WCF; @@ -66,10 +66,10 @@ public function execute() } elseif ($unfurlUrl->getImage()->fileID === null) { $fileLocation = $this->getOldFileLocation($unfurlUrl); - $file = UnfurlUrlEditor::saveUnfurlImage( + $file = new CreateUnfurlUrlImageFile( $fileLocation, \pathinfo($unfurlUrl->getImage()->imageUrl, \PATHINFO_FILENAME) - ); + )(); @\unlink($fileLocation);