From 37b078013e1be34e86d99f122175b430da87415e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bar=C3=A1=C5=A1ek?= Date: Thu, 14 Oct 2021 09:41:52 +0200 Subject: [PATCH] ImageStorage: Use strict policy. --- src/ImageStorage.php | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/ImageStorage.php b/src/ImageStorage.php index aec33e3..32e47f5 100644 --- a/src/ImageStorage.php +++ b/src/ImageStorage.php @@ -12,6 +12,8 @@ final class ImageStorage { + public const IMAGE_MIME_TYPES = ['image/gif', 'image/png', 'image/jpeg', 'image/webp']; + private string $storagePath; private string $relativeStoragePath; @@ -33,29 +35,55 @@ public function __construct(?string $storagePath = null, string $relativeStorage } + /** + * The method downloads the image from the physical URL and saves it to the internal storage. + * The download is checked to ensure that it is a valid data type for the image. + */ public function save(string $url): void { if (Validators::isUrl($url) === false) { throw new \InvalidArgumentException('Given input is not valid absolute URL, because "' . $url . '" given.'); } $storagePath = $this->getInternalPath($url); - if (\is_file($storagePath) === false) { - FileSystem::copy($url, $storagePath); + if (is_file($storagePath) === false) { // image does not exist in local storage + $content = FileSystem::read($url); // download image + $contentType = strtolower(finfo_file(finfo_open(FILEINFO_MIME_TYPE), $content)); + if (in_array($contentType, self::IMAGE_MIME_TYPES, true) === false) { + throw new \RuntimeException( + 'Security issue: Downloaded file "' . $url . '" is not valid image, ' + . 'because content type "' . $contentType . '" has been detected.', + ); + } + FileSystem::write($storagePath, $content); } } + /** + * Returns the absolute path for the internal data store. + * Specifies the absolute physical disk path where the image will be written to (or read from) based on the URL. + * The disk path is calculated by a deterministic algorithm for future content readability. + */ public function getInternalPath(string $url): string { return $this->storagePath . '/' . $this->getRelativeInternalUrl($url); } + /** + * Returns the physical URL to download the image directly to local disk storage. + */ public function getAbsoluteInternalUrl(string $url): string { try { $baseUrl = Url::get()->getBaseUrl(); } catch (\Throwable) { + if (PHP_SAPI === 'cli') { + throw new \LogicException( + __METHOD__ . ': Absolute URL is not available in CLI. ' + . 'Did you set context URL to "' . Url::class . '" service?', + ); + } $baseUrl = ''; } @@ -63,6 +91,9 @@ public function getAbsoluteInternalUrl(string $url): string } + /** + * Returns the relative URL to retrieve the image. + */ public function getRelativeInternalUrl(string $url): string { $originalFileName = (string) preg_replace_callback( @@ -76,10 +107,14 @@ public function getRelativeInternalUrl(string $url): string } + /** + * Generate an automatic unique hash based on the image URL + * so that file names from many different sources cannot collide. + */ private function resolvePrefixDir(string $url): string { if ($url === '') { - throw new \InvalidArgumentException('URL can not be empty string.'); + throw new \LogicException('URL can not be empty string.'); } if (preg_match('/wp-content.+(\d{4})\/(\d{2})/', $url, $urlParser)) { return $urlParser[1] . '-' . $urlParser[2];