From 62358702f8dc105aa81d6a9486efa136dab5de83 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Sun, 13 Sep 2026 21:56:32 +0200 Subject: [PATCH 1/4] add DatabaseObject::findByIDs() for bulk loading objects by id --- .../files/lib/data/DatabaseObject.class.php | 28 +++++++++++++++++++ .../data/DatabaseObjectDecorator.class.php | 16 ++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/wcfsetup/install/files/lib/data/DatabaseObject.class.php b/wcfsetup/install/files/lib/data/DatabaseObject.class.php index 97a9419261f..2b56669ffaa 100644 --- a/wcfsetup/install/files/lib/data/DatabaseObject.class.php +++ b/wcfsetup/install/files/lib/data/DatabaseObject.class.php @@ -2,6 +2,7 @@ namespace wcf\data; +use wcf\system\database\util\PreparedStatementConditionBuilder; use wcf\system\WCF; /** @@ -74,6 +75,33 @@ public function __construct(null|string|int $id, ?array $row = null, ?self $obje $this->handleData($row ?? []); } + /** + * Returns the objects with the given ids, indexed by their object id. + * + * Ids without a matching object are silently omitted from the result. + * + * @param int[] $ids + * @return static[] + * @since 6.3 + */ + public static function findByIDs(array $ids): array + { + if ($ids === []) { + return []; + } + + $conditionBuilder = new PreparedStatementConditionBuilder(); + $conditionBuilder->add(static::getDatabaseTableIndexName() . " IN (?)", [$ids]); + + $sql = "SELECT * + FROM " . static::getDatabaseTableName() . " + " . $conditionBuilder; + $statement = WCF::getDB()->prepare($sql); + $statement->execute($conditionBuilder->getParameters()); + + return $statement->fetchObjects(static::class, static::getDatabaseTableIndexName()); + } + /** * Stores the data of a database row. * diff --git a/wcfsetup/install/files/lib/data/DatabaseObjectDecorator.class.php b/wcfsetup/install/files/lib/data/DatabaseObjectDecorator.class.php index eb0863872de..a30531070b7 100644 --- a/wcfsetup/install/files/lib/data/DatabaseObjectDecorator.class.php +++ b/wcfsetup/install/files/lib/data/DatabaseObjectDecorator.class.php @@ -91,6 +91,20 @@ public function __call(string $name, array $arguments) return \call_user_func_array([$this->object, $name], $arguments); } + /** + * @since 6.3 + */ + #[\Override] + public static function findByIDs(array $ids): array + { + $baseClass = static::getBaseClass(); + + return \array_map( + static fn($object) => new static($object), + $baseClass::findByIDs($ids) + ); + } + #[\Override] public static function getDatabaseTableAlias() { @@ -118,7 +132,7 @@ public static function getDatabaseTableIndexName() /** * Returns the name of the base class. * - * @return string + * @return class-string */ public static function getBaseClass() { From 978ce4a0338480685b186d375c5412ab5c228893 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Sun, 13 Sep 2026 22:19:52 +0200 Subject: [PATCH 2/4] Add `FileBuilder` and deprecate `FileAction`/`FileEditor` --- .../file/CreateFileFromExistingFile.class.php | 120 ++++++++ .../file/CreateFileFromTemporary.class.php | 97 +++++++ .../lib/command/file/DeleteFile.class.php | 25 ++ .../lib/command/file/DeleteFiles.class.php | 45 +++ .../command/file/ReplaceFileSource.class.php | 43 ++- .../url/CreateUnfurlUrlImageFile.class.php | 7 +- .../lib/command/user/SetAvatar.class.php | 4 +- .../lib/command/user/SetCoverPhoto.class.php | 4 +- .../attachment/AttachmentBuilder.class.php | 4 +- .../files/lib/data/file/FileAction.class.php | 1 + .../files/lib/data/file/FileBuilder.class.php | 185 +++++++++++++ .../files/lib/data/file/FileEditor.class.php | 259 ++---------------- .../url/image/UnfurlUrlImageBuilder.class.php | 4 +- .../files/lib/data/user/UserAction.class.php | 8 +- .../user/cover/photo/UserCoverPhoto.class.php | 6 +- .../lib/event/file/FileDeleted.class.php | 21 ++ .../cronjob/FileCleanUpCronjob.class.php | 4 +- .../core/files/DeleteFile.class.php | 4 +- .../core/files/upload/SaveChunk.class.php | 7 +- .../file/processor/FileProcessor.class.php | 39 ++- .../AbstractAttachmentImporter.class.php | 7 +- .../importer/AbstractFileImporter.class.php | 10 +- .../AttachmentRebuildDataWorker.class.php | 7 +- .../worker/FileRebuildDataWorker.class.php | 34 ++- .../UnfurlUrlRebuildDataWorker.class.php | 4 +- .../worker/UserRebuildDataWorker.class.php | 11 +- .../install/files/lib/util/ExifUtil.class.php | 59 ++++ .../files/lib/util/ImageUtil.class.php | 14 + 28 files changed, 689 insertions(+), 344 deletions(-) create mode 100644 wcfsetup/install/files/lib/command/file/CreateFileFromExistingFile.class.php create mode 100644 wcfsetup/install/files/lib/command/file/CreateFileFromTemporary.class.php create mode 100644 wcfsetup/install/files/lib/command/file/DeleteFile.class.php create mode 100644 wcfsetup/install/files/lib/command/file/DeleteFiles.class.php create mode 100644 wcfsetup/install/files/lib/data/file/FileBuilder.class.php create mode 100644 wcfsetup/install/files/lib/event/file/FileDeleted.class.php diff --git a/wcfsetup/install/files/lib/command/file/CreateFileFromExistingFile.class.php b/wcfsetup/install/files/lib/command/file/CreateFileFromExistingFile.class.php new file mode 100644 index 00000000000..ac26610be49 --- /dev/null +++ b/wcfsetup/install/files/lib/command/file/CreateFileFromExistingFile.class.php @@ -0,0 +1,120 @@ + + * @since 6.3 + */ +final class CreateFileFromExistingFile +{ + /** + * @param null|array> $exifData + */ + public function __construct( + private readonly string $pathname, + private readonly string $originalFilename, + private readonly string $objectTypeName, + private readonly bool $copy = false, + private readonly ?int $uploadTime = null, + private readonly ?array $exifData = null, + ) {} + + public function __invoke(): ?File + { + if (!\is_readable($this->pathname)) { + return null; + } + + $objectType = FileProcessor::getInstance()->getObjectType($this->objectTypeName); + if ($objectType === null) { + throw new \RuntimeException("The object type '{$this->objectTypeName}' is not valid."); + } + + $mimeType = FileUtil::getMimeType($this->pathname); + $isImage = ImageUtil::isImageMimeType($mimeType); + + if ($this->exifData === null) { + $exifData = ExifUtil::getExifData($this->pathname); + + // Remove the `FILE` and `COMPUTED` section because those contain + // garbled data anyway and we do not need them in the first place. + unset($exifData['FILE'], $exifData['COMPUTED']); + + // We can also discard the `THUMBNAIL` section because it is a + // pointless feature and we’re not extracting it either. + unset($exifData['THUMBNAIL']); + + if ($exifData === []) { + $exifData = null; + } + } else { + $exifData = $this->exifData; + } + + $width = $height = null; + if ($isImage) { + try { + [$width, $height] = \getimagesize($this->pathname); + } catch (\Throwable) { + return null; + } + + $imageWasModified = false; + try { + $imageWasModified = ExifUtil::normalizeImageRotation( + $this->pathname, + $width, + $height, + $mimeType, + $exifData, + ); + + if ($imageWasModified && $exifData !== null) { + unset($exifData['IFD0']['Orientation']); + } + } catch (\Throwable) { + } + + if ($imageWasModified) { + [$width, $height] = \getimagesize($this->pathname); + } + } + + $file = FileBuilder::forCreate() + ->setFilename($this->originalFilename) + ->setFileSize(\filesize($this->pathname)) + ->setFileHash(\hash_file('sha256', $this->pathname)) + ->setFileExtension(File::getSafeFileExtension($mimeType, $this->originalFilename)) + ->setObjectType($objectType) + ->setMimeType($mimeType) + ->setDimensions($width, $height) + ->setUploadTime($this->uploadTime) + ->setExifData($exifData) + ->create(); + + $filePath = $file->getPath(); + if (!\is_dir($filePath)) { + \mkdir($filePath, recursive: true); + } + + if ($this->copy) { + \copy($this->pathname, $filePath . $file->getSourceFilename()); + } else { + \rename($this->pathname, $filePath . $file->getSourceFilename()); + } + + return $file; + } +} diff --git a/wcfsetup/install/files/lib/command/file/CreateFileFromTemporary.class.php b/wcfsetup/install/files/lib/command/file/CreateFileFromTemporary.class.php new file mode 100644 index 00000000000..f6e08ca64f8 --- /dev/null +++ b/wcfsetup/install/files/lib/command/file/CreateFileFromTemporary.class.php @@ -0,0 +1,97 @@ + + * @since 6.3 + */ +final class CreateFileFromTemporary +{ + public function __construct( + private readonly FileTemporary $fileTemporary, + ) {} + + public function __invoke(): File + { + $pathname = $this->fileTemporary->getPathname(); + $mimeType = FileUtil::getMimeType($pathname); + $isImage = ImageUtil::isImageMimeType($mimeType); + + $width = $height = null; + if ($isImage) { + [$width, $height] = \getimagesize($pathname); + } + + $exifData = $this->fileTemporary->exifData; + if ($exifData !== null) { + $exifData = \unserialize($exifData); + } + + $fileSize = $this->fileTemporary->fileSize; + $fileHash = $this->fileTemporary->fileHash; + if ($isImage) { + $imageWasModified = false; + try { + $imageWasModified = ExifUtil::normalizeImageRotation( + $pathname, + $width, + $height, + $mimeType, + $exifData, + ); + + if ($imageWasModified && $exifData !== null) { + unset($exifData['IFD0']['Orientation']); + } + } catch (\Throwable) { + } + + if ($imageWasModified) { + $fileSize = \filesize($pathname); + $fileHash = \hash_file('sha256', $pathname); + [$width, $height] = \getimagesize($pathname); + } + } + + $file = FileBuilder::forCreate() + ->setFilename($this->fileTemporary->filename) + ->setFileSize($fileSize) + ->setFileHash($fileHash) + ->setFileExtension(File::getSafeFileExtension($mimeType, $this->fileTemporary->filename)) + ->setObjectTypeID($this->fileTemporary->objectTypeID) + ->setMimeType($mimeType) + ->setDimensions($width, $height) + ->setUploadTime(\TIME_NOW) + ->setExifData($exifData) + ->create(); + + $filePath = $file->getPath(); + if (!\is_dir($filePath)) { + \mkdir($filePath, recursive: true); + } + + \rename( + $pathname, + $filePath . $file->getSourceFilename() + ); + + $event = new UploadCompleted($file); + EventHandler::getInstance()->fire($event); + + return $event->getFile(); + } +} diff --git a/wcfsetup/install/files/lib/command/file/DeleteFile.class.php b/wcfsetup/install/files/lib/command/file/DeleteFile.class.php new file mode 100644 index 00000000000..a9c3064e245 --- /dev/null +++ b/wcfsetup/install/files/lib/command/file/DeleteFile.class.php @@ -0,0 +1,25 @@ + + * @since 6.3 + */ +final class DeleteFile +{ + public function __construct( + private readonly File $file, + ) {} + + public function __invoke(): void + { + new DeleteFiles([$this->file])(); + } +} diff --git a/wcfsetup/install/files/lib/command/file/DeleteFiles.class.php b/wcfsetup/install/files/lib/command/file/DeleteFiles.class.php new file mode 100644 index 00000000000..813a191b6ec --- /dev/null +++ b/wcfsetup/install/files/lib/command/file/DeleteFiles.class.php @@ -0,0 +1,45 @@ + + * @since 6.3 + */ +final class DeleteFiles +{ + /** + * @param non-empty-list $files + */ + public function __construct( + private readonly array $files, + ) {} + + public function __invoke(): void + { + FileProcessor::getInstance()->delete( + $this->files + ); + + FileBuilder::deleteAll(\array_map( + static fn($object) => $object->fileID, + $this->files + )); + + foreach ($this->files as $file) { + EventHandler::getInstance()->fire( + new FileDeleted($file) + ); + } + } +} diff --git a/wcfsetup/install/files/lib/command/file/ReplaceFileSource.class.php b/wcfsetup/install/files/lib/command/file/ReplaceFileSource.class.php index 8f5b6998bae..bf58aadc7e7 100644 --- a/wcfsetup/install/files/lib/command/file/ReplaceFileSource.class.php +++ b/wcfsetup/install/files/lib/command/file/ReplaceFileSource.class.php @@ -3,7 +3,7 @@ namespace wcf\command\file; use wcf\data\file\File; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; use wcf\system\file\processor\exception\DamagedImage; use wcf\system\file\processor\FileProcessor; use wcf\system\WCF; @@ -101,17 +101,14 @@ private function replaceSource(): File $statement = WCF::getDB()->prepare($sql); $statement->execute([$this->file->fileID]); - (new FileEditor($this->file))->update([ - 'filename' => $filename, - 'fileSize' => $fileSize, - 'fileHash' => $fileHash, - 'fileExtension' => $fileExtension, - 'mimeType' => $mimeType, - 'width' => $width, - 'height' => $height, - ]); - - $updatedFile = new File($this->file->fileID); + $updatedFile = FileBuilder::forUpdate($this->file) + ->setFilename($filename) + ->setFileSize($fileSize) + ->setFileHash($fileHash) + ->setFileExtension($fileExtension) + ->setMimeType($mimeType) + ->setDimensions($width, $height) + ->update(); $path = \dirname($updatedFile->getPathname()); FileUtil::makePath($path); @@ -135,10 +132,9 @@ private function replaceSource(): File // original version with it. @\unlink($webpVariant); - (new FileEditor($updatedFile))->update([ - 'fileHashWebp' => null, - ]); - $updatedFile = new File($updatedFile->fileID); + $updatedFile = FileBuilder::forUpdate($updatedFile) + ->setFileHashWebp(null) + ->update(); } else { if (\file_exists($webpVariant)) { $newWebpVariant = $updatedFile->getPathnameWebp(); @@ -151,10 +147,9 @@ private function replaceSource(): File } else { // The variant file is missing on disk, clear the // stale reference. - (new FileEditor($updatedFile))->update([ - 'fileHashWebp' => null, - ]); - $updatedFile = new File($updatedFile->fileID); + $updatedFile = FileBuilder::forUpdate($updatedFile) + ->setFileHashWebp(null) + ->update(); } } } @@ -185,11 +180,9 @@ private function discardWebpVariantOfWebpFile(File $file): File \unlink($file->getPathnameWebp()); } - (new FileEditor($file))->update([ - 'fileHashWebp' => null, - ]); - - return new File($file->fileID); + return FileBuilder::forUpdate($file) + ->setFileHashWebp(null) + ->update(); } private function regenerateExistingThumbnails(File $file): File diff --git a/wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrlImageFile.class.php b/wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrlImageFile.class.php index 911d35fb404..157150e5de2 100644 --- a/wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrlImageFile.class.php +++ b/wcfsetup/install/files/lib/command/unfurl/url/CreateUnfurlUrlImageFile.class.php @@ -2,8 +2,9 @@ namespace wcf\command\unfurl\url; +use wcf\command\file\CreateFileFromExistingFile; use wcf\data\file\File; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; use wcf\data\unfurl\url\UnfurlUrl; use wcf\system\exception\SystemException; use wcf\system\image\adapter\exception\ImageNotProcessable; @@ -61,11 +62,11 @@ public function __invoke(): ?File // Save the webp file as a base64 encoded binary file \file_put_contents($binFile, \base64_encode($webpContent)); - return FileEditor::createFromExistingFile( + return new CreateFileFromExistingFile( $binFile, \pathinfo($this->originalFilename, \PATHINFO_BASENAME) . ".bin", 'com.woltlab.wcf.unfurl' - ); + )(); } catch (SystemException | ImageNotReadable $e) { return null; } catch (ImageNotProcessable $e) { diff --git a/wcfsetup/install/files/lib/command/user/SetAvatar.class.php b/wcfsetup/install/files/lib/command/user/SetAvatar.class.php index 1d373753e1a..988a6fbb2ee 100644 --- a/wcfsetup/install/files/lib/command/user/SetAvatar.class.php +++ b/wcfsetup/install/files/lib/command/user/SetAvatar.class.php @@ -2,8 +2,8 @@ namespace wcf\command\user; +use wcf\command\file\DeleteFile; use wcf\data\file\File; -use wcf\data\file\FileAction; use wcf\data\user\User; use wcf\data\user\UserEditor; use wcf\event\user\AvatarChanged; @@ -32,7 +32,7 @@ public function __construct( public function __invoke(): void { if ($this->file === null && $this->user->avatarFileID !== null) { - (new FileAction([$this->user->avatarFileID], 'delete'))->executeAction(); + new DeleteFile(new File($this->user->avatarFileID))(); } $pathname = null; diff --git a/wcfsetup/install/files/lib/command/user/SetCoverPhoto.class.php b/wcfsetup/install/files/lib/command/user/SetCoverPhoto.class.php index 2f52d5e93f2..567ef31011c 100644 --- a/wcfsetup/install/files/lib/command/user/SetCoverPhoto.class.php +++ b/wcfsetup/install/files/lib/command/user/SetCoverPhoto.class.php @@ -2,8 +2,8 @@ namespace wcf\command\user; +use wcf\command\file\DeleteFile; use wcf\data\file\File; -use wcf\data\file\FileAction; use wcf\data\user\User; use wcf\data\user\UserEditor; use wcf\event\user\CoverPhotoChanged; @@ -28,7 +28,7 @@ public function __construct( public function __invoke(): void { if ($this->file === null && $this->user->coverPhotoFileID !== null) { - (new FileAction([$this->user->coverPhotoFileID], 'delete'))->executeAction(); + new DeleteFile(new File($this->user->coverPhotoFileID))(); } (new UserEditor($this->user))->update([ diff --git a/wcfsetup/install/files/lib/data/attachment/AttachmentBuilder.class.php b/wcfsetup/install/files/lib/data/attachment/AttachmentBuilder.class.php index 33a4e769bb9..6375072d17b 100644 --- a/wcfsetup/install/files/lib/data/attachment/AttachmentBuilder.class.php +++ b/wcfsetup/install/files/lib/data/attachment/AttachmentBuilder.class.php @@ -4,7 +4,7 @@ use wcf\data\DatabaseObjectBuilder; use wcf\data\file\File; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; use wcf\data\file\thumbnail\FileThumbnail; use wcf\data\object\type\ObjectType; use wcf\data\user\User; @@ -158,7 +158,7 @@ protected static function beforeDeleteAll(array $objectIDs): void } if ($fileIDs !== []) { - FileEditor::deleteAll($fileIDs); + FileBuilder::deleteAll($fileIDs); } } diff --git a/wcfsetup/install/files/lib/data/file/FileAction.class.php b/wcfsetup/install/files/lib/data/file/FileAction.class.php index 7fca3e2c933..57cb3b9b3de 100644 --- a/wcfsetup/install/files/lib/data/file/FileAction.class.php +++ b/wcfsetup/install/files/lib/data/file/FileAction.class.php @@ -9,6 +9,7 @@ * @author Alexander Ebert * @copyright 2001-2023 WoltLab GmbH * @license GNU Lesser General Public License + * @deprecated 6.3 Use `FileBuilder` instead. * * @extends AbstractDatabaseObjectAction */ diff --git a/wcfsetup/install/files/lib/data/file/FileBuilder.class.php b/wcfsetup/install/files/lib/data/file/FileBuilder.class.php new file mode 100644 index 00000000000..25584af4a11 --- /dev/null +++ b/wcfsetup/install/files/lib/data/file/FileBuilder.class.php @@ -0,0 +1,185 @@ + + * @since 6.3 + * + * @extends DatabaseObjectBuilder + */ +final class FileBuilder extends DatabaseObjectBuilder +{ + /** + * Sets the original name of the file as provided by the uploader. + */ + public function setFilename(string $filename): static + { + $this->properties['filename'] = $filename; + + return $this; + } + + /** + * Sets the size of the file in bytes. + */ + public function setFileSize(int $fileSize): static + { + $this->properties['fileSize'] = $fileSize; + + return $this; + } + + /** + * Sets the sha256 hash of the file. + */ + public function setFileHash(string $fileHash): static + { + $this->properties['fileHash'] = $fileHash; + + return $this; + } + + /** + * Sets the file extension that is used to store the file, must be safe to + * be served by the webserver. See `File::getSafeFileExtension()`. + */ + public function setFileExtension(string $fileExtension): static + { + $this->properties['fileExtension'] = $fileExtension; + + return $this; + } + + /** + * Sets the object type of the file, must be an object type of the + * definition `com.woltlab.wcf.file`. + */ + public function setObjectType(ObjectType $objectType): static + { + return $this->setObjectTypeID($objectType->objectTypeID); + } + + /** + * Sets the id of the object type of the file. + */ + public function setObjectTypeID(?int $objectTypeID): static + { + $this->properties['objectTypeID'] = $objectTypeID; + + return $this; + } + + /** + * Sets the mime type of the file. + */ + public function setMimeType(string $mimeType): static + { + $this->properties['mimeType'] = $mimeType; + + return $this; + } + + /** + * Sets the dimensions of the file, images only. + */ + public function setDimensions(?int $width, ?int $height): static + { + return $this->setWidth($width)->setHeight($height); + } + + /** + * Sets the width of the file, images only. + */ + public function setWidth(?int $width): static + { + $this->properties['width'] = $width; + + return $this; + } + + /** + * Sets the height of the file, images only. + */ + public function setHeight(?int $height): static + { + $this->properties['height'] = $height; + + return $this; + } + + /** + * Sets the sha256 hash of the WebP variant of the file, `null` if there is + * no such variant. + */ + public function setFileHashWebp(?string $fileHashWebp): static + { + $this->properties['fileHashWebp'] = $fileHashWebp; + + return $this; + } + + /** + * Sets the timestamp at which the file has been uploaded. + */ + public function setUploadTime(?int $uploadTime): static + { + $this->properties['uploadTime'] = $uploadTime; + + return $this; + } + + /** + * Sets the exif data that has been extracted from the file. + * + * @param null|array> $exifData + */ + public function setExifData(?array $exifData): static + { + $this->properties['exifData'] = $exifData !== null ? \serialize($exifData) : null; + + return $this; + } + + #[\Override] + protected function getRequiredProperties(): array + { + return ['filename', 'fileSize', 'fileHash', 'fileExtension', 'mimeType']; + } + + #[\Override] + protected static function beforeDeleteAll(array $objectIDs): void + { + $fileList = new FileList(); + $fileList->loadThumbnails = true; + $fileList->getConditionBuilder()->add('fileID IN (?)', [$objectIDs]); + $fileList->readObjects(); + + $thumbnailIDs = []; + foreach ($fileList as $file) { + @\unlink($file->getPathname()); + + $pathnameWebp = $file->getPathnameWebp(); + if ($pathnameWebp !== null) { + @\unlink($pathnameWebp); + } + + $thumbnailIDs = [ + ...$thumbnailIDs, + ...\array_column($file->getThumbnails(), 'thumbnailID'), + ]; + } + + if ($thumbnailIDs !== []) { + FileThumbnailEditor::deleteAll($thumbnailIDs); + } + } +} diff --git a/wcfsetup/install/files/lib/data/file/FileEditor.class.php b/wcfsetup/install/files/lib/data/file/FileEditor.class.php index 8a14dbaabdf..72a8d102274 100644 --- a/wcfsetup/install/files/lib/data/file/FileEditor.class.php +++ b/wcfsetup/install/files/lib/data/file/FileEditor.class.php @@ -2,21 +2,18 @@ namespace wcf\data\file; +use wcf\command\file\CreateFileFromExistingFile; +use wcf\command\file\CreateFileFromTemporary; use wcf\data\DatabaseObjectEditor; use wcf\data\file\temporary\FileTemporary; use wcf\data\file\thumbnail\FileThumbnailEditor; -use wcf\event\file\UploadCompleted; -use wcf\system\event\EventHandler; -use wcf\system\file\processor\FileProcessor; -use wcf\system\image\ImageHandler; -use wcf\util\ExifUtil; -use wcf\util\FileUtil; /** * @author Alexander Ebert * @copyright 2001-2023 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.1 + * @deprecated 6.3 Use `FileBuilder` instead. * * @mixin File * @extends DatabaseObjectEditor @@ -32,6 +29,11 @@ public function deleteFiles(): void { @\unlink($this->getPathname()); + $pathnameWebp = $this->getPathnameWebp(); + if ($pathnameWebp !== null) { + @\unlink($pathnameWebp); + } + $thumbnailIDs = \array_column($this->getThumbnails(), 'thumbnailID'); if ($thumbnailIDs !== []) { FileThumbnailEditor::deleteAll($thumbnailIDs); @@ -41,99 +43,18 @@ public function deleteFiles(): void #[\Override] public static function deleteAll(array $objectIDs = []) { - $fileList = new FileList(); - $fileList->loadThumbnails = true; - $fileList->getConditionBuilder()->add("fileID IN (?)", [$objectIDs]); - $fileList->readObjects(); - $files = $fileList->getObjects(); - if (\count($files) === 0) { + if ($objectIDs === []) { return 0; } - foreach ($files as $file) { - (new FileEditor($file))->deleteFiles(); - } + FileBuilder::deleteAll($objectIDs); - return parent::deleteAll($objectIDs); + return \count($objectIDs); } public static function createFromTemporary(FileTemporary $fileTemporary): File { - $pathname = $fileTemporary->getPathname(); - $mimeType = FileUtil::getMimeType($pathname); - $isImage = match ($mimeType) { - 'image/gif' => true, - 'image/jpeg' => true, - 'image/png' => true, - 'image/webp' => true, - default => false, - }; - - $width = $height = null; - if ($isImage) { - [$width, $height] = \getimagesize($pathname); - } - - $exifData = $fileTemporary->exifData; - if ($exifData !== null) { - $exifData = \unserialize($exifData); - } - - $fileSize = $fileTemporary->fileSize; - $fileHash = $fileTemporary->fileHash; - if ($isImage) { - $imageWasModified = false; - try { - $imageWasModified = self::normalizeImageRotation( - $pathname, - $width, - $height, - $mimeType, - $exifData, - ); - - if ($imageWasModified && $exifData !== null) { - unset($exifData['IFD0']['Orientation']); - } - } catch (\Throwable) { - } - - if ($imageWasModified) { - $fileSize = \filesize($pathname); - $fileHash = \hash_file('sha256', $pathname); - [$width, $height] = \getimagesize($pathname); - } - } - - $fileAction = new FileAction([], 'create', ['data' => [ - 'filename' => $fileTemporary->filename, - 'fileSize' => $fileSize, - 'fileHash' => $fileHash, - 'fileExtension' => File::getSafeFileExtension($mimeType, $fileTemporary->filename), - 'objectTypeID' => $fileTemporary->objectTypeID, - 'mimeType' => $mimeType, - 'width' => $width, - 'height' => $height, - 'uploadTime' => \TIME_NOW, - 'exifData' => $exifData !== null ? \serialize($exifData) : null, - ]]); - $file = $fileAction->executeAction()['returnValues']; - \assert($file instanceof File); - - $filePath = $file->getPath(); - if (!\is_dir($filePath)) { - \mkdir($filePath, recursive: true); - } - - \rename( - $pathname, - $filePath . $file->getSourceFilename() - ); - - $event = new UploadCompleted($file); - EventHandler::getInstance()->fire($event); - - return $event->getFile(); + return new CreateFileFromTemporary($fileTemporary)(); } /** @@ -147,153 +68,13 @@ public static function createFromExistingFile( ?int $uploadTime = null, ?array $exifData = null, ): ?File { - if (!\is_readable($pathname)) { - return null; - } - - $objectType = FileProcessor::getInstance()->getObjectType($objectTypeName); - if ($objectType === null) { - throw new \RuntimeException("The object type '{$objectTypeName}' is not valid."); - } - - $mimeType = FileUtil::getMimeType($pathname); - $isImage = match ($mimeType) { - 'image/gif' => true, - 'image/jpeg' => true, - 'image/png' => true, - 'image/webp' => true, - default => false, - }; - - if ($exifData === null) { - $exifData = ExifUtil::getExifData($pathname); - - // Remove the `FILE` and `COMPUTED` section because those contain - // garbled data anyway and we do not need them in the first place. - unset($exifData['FILE'], $exifData['COMPUTED']); - - // We can also discard the `THUMBNAIL` section because it is a - // pointless feature and we’re not extracting it either. - unset($exifData['THUMBNAIL']); - - if ($exifData === []) { - $exifData = null; - } - } - - $width = $height = null; - if ($isImage) { - try { - [$width, $height] = \getimagesize($pathname); - } catch (\Throwable) { - return null; - } - - $imageWasModified = false; - try { - $imageWasModified = self::normalizeImageRotation( - $pathname, - $width, - $height, - $mimeType, - $exifData, - ); - - if ($imageWasModified && $exifData !== null) { - unset($exifData['IFD0']['Orientation']); - } - } catch (\Throwable) { - } - - if ($imageWasModified) { - [$width, $height] = \getimagesize($pathname); - } - } - - $fileAction = new FileAction([], 'create', ['data' => [ - 'filename' => $originalFilename, - 'fileSize' => \filesize($pathname), - 'fileHash' => \hash_file('sha256', $pathname), - 'fileExtension' => File::getSafeFileExtension($mimeType, $originalFilename), - 'objectTypeID' => $objectType->objectTypeID, - 'mimeType' => $mimeType, - 'width' => $width, - 'height' => $height, - 'uploadTime' => $uploadTime, - 'exifData' => $exifData !== null ? \serialize($exifData) : null, - ]]); - $file = $fileAction->executeAction()['returnValues']; - \assert($file instanceof File); - - $filePath = $file->getPath(); - if (!\is_dir($filePath)) { - \mkdir($filePath, recursive: true); - } - - if ($copy) { - \copy($pathname, $filePath . $file->getSourceFilename()); - } else { - \rename($pathname, $filePath . $file->getSourceFilename()); - } - - return $file; - } - - /** - * Normalizes the image rotation by rotating images that been taken while - * the camera was tilted or upside down. - * - * Rotating the image can cause the dimensions to change, the image size to - * differ and the file hash to be different. - * - * @param null|array> $exifData - * @return bool true if the image was modified. - */ - private static function normalizeImageRotation( - string $pathname, - int $width, - int $height, - string $mimeType, - ?array $exifData, - ): bool { - $adapter = ImageHandler::getInstance()->getAdapter(); - if (!$adapter->checkMemoryLimit($width, $height, $mimeType)) { - return false; - } - - $exifData ??= ExifUtil::getExifData($pathname); - if ($exifData === []) { - return false; - } - - $orientation = ExifUtil::getOrientation($exifData); - if ($orientation === ExifUtil::ORIENTATION_ORIGINAL) { - return false; - } - - $rotateByDegrees = match ($orientation) { - ExifUtil::ORIENTATION_180_ROTATE => 180, - ExifUtil::ORIENTATION_90_ROTATE => 90, - ExifUtil::ORIENTATION_270_ROTATE => 270, - // Any other rotation is unsupported. - default => null, - }; - - if ($rotateByDegrees === null) { - return false; - } - - $adapter->loadFile($pathname); - - $image = $adapter->rotate($rotateByDegrees); - if ($image instanceof \Imagick) { - $image->setImageOrientation(\Imagick::ORIENTATION_TOPLEFT); - } - - $adapter->load($image, $adapter->getType()); - - $adapter->writeImage($pathname); - - return true; + return new CreateFileFromExistingFile( + $pathname, + $originalFilename, + $objectTypeName, + $copy, + $uploadTime, + $exifData, + )(); } } 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 2f623247ae7..4dd3c19aba4 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,7 @@ namespace wcf\data\unfurl\url\image; use wcf\data\DatabaseObjectBuilder; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; use wcf\system\database\util\PreparedStatementConditionBuilder; use wcf\system\WCF; @@ -80,7 +80,7 @@ protected static function beforeDeleteAll(array $objectIDs): void $fileIDs = $statement->fetchAll(\PDO::FETCH_COLUMN); if ($fileIDs !== []) { - FileEditor::deleteAll($fileIDs); + FileBuilder::deleteAll($fileIDs); } } } diff --git a/wcfsetup/install/files/lib/data/user/UserAction.class.php b/wcfsetup/install/files/lib/data/user/UserAction.class.php index bc15e74444b..84ea108bf26 100644 --- a/wcfsetup/install/files/lib/data/user/UserAction.class.php +++ b/wcfsetup/install/files/lib/data/user/UserAction.class.php @@ -3,8 +3,9 @@ namespace wcf\data\user; use ParagonIE\ConstantTime\Hex; +use wcf\command\file\DeleteFiles; use wcf\data\AbstractDatabaseObjectAction; -use wcf\data\file\FileAction; +use wcf\data\file\File; use wcf\data\IClipboardAction; use wcf\data\ISearchAction; use wcf\data\object\type\ObjectTypeCache; @@ -150,8 +151,9 @@ public function delete() $avatarFileIDs[] = $user->avatarFileID; } } - if ($avatarFileIDs !== []) { - (new FileAction($avatarFileIDs, 'delete'))->executeAction(); + $avatarFiles = File::findByIDs($avatarFileIDs); + if ($avatarFiles !== []) { + new DeleteFiles($avatarFiles)(); } // delete profile comments and signature attachments diff --git a/wcfsetup/install/files/lib/data/user/cover/photo/UserCoverPhoto.class.php b/wcfsetup/install/files/lib/data/user/cover/photo/UserCoverPhoto.class.php index e724890fad3..990e6664a40 100644 --- a/wcfsetup/install/files/lib/data/user/cover/photo/UserCoverPhoto.class.php +++ b/wcfsetup/install/files/lib/data/user/cover/photo/UserCoverPhoto.class.php @@ -2,8 +2,8 @@ namespace wcf\data\user\cover\photo; +use wcf\command\file\DeleteFile; use wcf\data\file\File; -use wcf\data\file\FileAction; use wcf\data\user\User; /** @@ -11,7 +11,7 @@ * * @author Olaf Braun, Alexander Ebert * @copyright 2001-2024 WoltLab GmbH - * @license GNU Lesser General Public License + * @license GNU Lesser General Public License */ final class UserCoverPhoto implements IUserCoverPhoto { @@ -31,7 +31,7 @@ public function __construct( #[\Override] public function delete() { - (new FileAction([$this->file], 'delete'))->executeAction(); + new DeleteFile(new File($this->file->fileID))(); } #[\Override] diff --git a/wcfsetup/install/files/lib/event/file/FileDeleted.class.php b/wcfsetup/install/files/lib/event/file/FileDeleted.class.php new file mode 100644 index 00000000000..d8f88e1da65 --- /dev/null +++ b/wcfsetup/install/files/lib/event/file/FileDeleted.class.php @@ -0,0 +1,21 @@ + + * @since 6.3 + */ +final class FileDeleted implements IPsr14Event +{ + public function __construct( + public readonly File $file, + ) {} +} diff --git a/wcfsetup/install/files/lib/system/cronjob/FileCleanUpCronjob.class.php b/wcfsetup/install/files/lib/system/cronjob/FileCleanUpCronjob.class.php index 2d3c6f3a0e4..c8c469db013 100644 --- a/wcfsetup/install/files/lib/system/cronjob/FileCleanUpCronjob.class.php +++ b/wcfsetup/install/files/lib/system/cronjob/FileCleanUpCronjob.class.php @@ -3,7 +3,7 @@ namespace wcf\system\cronjob; use wcf\data\cronjob\Cronjob; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; use wcf\data\object\type\ObjectTypeCache; use wcf\system\WCF; @@ -37,7 +37,7 @@ public function execute(Cronjob $cronjob) return; } - FileEditor::deleteAll($fileIDs); + FileBuilder::deleteAll($fileIDs); } /** diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/DeleteFile.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/DeleteFile.class.php index a8cc3ec36a7..7fc0c4770a9 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/DeleteFile.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/DeleteFile.class.php @@ -6,7 +6,6 @@ use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; use wcf\data\file\File; -use wcf\data\file\FileAction; use wcf\http\Helper; use wcf\system\endpoint\DeleteRequest; use wcf\system\endpoint\IController; @@ -31,8 +30,7 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res throw new PermissionDeniedException(); } - $fileAction = new FileAction([$file], 'delete'); - $fileAction->executeAction(); + new \wcf\command\file\DeleteFile($file)(); return new JsonResponse([]); } diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/upload/SaveChunk.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/upload/SaveChunk.class.php index 3ebb0cca9e6..4a473a15afa 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/upload/SaveChunk.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/upload/SaveChunk.class.php @@ -5,7 +5,8 @@ use Laminas\Diactoros\Response\JsonResponse; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ServerRequestInterface; -use wcf\data\file\FileEditor; +use wcf\command\file\CreateFileFromTemporary; +use wcf\data\file\FileBuilder; use wcf\data\file\temporary\FileTemporary; use wcf\data\file\temporary\FileTemporaryEditor; use wcf\http\Helper; @@ -112,7 +113,7 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res throw new UserInputException('file', 'checksum'); } - $file = FileEditor::createFromTemporary($fileTemporary); + $file = new CreateFileFromTemporary($fileTemporary)(); $context = $fileTemporary->getContext(); (new FileTemporaryEditor($fileTemporary))->delete(); @@ -124,7 +125,7 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res try { $processor->validateUpload($file); } catch (UserInputException $exception) { - (new FileEditor($file))->delete(); + FileBuilder::delete($file); throw new UserInputException('validation', $exception->getType()); } diff --git a/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php index b086dac88d9..64778d6ade8 100644 --- a/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php @@ -3,7 +3,7 @@ namespace wcf\system\file\processor; use wcf\data\file\File; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; use wcf\data\file\thumbnail\FileThumbnailEditor; use wcf\data\file\thumbnail\FileThumbnailList; use wcf\data\object\type\ObjectType; @@ -160,11 +160,9 @@ public function generateWebpVariant(File $file): File if (!$canGenerateThumbnail) { if ($file->fileHashWebp !== null) { - (new FileEditor($file))->update([ - 'fileHashWebp' => null, - ]); - - return new File($file->fileID); + return FileBuilder::forUpdate($file) + ->setFileHashWebp(null) + ->update(); } return $file; @@ -220,11 +218,9 @@ public function generateWebpVariant(File $file): File } } - (new FileEditor($file))->update([ - 'fileHashWebp' => \hash_file('sha256', $filename), - ]); - - $file = new File($file->fileID); + $file = FileBuilder::forUpdate($file) + ->setFileHashWebp(\hash_file('sha256', $filename)) + ->update(); $pathname = $file->getPathnameWebp(); \assert($pathname !== null); @@ -425,17 +421,16 @@ public function copy(File $oldFile, string $objectType): File throw new \InvalidArgumentException("The object type '{$objectType}' is invalid."); } - $newFile = FileEditor::create([ - 'filename' => $oldFile->filename, - 'fileSize' => $oldFile->fileSize, - 'fileHash' => $oldFile->fileHash, - 'fileExtension' => $oldFile->fileExtension, - 'objectTypeID' => $objectTypeObj->objectTypeID, - 'mimeType' => $oldFile->mimeType, - 'width' => $oldFile->width, - 'height' => $oldFile->height, - 'fileHashWebp' => $oldFile->fileHashWebp, - ]); + $newFile = FileBuilder::forCreate() + ->setFilename($oldFile->filename) + ->setFileSize($oldFile->fileSize) + ->setFileHash($oldFile->fileHash) + ->setFileExtension($oldFile->fileExtension) + ->setObjectType($objectTypeObj) + ->setMimeType($oldFile->mimeType) + ->setDimensions($oldFile->width, $oldFile->height) + ->setFileHashWebp($oldFile->fileHashWebp) + ->create(); \copy($oldFile->getPathname(), $newFile->getPathname()); diff --git a/wcfsetup/install/files/lib/system/importer/AbstractAttachmentImporter.class.php b/wcfsetup/install/files/lib/system/importer/AbstractAttachmentImporter.class.php index 843eafb8203..45b028a3ac7 100644 --- a/wcfsetup/install/files/lib/system/importer/AbstractAttachmentImporter.class.php +++ b/wcfsetup/install/files/lib/system/importer/AbstractAttachmentImporter.class.php @@ -2,9 +2,10 @@ namespace wcf\system\importer; +use wcf\command\file\CreateFileFromExistingFile; use wcf\data\attachment\Attachment; use wcf\data\attachment\AttachmentBuilder; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; /** * Imports attachments. @@ -47,13 +48,13 @@ public function import(mixed $oldID, array $data, array $additionalData = []) // The source file must be preserved, therefore it is copied into the // storage of the file API instead of being moved. - $file = FileEditor::createFromExistingFile( + $file = new CreateFileFromExistingFile( $additionalData['fileLocation'], $data['filename'] ?? \basename($additionalData['fileLocation']), 'com.woltlab.wcf.attachment', true, $uploadTime ?: null - ); + )(); if ($file === null) { return 0; } diff --git a/wcfsetup/install/files/lib/system/importer/AbstractFileImporter.class.php b/wcfsetup/install/files/lib/system/importer/AbstractFileImporter.class.php index 54095e5089a..4b3fde96a9c 100644 --- a/wcfsetup/install/files/lib/system/importer/AbstractFileImporter.class.php +++ b/wcfsetup/install/files/lib/system/importer/AbstractFileImporter.class.php @@ -2,8 +2,9 @@ namespace wcf\system\importer; +use wcf\command\file\CreateFileFromExistingFile; use wcf\data\file\File; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; /** * Import files. @@ -33,7 +34,12 @@ protected function importFile(string $fileLocation, ?string $filename = null): ? } $filename = $filename ?: \basename($fileLocation); - $file = FileEditor::createFromExistingFile($fileLocation, $filename, $this->objectType, true); + $file = new CreateFileFromExistingFile( + $fileLocation, + $filename, + $this->objectType, + true + )(); if ($file === null) { return null; diff --git a/wcfsetup/install/files/lib/system/worker/AttachmentRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/AttachmentRebuildDataWorker.class.php index e7a7f96b7cd..3f3581b5cbb 100644 --- a/wcfsetup/install/files/lib/system/worker/AttachmentRebuildDataWorker.class.php +++ b/wcfsetup/install/files/lib/system/worker/AttachmentRebuildDataWorker.class.php @@ -2,10 +2,11 @@ namespace wcf\system\worker; +use wcf\command\file\CreateFileFromExistingFile; use wcf\data\attachment\Attachment; use wcf\data\attachment\AttachmentBuilder; use wcf\data\attachment\AttachmentList; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; use wcf\data\file\thumbnail\FileThumbnailList; use wcf\data\object\type\ObjectTypeCache; use wcf\system\cache\runtime\ArticleRuntimeCache; @@ -59,11 +60,11 @@ public function execute() $attachment->migrateStorage(); - $file = FileEditor::createFromExistingFile( + $file = new CreateFileFromExistingFile( $attachment->getLocation(), $attachment->filename, 'com.woltlab.wcf.attachment' - ); + )(); if ($file === null) { $defunctAttachmentIDs[] = $attachment->attachmentID; diff --git a/wcfsetup/install/files/lib/system/worker/FileRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/FileRebuildDataWorker.class.php index df63fad2d5c..16fc51a265d 100644 --- a/wcfsetup/install/files/lib/system/worker/FileRebuildDataWorker.class.php +++ b/wcfsetup/install/files/lib/system/worker/FileRebuildDataWorker.class.php @@ -3,7 +3,7 @@ namespace wcf\system\worker; use wcf\data\file\File; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; use wcf\data\file\FileList; use wcf\system\file\processor\exception\DamagedImage; use wcf\system\file\processor\FileProcessor; @@ -65,7 +65,7 @@ public function execute() } if ($damagedFileIDs !== []) { - FileEditor::deleteAll($damagedFileIDs); + FileBuilder::deleteAll($damagedFileIDs); } } @@ -132,12 +132,11 @@ private function fixMimeType(): void $renamedFileIDs[] = $file->fileID; } - (new FileEditor($file))->update([ - 'fileExtension' => $detectedFileExtension, - 'mimeType' => $mimeType, - 'width' => $width, - 'height' => $height, - ]); + FileBuilder::forUpdate($file) + ->setFileExtension($detectedFileExtension) + ->setMimeType($mimeType) + ->setDimensions($width, $height) + ->update(); $reloadFiles = true; } @@ -178,22 +177,21 @@ private function fixFile(File $file): File } // The file does exist but under its WebP filename. - (new FileEditor($file))->update([ - 'filename' => \sprintf( + $updatedFile = FileBuilder::forUpdate($file) + ->setFilename(\sprintf( "%s.webp", \preg_replace( '~\.(?:jpe?g|png)$~i', '', $file->filename, ) - ), - 'fileSize' => \filesize($pathnameWebp), - 'fileHash' => $file->fileHashWebp, - 'fileHashWebp' => null, - 'mimeType' => 'image/webp', - ]); - - $updatedFile = new File($file->fileID); + )) + ->setFileSize(\filesize($pathnameWebp)) + ->setFileHash($file->fileHashWebp) + ->setFileHashWebp(null) + ->setMimeType('image/webp') + ->update(); + $updatedFile->getProcessor()?->sourceFilenameChanged($updatedFile); return $updatedFile; diff --git a/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php index 7200360f74e..6920f55b4e7 100644 --- a/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php +++ b/wcfsetup/install/files/lib/system/worker/UnfurlUrlRebuildDataWorker.class.php @@ -3,7 +3,7 @@ namespace wcf\system\worker; use wcf\command\unfurl\url\CreateUnfurlUrlImageFile; -use wcf\data\file\FileEditor; +use wcf\data\file\FileBuilder; use wcf\data\unfurl\url\UnfurlUrl; use wcf\data\unfurl\url\UnfurlUrlList; use wcf\system\database\util\PreparedStatementConditionBuilder; @@ -91,7 +91,7 @@ public function execute() } if ($deleteFileIDs !== []) { - FileEditor::deleteAll($deleteFileIDs); + FileBuilder::deleteAll($deleteFileIDs); } } diff --git a/wcfsetup/install/files/lib/system/worker/UserRebuildDataWorker.class.php b/wcfsetup/install/files/lib/system/worker/UserRebuildDataWorker.class.php index 470fa29f8b5..9b415c57e32 100644 --- a/wcfsetup/install/files/lib/system/worker/UserRebuildDataWorker.class.php +++ b/wcfsetup/install/files/lib/system/worker/UserRebuildDataWorker.class.php @@ -2,7 +2,8 @@ namespace wcf\system\worker; -use wcf\data\file\FileEditor; +use wcf\command\file\CreateFileFromExistingFile; +use wcf\data\file\FileBuilder; use wcf\data\file\FileList; use wcf\data\reaction\type\ReactionTypeCache; use wcf\data\user\avatar\UserAvatarEditor; @@ -333,11 +334,11 @@ public function execute() $adapter->writeImage($adapter->getImage(), $avatar->getLocation()); } - $file = FileEditor::createFromExistingFile( + $file = new CreateFileFromExistingFile( $avatar->getLocation(), $avatar->avatarName, 'com.woltlab.wcf.user.avatar' - ); + )(); $editor->delete(); if ($file === null) { @@ -365,11 +366,11 @@ public function execute() $userProfiles->getConditionBuilder()->add("user_table.coverPhotoHash IS NOT NULL"); $userProfiles->readObjects(); foreach ($userProfiles as $user) { - $file = FileEditor::createFromExistingFile( + $file = new CreateFileFromExistingFile( UserCoverPhoto::getLegacyLocation($user, false), $user->coverPhotoHash . '.' . $user->coverPhotoExtension, 'com.woltlab.wcf.user.coverPhoto', - ); + )(); new SetCoverPhoto($user, $file)(); diff --git a/wcfsetup/install/files/lib/util/ExifUtil.class.php b/wcfsetup/install/files/lib/util/ExifUtil.class.php index 69a9de2a76c..819fad3a928 100644 --- a/wcfsetup/install/files/lib/util/ExifUtil.class.php +++ b/wcfsetup/install/files/lib/util/ExifUtil.class.php @@ -2,6 +2,7 @@ namespace wcf\util; +use wcf\system\image\ImageHandler; use WoltLab\WebpExif\Decoder; use WoltLab\WebpExif\Encoder; use WoltLab\WebpExif\Exception\WebpExifException; @@ -413,4 +414,62 @@ private static function gcd(int $a, int $b): int return $a; } + + /** + * Normalizes the image rotation by rotating images that been taken while + * the camera was tilted or upside down. + * + * Rotating the image can cause the dimensions to change, the image size to + * differ and the file hash to be different. + * + * @param null|array> $exifData + * @return bool true if the image was modified. + * @since 6.3 + */ + public static function normalizeImageRotation( + string $pathname, + int $width, + int $height, + string $mimeType, + ?array $exifData, + ): bool { + $adapter = ImageHandler::getInstance()->getAdapter(); + if (!$adapter->checkMemoryLimit($width, $height, $mimeType)) { + return false; + } + + $exifData ??= ExifUtil::getExifData($pathname); + if ($exifData === []) { + return false; + } + + $orientation = ExifUtil::getOrientation($exifData); + if ($orientation === ExifUtil::ORIENTATION_ORIGINAL) { + return false; + } + + $rotateByDegrees = match ($orientation) { + ExifUtil::ORIENTATION_180_ROTATE => 180, + ExifUtil::ORIENTATION_90_ROTATE => 90, + ExifUtil::ORIENTATION_270_ROTATE => 270, + // Any other rotation is unsupported. + default => null, + }; + + if ($rotateByDegrees === null) { + return false; + } + + $adapter->loadFile($pathname); + + $image = $adapter->rotate($rotateByDegrees); + if ($image instanceof \Imagick) { + $image->setImageOrientation(\Imagick::ORIENTATION_TOPLEFT); + } + + $adapter->load($image, $adapter->getType()); + $adapter->writeImage($pathname); + + return true; + } } diff --git a/wcfsetup/install/files/lib/util/ImageUtil.class.php b/wcfsetup/install/files/lib/util/ImageUtil.class.php index 78819b6ce80..0e1d048dbd4 100644 --- a/wcfsetup/install/files/lib/util/ImageUtil.class.php +++ b/wcfsetup/install/files/lib/util/ImageUtil.class.php @@ -255,6 +255,20 @@ public static function createWebpVariant(string $sourceLocation, string $outputF } } + /** + * @since 6.3 + */ + public static function isImageMimeType(string $mimeType): bool + { + return match ($mimeType) { + 'image/gif' => true, + 'image/jpeg' => true, + 'image/png' => true, + 'image/webp' => true, + default => false, + }; + } + /** * Forbid creation of ImageUtil objects. */ From 04727ea3e7ebcfcc0f0d7c8c827132076c8678c6 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 15 Sep 2026 16:13:17 +0200 Subject: [PATCH 3/4] Add `FileTemporaryBuilder` and deprecate `FileTemporaryAction`/`FileTemporaryEditor` --- .../temporary/FileTemporaryAction.class.php | 1 + .../temporary/FileTemporaryBuilder.class.php | 169 ++++++++++++++++++ .../temporary/FileTemporaryEditor.class.php | 1 + .../core/files/PrepareUpload.class.php | 33 ++-- .../core/files/upload/SaveChunk.class.php | 14 +- 5 files changed, 189 insertions(+), 29 deletions(-) create mode 100644 wcfsetup/install/files/lib/data/file/temporary/FileTemporaryBuilder.class.php diff --git a/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryAction.class.php b/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryAction.class.php index f7d47c3d5e1..760ddbdc3cf 100644 --- a/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryAction.class.php +++ b/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryAction.class.php @@ -9,6 +9,7 @@ * @copyright 2001-2023 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.1 + * @deprecated 6.3 Use `FileTemporaryBuilder` instead. * * @extends AbstractDatabaseObjectAction */ diff --git a/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryBuilder.class.php b/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryBuilder.class.php new file mode 100644 index 00000000000..de6e8f4d073 --- /dev/null +++ b/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryBuilder.class.php @@ -0,0 +1,169 @@ + + * @since 6.3 + * + * @extends DatabaseObjectBuilder + */ +final class FileTemporaryBuilder extends DatabaseObjectBuilder +{ + /** + * Sets the identifier of the temporary file that is used to reference it + * while the upload is in progress. + */ + public function setIdentifier(string $identifier): static + { + return $this->setID($identifier); + } + + /** + * Sets the timestamp at which the upload has been announced. + */ + public function setTime(int $time): static + { + $this->properties['time'] = $time; + + return $this; + } + + /** + * Sets the original name of the file as provided by the uploader. + */ + public function setFilename(string $filename): static + { + $this->properties['filename'] = $filename; + + return $this; + } + + /** + * Sets the expected size of the file in bytes. + */ + public function setFileSize(int $fileSize): static + { + $this->properties['fileSize'] = $fileSize; + + return $this; + } + + /** + * Sets the expected sha256 hash of the file. + */ + public function setFileHash(string $fileHash): static + { + $this->properties['fileHash'] = $fileHash; + + return $this; + } + + /** + * Sets the object type of the file, must be an object type of the + * definition `com.woltlab.wcf.file`. + */ + public function setObjectType(?ObjectType $objectType): static + { + return $this->setObjectTypeID($objectType?->objectTypeID); + } + + /** + * Sets the id of the object type of the file. + */ + public function setObjectTypeID(?int $objectTypeID): static + { + $this->properties['objectTypeID'] = $objectTypeID; + + return $this; + } + + /** + * Sets the context of the upload as provided by the uploader, must be a + * JSON encoded string. + */ + public function setContext(?string $context): static + { + $this->properties['context'] = $context; + + return $this; + } + + /** + * Sets the number of chunks the file is split into, marking all of them as + * pending. + * + * @throws \InvalidArgumentException if the number of chunks is less than 1 + */ + public function setNumberOfChunks(int $numberOfChunks): static + { + if ($numberOfChunks < 1) { + throw new \InvalidArgumentException("The number of chunks must be at least 1, '{$numberOfChunks}' given."); + } + + $this->properties['chunks'] = \str_repeat('0', $numberOfChunks); + + return $this; + } + + /** + * Marks the chunk identified by its sequence number as written. + * + * @throws \OutOfRangeException if there is no chunk with the given sequence number + */ + public function markChunkAsWritten(int $sequenceNo): static + { + $chunks = $this->properties['chunks'] ?? $this->getObject()->chunks; + if ($sequenceNo < 0 || $sequenceNo >= \strlen($chunks)) { + throw new \OutOfRangeException( + \sprintf( + "Cannot access chunk #%d of %d", + $sequenceNo, + \strlen($chunks), + ), + ); + } + + $chunks[$sequenceNo] = '1'; + $this->properties['chunks'] = $chunks; + + return $this; + } + + /** + * Sets the exif data that has been extracted from the file. + * + * @param null|array> $exifData + */ + public function setExifData(?array $exifData): static + { + $this->properties['exifData'] = $exifData !== null ? \serialize($exifData) : null; + + return $this; + } + + #[\Override] + protected function getRequiredProperties(): array + { + return ['identifier', 'time', 'filename', 'fileSize', 'fileHash', 'chunks']; + } + + #[\Override] + protected static function beforeDeleteAll(array $objectIDs): void + { + $fileTemporaryList = new FileTemporaryList(); + $fileTemporaryList->getConditionBuilder()->add('identifier IN (?)', [$objectIDs]); + $fileTemporaryList->readObjects(); + + foreach ($fileTemporaryList as $fileTemporary) { + @\unlink($fileTemporary->getPathname()); + } + } +} diff --git a/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryEditor.class.php b/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryEditor.class.php index 3ce0b8a3566..1330a7638dc 100644 --- a/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryEditor.class.php +++ b/wcfsetup/install/files/lib/data/file/temporary/FileTemporaryEditor.class.php @@ -9,6 +9,7 @@ * @copyright 2001-2023 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.1 + * @deprecated 6.3 Use `FileTemporaryBuilder` instead. * * @mixin FileTemporary * @extends DatabaseObjectEditor diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/PrepareUpload.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/PrepareUpload.class.php index 11ebbf20d05..6493bb8f789 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/PrepareUpload.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/PrepareUpload.class.php @@ -6,7 +6,7 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ResponseInterface; use wcf\data\file\temporary\FileTemporary; -use wcf\data\file\temporary\FileTemporaryAction; +use wcf\data\file\temporary\FileTemporaryBuilder; use wcf\http\Helper; use wcf\system\endpoint\IController; use wcf\system\endpoint\PostRequest; @@ -84,26 +84,17 @@ private function createTemporaryFile(PostUploadParameters $parameters, int $numb } } - $exifData = $this->parseExifData($exifBytes, $parameters->ignoreExifRotation); - if ($exifData !== null) { - $exifData = \serialize($exifData); - } - - $action = new FileTemporaryAction([], 'create', [ - 'data' => [ - 'identifier' => $identifier, - 'time' => \TIME_NOW, - 'filename' => $parameters->filename, - 'fileSize' => $parameters->fileSize, - 'fileHash' => $parameters->fileHash, - 'objectTypeID' => $objectType?->objectTypeID, - 'context' => $parameters->context, - 'chunks' => \str_repeat('0', $numberOfChunks), - 'exifData' => $exifData, - ], - ]); - - return $action->executeAction()['returnValues']; + return FileTemporaryBuilder::forCreate() + ->setIdentifier($identifier) + ->setTime(\TIME_NOW) + ->setFilename($parameters->filename) + ->setFileSize($parameters->fileSize) + ->setFileHash($parameters->fileHash) + ->setObjectType($objectType) + ->setContext($parameters->context) + ->setNumberOfChunks($numberOfChunks) + ->setExifData($this->parseExifData($exifBytes, $parameters->ignoreExifRotation)) + ->create(); } /** diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/upload/SaveChunk.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/upload/SaveChunk.class.php index 4a473a15afa..d85185c406c 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/files/upload/SaveChunk.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/files/upload/SaveChunk.class.php @@ -8,7 +8,7 @@ use wcf\command\file\CreateFileFromTemporary; use wcf\data\file\FileBuilder; use wcf\data\file\temporary\FileTemporary; -use wcf\data\file\temporary\FileTemporaryEditor; +use wcf\data\file\temporary\FileTemporaryBuilder; use wcf\http\Helper; use wcf\system\endpoint\IController; use wcf\system\endpoint\PostRequest; @@ -99,14 +99,12 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res } // Mark the chunk as written. - $chunks = $fileTemporary->chunks; - $chunks[$sequenceNo] = '1'; - (new FileTemporaryEditor($fileTemporary))->update([ - 'chunks' => $chunks, - ]); + $fileTemporary = FileTemporaryBuilder::forUpdate($fileTemporary) + ->markChunkAsWritten($sequenceNo) + ->update(); // Check if we have all chunks. - if ($chunks === \str_repeat('1', $fileTemporary->getChunkCount())) { + if ($fileTemporary->chunks === \str_repeat('1', $fileTemporary->getChunkCount())) { // Check if the final result matches the expected checksum. $checksum = \hash_file('sha256', $tmpPath . $fileTemporary->getFilename()); if ($checksum !== $fileTemporary->fileHash) { @@ -116,7 +114,7 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res $file = new CreateFileFromTemporary($fileTemporary)(); $context = $fileTemporary->getContext(); - (new FileTemporaryEditor($fileTemporary))->delete(); + FileTemporaryBuilder::delete($fileTemporary); unset($fileTemporary); $processor = $file->getProcessor(); From 9f54a22bc86ae48b469231c2920ff9d3c6e28c01 Mon Sep 17 00:00:00 2001 From: Marcel Werk Date: Tue, 15 Sep 2026 16:22:07 +0200 Subject: [PATCH 4/4] Add `FileThumbnailBuilder` and deprecate `FileThumbnailAction`/`FileThumbnailEditor` --- ...CreateThumbnailFromTemporaryFile.class.php | 50 +++++++ .../files/lib/data/file/FileBuilder.class.php | 4 +- .../thumbnail/FileThumbnailAction.class.php | 1 + .../thumbnail/FileThumbnailBuilder.class.php | 137 ++++++++++++++++++ .../thumbnail/FileThumbnailEditor.class.php | 44 +----- .../file/processor/FileProcessor.class.php | 26 ++-- 6 files changed, 210 insertions(+), 52 deletions(-) create mode 100644 wcfsetup/install/files/lib/command/file/thumbnail/CreateThumbnailFromTemporaryFile.class.php create mode 100644 wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailBuilder.class.php diff --git a/wcfsetup/install/files/lib/command/file/thumbnail/CreateThumbnailFromTemporaryFile.class.php b/wcfsetup/install/files/lib/command/file/thumbnail/CreateThumbnailFromTemporaryFile.class.php new file mode 100644 index 00000000000..64197d364c7 --- /dev/null +++ b/wcfsetup/install/files/lib/command/file/thumbnail/CreateThumbnailFromTemporaryFile.class.php @@ -0,0 +1,50 @@ + + * @since 6.3 + */ +final class CreateThumbnailFromTemporaryFile +{ + public function __construct( + private readonly File $file, + private readonly ThumbnailFormat $format, + private readonly string $pathname, + ) {} + + public function __invoke(): FileThumbnail + { + [$width, $height] = \getimagesize($this->pathname); + + $fileThumbnail = FileThumbnailBuilder::forCreate() + ->setFile($this->file) + ->setFormat($this->format) + ->setFileHash(\hash_file('sha256', $this->pathname)) + ->setFileExtension('webp') + ->setDimensions($width, $height) + ->create(); + + $filePath = $fileThumbnail->getPath(); + if (!\is_dir($filePath)) { + \mkdir($filePath, recursive: true); + } + + \rename( + $this->pathname, + $filePath . $fileThumbnail->getSourceFilename() + ); + + return $fileThumbnail; + } +} diff --git a/wcfsetup/install/files/lib/data/file/FileBuilder.class.php b/wcfsetup/install/files/lib/data/file/FileBuilder.class.php index 25584af4a11..0abf47d22ab 100644 --- a/wcfsetup/install/files/lib/data/file/FileBuilder.class.php +++ b/wcfsetup/install/files/lib/data/file/FileBuilder.class.php @@ -3,7 +3,7 @@ namespace wcf\data\file; use wcf\data\DatabaseObjectBuilder; -use wcf\data\file\thumbnail\FileThumbnailEditor; +use wcf\data\file\thumbnail\FileThumbnailBuilder; use wcf\data\object\type\ObjectType; /** @@ -179,7 +179,7 @@ protected static function beforeDeleteAll(array $objectIDs): void } if ($thumbnailIDs !== []) { - FileThumbnailEditor::deleteAll($thumbnailIDs); + FileThumbnailBuilder::deleteAll($thumbnailIDs); } } } diff --git a/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailAction.class.php b/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailAction.class.php index 19573512c19..6e41539261e 100644 --- a/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailAction.class.php +++ b/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailAction.class.php @@ -9,6 +9,7 @@ * @copyright 2001-2024 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.1 + * @deprecated 6.3 Use `FileThumbnailBuilder` instead. * * @extends AbstractDatabaseObjectAction */ diff --git a/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailBuilder.class.php b/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailBuilder.class.php new file mode 100644 index 00000000000..f2b1b78d487 --- /dev/null +++ b/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailBuilder.class.php @@ -0,0 +1,137 @@ + + * @since 6.3 + * + * @extends DatabaseObjectBuilder + */ +final class FileThumbnailBuilder extends DatabaseObjectBuilder +{ + /** + * Sets the file the thumbnail belongs to. + */ + public function setFile(File $file): static + { + return $this->setFileID($file->fileID); + } + + /** + * Sets the id of the file the thumbnail belongs to. + */ + public function setFileID(int $fileID): static + { + $this->properties['fileID'] = $fileID; + + return $this; + } + + /** + * Sets the format of the thumbnail, including the checksum that is used to + * detect thumbnails that need to be regenerated. + */ + public function setFormat(ThumbnailFormat $format): static + { + return $this->setIdentifier($format->identifier) + ->setFormatChecksum($format->toChecksum()); + } + + /** + * Sets the identifier of the thumbnail format, empty for the default + * thumbnail of a file. + */ + public function setIdentifier(string $identifier): static + { + $this->properties['identifier'] = $identifier; + + return $this; + } + + /** + * Sets the checksum of the thumbnail format, `null` if the thumbnail does + * not belong to a format. + */ + public function setFormatChecksum(?string $formatChecksum): static + { + $this->properties['formatChecksum'] = $formatChecksum; + + return $this; + } + + /** + * Sets the sha256 hash of the thumbnail. + */ + public function setFileHash(string $fileHash): static + { + $this->properties['fileHash'] = $fileHash; + + return $this; + } + + /** + * Sets the file extension that is used to store the thumbnail, must be safe + * to be served by the webserver. + */ + public function setFileExtension(string $fileExtension): static + { + $this->properties['fileExtension'] = $fileExtension; + + return $this; + } + + /** + * Sets the dimensions of the thumbnail. + */ + public function setDimensions(int $width, int $height): static + { + return $this->setWidth($width)->setHeight($height); + } + + /** + * Sets the width of the thumbnail. + */ + public function setWidth(int $width): static + { + $this->properties['width'] = $width; + + return $this; + } + + /** + * Sets the height of the thumbnail. + */ + public function setHeight(int $height): static + { + $this->properties['height'] = $height; + + return $this; + } + + #[\Override] + protected function getRequiredProperties(): array + { + return ['fileID', 'identifier', 'fileHash', 'fileExtension', 'width', 'height']; + } + + #[\Override] + protected static function beforeDeleteAll(array $objectIDs): void + { + $thumbnailList = new FileThumbnailList(); + $thumbnailList->getConditionBuilder()->add('thumbnailID IN (?)', [$objectIDs]); + $thumbnailList->readObjects(); + + foreach ($thumbnailList as $thumbnail) { + @\unlink($thumbnail->getPathname()); + } + } +} diff --git a/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailEditor.class.php b/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailEditor.class.php index cd812853b21..d7db90de402 100644 --- a/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailEditor.class.php +++ b/wcfsetup/install/files/lib/data/file/thumbnail/FileThumbnailEditor.class.php @@ -2,6 +2,7 @@ namespace wcf\data\file\thumbnail; +use wcf\command\file\thumbnail\CreateThumbnailFromTemporaryFile; use wcf\data\DatabaseObjectEditor; use wcf\data\file\File; use wcf\system\file\processor\ThumbnailFormat; @@ -11,6 +12,7 @@ * @copyright 2001-2024 WoltLab GmbH * @license GNU Lesser General Public License * @since 6.1 + * @deprecated 6.3 Use `FileThumbnailBuilder` instead. * * @mixin FileThumbnail * @extends DatabaseObjectEditor @@ -24,25 +26,19 @@ class FileThumbnailEditor extends DatabaseObjectEditor public function deleteFiles(): void { - @\unlink($this->getPath() . $this->getSourceFilename()); + @\unlink($this->getPathname()); } #[\Override] public static function deleteAll(array $objectIDs = []) { - $thumbnailList = new FileThumbnailList(); - $thumbnailList->getConditionBuilder()->add("thumbnailID IN (?)", [$objectIDs]); - $thumbnailList->readObjects(); - - if (\count($thumbnailList) === 0) { + if ($objectIDs === []) { return 0; } - foreach ($thumbnailList as $thumbnail) { - (new FileThumbnailEditor($thumbnail))->deleteFiles(); - } + FileThumbnailBuilder::deleteAll($objectIDs); - return parent::deleteAll($objectIDs); + return \count($objectIDs); } public static function createFromTemporaryFile( @@ -50,32 +46,6 @@ public static function createFromTemporaryFile( ThumbnailFormat $format, string $filename ): FileThumbnail { - [$width, $height] = \getimagesize($filename); - - $action = new FileThumbnailAction([], 'create', [ - 'data' => [ - 'fileID' => $file->fileID, - 'identifier' => $format->identifier, - 'fileHash' => \hash_file('sha256', $filename), - 'fileExtension' => 'webp', - 'width' => $width, - 'height' => $height, - 'formatChecksum' => $format->toChecksum(), - ], - ]); - $fileThumbnail = $action->executeAction()['returnValues']; - \assert($fileThumbnail instanceof FileThumbnail); - - $filePath = $fileThumbnail->getPath(); - if (!\is_dir($filePath)) { - \mkdir($filePath, recursive: true); - } - - \rename( - $filename, - $filePath . $fileThumbnail->getSourceFilename() - ); - - return $fileThumbnail; + return new CreateThumbnailFromTemporaryFile($file, $format, $filename)(); } } diff --git a/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php b/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php index 64778d6ade8..0fc69451a2f 100644 --- a/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php +++ b/wcfsetup/install/files/lib/system/file/processor/FileProcessor.class.php @@ -4,7 +4,7 @@ use wcf\data\file\File; use wcf\data\file\FileBuilder; -use wcf\data\file\thumbnail\FileThumbnailEditor; +use wcf\data\file\thumbnail\FileThumbnailBuilder; use wcf\data\file\thumbnail\FileThumbnailList; use wcf\data\object\type\ObjectType; use wcf\data\object\type\ObjectTypeCache; @@ -15,6 +15,7 @@ use wcf\system\exception\SystemException; use wcf\command\file\ReplaceFileSource; use wcf\command\file\ReplaceWithWebpVariant; +use wcf\command\file\thumbnail\CreateThumbnailFromTemporaryFile; use wcf\system\file\processor\exception\DamagedImage; use wcf\system\image\adapter\exception\ImageNotProcessable; use wcf\system\image\adapter\exception\ImageNotReadable; @@ -265,7 +266,7 @@ public function generateThumbnails(File $file): void // There currently is a thumbnail for this format but the // conditions for its existence are no longer met. if ($existingThumbnail !== null) { - FileThumbnailEditor::deleteAll([$existingThumbnail->thumbnailID]); + FileThumbnailBuilder::deleteAll([$existingThumbnail->thumbnailID]); } continue; @@ -274,7 +275,7 @@ public function generateThumbnails(File $file): void if ($existingThumbnail !== null) { if ($existingThumbnail->needsRebuild($format)) { // There currently is a thumbnail but it is no longer valid. - FileThumbnailEditor::deleteAll([$existingThumbnail->thumbnailID]); + FileThumbnailBuilder::deleteAll([$existingThumbnail->thumbnailID]); } else { // This thumbnail is still fine. continue; @@ -318,7 +319,7 @@ public function generateThumbnails(File $file): void $imageAdapter->saveImageAs($image, $filename, 'webp', 80); } - $fileThumbnail = FileThumbnailEditor::createFromTemporaryFile($file, $format, $filename); + $fileThumbnail = new CreateThumbnailFromTemporaryFile($file, $format, $filename)(); $processor->adoptThumbnail($fileThumbnail); } } @@ -490,15 +491,14 @@ private function copyThumbnails(int $oldFileID, int $newFileID): void $thumbnailList->readObjects(); foreach ($thumbnailList as $oldThumbnail) { - $newThumbnail = FileThumbnailEditor::create([ - 'fileID' => $newFileID, - 'identifier' => $oldThumbnail->identifier, - 'fileHash' => $oldThumbnail->fileHash, - 'fileExtension' => $oldThumbnail->fileExtension, - 'width' => $oldThumbnail->width, - 'height' => $oldThumbnail->height, - 'formatChecksum' => $oldThumbnail->formatChecksum, - ]); + $newThumbnail = FileThumbnailBuilder::forCreate() + ->setFileID($newFileID) + ->setIdentifier($oldThumbnail->identifier) + ->setFileHash($oldThumbnail->fileHash) + ->setFileExtension($oldThumbnail->fileExtension) + ->setDimensions($oldThumbnail->width, $oldThumbnail->height) + ->setFormatChecksum($oldThumbnail->formatChecksum) + ->create(); \copy( $oldThumbnail->getPath() . $oldThumbnail->getSourceFilename(),