diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d862e03 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Apps Dev Team + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/composer.json b/composer.json index fac40cf..0207ae4 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "src/" ] }, + "license": "MIT", "require": { "nette/utils": "^2.0|^3.0|^4.0" }, diff --git a/src/Filters/Image.php b/src/Filters/Image.php index 8b5d9e7..556a82d 100644 --- a/src/Filters/Image.php +++ b/src/Filters/Image.php @@ -6,7 +6,6 @@ use ADT\Utils\FileSystem; use Exception; use Nette\Utils\ImageException; -use Nette\Utils\UnknownImageFileException; class Image { @@ -25,66 +24,54 @@ public function __construct(string $path, string $dir = 'thumbnails', int $multi IMAGETYPE_WEBP => 'webp' ]; + const ExtensionsToFormat = [ + 'webp' => IMAGETYPE_WEBP + ]; + /** - * @throws ImageException - * @throws UnknownImageFileException * @throws Exception */ - public function format(string $url, int $width, int $height, int $mode = \Nette\Utils\Image::FIT, int $format = IMAGETYPE_WEBP): string + public function format(string $url, int $width, int $height, int $mode = \Nette\Utils\Image::OrSmaller, int $format = IMAGETYPE_WEBP): string { + $originalUrl = $url; + $isRemoteUrl = $this->isRemoteUrl($url); // original file does not exist if ($isRemoteUrl) { $contents = @file_get_contents($url); if (!$contents) { - return $url; + return $originalUrl; } list($urlWithoutExtension,) = $this->splitUrlOnLastDot($this->removeProtocol($url)); } else { $url = trim($url, '/'); - if (!file_exists($this->path . '/' . $url)) { - return $url; + return $originalUrl; + } + $contents = file_get_contents($this->path . '/' . $url); + + if ($this->isAnimatedGif($contents)) { + return $originalUrl; } list($urlWithoutExtension,) = $this->splitUrlOnLastDot($url); } - - $width = $width * $this->multiplier; $height = $height * $this->multiplier; - $newPath = $this->path . '/' . $this->dir; - $newFileName = $urlWithoutExtension . '_' . $width . '_' . $height . '_' . $mode . '.' . self::FormatToExtensions[$format]; - - // thumbnail already exists - if (file_exists($newPath . '/' . $newFileName)) { - goto end; + $ext = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION); + if ($ext === 'svg') { + return $originalUrl; } - FileSystem::createDirAtomically(dirname($newPath . '/' . $newFileName)); - - $prevErrorHandler = set_error_handler(function ($errno, $errstr) use (&$prevErrorHandler) { - if ($errno === E_USER_WARNING && $errstr === 'Nette\Utils\Image::fromFile(): gd-png: libpng warning: iCCP: known incorrect sRGB profile') { - return true; - } - return $prevErrorHandler ? $prevErrorHandler(...func_get_args()) : false; - }); - if ($isRemoteUrl) { - $image = \Nette\Utils\Image::fromString($contents); - } else { - $image = \Nette\Utils\Image::fromFile($this->path . '/' . $url); - } - set_error_handler($prevErrorHandler); - $image->resize($width, $height, $mode); - $image->save($newPath . '/' . $newFileName, 100, $format); + $newFile = $this->dir . '/' . $urlWithoutExtension . '_' . $width . '_' . $height . '_' . $mode . '_' . $ext . '.' . self::FormatToExtensions[$format]; - end: + $this->createImage($contents, $width, $height, $mode, $format, $this->path . '/' . $newFile); - return '/' . $this->dir . '/' . $newFileName; + return '/' . $newFile; } private function isRemoteUrl(string $url): bool @@ -98,6 +85,45 @@ private function isRemoteUrl(string $url): bool return false; } + /** + * Thanks to ZeBadger for original example, and Davide Gualano for pointing me to it + * Original at http://it.php.net/manual/en/function.imagecreatefromgif.php#59787 + **/ + private function isAnimatedGif($fileContents): bool + { + $raw = $fileContents; + + $offset = 0; + $frames = 0; + while ($frames < 2) + { + $where1 = strpos($raw, "\x00\x21\xF9\x04", $offset); + if ( $where1 === false ) + { + break; + } + else + { + $offset = $where1 + 1; + $where2 = strpos( $raw, "\x00\x2C", $offset ); + if ( $where2 === false ) + { + break; + } + else + { + if ( $where1 + 8 == $where2 ) + { + $frames ++; + } + $offset = $where2 + 1; + } + } + } + + return $frames > 1; + } + private function splitUrlOnLastDot(string $url): array { $lastDotPos = strrpos($url, '.'); @@ -115,4 +141,76 @@ private function removeProtocol(string $url): string { return preg_replace("/^https?:\/\//", "", $url); } + + /** + * @throws ImageException + */ + public function createImageFromThumbnailUrl(string $url): bool + { + $info = pathinfo($url); + if (empty($info['extension'])) { + return false; + } + $format = $info['extension']; + if (!array_key_exists($format, static::ExtensionsToFormat)) { + return false; + } + + if (empty($info['filename'])) { + return false; + } + $fileInfo = pathinfo($info['filename']); + + if (empty($fileInfo['filename'])) { + return false; + } + $segments = explode('_', $fileInfo['filename']); + + $extension = array_pop($segments); + $mode = (int)array_pop($segments); + $height = (int)array_pop($segments); + $width = (int)array_pop($segments); + $originalFile = $info['dirname'] . '/' . implode('_', $segments) . '.' . $extension; + if (!file_exists($this->path . '/' . $originalFile)) { + return false; + } + $this->createImage(file_get_contents($this->path . '/' . $originalFile), $width, $height, $mode, static::ExtensionsToFormat[$format], $this->path . '/' . $this->dir . '/' . $url); + + return true; + } + + /** + * @throws ImageException + * @throws Exception + */ + protected function createImage(string $contents, int $width, int $height, int $mode, int $format, string $newFile): void + { + // thumbnail already exists + if (file_exists($newFile)) { + return; + } + + FileSystem::createDirAtomically(dirname($newFile)); + + $prevErrorHandler = set_error_handler(function ($errno, $errstr) use (&$prevErrorHandler) { + if ($errno === E_USER_WARNING && $errstr === 'Nette\Utils\Image::fromString(): gd-png: libpng warning: iCCP: known incorrect sRGB profile') { + return true; + } + return $prevErrorHandler ? $prevErrorHandler(...func_get_args()) : false; + }); + $image = \Nette\Utils\Image::fromString($contents); + set_error_handler($prevErrorHandler); + $image->resize($width, $height, $mode); + $image->save($newFile, 100, $format); + } + + public function getPath(): string + { + return $this->path; + } + + public function getDir(): string + { + return $this->dir; + } } diff --git a/src/Guzzle.php b/src/Guzzle.php index ada18ab..f9d72c0 100644 --- a/src/Guzzle.php +++ b/src/Guzzle.php @@ -10,6 +10,8 @@ class Guzzle { + private const MAX_BODY_LENGTH = 10000; + /** * @throws Throwable */ @@ -18,13 +20,39 @@ public static function handleException(Throwable $e): ?Exception if ($e instanceof GuzzleException) { $message = ''; if ($e instanceof ConnectException || $e instanceof RequestException) { - $message = "--- REQUEST ---\n" . Message::toString($e->getRequest()) . "\n --- RESPONSE ---\n"; + $message = "--- REQUEST ---\n" . self::sanitizeMessage(Message::toString($e->getRequest())) . "\n --- RESPONSE ---\n"; } - $message .= ($e instanceof RequestException && $e->getResponse() ? Message::toString($e->getResponse()) : $e->getMessage()); + $message .= ($e instanceof RequestException && $e->getResponse() ? self::sanitizeMessage(Message::toString($e->getResponse())) : $e->getMessage()); throw new Exception($message); } throw $e; } + + private static function sanitizeMessage(string $message): string + { + // Odstraneni binarnich dat (null byty apod.) + if (preg_match('/[^\x20-\x7E\x0A\x0D\t]/u', $message)) { + // Najdeme konec hlavicek (prazdny radek) + $headerEnd = strpos($message, "\r\n\r\n"); + if ($headerEnd === false) { + $headerEnd = strpos($message, "\n\n"); + } + + if ($headerEnd !== false) { + $headers = substr($message, 0, $headerEnd); + return $headers . "\n\n[binary data removed]"; + } + + return '[binary data removed]'; + } + + // Oriznuti prilis dlouhych textovych odpovedi + if (strlen($message) > self::MAX_BODY_LENGTH) { + return substr($message, 0, self::MAX_BODY_LENGTH) . "\n\n... [truncated, total " . strlen($message) . " bytes]"; + } + + return $message; + } } diff --git a/src/JsComponents.php b/src/JsComponents.php index 12b9686..2e49c73 100644 --- a/src/JsComponents.php +++ b/src/JsComponents.php @@ -2,17 +2,25 @@ namespace ADT\Utils; +use Nette\Utils\Json; + class JsComponents { protected array $components = []; public function generateConfig(): string { - return json_encode($this->components); + return Json::encode($this->components); } public function setRecaptcha(string $siteKey): string { return $this->components['recaptcha']['siteKey'] = $siteKey; } + + public function setComponents(array $components): self + { + $this->components = array_merge($this->components, $components); + return $this; + } } diff --git a/src/Utils.php b/src/Utils.php new file mode 100644 index 0000000..ca2c655 --- /dev/null +++ b/src/Utils.php @@ -0,0 +1,32 @@ +