diff --git a/src/controllers/AssetsController.php b/src/controllers/AssetsController.php index 16fca1b..e791dfb 100644 --- a/src/controllers/AssetsController.php +++ b/src/controllers/AssetsController.php @@ -11,9 +11,12 @@ use craft\fields\Assets as AssetsField; use craft\helpers\Assets; use craft\helpers\Db; +use craft\helpers\FileHelper; +use craft\helpers\Image; use craft\models\Volume; use craft\web\Controller; use DateTime; +use Throwable; use yii\base\Event; use yii\base\Exception; use yii\base\Model; @@ -95,8 +98,6 @@ public function actionCreateAsset(): Response $originalFilename = $this->request->getRequiredBodyParam('originalFilename'); $targetFilename = $this->request->getRequiredBodyParam('targetFilename'); $size = $this->request->getBodyParam('size'); - $width = $this->request->getBodyParam('width'); - $height = $this->request->getBodyParam('height'); $elementsService = Craft::$app->getElements(); $lastModifiedMs = (int) $this->request->getBodyParam('lastModified'); $dateModified = $lastModifiedMs @@ -160,8 +161,6 @@ public function actionCreateAsset(): Response $asset->avoidFilenameConflicts = true; $asset->dateModified = $dateModified; $asset->size = $size; - $asset->width = $width; - $asset->height = $height; // Setting newFolderId, so that extension validation on newLocation occurs $asset->newFolderId = $folder->id; @@ -172,6 +171,8 @@ public function actionCreateAsset(): Response // Handle special characters that have been encoded from the presigned URL $asset->folderPath = is_string($folder->path) ? Fs::urlEncodePathSegments($folder->path) : $asset->folderPath; + [$asset->width, $asset->height] = $this->uploadedImageDimensions($asset, $filename); + if (!$selectionCondition) { $asset->newFilename = $targetFilename; } @@ -240,8 +241,6 @@ public function actionReplaceFile(): Response $filename = $this->request->getBodyParam('filename'); $targetFilename = $this->request->getBodyParam('targetFilename'); $size = $this->request->getBodyParam('size'); - $width = $this->request->getBodyParam('width'); - $height = $this->request->getBodyParam('height'); $lastModifiedMs = (int) $this->request->getBodyParam('lastModified'); $dateModified = $lastModifiedMs ? DateTime::createFromFormat('U', (string) floor($lastModifiedMs / 1000)) @@ -272,8 +271,6 @@ public function actionReplaceFile(): Response // Handle the Element Action if ($assetToReplace !== null && $filename) { - $assetToReplace->width = $width; - $assetToReplace->height = $height; $assetToReplace->size = $size; $assetToReplace->dateModified = $dateModified; if (!$this->replaceAssetFile($assetToReplace, $filename, $targetFilename)) { @@ -357,6 +354,7 @@ public function replaceAssetFile(Asset $asset, string $filename, string $targetF $asset->avoidFilenameConflicts = true; $asset->setScenario(Asset::SCENARIO_REPLACE); $asset->setFilename($filename); + [$asset->width, $asset->height] = $this->uploadedImageDimensions($asset, $filename); $asset->newFilename = $targetFilename; $saved = $this->saveAsset($asset); @@ -396,4 +394,47 @@ private function volumeSubpath(Volume $volume): string { return method_exists($volume, 'getSubpath') ? $volume->getSubpath() : ''; } + + protected function uploadedImageDimensions(Asset $asset, string $filename): array + { + if (Assets::getFileKindByExtension($filename) !== Asset::KIND_IMAGE) { + return [null, null]; + } + + return $this->readUploadedImageDimensions($asset) ?? [null, null]; + } + + protected function readUploadedImageDimensions(Asset $asset): ?array + { + $stream = null; + $tempPath = null; + + try { + $stream = $asset->getVolume()->getFs()->getFileStream($asset->getPath()); + $imageSize = Image::imageSizeByStream($stream); + + if ($imageSize === false || !isset($imageSize[0], $imageSize[1])) { + fclose($stream); + $stream = null; + + $tempPath = $asset->getCopyOfFile(); + $imageSize = Image::imageSize($tempPath); + } + + return [ + (int)$imageSize[0] ?: null, + (int)$imageSize[1] ?: null, + ]; + } catch (Throwable) { + return null; + } finally { + if (is_resource($stream)) { + fclose($stream); + } + + if ($tempPath !== null) { + FileHelper::unlink($tempPath); + } + } + } } diff --git a/src/fs/Fs.php b/src/fs/Fs.php index 5bdbd44..8c05a06 100644 --- a/src/fs/Fs.php +++ b/src/fs/Fs.php @@ -94,7 +94,7 @@ public function createUrl(string $path = ''): UriInterface { if ($this->useLocalFs) { return Modifier::wrap($this->getLocalFs()->getRootUrl() ?? '/') - ->appendSegment($this->createPath($path)) + ->appendPath($this->createPath($path)) ->unwrap(); } @@ -102,12 +102,12 @@ public function createUrl(string $path = ''): UriInterface if ($baseUrl) { return Modifier::wrap($baseUrl) - ->appendSegment($this->createPath($path)) + ->appendPath($this->createPath($path)) ->unwrap(); } return Modifier::wrap(Module::getInstance()->getConfig()->cdnBaseUrl) - ->appendSegment($this->createBucketPath($path)) + ->appendPath($this->createBucketPath($path)) ->unwrap(); } diff --git a/tests/unit/AssetsControllerTest.php b/tests/unit/AssetsControllerTest.php index d6d85dd..f6756e9 100644 --- a/tests/unit/AssetsControllerTest.php +++ b/tests/unit/AssetsControllerTest.php @@ -5,6 +5,7 @@ use Codeception\Test\Unit; use Craft; use craft\cloud\controllers\AssetsController; +use craft\elements\Asset; use craft\models\Volume; use ReflectionMethod; @@ -39,6 +40,56 @@ public function testVolumeSubpathReturnsVolumeSubpathOnCraft5(): void $this->assertSame('volume-prefix/', $this->invokeVolumeSubpath($volume)); } + public function testImageUploadsUseUploadedImageDimensions(): void + { + $controller = new DimensionTestAssetsController('cloud-assets', Craft::$app); + $controller->uploadedImageDimensions = [2139, 3020]; + + $this->assertSame([2139, 3020], $controller->uploadedImageDimensionsForTest( + new Asset(), + 'upload.jpeg', + )); + $this->assertSame(1, $controller->readCount); + } + + public function testImageUploadsUseNullDimensionsWhenUploadedImageDimensionsCannotBeRead(): void + { + $controller = new DimensionTestAssetsController('cloud-assets', Craft::$app); + + $this->assertSame([null, null], $controller->uploadedImageDimensionsForTest( + new Asset(), + 'upload.jpeg', + )); + $this->assertSame(1, $controller->readCount); + } + + public function testNonImageUploadsUseNullDimensions(): void + { + $controller = new DimensionTestAssetsController('cloud-assets', Craft::$app); + $controller->uploadedImageDimensions = [2139, 3020]; + + $this->assertSame([null, null], $controller->uploadedImageDimensionsForTest( + new Asset(), + 'document.pdf', + )); + $this->assertSame(0, $controller->readCount); + } + + public function testReplacementUploadsUseServerDimensionsForUploadedFile(): void + { + $asset = new Asset(); + $asset->folderPath = 'uploads'; + + $controller = new DimensionTestAssetsController('cloud-assets', Craft::$app); + $controller->uploadedImageDimensions = [1080, 1440]; + + $this->assertSame([1080, 1440], $controller->uploadedImageDimensionsForTest( + $asset, + 'upload-replacement.jpeg', + )); + $this->assertSame(1, $controller->readCount); + } + private function invokeVolumeSubpath(Volume $volume): string { $controller = new AssetsController('cloud-assets', Craft::$app); @@ -48,3 +99,23 @@ private function invokeVolumeSubpath(Volume $volume): string return $method->invoke($controller, $volume); } } + +class DimensionTestAssetsController extends AssetsController +{ + public ?array $uploadedImageDimensions = null; + public int $readCount = 0; + + public function uploadedImageDimensionsForTest(Asset $asset, string $filename): array + { + $asset->setFilename($filename); + + return $this->uploadedImageDimensions($asset, $filename); + } + + protected function readUploadedImageDimensions(Asset $asset): ?array + { + $this->readCount++; + + return $this->uploadedImageDimensions; + } +}