From: Marcel Werk Date: Sat, 23 Mar 2019 22:26:01 +0000 (+0100) Subject: Rotate uploaded images based on the orientation stored in the exif data X-Git-Tag: 5.2.0_Alpha_1~187 X-Git-Url: https://git.stricted.de/?a=commitdiff_plain;h=075fe3707064830dfed33d7fe2fd914137bdf67f;p=GitHub%2FWoltLab%2FWCF.git Rotate uploaded images based on the orientation stored in the exif data Closes #2876 --- diff --git a/wcfsetup/install/files/lib/data/user/UserProfileAction.class.php b/wcfsetup/install/files/lib/data/user/UserProfileAction.class.php index 2406e7c237..718a7c2754 100644 --- a/wcfsetup/install/files/lib/data/user/UserProfileAction.class.php +++ b/wcfsetup/install/files/lib/data/user/UserProfileAction.class.php @@ -1,27 +1,24 @@ parameters['userID']) ? intval($this->parameters['userID']) : WCF::getUser()->userID)); + /** @noinspection PhpUndefinedMethodInspection */ + $this->parameters['__files']->saveFiles($saveStrategy); + if ($this->uploadFile->getValidationErrorType()) { return [ 'filesize' => $this->uploadFile->getFilesize(), @@ -566,58 +567,9 @@ class UserProfileAction extends UserAction { 'errorType' => $this->uploadFile->getValidationErrorType() ]; } - - try { - // shrink cover photo if necessary - $fileLocation = $this->enforceCoverPhotoDimensions($this->uploadFile->getLocation()); - } - catch (UserInputException $e) { - return [ - 'filesize' => $this->uploadFile->getFilesize(), - 'errorMessage' => WCF::getLanguage()->getDynamicVariable('wcf.user.coverPhoto.upload.error.' . $e->getType(), [ - 'file' => $this->uploadFile - ]), - 'errorType' => $e->getType() - ]; - } - - // delete old cover photo - if ($this->user->coverPhotoHash) { - UserProfileRuntimeCache::getInstance()->getObject($this->user->userID)->getCoverPhoto()->delete(); - } - - // update user - (new UserEditor($this->user))->update([ - // always generate a new hash to invalidate the browser cache and to avoid filename guessing - 'coverPhotoHash' => StringUtil::getRandomID(), - 'coverPhotoExtension' => $this->uploadFile->getFileExtension() - ]); - - // force-reload the user profile to use a predictable code-path to fetch the cover photo - UserProfileRuntimeCache::getInstance()->removeObject($this->user->userID); - $userProfile = UserProfileRuntimeCache::getInstance()->getObject($this->user->userID); - $coverPhoto = $userProfile->getCoverPhoto(); - - // check images directory and create subdirectory if necessary - $dir = dirname($coverPhoto->getLocation()); - if (!@file_exists($dir)) { - FileUtil::makePath($dir); - } - - if (@copy($fileLocation, $coverPhoto->getLocation())) { - @unlink($fileLocation); - - return [ - 'url' => $coverPhoto->getURL() - ]; - } else { return [ - 'filesize' => $this->uploadFile->getFilesize(), - 'errorMessage' => WCF::getLanguage()->getDynamicVariable('wcf.user.coverPhoto.upload.error.uploadFailed', [ - 'file' => $this->uploadFile - ]), - 'errorType' => 'uploadFailed' + 'url' => $saveStrategy->getCoverPhoto()->getURL() ]; } } @@ -688,40 +640,4 @@ class UserProfileAction extends UserAction { return $optionHandler; } - - /** - * Enforces dimensions for given cover photo. - * - * @param string $filename - * @return string - * @throws UserInputException - */ - protected function enforceCoverPhotoDimensions($filename) { - $imageData = getimagesize($filename); - if ($imageData[0] > UserCoverPhoto::MAX_WIDTH || $imageData[1] > UserCoverPhoto::MAX_HEIGHT) { - try { - $adapter = ImageHandler::getInstance()->getAdapter(); - $adapter->loadFile($filename); - $filename = FileUtil::getTemporaryFilename(); - $thumbnail = $adapter->createThumbnail(UserCoverPhoto::MAX_WIDTH, UserCoverPhoto::MAX_HEIGHT); - $adapter->writeImage($thumbnail, $filename); - } - catch (SystemException $e) { - throw new UserInputException('coverPhoto', 'maxSize'); - } - - // check dimensions (after shrink) - $imageData = getimagesize($filename); - if ($imageData[0] < UserCoverPhoto::MIN_WIDTH || $imageData[1] < UserCoverPhoto::MIN_HEIGHT) { - throw new UserInputException('coverPhoto', 'maxSize'); - } - - // check filesize (after shrink) - if (@filesize($filename) > WCF::getSession()->getPermission('user.profile.coverPhoto.maxSize')) { - throw new UserInputException('coverPhoto', 'maxSize'); - } - } - - return $filename; - } } diff --git a/wcfsetup/install/files/lib/data/user/avatar/UserAvatarAction.class.php b/wcfsetup/install/files/lib/data/user/avatar/UserAvatarAction.class.php index b96cb3ca43..94682ae161 100644 --- a/wcfsetup/install/files/lib/data/user/avatar/UserAvatarAction.class.php +++ b/wcfsetup/install/files/lib/data/user/avatar/UserAvatarAction.class.php @@ -7,7 +7,7 @@ use wcf\system\exception\IllegalLinkException; use wcf\system\exception\PermissionDeniedException; use wcf\system\exception\SystemException; use wcf\system\exception\UserInputException; -use wcf\system\image\ImageHandler; +use wcf\system\upload\AvatarUploadFileSaveStrategy; use wcf\system\upload\AvatarUploadFileValidationStrategy; use wcf\system\upload\UploadFile; use wcf\system\user\storage\UserStorageHandler; @@ -72,77 +72,21 @@ class UserAvatarAction extends AbstractDatabaseObjectAction { * Handles uploaded attachments. */ public function upload() { - /** @var UploadFile[] $files */ + /** @var UploadFile $file */ + $file = $this->parameters['__files']->getFiles()[0]; + $saveStrategy = new AvatarUploadFileSaveStrategy((!empty($this->parameters['userID']) ? intval($this->parameters['userID']) : WCF::getUser()->userID)); /** @noinspection PhpUndefinedMethodInspection */ - $files = $this->parameters['__files']->getFiles(); - $userID = (!empty($this->parameters['userID']) ? intval($this->parameters['userID']) : WCF::getUser()->userID); - $user = ($userID != WCF::getUser()->userID ? new User($userID) : WCF::getUser()); - $file = $files[0]; + $this->parameters['__files']->saveFiles($saveStrategy); - try { - if (!$file->getValidationErrorType()) { - // shrink avatar if necessary - $fileLocation = $this->enforceDimensions($file->getLocation()); - $imageData = getimagesize($fileLocation); - - $data = [ - 'avatarName' => $file->getFilename(), - 'avatarExtension' => $file->getFileExtension(), - 'width' => $imageData[0], - 'height' => $imageData[1], - 'userID' => $userID, - 'fileHash' => sha1_file($fileLocation) - ]; - - // create avatar - $avatar = UserAvatarEditor::create($data); - - // check avatar directory - // and create subdirectory if necessary - $dir = dirname($avatar->getLocation()); - if (!@file_exists($dir)) { - FileUtil::makePath($dir); - } - - // move uploaded file - if (@copy($fileLocation, $avatar->getLocation())) { - @unlink($fileLocation); - - // delete old avatar - if ($user->avatarID) { - $action = new UserAvatarAction([$user->avatarID], 'delete'); - $action->executeAction(); - } - - // update user - $userEditor = new UserEditor($user); - $userEditor->update([ - 'avatarID' => $avatar->avatarID, - 'enableGravatar' => 0 - ]); - - // reset user storage - UserStorageHandler::getInstance()->reset([$userID], 'avatar'); - - // return result - return [ - 'avatarID' => $avatar->avatarID, - 'url' => $avatar->getURL(96) - ]; - } - else { - // moving failed; delete avatar - $editor = new UserAvatarEditor($avatar); - $editor->delete(); - throw new UserInputException('avatar', 'uploadFailed'); - } - } + if ($file->getValidationErrorType()) { + return ['errorType' => $file->getValidationErrorType()]; } - catch (UserInputException $e) { - $file->setValidationErrorType($e->getType()); + else { + return [ + 'avatarID' => $saveStrategy->getAvatar()->avatarID, + 'url' => $saveStrategy->getAvatar()->getURL(96) + ]; } - - return ['errorType' => $file->getValidationErrorType()]; } /** @@ -276,23 +220,17 @@ class UserAvatarAction extends AbstractDatabaseObjectAction { * @throws UserInputException */ protected function enforceDimensions($filename) { - $imageData = getimagesize($filename); - if ($imageData[0] > UserAvatar::AVATAR_SIZE || $imageData[1] > UserAvatar::AVATAR_SIZE) { - try { - $adapter = ImageHandler::getInstance()->getAdapter(); - $adapter->loadFile($filename); - $filename = FileUtil::getTemporaryFilename(); - $thumbnail = $adapter->createThumbnail(UserAvatar::AVATAR_SIZE, UserAvatar::AVATAR_SIZE, false); - $adapter->writeImage($thumbnail, $filename); - } - catch (SystemException $e) { - throw new UserInputException('avatar', 'tooLarge'); - } - - // check filesize (after shrink) - if (@filesize($filename) > WCF::getSession()->getPermission('user.profile.avatar.maxSize')) { - throw new UserInputException('avatar', 'tooLarge'); - } + try { + $filename = ImageUtil::enforceDimensions($filename, UserAvatar::AVATAR_SIZE, UserAvatar::AVATAR_SIZE); + } + /** @noinspection PhpRedundantCatchClauseInspection */ + catch (SystemException $e) { + throw new UserInputException('avatar', 'tooLarge'); + } + + // check filesize (after shrink) + if (@filesize($filename) > WCF::getSession()->getPermission('user.profile.avatar.maxSize')) { + throw new UserInputException('avatar', 'tooLarge'); } return $filename; diff --git a/wcfsetup/install/files/lib/system/upload/AvatarUploadFileSaveStrategy.class.php b/wcfsetup/install/files/lib/system/upload/AvatarUploadFileSaveStrategy.class.php new file mode 100644 index 0000000000..9dcfe383f9 --- /dev/null +++ b/wcfsetup/install/files/lib/system/upload/AvatarUploadFileSaveStrategy.class.php @@ -0,0 +1,130 @@ + + * @package WoltLabSuite\Core\System\Upload + * @since 5.2 + */ +class AvatarUploadFileSaveStrategy implements IUploadFileSaveStrategy { + /** + * @var integer + */ + protected $userID = 0; + + /** + * @var User + */ + protected $user; + + /** + * @var UserAvatar + */ + protected $avatar; + + /** + * Creates a new instance of AvatarUploadFileSaveStrategy. + * + * @param integer $userID + */ + public function __construct($userID = null) { + $this->userID = ($userID ?: WCF::getUser()->userID); + $this->user = ($this->userID != WCF::getUser()->userID ? new User($userID) : WCF::getUser()); + } + + /** + * @return UserAvatar + */ + public function getAvatar() { + return $this->avatar; + } + + /** + * @inheritDoc + */ + public function save(UploadFile $uploadFile) { + if (!$uploadFile->getValidationErrorType()) { + // rotate avatar if necessary + /** @noinspection PhpUnusedLocalVariableInspection */ + $fileLocation = ImageUtil::fixOrientation($uploadFile->getLocation()); + + // shrink avatar if necessary + try { + $fileLocation = ImageUtil::enforceDimensions($fileLocation, UserAvatar::AVATAR_SIZE, UserAvatar::AVATAR_SIZE, false); + } + /** @noinspection PhpRedundantCatchClauseInspection */ + catch (SystemException $e) { + $uploadFile->setValidationErrorType('tooLarge'); + return; + } + + // check filesize (after shrink) + if (@filesize($fileLocation) > WCF::getSession()->getPermission('user.profile.avatar.maxSize')) { + $uploadFile->setValidationErrorType('tooLarge'); + return; + } + + $imageData = getimagesize($fileLocation); + $data = [ + 'avatarName' => $uploadFile->getFilename(), + 'avatarExtension' => $uploadFile->getFileExtension(), + 'width' => $imageData[0], + 'height' => $imageData[1], + 'userID' => $this->userID, + 'fileHash' => sha1_file($fileLocation) + ]; + + // create avatar + $this->avatar = UserAvatarEditor::create($data); + + // check avatar directory + // and create subdirectory if necessary + $dir = dirname($this->avatar->getLocation()); + if (!@file_exists($dir)) { + FileUtil::makePath($dir); + } + + // move uploaded file + if (@copy($fileLocation, $this->avatar->getLocation())) { + @unlink($fileLocation); + + // delete old avatar + if ($this->user->avatarID) { + $action = new UserAvatarAction([$this->user->avatarID], 'delete'); + $action->executeAction(); + } + + // update user + $userEditor = new UserEditor($this->user); + $userEditor->update([ + 'avatarID' => $this->avatar->avatarID, + 'enableGravatar' => 0 + ]); + + // reset user storage + UserStorageHandler::getInstance()->reset([$this->userID], 'avatar'); + } + else { + // moving failed; delete avatar + $editor = new UserAvatarEditor($this->avatar); + $editor->delete(); + + $uploadFile->setValidationErrorType('uploadFailed'); + } + } + } +} diff --git a/wcfsetup/install/files/lib/system/upload/UserCoverPhotoUploadFileSaveStrategy.class.php b/wcfsetup/install/files/lib/system/upload/UserCoverPhotoUploadFileSaveStrategy.class.php new file mode 100644 index 0000000000..c7d578598a --- /dev/null +++ b/wcfsetup/install/files/lib/system/upload/UserCoverPhotoUploadFileSaveStrategy.class.php @@ -0,0 +1,128 @@ + + * @package WoltLabSuite\Core\System\Upload + * @since 5.2 + */ +class UserCoverPhotoUploadFileSaveStrategy implements IUploadFileSaveStrategy { + /** + * @var integer + */ + protected $userID = 0; + + /** + * @var User + */ + protected $user; + + /** + * @var IUserCoverPhoto + */ + protected $coverPhoto; + + /** + * Creates a new instance of UserCoverPhotoUploadFileSaveStrategy. + * + * @param integer $userID + */ + public function __construct($userID = null) { + $this->userID = ($userID ?: WCF::getUser()->userID); + $this->user = ($this->userID != WCF::getUser()->userID ? new User($userID) : WCF::getUser()); + } + + /** + * @return IUserCoverPhoto + */ + public function getCoverPhoto() { + return $this->coverPhoto; + } + + /** + * @inheritDoc + */ + public function save(UploadFile $uploadFile) { + if (!$uploadFile->getValidationErrorType()) { + // rotate image if necessary + /** @noinspection PhpUnusedLocalVariableInspection */ + $fileLocation = ImageUtil::fixOrientation($uploadFile->getLocation()); + + // shrink cover photo if necessary + try { + $newFileLocation = ImageUtil::enforceDimensions($fileLocation, UserCoverPhoto::MAX_WIDTH, UserCoverPhoto::MAX_HEIGHT); + } + /** @noinspection PhpRedundantCatchClauseInspection */ + catch (SystemException $e) { + $uploadFile->setValidationErrorType('maxSize'); + return; + } + + if ($newFileLocation != $fileLocation) { + // check dimensions (after shrink) + $imageData = getimagesize($newFileLocation); + if ($imageData[0] < UserCoverPhoto::MIN_WIDTH || $imageData[1] < UserCoverPhoto::MIN_HEIGHT) { + $uploadFile->setValidationErrorType('tooLarge'); + return; + } + + // check filesize (after shrink) + if (@filesize($newFileLocation) > WCF::getSession()->getPermission('user.profile.coverPhoto.maxSize')) { + $uploadFile->setValidationErrorType('tooLarge'); + return; + } + } + $fileLocation = $newFileLocation; + + // delete old cover photo + if ($this->user->coverPhotoHash) { + UserProfileRuntimeCache::getInstance()->getObject($this->user->userID)->getCoverPhoto()->delete(); + } + + // update user + (new UserEditor($this->user))->update([ + // always generate a new hash to invalidate the browser cache and to avoid filename guessing + 'coverPhotoHash' => StringUtil::getRandomID(), + 'coverPhotoExtension' => $uploadFile->getFileExtension() + ]); + + // force-reload the user profile to use a predictable code-path to fetch the cover photo + UserProfileRuntimeCache::getInstance()->removeObject($this->user->userID); + $userProfile = UserProfileRuntimeCache::getInstance()->getObject($this->user->userID); + $this->coverPhoto = $userProfile->getCoverPhoto(); + + // check images directory and create subdirectory if necessary + $dir = dirname($this->coverPhoto->getLocation()); + if (!@file_exists($dir)) { + FileUtil::makePath($dir); + } + + // move uploaded file + if (!@copy($fileLocation, $this->coverPhoto->getLocation())) { + // copy failed + @unlink($fileLocation); + (new UserEditor($this->user))->update([ + 'coverPhotoHash' => '', + 'coverPhotoExtension' => '' + ]); + $uploadFile->setValidationErrorType('uploadFailed'); + } + + @unlink($fileLocation); + } + } +} diff --git a/wcfsetup/install/files/lib/util/ImageUtil.class.php b/wcfsetup/install/files/lib/util/ImageUtil.class.php index 0e0cb3822d..b0e89beeec 100644 --- a/wcfsetup/install/files/lib/util/ImageUtil.class.php +++ b/wcfsetup/install/files/lib/util/ImageUtil.class.php @@ -1,5 +1,7 @@ $maxWidth || $imageData[1] > $maxHeight) { + $adapter = ImageHandler::getInstance()->getAdapter(); + $adapter->loadFile($filename); + $filename = FileUtil::getTemporaryFilename(); + $thumbnail = $adapter->createThumbnail($maxWidth, $maxHeight, $obtainDimensions); + $adapter->writeImage($thumbnail, $filename); + } + + return $filename; + } + + /** + * Rotates the given image based on the orientation stored in the exif data. + * + * @param string $filename + * @return string new filename if file was changed, otherwise old filename + * @since 5.2 + */ + public static function fixOrientation($filename) { + try { + $exifData = ExifUtil::getExifData($filename); + if (!empty($exifData)) { + $orientation = ExifUtil::getOrientation($exifData); + if ($orientation != ExifUtil::ORIENTATION_ORIGINAL) { + $adapter = ImageHandler::getInstance()->getAdapter(); + $adapter->loadFile($filename); + + $newImage = null; + switch ($orientation) { + case ExifUtil::ORIENTATION_180_ROTATE: + $newImage = $adapter->rotate(180); + break; + + case ExifUtil::ORIENTATION_90_ROTATE: + $newImage = $adapter->rotate(90); + break; + + case ExifUtil::ORIENTATION_270_ROTATE: + $newImage = $adapter->rotate(270); + break; + + case ExifUtil::ORIENTATION_HORIZONTAL_FLIP: + case ExifUtil::ORIENTATION_VERTICAL_FLIP: + case ExifUtil::ORIENTATION_VERTICAL_FLIP_270_ROTATE: + case ExifUtil::ORIENTATION_HORIZONTAL_FLIP_270_ROTATE: + // unsupported + break; + } + + if ($newImage !== null) { + $adapter->load($newImage, $adapter->getType()); + } + + $newFilename = FileUtil::getTemporaryFilename(); + $adapter->writeImage($newFilename); + $filename = $newFilename; + } + } + } + catch (SystemException $e) {} + + return $filename; + } + /** * Forbid creation of ImageUtil objects. */