diff --git a/app/Config/Mimes.php b/app/Config/Mimes.php index c2db7340cd7e..c8f3297336a3 100644 --- a/app/Config/Mimes.php +++ b/app/Config/Mimes.php @@ -259,6 +259,7 @@ class Mimes 'image/x-png', ], 'webp' => 'image/webp', + 'avif' => 'image/avif', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'css' => [ diff --git a/app/Config/Publisher.php b/app/Config/Publisher.php index bf03be1db7e0..0cb6769972fc 100644 --- a/app/Config/Publisher.php +++ b/app/Config/Publisher.php @@ -23,6 +23,6 @@ class Publisher extends BasePublisher */ public $restrictions = [ ROOTPATH => '*', - FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', + FCPATH => '#\.(s?css|js|map|html?|xml|json|webmanifest|ttf|eot|woff2?|gif|jpe?g|tiff?|png|webp|avif|bmp|ico|svg)$#i', ]; } diff --git a/system/Config/Publisher.php b/system/Config/Publisher.php index 7e1b0ffb54f2..3038644e4aeb 100644 --- a/system/Config/Publisher.php +++ b/system/Config/Publisher.php @@ -32,7 +32,7 @@ class Publisher extends BaseConfig */ public $restrictions = [ ROOTPATH => '*', - FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|bmp|ico|svg)$#i', + FCPATH => '#\.(?css|js|map|htm?|xml|json|webmanifest|tff|eot|woff?|gif|jpe?g|tiff?|png|webp|avif|bmp|ico|svg)$#i', ]; /** diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php index e38c5836ccca..442614b3d00f 100644 --- a/system/Images/Handlers/BaseHandler.php +++ b/system/Images/Handlers/BaseHandler.php @@ -115,6 +115,7 @@ abstract class BaseHandler implements ImageHandlerInterface protected $supportTransparency = [ IMAGETYPE_PNG, IMAGETYPE_WEBP, + IMAGETYPE_AVIF, ]; /** @@ -682,7 +683,7 @@ abstract public function getVersion(); * * @return bool */ - abstract public function save(?string $target = null, int $quality = 90); + abstract public function save(?string $target = null, int $quality = 90, int $speed = -1); /** * Does the driver-specific processing of the image. diff --git a/system/Images/Handlers/GDHandler.php b/system/Images/Handlers/GDHandler.php index 01c05384c744..851a6d21c55a 100644 --- a/system/Images/Handlers/GDHandler.php +++ b/system/Images/Handlers/GDHandler.php @@ -178,7 +178,7 @@ protected function process(string $action) $dest = $create($this->width, $this->height); - // for png and webp we can actually preserve transparency + // for png, webp and avif we can actually preserve transparency if (in_array($this->image()->imageType, $this->supportTransparency, true)) { imagealphablending($dest, false); imagesavealpha($dest, true); @@ -202,7 +202,7 @@ protected function process(string $action) * * @param non-empty-string|null $target */ - public function save(?string $target = null, int $quality = 90): bool + public function save(?string $target = null, int $quality = 90, int $speed = -1): bool { $original = $target; $target = ($target === null || $target === '') ? $this->image()->getPathname() : $target; @@ -222,7 +222,7 @@ public function save(?string $target = null, int $quality = 90): bool $this->ensureResource(); - // for png and webp we can actually preserve transparency + // for png, webp and avif we can actually preserve transparency if (in_array($this->image()->imageType, $this->supportTransparency, true)) { imagepalettetotruecolor($this->resource); imagealphablending($this->resource, false); @@ -270,6 +270,16 @@ public function save(?string $target = null, int $quality = 90): bool } break; + case IMAGETYPE_AVIF: + if (! function_exists('imageavif')) { + throw ImageException::forInvalidImageCreate(lang('Images.avifNotSupported')); + } + + if (! @imageavif($this->resource, $target, $quality, $speed)) { + throw ImageException::forSaveFailed(); + } + break; + default: throw ImageException::forInvalidImageCreate(); } @@ -361,6 +371,13 @@ protected function getImageResource(string $path, int $imageType) return imagecreatefromwebp($path); + case IMAGETYPE_AVIF: + if (! function_exists('imagecreatefromavif')) { + throw ImageException::forInvalidImageCreate(lang('Images.avifNotSupported')); + } + + return imagecreatefromavif($path); + default: throw ImageException::forInvalidImageCreate('Ima'); } diff --git a/system/Images/Image.php b/system/Images/Image.php index 55754e339535..b69b78fa6b27 100644 --- a/system/Images/Image.php +++ b/system/Images/Image.php @@ -113,6 +113,7 @@ public function getProperties(bool $return = false) IMAGETYPE_JPEG => 'jpeg', IMAGETYPE_PNG => 'png', IMAGETYPE_WEBP => 'webp', + IMAGETYPE_AVIF => 'avif', ]; $mime = 'image/' . ($types[$vals[2]] ?? 'jpg'); diff --git a/system/Language/en/Images.php b/system/Language/en/Images.php index 2af9e0a2d1d3..069db5886631 100644 --- a/system/Language/en/Images.php +++ b/system/Language/en/Images.php @@ -20,6 +20,7 @@ 'jpgNotSupported' => 'JPG images are not supported.', 'pngNotSupported' => 'PNG images are not supported.', 'webpNotSupported' => 'WEBP images are not supported.', + 'avifNotSupported' => 'AVIF images are not supported.', 'fileNotSupported' => 'The supplied file is not a supported image type.', 'unsupportedImageCreate' => 'Your server does not support the required functionality to process this type of image.', 'jpgOrPngRequired' => 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.', diff --git a/tests/system/AutoReview/CreateNewChangelogTest.php b/tests/system/AutoReview/CreateNewChangelogTest.php index fc348565464f..b6a89e047f88 100644 --- a/tests/system/AutoReview/CreateNewChangelogTest.php +++ b/tests/system/AutoReview/CreateNewChangelogTest.php @@ -65,6 +65,12 @@ protected function setUp(): void #[DataProvider('provideCreateNewChangelog')] public function testCreateNewChangelog(string $mode): void { + $output = exec('git status --porcelain | wc -l'); + + if ($output !== '0') { + $this->markTestSkipped('You have uncommitted operations that will be erased by this test.'); + } + $currentVersion = $this->currentVersion; $newVersion = $this->incrementVersion($currentVersion, $mode); diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php index d85d1a142ffd..c75fd3c47cf8 100644 --- a/tests/system/Images/GDHandlerTest.php +++ b/tests/system/Images/GDHandlerTest.php @@ -319,10 +319,14 @@ public function testMoreText(): void public function testImageCreation(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! function_exists('imagecreatefromavif')) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type); @@ -334,10 +338,14 @@ public function testImageCreation(): void public function testImageCopy(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! function_exists('imagecreatefromavif')) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type); @@ -353,9 +361,13 @@ public function testImageCopy(): void public function testImageCopyWithNoTargetAndMaxQuality(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { $this->handler->withFile($this->origin . 'ci-logo.' . $type); - $this->handler->save(null, 100); + if($type === 'avif') { + $this->handler->save(null, 100, 10); + } else { + $this->handler->save(null, 100); + } $this->assertFileExists($this->origin . 'ci-logo.' . $type); $this->assertSame( @@ -367,10 +379,14 @@ public function testImageCopyWithNoTargetAndMaxQuality(): void public function testImageCompressionGetResource(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! function_exists('imagecreatefromavif')) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type); @@ -387,10 +403,14 @@ public function testImageCompressionGetResource(): void public function testImageCompressionWithResource(): void { - foreach (['gif', 'jpeg', 'png', 'webp'] as $type) { + foreach (['gif', 'jpeg', 'png', 'webp', 'avif'] as $type) { if ($type === 'webp' && ! function_exists('imagecreatefromwebp')) { $this->expectException(ImageException::class); - $this->expectExceptionMessage('Your server does not support the GD function required to process this type of image.'); + $this->expectExceptionMessage('Your server does not support the GD function required to process a webp image.'); + } + if ($type === 'avif' && ! function_exists('imagecreatefromavif')) { + $this->expectException(ImageException::class); + $this->expectExceptionMessage('Your server does not support the GD function required to process an avif image.'); } $this->handler->withFile($this->origin . 'ci-logo.' . $type) @@ -423,6 +443,15 @@ public function testImageConvertPngToWebp(): void $this->assertSame(IMAGETYPE_WEBP, exif_imagetype($saved)); } + public function testImageConvertPngToAvif(): void + { + $this->handler->withFile($this->origin . 'rocket.png'); + $this->handler->convert(IMAGETYPE_AVIF); + $saved = $this->start . 'work/rocket.avif'; + $this->handler->save($saved); + $this->assertSame(IMAGETYPE_AVIF, exif_imagetype($saved)); + } + public function testImageReorientLandscape(): void { for ($i = 0; $i <= 8; $i++) { diff --git a/tests/system/Models/UpdateModelTest.php b/tests/system/Models/UpdateModelTest.php index 7f0f6dd98527..2e2e61c03e34 100644 --- a/tests/system/Models/UpdateModelTest.php +++ b/tests/system/Models/UpdateModelTest.php @@ -19,6 +19,7 @@ use CodeIgniter\Entity\Entity; use CodeIgniter\Exceptions\InvalidArgumentException; use Config\Database; +use DateTimeInterface; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use stdClass; @@ -208,13 +209,17 @@ public function testUpdateBatchValidationFail(): void public function testUpdateBatchWithEntity(): void { $entity1 = new class () extends Entity { - protected $id; - protected $name; - protected $email; - protected $country; - protected $deleted; - protected $created_at; - protected $updated_at; + protected int $id; + protected string $name; + protected string $email; + protected string $country; + protected bool $deleted; + protected DateTimeInterface $created_at; + protected DateTimeInterface $updated_at; + + /** + * @var array{'datamap': array{}, 'dates': array{string, string, string}, 'casts': array{}} + */ protected $_options = [ 'datamap' => [], 'dates' => [ @@ -227,13 +232,17 @@ public function testUpdateBatchWithEntity(): void }; $entity2 = new class () extends Entity { - protected $id; - protected $name; - protected $email; - protected $country; - protected $deleted; - protected $created_at; - protected $updated_at; + protected int $id; + protected string $name; + protected string $email; + protected string $country; + protected bool $deleted; + protected DateTimeInterface $created_at; + protected DateTimeInterface $updated_at; + + /** + * @var array{'datamap': array{}, 'dates': array{string, string, string}, 'casts': array{}} + */ protected $_options = [ 'datamap' => [], 'dates' => [ @@ -399,13 +408,17 @@ public function testUpdateWithEntityNoAllowedFields(): void $this->createModel(UserModel::class); $entity = new class () extends Entity { - protected $id; - protected $name; - protected $email; - protected $country; - protected $deleted; - protected $created_at; - protected $updated_at; + protected int $id; + protected string $name; + protected string $email; + protected string $country; + protected bool $deleted; + protected DateTimeInterface $created_at; + protected DateTimeInterface $updated_at; + + /** + * @var array{'datamap': array{}, 'dates': array{string, string, string}, 'casts': array{}} + */ protected $_options = [ 'datamap' => [], 'dates' => [ diff --git a/user_guide_src/source/database/query_builder.rst b/user_guide_src/source/database/query_builder.rst index e901869fb5f7..480b8c18d92e 100644 --- a/user_guide_src/source/database/query_builder.rst +++ b/user_guide_src/source/database/query_builder.rst @@ -444,7 +444,7 @@ searches. .. literalinclude:: query_builder/039.php If you want to control where the wildcard (**%**) is placed, you can use - an optional third argument. Your options are ``before``, ``after`` and + an optional third argument. Your options are ``none``, ``before``, ``after`` and ``both`` (which is the default). .. literalinclude:: query_builder/040.php diff --git a/user_guide_src/source/database/query_builder/040.php b/user_guide_src/source/database/query_builder/040.php index 519686f9e4d9..d72a590cc11b 100644 --- a/user_guide_src/source/database/query_builder/040.php +++ b/user_guide_src/source/database/query_builder/040.php @@ -1,5 +1,6 @@ like('title', 'match', 'none'); // Produces: WHERE `title` LIKE 'match' ESCAPE '!' $builder->like('title', 'match', 'before'); // Produces: WHERE `title` LIKE '%match' ESCAPE '!' $builder->like('title', 'match', 'after'); // Produces: WHERE `title` LIKE 'match%' ESCAPE '!' $builder->like('title', 'match', 'both'); // Produces: WHERE `title` LIKE '%match%' ESCAPE '!' diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon index b277717823bf..1bc172390faa 100644 --- a/utils/phpstan-baseline/loader.neon +++ b/utils/phpstan-baseline/loader.neon @@ -1,4 +1,4 @@ -# total 2116 errors +# total 2092 errors includes: - argument.type.neon diff --git a/utils/phpstan-baseline/missingType.property.neon b/utils/phpstan-baseline/missingType.property.neon index df409e85e4df..66f4f74b5ff3 100644 --- a/utils/phpstan-baseline/missingType.property.neon +++ b/utils/phpstan-baseline/missingType.property.neon @@ -1,4 +1,4 @@ -# total 101 errors +# total 77 errors parameters: ignoreErrors: @@ -386,123 +386,3 @@ parameters: message: '#^Property CodeIgniter\\Model@anonymous/tests/system/Models/SaveModelTest\.php\:290\:\:\$name has no type specified\.$#' count: 1 path: ../../tests/system/Models/SaveModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$_options has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$country has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$created_at has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$deleted has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$email has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$id has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$name has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:210\:\:\$updated_at has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$_options has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$country has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$created_at has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$deleted has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$email has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$id has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$name has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:229\:\:\$updated_at has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$_options has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$country has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$created_at has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$deleted has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$email has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$id has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$name has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php - - - - message: '#^Property CodeIgniter\\Entity\\Entity@anonymous/tests/system/Models/UpdateModelTest\.php\:401\:\:\$updated_at has no type specified\.$#' - count: 1 - path: ../../tests/system/Models/UpdateModelTest.php