diff --git a/app/Config/CURLRequest.php b/app/Config/CURLRequest.php
index 5a3d4e9311b2..bc5947fb7bb7 100644
--- a/app/Config/CURLRequest.php
+++ b/app/Config/CURLRequest.php
@@ -6,6 +6,22 @@
 
 class CURLRequest extends BaseConfig
 {
+    /**
+     * --------------------------------------------------------------------------
+     * CURLRequest Share Connection Options
+     * --------------------------------------------------------------------------
+     *
+     * Share connection options between requests.
+     *
+     * @var list<int>
+     *
+     * @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect
+     */
+    public array $shareConnectionOptions = [
+        CURL_LOCK_DATA_CONNECT,
+        CURL_LOCK_DATA_DNS,
+    ];
+
     /**
      * --------------------------------------------------------------------------
      * CURLRequest Share Options
diff --git a/app/Config/Email.php b/app/Config/Email.php
index 4dce650b32ec..77c573ad3acb 100644
--- a/app/Config/Email.php
+++ b/app/Config/Email.php
@@ -30,6 +30,11 @@ class Email extends BaseConfig
      */
     public string $SMTPHost = '';
 
+    /**
+     * Which SMTP authentication method to use: login, plain
+     */
+    public string $SMTPAuthMethod = 'login';
+
     /**
      * SMTP Username
      */
diff --git a/app/Config/Format.php b/app/Config/Format.php
index 0d334d72b3fb..5bf56c22fd14 100644
--- a/app/Config/Format.php
+++ b/app/Config/Format.php
@@ -61,4 +61,13 @@ class Format extends BaseConfig
         'application/xml'  => 0,
         'text/xml'         => 0,
     ];
+
+    /**
+     * --------------------------------------------------------------------------
+     * Maximum depth for JSON encoding.
+     * --------------------------------------------------------------------------
+     *
+     * This value determines how deep the JSON encoder will traverse nested structures.
+     */
+    public int $jsonEncodeDepth = 512;
 }
diff --git a/app/Config/Images.php b/app/Config/Images.php
index a33ddadb9a5c..68e415e049e0 100644
--- a/app/Config/Images.php
+++ b/app/Config/Images.php
@@ -16,6 +16,8 @@ class Images extends BaseConfig
     /**
      * The path to the image library.
      * Required for ImageMagick, GraphicsMagick, or NetPBM.
+     *
+     * @deprecated 4.7.0 No longer used.
      */
     public string $libraryPath = '/usr/local/bin/convert';
 
diff --git a/app/Config/Paths.php b/app/Config/Paths.php
index 3dc9c5d93951..61010e10355a 100644
--- a/app/Config/Paths.php
+++ b/app/Config/Paths.php
@@ -75,4 +75,16 @@ class Paths
      * is used when no value is provided to `Services::renderer()`.
      */
     public string $viewDirectory = __DIR__ . '/../Views';
+
+    /**
+     * ---------------------------------------------------------------
+     * ENVIRONMENT DIRECTORY NAME
+     * ---------------------------------------------------------------
+     *
+     * This variable must contain the name of the directory where
+     * the .env file is located.
+     * Please consider security implications when changing this
+     * value - the directory should not be publicly accessible.
+     */
+    public string $envDirectory = __DIR__ . '/../../';
 }
diff --git a/system/Boot.php b/system/Boot.php
index ba3675516b16..5b228664146f 100644
--- a/system/Boot.php
+++ b/system/Boot.php
@@ -170,7 +170,8 @@ public static function preload(Paths $paths): void
     protected static function loadDotEnv(Paths $paths): void
     {
         require_once $paths->systemDirectory . '/Config/DotEnv.php';
-        (new DotEnv($paths->appDirectory . '/../'))->load();
+        $envDirectory = $paths->envDirectory ?? $paths->appDirectory . '/../';
+        (new DotEnv($envDirectory))->load();
     }
 
     protected static function defineEnvironment(): void
diff --git a/system/Commands/Encryption/GenerateKey.php b/system/Commands/Encryption/GenerateKey.php
index a3fdbd4393a9..b34b422f7bfe 100644
--- a/system/Commands/Encryption/GenerateKey.php
+++ b/system/Commands/Encryption/GenerateKey.php
@@ -17,6 +17,7 @@
 use CodeIgniter\CLI\CLI;
 use CodeIgniter\Config\DotEnv;
 use CodeIgniter\Encryption\Encryption;
+use Config\Paths;
 
 /**
  * Generates a new encryption key.
@@ -101,7 +102,7 @@ public function run(array $params)
         // force DotEnv to reload the new env vars
         putenv('encryption.key');
         unset($_ENV['encryption.key'], $_SERVER['encryption.key']);
-        $dotenv = new DotEnv(ROOTPATH);
+        $dotenv = new DotEnv((new Paths())->envDirectory ?? ROOTPATH);
         $dotenv->load();
 
         CLI::write('Application\'s new encryption key was successfully set.', 'green');
@@ -155,7 +156,7 @@ protected function confirmOverwrite(array $params): bool
     protected function writeNewEncryptionKeyToFile(string $oldKey, string $newKey): bool
     {
         $baseEnv = ROOTPATH . 'env';
-        $envFile = ROOTPATH . '.env';
+        $envFile = ((new Paths())->envDirectory ?? ROOTPATH) . '.env';
 
         if (! is_file($envFile)) {
             if (! is_file($baseEnv)) {
diff --git a/system/Commands/Utilities/Environment.php b/system/Commands/Utilities/Environment.php
index 22794fe9d51d..99a90415ceea 100644
--- a/system/Commands/Utilities/Environment.php
+++ b/system/Commands/Utilities/Environment.php
@@ -16,6 +16,7 @@
 use CodeIgniter\CLI\BaseCommand;
 use CodeIgniter\CLI\CLI;
 use CodeIgniter\Config\DotEnv;
+use Config\Paths;
 
 /**
  * Command to display the current environment,
@@ -119,7 +120,7 @@ public function run(array $params)
         // however we cannot redefine the ENVIRONMENT constant
         putenv('CI_ENVIRONMENT');
         unset($_ENV['CI_ENVIRONMENT'], $_SERVER['CI_ENVIRONMENT']);
-        (new DotEnv(ROOTPATH))->load();
+        (new DotEnv((new Paths())->envDirectory ?? ROOTPATH))->load();
 
         CLI::write(sprintf('Environment is successfully changed to "%s".', $env), 'green');
         CLI::write('The ENVIRONMENT constant will be changed in the next script execution.');
@@ -134,7 +135,7 @@ public function run(array $params)
     private function writeNewEnvironmentToEnvFile(string $newEnv): bool
     {
         $baseEnv = ROOTPATH . 'env';
-        $envFile = ROOTPATH . '.env';
+        $envFile = ((new Paths())->envDirectory ?? ROOTPATH) . '.env';
 
         if (! is_file($envFile)) {
             if (! is_file($baseEnv)) {
diff --git a/system/Cookie/Cookie.php b/system/Cookie/Cookie.php
index ad461c721112..764c8e5bc3e9 100644
--- a/system/Cookie/Cookie.php
+++ b/system/Cookie/Cookie.php
@@ -793,11 +793,11 @@ protected function validateSameSite(string $samesite, bool $secure): void
             $samesite = self::SAMESITE_LAX;
         }
 
-        if (! in_array(strtolower($samesite), self::ALLOWED_SAMESITE_VALUES, true)) {
+        if (! in_array(ucfirst(strtolower($samesite)), self::ALLOWED_SAMESITE_VALUES, true)) {
             throw CookieException::forInvalidSameSite($samesite);
         }
 
-        if (strtolower($samesite) === self::SAMESITE_NONE && ! $secure) {
+        if (ucfirst(strtolower($samesite)) === self::SAMESITE_NONE && ! $secure) {
             throw CookieException::forInvalidSameSiteNone();
         }
     }
diff --git a/system/Cookie/CookieInterface.php b/system/Cookie/CookieInterface.php
index b4a7ea3d7320..af86dd4c0c60 100644
--- a/system/Cookie/CookieInterface.php
+++ b/system/Cookie/CookieInterface.php
@@ -25,20 +25,20 @@ interface CookieInterface
      * first-party and cross-origin requests. If `SameSite=None` is set,
      * the cookie `Secure` attribute must also be set (or the cookie will be blocked).
      */
-    public const SAMESITE_NONE = 'none';
+    public const SAMESITE_NONE = 'None';
 
     /**
      * Cookies are not sent on normal cross-site subrequests (for example to
      * load images or frames into a third party site), but are sent when a
      * user is navigating to the origin site (i.e. when following a link).
      */
-    public const SAMESITE_LAX = 'lax';
+    public const SAMESITE_LAX = 'Lax';
 
     /**
      * Cookies will only be sent in a first-party context and not be sent
      * along with requests initiated by third party websites.
      */
-    public const SAMESITE_STRICT = 'strict';
+    public const SAMESITE_STRICT = 'Strict';
 
     /**
      * RFC 6265 allowed values for the "SameSite" attribute.
@@ -57,7 +57,7 @@ interface CookieInterface
      * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Date
      * @see https://tools.ietf.org/html/rfc7231#section-7.1.1.2
      */
-    public const EXPIRES_FORMAT = 'D, d-M-Y H:i:s T';
+    public const EXPIRES_FORMAT = 'D, d M Y H:i:s T';
 
     /**
      * Returns a unique identifier for the cookie consisting
diff --git a/system/Email/Email.php b/system/Email/Email.php
index 9ed8465daa66..c59aecbee0dd 100644
--- a/system/Email/Email.php
+++ b/system/Email/Email.php
@@ -281,6 +281,11 @@ class Email
      */
     protected $SMTPAuth = false;
 
+    /**
+     * Which SMTP authentication method to use: login, plain
+     */
+    protected string $SMTPAuthMethod = 'login';
+
     /**
      * Whether to send a Reply-To header
      *
@@ -2025,45 +2030,72 @@ protected function SMTPAuthenticate()
             return true;
         }
 
-        if ($this->SMTPUser === '' && $this->SMTPPass === '') {
+        // If no username or password is set
+        if ($this->SMTPUser === '' || $this->SMTPPass === '') {
             $this->setErrorMessage(lang('Email.noSMTPAuth'));
 
             return false;
         }
 
-        $this->sendData('AUTH LOGIN');
+        // normalize in case user entered capital words LOGIN/PLAIN
+        $this->SMTPAuthMethod = strtolower($this->SMTPAuthMethod);
+
+        // Validate supported authentication methods
+        if (! in_array($this->SMTPAuthMethod, ['login', 'plain'], true)) {
+            $this->setErrorMessage(lang('Email.invalidSMTPAuthMethod', [$this->SMTPAuthMethod]));
+
+            return false;
+        }
+
+        $upperAuthMethod = strtoupper($this->SMTPAuthMethod);
+        // send initial 'AUTH' command
+        $this->sendData('AUTH ' . $upperAuthMethod);
         $reply = $this->getSMTPData();
 
         if (str_starts_with($reply, '503')) {    // Already authenticated
             return true;
         }
 
+        // if 'AUTH' command is unsuported by the server
         if (! str_starts_with($reply, '334')) {
-            $this->setErrorMessage(lang('Email.failedSMTPLogin', [$reply]));
+            $this->setErrorMessage(lang('Email.failureSMTPAuthMethod', [$upperAuthMethod]));
 
             return false;
         }
 
-        $this->sendData(base64_encode($this->SMTPUser));
-        $reply = $this->getSMTPData();
+        switch ($this->SMTPAuthMethod) {
+            case 'login':
+                $this->sendData(base64_encode($this->SMTPUser));
+                $reply = $this->getSMTPData();
 
-        if (! str_starts_with($reply, '334')) {
-            $this->setErrorMessage(lang('Email.SMTPAuthUsername', [$reply]));
+                if (! str_starts_with($reply, '334')) {
+                    $this->setErrorMessage(lang('Email.SMTPAuthUsername', [$reply]));
 
-            return false;
+                    return false;
+                }
+
+                $this->sendData(base64_encode($this->SMTPPass));
+                break;
+
+            case 'plain':
+                // send credentials as the single second command
+                $authString = "\0" . $this->SMTPUser . "\0" . $this->SMTPPass;
+
+                $this->sendData(base64_encode($authString));
+                break;
         }
 
-        $this->sendData(base64_encode($this->SMTPPass));
         $reply = $this->getSMTPData();
+        if (! str_starts_with($reply, '235')) {  // Authentication failed
+            $errorMessage = $this->SMTPAuthMethod === 'plain' ? 'Email.SMTPAuthCredentials' : 'Email.SMTPAuthPassword';
 
-        if (! str_starts_with($reply, '235')) {
-            $this->setErrorMessage(lang('Email.SMTPAuthPassword', [$reply]));
+            $this->setErrorMessage(lang($errorMessage, [$reply]));
 
             return false;
         }
 
         if ($this->SMTPKeepAlive) {
-            $this->SMTPAuth = false;
+            $this->SMTPAuth = false; // Prevent re-authentication for keep-alive sessions
         }
 
         return true;
diff --git a/system/Format/JSONFormatter.php b/system/Format/JSONFormatter.php
index a6ba87cd724a..1ca6feb6d580 100644
--- a/system/Format/JSONFormatter.php
+++ b/system/Format/JSONFormatter.php
@@ -41,7 +41,7 @@ public function format($data)
             $options |= JSON_PRETTY_PRINT;
         }
 
-        $result = json_encode($data, $options, 512);
+        $result = json_encode($data, $options, $config->jsonEncodeDepth ?? 512);
 
         if (! in_array(json_last_error(), [JSON_ERROR_NONE, JSON_ERROR_RECURSION], true)) {
             throw FormatException::forInvalidJSON(json_last_error_msg());
diff --git a/system/HTTP/CURLRequest.php b/system/HTTP/CURLRequest.php
index 5e8c5847ca02..f0a4b8153716 100644
--- a/system/HTTP/CURLRequest.php
+++ b/system/HTTP/CURLRequest.php
@@ -17,6 +17,7 @@
 use CodeIgniter\HTTP\Exceptions\HTTPException;
 use Config\App;
 use Config\CURLRequest as ConfigCURLRequest;
+use CurlShareHandle;
 
 /**
  * A lightweight HTTP client for sending synchronous HTTP requests via cURL.
@@ -101,6 +102,11 @@ class CURLRequest extends OutgoingRequest
      */
     private readonly bool $shareOptions;
 
+    /**
+     * The share connection instance.
+     */
+    protected ?CurlShareHandle $shareConnection = null;
+
     /**
      * Takes an array of options to set the following possible class properties:
      *
@@ -129,6 +135,20 @@ public function __construct(App $config, URI $uri, ?ResponseInterface $response
 
         $this->config = $this->defaultConfig;
         $this->parseOptions($options);
+
+        // Share Connection
+        $optShareConnection = config(ConfigCURLRequest::class)->shareConnectionOptions ?? [
+            CURL_LOCK_DATA_CONNECT,
+            CURL_LOCK_DATA_DNS,
+        ];
+
+        if ($optShareConnection !== []) {
+            $this->shareConnection = curl_share_init();
+
+            foreach (array_unique($optShareConnection) as $opt) {
+                curl_share_setopt($this->shareConnection, CURLSHOPT_SHARE, $opt);
+            }
+        }
     }
 
     /**
@@ -364,8 +384,12 @@ public function send(string $method, string $url)
 
         $curlOptions[CURLOPT_URL]            = $url;
         $curlOptions[CURLOPT_RETURNTRANSFER] = true;
-        $curlOptions[CURLOPT_HEADER]         = true;
-        $curlOptions[CURLOPT_FRESH_CONNECT]  = true;
+
+        if ($this->shareConnection instanceof CurlShareHandle) {
+            $curlOptions[CURLOPT_SHARE] = $this->shareConnection;
+        }
+
+        $curlOptions[CURLOPT_HEADER] = true;
         // Disable @file uploads in post data.
         $curlOptions[CURLOPT_SAFE_UPLOAD] = true;
 
@@ -602,6 +626,16 @@ protected function setCURLOptions(array $curlOptions = [], array $config = [])
             }
         }
 
+        // DNS Cache Timeout
+        if (isset($config['dns_cache_timeout']) && is_numeric($config['dns_cache_timeout']) && $config['dns_cache_timeout'] >= -1) {
+            $curlOptions[CURLOPT_DNS_CACHE_TIMEOUT] = (int) $config['dns_cache_timeout'];
+        }
+
+        // Fresh Connect (default true)
+        $curlOptions[CURLOPT_FRESH_CONNECT] = isset($config['fresh_connect']) && is_bool($config['fresh_connect'])
+            ? $config['fresh_connect']
+            : true;
+
         // Timeout
         $curlOptions[CURLOPT_TIMEOUT_MS] = (float) $config['timeout'] * 1000;
 
diff --git a/system/Helpers/text_helper.php b/system/Helpers/text_helper.php
index c8067a88a74b..b8298953bdd3 100644
--- a/system/Helpers/text_helper.php
+++ b/system/Helpers/text_helper.php
@@ -545,10 +545,8 @@ function reduce_multiples(string $str, string $character = ',', bool $trim = fal
      *
      * Useful for generating passwords or hashes.
      *
-     * @param string $type Type of random string.  basic, alpha, alnum, numeric, nozero, md5, sha1, and crypto
+     * @param string $type Type of random string:  alpha, alnum, numeric, nozero, or crypto
      * @param int    $len  Number of characters
-     *
-     * @deprecated The type 'basic', 'md5', and 'sha1' are deprecated. They are not cryptographically secure.
      */
     function random_string(string $type = 'alnum', int $len = 8): string
     {
@@ -578,12 +576,6 @@ function random_string(string $type = 'alnum', int $len = 8): string
 
                 return sprintf('%0' . $len . 'd', $rand);
 
-            case 'md5':
-                return md5(uniqid((string) mt_rand(), true));
-
-            case 'sha1':
-                return sha1(uniqid((string) mt_rand(), true));
-
             case 'crypto':
                 if ($len % 2 !== 0) {
                     throw new InvalidArgumentException(
@@ -594,8 +586,12 @@ function random_string(string $type = 'alnum', int $len = 8): string
                 return bin2hex(random_bytes($len / 2));
         }
 
-        // 'basic' type treated as default
-        return (string) mt_rand();
+        throw new InvalidArgumentException(
+            sprintf(
+                'Invalid type "%s". Accepted types: alpha, alnum, numeric, nozero, or crypto.',
+                $type,
+            ),
+        );
     }
 }
 
diff --git a/system/I18n/TimeTrait.php b/system/I18n/TimeTrait.php
index 42278c960c43..6d3e7d6bb98d 100644
--- a/system/I18n/TimeTrait.php
+++ b/system/I18n/TimeTrait.php
@@ -749,6 +749,39 @@ public function addMonths(int $months)
         return $time->add(DateInterval::createFromDateString("{$months} months"));
     }
 
+    /**
+     * Returns a new Time instance with $months calendar months added to the time.
+     */
+    public function addCalendarMonths(int $months): static
+    {
+        $time = clone $this;
+
+        $year  = (int) $time->getYear();
+        $month = (int) $time->getMonth();
+        $day   = (int) $time->getDay();
+
+        // Adjust total months since year 0
+        $totalMonths = ($year * 12 + $month - 1) + $months;
+
+        // Recalculate year and month
+        $newYear  = intdiv($totalMonths, 12);
+        $newMonth = $totalMonths % 12 + 1;
+
+        // Get last day of new month
+        $lastDayOfMonth = cal_days_in_month(CAL_GREGORIAN, $newMonth, $newYear);
+        $correctedDay   = min($day, $lastDayOfMonth);
+
+        return static::create($newYear, $newMonth, $correctedDay, (int) $this->getHour(), (int) $this->getMinute(), (int) $this->getSecond(), $this->getTimezone(), $this->locale);
+    }
+
+    /**
+     * Returns a new Time instance with $months calendar months subtracted from the time
+     */
+    public function subCalendarMonths(int $months): static
+    {
+        return $this->addCalendarMonths(-$months);
+    }
+
     /**
      * Returns a new Time instance with $years added to the time.
      *
diff --git a/system/Images/Exceptions/ImageException.php b/system/Images/Exceptions/ImageException.php
index 91bf416d20ac..46651fbf706d 100644
--- a/system/Images/Exceptions/ImageException.php
+++ b/system/Images/Exceptions/ImageException.php
@@ -100,6 +100,8 @@ public static function forSaveFailed()
     /**
      * Thrown when the image library path is invalid.
      *
+     * @deprecated 4.7.0 No longer used.
+     *
      * @return static
      */
     public static function forInvalidImageLibraryPath(?string $path = null)
diff --git a/system/Images/Handlers/BaseHandler.php b/system/Images/Handlers/BaseHandler.php
index de3e15974a1b..e38c5836ccca 100644
--- a/system/Images/Handlers/BaseHandler.php
+++ b/system/Images/Handlers/BaseHandler.php
@@ -769,4 +769,14 @@ public function getHeight()
     {
         return ($this->resource !== null) ? $this->_getHeight() : $this->height;
     }
+
+    /**
+     * Placeholder method for implementing metadata clearing logic.
+     *
+     * This method should be implemented to remove or reset metadata as needed.
+     */
+    public function clearMetadata(): static
+    {
+        return $this;
+    }
 }
diff --git a/system/Images/Handlers/ImageMagickHandler.php b/system/Images/Handlers/ImageMagickHandler.php
index 233ea2d07149..2e8b56acd858 100644
--- a/system/Images/Handlers/ImageMagickHandler.php
+++ b/system/Images/Handlers/ImageMagickHandler.php
@@ -13,27 +13,30 @@
 
 namespace CodeIgniter\Images\Handlers;
 
-use CodeIgniter\I18n\Time;
 use CodeIgniter\Images\Exceptions\ImageException;
 use Config\Images;
-use Exception;
 use Imagick;
+use ImagickDraw;
+use ImagickDrawException;
+use ImagickException;
+use ImagickPixel;
+use ImagickPixelException;
 
 /**
- * Class ImageMagickHandler
- *
- * FIXME - This needs conversion & unit testing, to use the imagick extension
+ * Image handler for Imagick extension.
  */
 class ImageMagickHandler extends BaseHandler
 {
     /**
-     * Stores image resource in memory.
+     * Stores Imagick instance.
      *
-     * @var string|null
+     * @var Imagick|null
      */
     protected $resource;
 
     /**
+     * Constructor.
+     *
      * @param Images $config
      *
      * @throws ImageException
@@ -42,25 +45,95 @@ public function __construct($config = null)
     {
         parent::__construct($config);
 
-        if (! extension_loaded('imagick') && ! class_exists(Imagick::class)) {
-            throw ImageException::forMissingExtension('IMAGICK'); // @codeCoverageIgnore
+        if (! extension_loaded('imagick')) {
+            throw ImageException::forMissingExtension('IMAGICK');  // @codeCoverageIgnore
         }
+    }
 
-        $cmd = $this->config->libraryPath;
+    /**
+     * Loads the image for manipulation.
+     *
+     * @return void
+     *
+     * @throws ImageException
+     */
+    protected function ensureResource()
+    {
+        if (! $this->resource instanceof Imagick) {
+            // Verify that we have a valid image
+            $this->image();
 
-        if ($cmd === '') {
-            throw ImageException::forInvalidImageLibraryPath($cmd);
-        }
+            try {
+                $this->resource = new Imagick();
+                $this->resource->readImage($this->image()->getPathname());
 
-        if (preg_match('/convert$/i', $cmd) !== 1) {
-            $cmd = rtrim($cmd, '\/') . '/convert';
+                // Check for valid image
+                if ($this->resource->getImageWidth() === 0 || $this->resource->getImageHeight() === 0) {
+                    throw ImageException::forInvalidImageCreate($this->image()->getPathname());
+                }
 
-            $this->config->libraryPath = $cmd;
+                $this->supportedFormatCheck();
+            } catch (ImagickException $e) {
+                throw ImageException::forInvalidImageCreate($e->getMessage());
+            }
         }
+    }
 
-        if (! is_file($cmd)) {
-            throw ImageException::forInvalidImageLibraryPath($cmd);
+    /**
+     * Handles all the grunt work of resizing, etc.
+     *
+     * @param string $action  Type of action to perform
+     * @param int    $quality Quality setting for Imagick operations
+     *
+     * @return $this
+     *
+     * @throws ImageException
+     */
+    protected function process(string $action, int $quality = 100)
+    {
+        $this->image();
+
+        $this->ensureResource();
+
+        try {
+            switch ($action) {
+                case 'resize':
+                    $this->resource->resizeImage(
+                        $this->width,
+                        $this->height,
+                        Imagick::FILTER_LANCZOS,
+                        0,
+                    );
+                    break;
+
+                case 'crop':
+                    $width  = $this->width;
+                    $height = $this->height;
+                    $xAxis  = $this->xAxis ?? 0;
+                    $yAxis  = $this->yAxis ?? 0;
+
+                    $this->resource->cropImage(
+                        $width,
+                        $height,
+                        $xAxis,
+                        $yAxis,
+                    );
+
+                    // Reset canvas to cropped size
+                    $this->resource->setImagePage(0, 0, 0, 0);
+                    break;
+            }
+
+            // Handle transparency for supported image types
+            if (in_array($this->image()->imageType, $this->supportTransparency, true)
+                && $this->resource->getImageAlphaChannel() === Imagick::ALPHACHANNEL_UNDEFINED) {
+                $this->resource->setImageAlphaChannel(Imagick::ALPHACHANNEL_OPAQUE);
+            }
+        } catch (ImagickException) {
+            throw ImageException::forImageProcessFailed();
         }
+
+        return $this;
     }
 
     /**
@@ -68,50 +141,56 @@ public function __construct($config = null)
      *
      * @return ImageMagickHandler
      *
-     * @throws Exception
+     * @throws ImagickException
      */
     public function _resize(bool $maintainRatio = false)
     {
-        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
-        $destination = $this->getResourcePath();
+        if ($maintainRatio) {
+            // If maintaining a ratio, we need a custom approach
+            $this->ensureResource();
 
-        $escape = '\\';
+            // Use thumbnailImage which preserves an aspect ratio
+            $this->resource->thumbnailImage($this->width, $this->height, true);
 
-        if (PHP_OS_FAMILY === 'Windows') {
-            $escape = '';
+            return $this;
         }
 
-        $action = $maintainRatio
-            ? ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination)
-            : ' -resize ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . "{$escape}! " . escapeshellarg($source) . ' ' . escapeshellarg($destination);
-
-        $this->process($action);
-
-        return $this;
+        // Use the common process() method for normal resizing
+        return $this->process('resize');
     }
 
     /**
      * Crops the image.
      *
-     * @return bool|ImageMagickHandler
+     * @return $this
      *
-     * @throws Exception
+     * @throws ImagickException
      */
     public function _crop()
     {
-        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
-        $destination = $this->getResourcePath();
+        // Use the common process() method for cropping
+        $result = $this->process('crop');
 
-        $extent = ' ';
-        if ($this->xAxis >= $this->width || $this->yAxis > $this->height) {
-            $extent = ' -background transparent -extent ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . ' ';
-        }
+        // Handle a case where crop dimensions exceed the original image size
+        if ($this->resource instanceof Imagick) {
+            $imgWidth  = $this->resource->getImageWidth();
+            $imgHeight = $this->resource->getImageHeight();
 
-        $action = ' -crop ' . ($this->width ?? 0) . 'x' . ($this->height ?? 0) . '+' . ($this->xAxis ?? 0) . '+' . ($this->yAxis ?? 0) . $extent . escapeshellarg($source) . ' ' . escapeshellarg($destination);
+            if ($this->xAxis >= $imgWidth || $this->yAxis >= $imgHeight) {
+                // Create transparent background
+                $background = new Imagick();
+                $background->newImage($this->width, $this->height, new ImagickPixel('transparent'));
+                $background->setImageFormat($this->resource->getImageFormat());
 
-        $this->process($action);
+                // Composite our image on the background
+                $background->compositeImage($this->resource, Imagick::COMPOSITE_OVER, 0, 0);
 
-        return $this;
+                // Replace our resource
+                $this->resource = $background;
+            }
+        }
+
+        return $result;
     }
 
     /**
@@ -120,18 +199,18 @@ public function _crop()
      *
      * @return $this
      *
-     * @throws Exception
+     * @throws ImagickException
      */
     protected function _rotate(int $angle)
     {
-        $angle = '-rotate ' . $angle;
-
-        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
-        $destination = $this->getResourcePath();
+        $this->ensureResource();
 
-        $action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
+        // Create transparent background
+        $this->resource->setImageBackgroundColor(new ImagickPixel('transparent'));
+        $this->resource->rotateImage(new ImagickPixel('transparent'), $angle);
 
-        $this->process($action);
+        // Reset canvas dimensions
+        $this->resource->setImagePage($this->resource->getImageWidth(), $this->resource->getImageHeight(), 0, 0);
 
         return $this;
     }
@@ -141,88 +220,100 @@ protected function _rotate(int $angle)
      *
      * @return $this
      *
-     * @throws Exception
+     * @throws ImagickException|ImagickPixelException
      */
     protected function _flatten(int $red = 255, int $green = 255, int $blue = 255)
     {
-        $flatten = "-background 'rgb({$red},{$green},{$blue})' -flatten";
-
-        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
-        $destination = $this->getResourcePath();
-
-        $action = ' ' . $flatten . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
+        $this->ensureResource();
 
-        $this->process($action);
+        // Create background
+        $bg = new ImagickPixel("rgb({$red},{$green},{$blue})");
+
+        // Create a new canvas with the background color
+        $canvas = new Imagick();
+        $canvas->newImage(
+            $this->resource->getImageWidth(),
+            $this->resource->getImageHeight(),
+            $bg,
+            $this->resource->getImageFormat(),
+        );
+
+        // Composite our image on the background
+        $canvas->compositeImage(
+            $this->resource,
+            Imagick::COMPOSITE_OVER,
+            0,
+            0,
+        );
+
+        // Replace our resource with the flattened version
+        $this->resource->clear();
+        $this->resource = $canvas;
 
         return $this;
     }
 
     /**
-     * Flips an image along it's vertical or horizontal axis.
+     * Flips an image along its vertical or horizontal axis.
      *
      * @return $this
      *
-     * @throws Exception
+     * @throws ImagickException
      */
     protected function _flip(string $direction)
     {
-        $angle = $direction === 'horizontal' ? '-flop' : '-flip';
-
-        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
-        $destination = $this->getResourcePath();
-
-        $action = ' ' . $angle . ' ' . escapeshellarg($source) . ' ' . escapeshellarg($destination);
+        $this->ensureResource();
 
-        $this->process($action);
+        if ($direction === 'horizontal') {
+            $this->resource->flopImage();
+        } else {
+            $this->resource->flipImage();
+        }
 
         return $this;
     }
 
     /**
-     * Get driver version
+     * Get a driver version
+     *
+     * @return string
      */
-    public function getVersion(): string
+    public function getVersion()
     {
-        $versionString = $this->process('-version')[0];
-        preg_match('/ImageMagick\s(?P<version>[\S]+)/', $versionString, $matches);
+        $version = Imagick::getVersion();
+
+        if (preg_match('/ImageMagick\s+(\d+\.\d+\.\d+)/', $version['versionString'], $matches)) {
+            return $matches[1];
+        }
 
-        return $matches['version'];
+        return '';
     }
 
     /**
-     * Handles all of the grunt work of resizing, etc.
+     * Check if a given image format is supported
      *
-     * @return array Lines of output from shell command
+     * @return void
      *
-     * @throws Exception
+     * @throws ImageException
      */
-    protected function process(string $action, int $quality = 100): array
+    protected function supportedFormatCheck()
     {
-        if ($action !== '-version') {
-            $this->supportedFormatCheck();
-        }
-
-        $cmd = $this->config->libraryPath;
-        $cmd .= $action === '-version' ? ' ' . $action : ' -quality ' . $quality . ' ' . $action;
-
-        $retval = 1;
-        $output = [];
-        // exec() might be disabled
-        if (function_usable('exec')) {
-            @exec($cmd, $output, $retval);
+        if (! $this->resource instanceof Imagick) {
+            return;
         }
 
-        // Did it work?
-        if ($retval > 0) {
-            throw ImageException::forImageProcessFailed();
+        switch ($this->image()->imageType) {
+            case IMAGETYPE_WEBP:
+                if (! in_array('WEBP', Imagick::queryFormats(), true)) {
+                    throw ImageException::forInvalidImageCreate(lang('images.webpNotSupported'));
+                }
+                break;
         }
-
-        return $output;
     }
 
     /**
-     * Saves any changes that have been made to file. If no new filename is
-     * provided, the existing image is overwritten, otherwise a copy of the
+     * Saves any changes that have been made to the file. If no new filename is
+     * provided, the existing image is overwritten; otherwise a copy of the
      * file is made at $target.
      *
      * Example:
@@ -230,6 +321,8 @@ protected function process(string $action, int $quality = 100): array
      *          ->save();
      *
      * @param non-empty-string|null $target
+     *
+     * @throws ImagickException
      */
     public function save(?string $target = null, int $quality = 90): bool
     {
@@ -238,7 +331,7 @@ public function save(?string $target = null, int $quality = 90): bool
 
         // If no new resource has been created, then we're
         // simply copy the existing one.
-        if (empty($this->resource) && $quality === 100) {
+        if (! $this->resource instanceof Imagick && $quality === 100) {
             if ($original === null) {
                 return true;
             }
@@ -251,192 +344,172 @@ public function save(?string $target = null, int $quality = 90): bool
 
         $this->ensureResource();
 
-        // Copy the file through ImageMagick so that it has
-        // a chance to convert file format.
-        $action = escapeshellarg($this->resource) . ' ' . escapeshellarg($target);
-
-        $this->process($action, $quality);
+        $this->resource->setImageCompressionQuality($quality);
 
-        unlink($this->resource);
-
-        return true;
-    }
-
-    /**
-     * Get Image Resource
-     *
-     * This simply creates an image resource handle
-     * based on the type of image being processed.
-     * Since ImageMagick is used on the cli, we need to
-     * ensure we have a temporary file on the server
-     * that we can use.
-     *
-     * To ensure we can use all features, like transparency,
-     * during the process, we'll use a PNG as the temp file type.
-     *
-     * @return string
-     *
-     * @throws Exception
-     */
-    protected function getResourcePath()
-    {
-        if ($this->resource !== null) {
-            return $this->resource;
+        if ($target !== null) {
+            $extension = pathinfo($target, PATHINFO_EXTENSION);
+            $this->resource->setImageFormat($extension);
         }
 
-        $this->resource = WRITEPATH . 'cache/' . Time::now()->getTimestamp() . '_' . bin2hex(random_bytes(10)) . '.png';
-
-        $name = basename($this->resource);
-        $path = pathinfo($this->resource, PATHINFO_DIRNAME);
+        try {
+            $result = $this->resource->writeImage($target);
 
-        $this->image()->copy($path, $name);
-
-        return $this->resource;
-    }
+            chmod($target, $this->filePermissions);
 
-    /**
-     * Make the image resource object if needed
-     *
-     * @return void
-     *
-     * @throws Exception
-     */
-    protected function ensureResource()
-    {
-        $this->getResourcePath();
-
-        $this->supportedFormatCheck();
-    }
+            $this->resource->clear();
+            $this->resource = null;
 
-    /**
-     * Check if given image format is supported
-     *
-     * @return void
-     *
-     * @throws ImageException
-     */
-    protected function supportedFormatCheck()
-    {
-        switch ($this->image()->imageType) {
-            case IMAGETYPE_WEBP:
-                if (! in_array('WEBP', Imagick::queryFormats(), true)) {
-                    throw ImageException::forInvalidImageCreate(lang('images.webpNotSupported'));
-                }
-                break;
+            return $result;
+        } catch (ImagickException) {
+            throw ImageException::forSaveFailed();
         }
     }
 
     /**
      * Handler-specific method for overlaying text on an image.
      *
-     * @throws Exception
+     * @throws ImagickDrawException|ImagickException|ImagickPixelException
      */
     protected function _text(string $text, array $options = [])
     {
-        $xAxis   = 0;
-        $yAxis   = 0;
-        $gravity = '';
-        $cmd     = '';
-
-        // Reverse the vertical offset
-        // When the image is positioned at the bottom
-        // we don't want the vertical offset to push it
-        // further down. We want the reverse, so we'll
-        // invert the offset. Note: The horizontal
-        // offset flips itself automatically
-        if ($options['vAlign'] === 'bottom') {
-            $options['vOffset'] *= -1;
+        $this->ensureResource();
+
+        $draw = new ImagickDraw();
+
+        if (isset($options['fontPath'])) {
+            $draw->setFont($options['fontPath']);
         }
 
-        if ($options['hAlign'] === 'right') {
-            $options['hOffset'] *= -1;
+        if (isset($options['fontSize'])) {
+            $draw->setFontSize($options['fontSize']);
         }
 
-        // Font
-        if (! empty($options['fontPath'])) {
-            $cmd .= ' -font ' . escapeshellarg($options['fontPath']);
+        if (isset($options['color'])) {
+            $color = $options['color'];
+
+            // Shorthand hex, #f00
+            if (strlen($color) === 3) {
+                $color = implode('', array_map(str_repeat(...), str_split($color), [2, 2, 2]));
+            }
+
+            [$r, $g, $b] = sscanf("#{$color}", '#%02x%02x%02x');
+            $opacity     = $options['opacity'] ?? 1.0;
+            $draw->setFillColor(new ImagickPixel("rgba({$r},{$g},{$b},{$opacity})"));
         }
 
-        if (isset($options['hAlign'], $options['vAlign'])) {
+        // Calculate text positioning
+        $imgWidth  = $this->resource->getImageWidth();
+        $imgHeight = $this->resource->getImageHeight();
+        $xAxis     = 0;
+        $yAxis     = 0;
+
+        // Default padding
+        $padding = $options['padding'] ?? 0;
+
+        if (isset($options['hAlign'])) {
+            $hOffset = $options['hOffset'] ?? 0;
+
             switch ($options['hAlign']) {
                 case 'left':
-                    $xAxis   = $options['hOffset'] + $options['padding'];
-                    $yAxis   = $options['vOffset'] + $options['padding'];
-                    $gravity = $options['vAlign'] === 'top' ? 'NorthWest' : 'West';
-                    if ($options['vAlign'] === 'bottom') {
-                        $gravity = 'SouthWest';
-                        $yAxis   = $options['vOffset'] - $options['padding'];
-                    }
+                    $xAxis = $hOffset + $padding;
+                    $draw->setTextAlignment(Imagick::ALIGN_LEFT);
                     break;
 
                 case 'center':
-                    $xAxis   = $options['hOffset'] + $options['padding'];
-                    $yAxis   = $options['vOffset'] + $options['padding'];
-                    $gravity = $options['vAlign'] === 'top' ? 'North' : 'Center';
-                    if ($options['vAlign'] === 'bottom') {
-                        $yAxis   = $options['vOffset'] - $options['padding'];
-                        $gravity = 'South';
-                    }
+                    $xAxis = $imgWidth / 2 + $hOffset;
+                    $draw->setTextAlignment(Imagick::ALIGN_CENTER);
                     break;
 
                 case 'right':
-                    $xAxis   = $options['hOffset'] - $options['padding'];
-                    $yAxis   = $options['vOffset'] + $options['padding'];
-                    $gravity = $options['vAlign'] === 'top' ? 'NorthEast' : 'East';
-                    if ($options['vAlign'] === 'bottom') {
-                        $gravity = 'SouthEast';
-                        $yAxis   = $options['vOffset'] - $options['padding'];
-                    }
+                    $xAxis = $imgWidth - $hOffset - $padding;
+                    $draw->setTextAlignment(Imagick::ALIGN_RIGHT);
                     break;
             }
+        }
 
-            $xAxis = $xAxis >= 0 ? '+' . $xAxis : $xAxis;
-            $yAxis = $yAxis >= 0 ? '+' . $yAxis : $yAxis;
+        if (isset($options['vAlign'])) {
+            $vOffset = $options['vOffset'] ?? 0;
 
-            $cmd .= ' -gravity ' . escapeshellarg($gravity) . ' -geometry ' . escapeshellarg("{$xAxis}{$yAxis}");
-        }
+            switch ($options['vAlign']) {
+                case 'top':
+                    $yAxis = $vOffset + $padding + ($options['fontSize'] ?? 16);
+                    break;
 
-        // Color
-        if (isset($options['color'])) {
-            [$r, $g, $b] = sscanf("#{$options['color']}", '#%02x%02x%02x');
+                case 'middle':
+                    $yAxis = $imgHeight / 2 + $vOffset;
+                    break;
 
-            $cmd .= ' -fill ' . escapeshellarg("rgba({$r},{$g},{$b},{$options['opacity']})");
+                case 'bottom':
+                    // Note: Vertical offset is inverted for bottom alignment as per original implementation
+                    $yAxis = $vOffset < 0 ? $imgHeight + $vOffset - $padding : $imgHeight - $vOffset - $padding;
+                    break;
+            }
         }
 
-        // Font Size - use points....
-        if (isset($options['fontSize'])) {
-            $cmd .= ' -pointsize ' . escapeshellarg((string) $options['fontSize']);
-        }
+        if (isset($options['withShadow'])) {
+            $shadow = clone $draw;
 
-        // Text
-        $cmd .= ' -annotate 0 ' . escapeshellarg($text);
+            if (isset($options['shadowColor'])) {
+                $shadowColor = $options['shadowColor'];
+
+                // Shorthand hex, #f00
+                if (strlen($shadowColor) === 3) {
+                    $shadowColor = implode('', array_map(str_repeat(...), str_split($shadowColor), [2, 2, 2]));
+                }
+
+                [$sr, $sg, $sb] = sscanf("#{$shadowColor}", '#%02x%02x%02x');
+                $shadow->setFillColor(new ImagickPixel("rgb({$sr},{$sg},{$sb})"));
+            } else {
+                $shadow->setFillColor(new ImagickPixel('rgba(0,0,0,0.5)'));
+            }
 
-        $source      = ! empty($this->resource) ? $this->resource : $this->image()->getPathname();
-        $destination = $this->getResourcePath();
+            $offset = $options['shadowOffset'] ?? 3;
 
-        $cmd = ' ' . escapeshellarg($source) . ' ' . $cmd . ' ' . escapeshellarg($destination);
+            $this->resource->annotateImage(
+                $shadow,
+                $xAxis + $offset,
+                $yAxis + $offset,
+                0,
+                $text,
+            );
+        }
 
-        $this->process($cmd);
+        // Draw the main text
+        $this->resource->annotateImage(
+            $draw,
+            $xAxis,
+            $yAxis,
+            0,
+            $text,
+        );
     }
 
     /**
      * Return the width of an image.
      *
      * @return int
+     *
+     * @throws ImagickException
      */
     public function _getWidth()
     {
-        return imagesx(imagecreatefromstring(file_get_contents($this->resource)));
+        $this->ensureResource();
+
+        return $this->resource->getImageWidth();
     }
 
     /**
      * Return the height of an image.
      *
      * @return int
+     *
+     * @throws ImagickException
      */
     public function _getHeight()
     {
-        return imagesy(imagecreatefromstring(file_get_contents($this->resource)));
+        $this->ensureResource();
+
+        return $this->resource->getImageHeight();
     }
 
     /**
@@ -464,4 +537,20 @@ public function reorient(bool $silent = false)
             default => $this,
         };
     }
+
+    /**
+     * Clears metadata from the image.
+     *
+     * @return $this
+     *
+     * @throws ImagickException
+     */
+    public function clearMetadata(): static
+    {
+        $this->ensureResource();
+
+        $this->resource->stripImage();
+
+        return $this;
+    }
 }
diff --git a/system/Images/ImageHandlerInterface.php b/system/Images/ImageHandlerInterface.php
index 93b2009d6a93..a96977214ec8 100644
--- a/system/Images/ImageHandlerInterface.php
+++ b/system/Images/ImageHandlerInterface.php
@@ -149,4 +149,11 @@ public function text(string $text, array $options = []);
      * @return bool
      */
     public function save(?string $target = null, int $quality = 90);
+
+    /**
+     * Clear metadata before saving image as a new file.
+     *
+     * @return $this
+     */
+    public function clearMetadata(): static;
 }
diff --git a/system/Language/en/Email.php b/system/Language/en/Email.php
index 23a97a75f23c..44d4c03cae3e 100644
--- a/system/Language/en/Email.php
+++ b/system/Language/en/Email.php
@@ -13,23 +13,27 @@
 
 // Email language settings
 return [
-    'mustBeArray'          => 'The email validation method must be passed an array.',
-    'invalidAddress'       => 'Invalid email address: "{0}"',
-    'attachmentMissing'    => 'Unable to locate the following email attachment: "{0}"',
-    'attachmentUnreadable' => 'Unable to open this attachment: "{0}"',
-    'noFrom'               => 'Cannot send mail with no "From" header.',
-    'noRecipients'         => 'You must include recipients: To, Cc, or Bcc',
-    'sendFailurePHPMail'   => 'Unable to send email using PHP mail(). Your server might not be configured to send mail using this method.',
-    'sendFailureSendmail'  => 'Unable to send email using Sendmail. Your server might not be configured to send mail using this method.',
-    'sendFailureSmtp'      => 'Unable to send email using SMTP. Your server might not be configured to send mail using this method.',
-    'sent'                 => 'Your message has been successfully sent using the following protocol: {0}',
-    'noSocket'             => 'Unable to open a socket to Sendmail. Please check settings.',
-    'noHostname'           => 'You did not specify a SMTP hostname.',
-    'SMTPError'            => 'The following SMTP error was encountered: {0}',
-    'noSMTPAuth'           => 'Error: You must assign an SMTP username and password.',
-    'failedSMTPLogin'      => 'Failed to send AUTH LOGIN command. Error: {0}',
-    'SMTPAuthUsername'     => 'Failed to authenticate username. Error: {0}',
-    'SMTPAuthPassword'     => 'Failed to authenticate password. Error: {0}',
-    'SMTPDataFailure'      => 'Unable to send data: {0}',
-    'exitStatus'           => 'Exit status code: {0}',
+    'mustBeArray'           => 'The email validation method must be passed an array.',
+    'invalidAddress'        => 'Invalid email address: "{0}"',
+    'attachmentMissing'     => 'Unable to locate the following email attachment: "{0}"',
+    'attachmentUnreadable'  => 'Unable to open this attachment: "{0}"',
+    'noFrom'                => 'Cannot send mail with no "From" header.',
+    'noRecipients'          => 'You must include recipients: To, Cc, or Bcc',
+    'sendFailurePHPMail'    => 'Unable to send email using PHP mail(). Your server might not be configured to send mail using this method.',
+    'sendFailureSendmail'   => 'Unable to send email using Sendmail. Your server might not be configured to send mail using this method.',
+    'sendFailureSmtp'       => 'Unable to send email using SMTP. Your server might not be configured to send mail using this method.',
+    'sent'                  => 'Your message has been successfully sent using the following protocol: {0}',
+    'noSocket'              => 'Unable to open a socket to Sendmail. Please check settings.',
+    'noHostname'            => 'You did not specify a SMTP hostname.',
+    'SMTPError'             => 'The following SMTP error was encountered: {0}',
+    'noSMTPAuth'            => 'Error: You must assign an SMTP username and password.',
+    'invalidSMTPAuthMethod' => 'Error: SMTP authorization method "{0}" is not supported in codeigniter, set either "login" or "plain" authorization method',
+    'failureSMTPAuthMethod' => 'Unable to initiate AUTH command. Your server might not be configured to use AUTH {0} authentication method.',
+    'SMTPAuthCredentials'   => 'Failed to authenticate user credentials. Error: {0}',
+    'SMTPAuthUsername'      => 'Failed to authenticate username. Error: {0}',
+    'SMTPAuthPassword'      => 'Failed to authenticate password. Error: {0}',
+    'SMTPDataFailure'       => 'Unable to send data: {0}',
+    'exitStatus'            => 'Exit status code: {0}',
+    // @deprecated
+    'failedSMTPLogin' => 'Failed to send AUTH LOGIN command. Error: {0}',
 ];
diff --git a/system/Language/en/Images.php b/system/Language/en/Images.php
index 41dbe973aaa5..cdfe9ccbb181 100644
--- a/system/Language/en/Images.php
+++ b/system/Language/en/Images.php
@@ -24,7 +24,6 @@
     'unsupportedImageCreate' => 'Your server does not support the GD function required to process this type of image.',
     'jpgOrPngRequired'       => 'The image resize protocol specified in your preferences only works with JPEG or PNG image types.',
     'rotateUnsupported'      => 'Image rotation does not appear to be supported by your server.',
-    'libPathInvalid'         => 'The path to your image library is not correct. Please set the correct path in your image preferences. "{0}"',
     'imageProcessFailed'     => 'Image processing failed. Please verify that your server supports the chosen protocol and that the path to your image library is correct.',
     'rotationAngleRequired'  => 'An angle of rotation is required to rotate the image.',
     'invalidPath'            => 'The path to the image is not correct.',
@@ -33,4 +32,7 @@
     'saveFailed'             => 'Unable to save the image. Please make sure the image and file directory are writable.',
     'invalidDirection'       => 'Flip direction can be only "vertical" or "horizontal". Given: "{0}"',
     'exifNotSupported'       => 'Reading EXIF data is not supported by this PHP installation.',
+
+    // @deprecated
+    'libPathInvalid' => 'The path to your image library is not correct. Please set the correct path in your image preferences. "{0}"',
 ];
diff --git a/system/Validation/Validation.php b/system/Validation/Validation.php
index d3f2a3dc828f..6eefe135ea1d 100644
--- a/system/Validation/Validation.php
+++ b/system/Validation/Validation.php
@@ -823,8 +823,12 @@ protected function fillPlaceholders(array $rules, array $data): array
                             continue;
                         }
 
-                        // Replace the placeholder in the rule
-                        $ruleSet = str_replace('{' . $field . '}', (string) $data[$field], $ruleSet);
+                        // Replace the placeholder in the current rule string
+                        if (str_starts_with($row, 'regex_match[')) {
+                            $row = str_replace('{{' . $field . '}}', (string) $data[$field], $row);
+                        } else {
+                            $row = str_replace('{' . $field . '}', (string) $data[$field], $row);
+                        }
                     }
                 }
             }
@@ -840,7 +844,13 @@ protected function fillPlaceholders(array $rules, array $data): array
      */
     private function retrievePlaceholders(string $rule, array $data): array
     {
-        preg_match_all('/{(.+?)}/', $rule, $matches);
+        if (str_starts_with($rule, 'regex_match[')) {
+            // For regex_match rules, only look for double-bracket placeholders
+            preg_match_all('/\{\{((?:(?![{}]).)+?)\}\}/', $rule, $matches);
+        } else {
+            // For all other rules, use single-bracket placeholders
+            preg_match_all('/{(.+?)}/', $rule, $matches);
+        }
 
         return array_intersect($matches[1], array_keys($data));
     }
diff --git a/tests/system/Cookie/CookieTest.php b/tests/system/Cookie/CookieTest.php
index e4421a8bc443..1438cfb9d513 100644
--- a/tests/system/Cookie/CookieTest.php
+++ b/tests/system/Cookie/CookieTest.php
@@ -155,14 +155,14 @@ public function testExpirationTime(): void
         // expires => 0
         $cookie = new Cookie('test', 'value');
         $this->assertSame(0, $cookie->getExpiresTimestamp());
-        $this->assertSame('Thu, 01-Jan-1970 00:00:00 GMT', $cookie->getExpiresString());
+        $this->assertSame('Thu, 01 Jan 1970 00:00:00 GMT', $cookie->getExpiresString());
         $this->assertTrue($cookie->isExpired());
         $this->assertSame(0, $cookie->getMaxAge());
 
         $date   = new DateTimeImmutable('2021-01-10 00:00:00 GMT', new DateTimeZone('UTC'));
         $cookie = new Cookie('test', 'value', ['expires' => $date]);
         $this->assertSame((int) $date->format('U'), $cookie->getExpiresTimestamp());
-        $this->assertSame('Sun, 10-Jan-2021 00:00:00 GMT', $cookie->getExpiresString());
+        $this->assertSame('Sun, 10 Jan 2021 00:00:00 GMT', $cookie->getExpiresString());
     }
 
     /**
@@ -272,7 +272,7 @@ public function testStringCastingOfCookies(): void
             $a->toHeaderString(),
         );
         $this->assertSame(
-            "cookie=monster; Expires=Sun, 14-Feb-2021 00:00:00 GMT; Max-Age={$max}; Path=/web; Domain=localhost; HttpOnly; SameSite=Lax",
+            "cookie=monster; Expires=Sun, 14 Feb 2021 00:00:00 GMT; Max-Age={$max}; Path=/web; Domain=localhost; HttpOnly; SameSite=Lax",
             (string) $b,
         );
         $this->assertSame(
@@ -280,7 +280,7 @@ public function testStringCastingOfCookies(): void
             (string) $c,
         );
         $this->assertSame(
-            'cookie=deleted; Expires=Thu, 01-Jan-1970 00:00:00 GMT; Max-Age=0; Path=/; HttpOnly; SameSite=Lax',
+            'cookie=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Max-Age=0; Path=/; HttpOnly; SameSite=Lax',
             (string) $d,
         );
 
diff --git a/tests/system/HTTP/CURLRequestShareOptionsTest.php b/tests/system/HTTP/CURLRequestShareOptionsTest.php
index 8f6277cf9501..54e8e46a0bc8 100644
--- a/tests/system/HTTP/CURLRequestShareOptionsTest.php
+++ b/tests/system/HTTP/CURLRequestShareOptionsTest.php
@@ -28,13 +28,18 @@
 #[Group('Others')]
 final class CURLRequestShareOptionsTest extends CURLRequestTest
 {
-    protected function getRequest(array $options = []): MockCURLRequest
+    protected function getRequest(array $options = [], ?array $shareConnectionOptions = null): MockCURLRequest
     {
         $uri = isset($options['baseURI']) ? new URI($options['baseURI']) : new URI();
         $app = new App();
 
         $config               = new ConfigCURLRequest();
         $config->shareOptions = true;
+
+        if ($shareConnectionOptions !== null) {
+            $config->shareConnectionOptions = $shareConnectionOptions;
+        }
+
         Factories::injectMock('config', 'CURLRequest', $config);
 
         return new MockCURLRequest(($app), $uri, new Response($app), $options);
diff --git a/tests/system/HTTP/CURLRequestTest.php b/tests/system/HTTP/CURLRequestTest.php
index 6d1b328c349b..2c19f7a7c9af 100644
--- a/tests/system/HTTP/CURLRequestTest.php
+++ b/tests/system/HTTP/CURLRequestTest.php
@@ -21,7 +21,9 @@
 use Config\App;
 use Config\CURLRequest as ConfigCURLRequest;
 use CURLFile;
+use CurlShareHandle;
 use PHPUnit\Framework\Attributes\BackupGlobals;
+use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\Attributes\Group;
 
 /**
@@ -44,14 +46,20 @@ protected function setUp(): void
 
     /**
      * @param array<string, mixed> $options
+     * @param array<int, int>|null $shareConnectionOptions
      */
-    protected function getRequest(array $options = []): MockCURLRequest
+    protected function getRequest(array $options = [], ?array $shareConnectionOptions = null): MockCURLRequest
     {
         $uri = isset($options['baseURI']) ? new URI($options['baseURI']) : new URI();
         $app = new App();
 
         $config               = new ConfigCURLRequest();
         $config->shareOptions = false;
+
+        if ($shareConnectionOptions !== null) {
+            $config->shareConnectionOptions = $shareConnectionOptions;
+        }
+
         Factories::injectMock('config', 'CURLRequest', $config);
 
         return new MockCURLRequest(($app), $uri, new Response($app), $options);
@@ -582,6 +590,28 @@ public function testProxyuOption(): void
         $this->assertTrue($options[CURLOPT_HTTPPROXYTUNNEL]);
     }
 
+    public function testFreshConnectDefault(): void
+    {
+        $this->request->request('get', 'http://example.com');
+
+        $options = $this->request->curl_options;
+
+        $this->assertArrayHasKey(CURLOPT_FRESH_CONNECT, $options);
+        $this->assertTrue($options[CURLOPT_FRESH_CONNECT]);
+    }
+
+    public function testFreshConnectFalseOption(): void
+    {
+        $this->request->request('get', 'http://example.com', [
+            'fresh_connect' => false,
+        ]);
+
+        $options = $this->request->curl_options;
+
+        $this->assertArrayHasKey(CURLOPT_FRESH_CONNECT, $options);
+        $this->assertFalse($options[CURLOPT_FRESH_CONNECT]);
+    }
+
     public function testDebugOptionTrue(): void
     {
         $this->request->request('get', 'http://example.com', [
@@ -1212,6 +1242,112 @@ public function testForceResolveIPUnknown(): void
         $this->assertSame(\CURL_IPRESOLVE_WHATEVER, $options[CURLOPT_IPRESOLVE]);
     }
 
+    public function testShareConnectionDefault(): void
+    {
+        $this->request->request('GET', 'http://example.com');
+
+        $options = $this->request->curl_options;
+
+        $this->assertArrayHasKey(CURLOPT_SHARE, $options);
+        $this->assertInstanceOf(CurlShareHandle::class, $options[CURLOPT_SHARE]);
+    }
+
+    public function testShareConnectionEmpty(): void
+    {
+        $request = $this->getRequest(shareConnectionOptions: []);
+        $request->request('GET', 'http://example.com');
+
+        $options = $request->curl_options;
+
+        $this->assertArrayNotHasKey(CURLOPT_SHARE, $options);
+    }
+
+    public function testShareConnectionDuplicate(): void
+    {
+        $request = $this->getRequest(shareConnectionOptions: [
+            CURL_LOCK_DATA_CONNECT,
+            CURL_LOCK_DATA_DNS,
+            CURL_LOCK_DATA_CONNECT,
+            CURL_LOCK_DATA_DNS,
+        ]);
+        $request->request('GET', 'http://example.com');
+
+        $options = $request->curl_options;
+
+        $this->assertArrayHasKey(CURLOPT_SHARE, $options);
+        $this->assertInstanceOf(CurlShareHandle::class, $options[CURLOPT_SHARE]);
+    }
+
+    #[DataProvider('provideDNSCacheTimeoutOption')]
+    public function testDNSCacheTimeoutOption(int|string|null $input, bool $expectedHasKey, ?int $expectedValue = null): void
+    {
+        $this->request->request('POST', '/post', [
+            'dns_cache_timeout' => $input,
+        ]);
+
+        $options = $this->request->curl_options;
+
+        if ($expectedHasKey) {
+            $this->assertArrayHasKey(CURLOPT_DNS_CACHE_TIMEOUT, $options);
+            $this->assertSame($expectedValue, $options[CURLOPT_DNS_CACHE_TIMEOUT]);
+        } else {
+            $this->assertArrayNotHasKey(CURLOPT_DNS_CACHE_TIMEOUT, $options);
+        }
+    }
+
+    /**
+     * @return iterable<string, array{input: int|string|null, expectedHasKey: bool, expectedValue?: int}>
+     *
+     * @see https://curl.se/libcurl/c/CURLOPT_DNS_CACHE_TIMEOUT.html
+     */
+    public static function provideDNSCacheTimeoutOption(): iterable
+    {
+        yield from [
+            'valid timeout (integer)' => [
+                'input'          => 160,
+                'expectedHasKey' => true,
+                'expectedValue'  => 160,
+            ],
+            'valid timeout (numeric string)' => [
+                'input'          => '180',
+                'expectedHasKey' => true,
+                'expectedValue'  => 180,
+            ],
+            'valid timeout (zero / disabled)' => [
+                'input'          => 0,
+                'expectedHasKey' => true,
+                'expectedValue'  => 0,
+            ],
+            'valid timeout (zero / disabled using string)' => [
+                'input'          => '0',
+                'expectedHasKey' => true,
+                'expectedValue'  => 0,
+            ],
+            'valid timeout (forever)' => [
+                'input'          => -1,
+                'expectedHasKey' => true,
+                'expectedValue'  => -1,
+            ],
+            'valid timeout (forever using string)' => [
+                'input'          => '-1',
+                'expectedHasKey' => true,
+                'expectedValue'  => -1,
+            ],
+            'invalid timeout (null)' => [
+                'input'          => null,
+                'expectedHasKey' => false,
+            ],
+            'invalid timeout (string)' => [
+                'input'          => 'is_wrong',
+                'expectedHasKey' => false,
+            ],
+            'invalid timeout (negative number / below -1)' => [
+                'input'          => -2,
+                'expectedHasKey' => false,
+            ],
+        ];
+    }
+
     public function testCookieOption(): void
     {
         $holder = SUPPORTPATH . 'HTTP/Files/CookiesHolder.txt';
diff --git a/tests/system/Helpers/TextHelperTest.php b/tests/system/Helpers/TextHelperTest.php
index 46748c94a745..a14c722816ae 100644
--- a/tests/system/Helpers/TextHelperTest.php
+++ b/tests/system/Helpers/TextHelperTest.php
@@ -130,12 +130,8 @@ public function testRandomString(): void
         $this->assertSame(16, strlen(random_string('numeric', 16)));
         $this->assertSame(8, strlen(random_string('numeric')));
 
-        $this->assertIsString(random_string('basic'));
         $this->assertSame(16, strlen($random = random_string('crypto', 16)));
         $this->assertIsString($random);
-
-        $this->assertSame(32, strlen($random = random_string('md5')));
-        $this->assertSame(40, strlen($random = random_string('sha1')));
     }
 
     /**
@@ -151,6 +147,16 @@ public function testRandomStringCryptoOddNumber(): void
         random_string('crypto', 9);
     }
 
+    public function testRandomStringWithUnsupportedType(): void
+    {
+        $this->expectException(InvalidArgumentException::class);
+        $this->expectExceptionMessage(
+            'Invalid type "basic". Accepted types: alpha, alnum, numeric, nozero, or crypto.',
+        );
+
+        random_string('basic');
+    }
+
     public function testIncrementString(): void
     {
         $this->assertSame('my-test_1', increment_string('my-test'));
diff --git a/tests/system/I18n/TimeTest.php b/tests/system/I18n/TimeTest.php
index af45abcbe4b8..4f76636ac7eb 100644
--- a/tests/system/I18n/TimeTest.php
+++ b/tests/system/I18n/TimeTest.php
@@ -812,6 +812,22 @@ public function testCanAddMonthsOverYearBoundary(): void
         $this->assertSame('2018-02-10 13:20:33', $newTime->toDateTimeString());
     }
 
+    public function testCanAddCalendarMonths(): void
+    {
+        $time    = Time::parse('January 31, 2017 13:20:33', 'America/Chicago');
+        $newTime = $time->addCalendarMonths(1);
+        $this->assertSame('2017-01-31 13:20:33', $time->toDateTimeString());
+        $this->assertSame('2017-02-28 13:20:33', $newTime->toDateTimeString());
+    }
+
+    public function testCanAddCalendarMonthsOverYearBoundary(): void
+    {
+        $time    = Time::parse('January 31, 2017 13:20:33', 'America/Chicago');
+        $newTime = $time->addCalendarMonths(13);
+        $this->assertSame('2017-01-31 13:20:33', $time->toDateTimeString());
+        $this->assertSame('2018-02-28 13:20:33', $newTime->toDateTimeString());
+    }
+
     public function testCanAddYears(): void
     {
         $time    = Time::parse('January 10, 2017 13:20:33', 'America/Chicago');
@@ -860,6 +876,22 @@ public function testCanSubtractMonths(): void
         $this->assertSame('2016-10-10 13:20:33', $newTime->toDateTimeString());
     }
 
+    public function testCanSubtractCalendarMonths(): void
+    {
+        $time    = Time::parse('March 31, 2017 13:20:33', 'America/Chicago');
+        $newTime = $time->subCalendarMonths(1);
+        $this->assertSame('2017-03-31 13:20:33', $time->toDateTimeString());
+        $this->assertSame('2017-02-28 13:20:33', $newTime->toDateTimeString());
+    }
+
+    public function testCanSubtractCalendarMonthsOverYearBoundary(): void
+    {
+        $time    = Time::parse('March 31, 2017 13:20:33', 'America/Chicago');
+        $newTime = $time->subCalendarMonths(13);
+        $this->assertSame('2017-03-31 13:20:33', $time->toDateTimeString());
+        $this->assertSame('2016-02-29 13:20:33', $newTime->toDateTimeString());
+    }
+
     public function testCanSubtractYears(): void
     {
         $time    = Time::parse('January 10, 2017 13:20:33', 'America/Chicago');
diff --git a/tests/system/Images/GDHandlerTest.php b/tests/system/Images/GDHandlerTest.php
index 121ce040904a..d85d1a142ffd 100644
--- a/tests/system/Images/GDHandlerTest.php
+++ b/tests/system/Images/GDHandlerTest.php
@@ -454,4 +454,13 @@ public function testImageReorientPortrait(): void
             $this->assertSame(['red' => 62, 'green' => 62, 'blue' => 62, 'alpha' => 0], $rgb);
         }
     }
+
+    public function testClearMetadataReturnsSelf(): void
+    {
+        $this->handler->withFile($this->path);
+
+        $result = $this->handler->clearMetadata();
+
+        $this->assertSame($this->handler, $result);
+    }
 }
diff --git a/tests/system/Images/ImageMagickHandlerTest.php b/tests/system/Images/ImageMagickHandlerTest.php
index 016ee34caa34..9c322c8c520f 100644
--- a/tests/system/Images/ImageMagickHandlerTest.php
+++ b/tests/system/Images/ImageMagickHandlerTest.php
@@ -16,11 +16,9 @@
 use CodeIgniter\Config\Services;
 use CodeIgniter\Images\Exceptions\ImageException;
 use CodeIgniter\Images\Handlers\BaseHandler;
-use CodeIgniter\Images\Handlers\ImageMagickHandler;
 use CodeIgniter\Test\CIUnitTestCase;
 use Config\Images;
 use Imagick;
-use PHPUnit\Framework\Attributes\DataProvider;
 use PHPUnit\Framework\Attributes\Group;
 use PHPUnit\Framework\Attributes\RequiresPhpExtension;
 
@@ -46,6 +44,10 @@ final class ImageMagickHandlerTest extends CIUnitTestCase
 
     protected function setUp(): void
     {
+        if (! extension_loaded('imagick')) {
+            $this->markTestSkipped('The IMAGICK extension is not available.');
+        }
+
         $this->root = WRITEPATH . 'cache/';
 
         // cleanup everything
@@ -58,54 +60,10 @@ protected function setUp(): void
         $this->path = $this->origin . 'ci-logo.png';
 
         // get our locally available `convert`
-        $config = new Images();
-        $found  = false;
-
-        foreach ([
-            '/usr/bin/convert',
-            trim((string) shell_exec('which convert')),
-            $config->libraryPath,
-        ] as $convert) {
-            if (is_file($convert)) {
-                $config->libraryPath = $convert;
-
-                $found = true;
-                break;
-            }
-        }
-
-        if (! $found) {
-            $this->markTestSkipped('Cannot test imagick as there is no available convert program.');
-        }
-
+        $config        = new Images();
         $this->handler = Services::image('imagick', $config, false);
     }
 
-    #[DataProvider('provideNonexistentLibraryPathTerminatesProcessing')]
-    public function testNonexistentLibraryPathTerminatesProcessing(string $path, string $invalidPath): void
-    {
-        $this->expectException(ImageException::class);
-        $this->expectExceptionMessage(lang('Images.libPathInvalid', [$invalidPath]));
-
-        $config = new Images();
-
-        $config->libraryPath = $path;
-
-        new ImageMagickHandler($config);
-    }
-
-    /**
-     * @return iterable<string, list<string>>
-     */
-    public static function provideNonexistentLibraryPathTerminatesProcessing(): iterable
-    {
-        yield 'empty string' => ['', ''];
-
-        yield 'invalid file' => ['/var/log/convert', '/var/log/convert'];
-
-        yield 'nonexistent file' => ['/var/www/file', '/var/www/file/convert'];
-    }
-
     public function testGetVersion(): void
     {
         $version = $this->handler->getVersion();
@@ -458,13 +416,12 @@ public function testImageReorientLandscape(): void
 
             $this->handler->withFile($source);
             $this->handler->reorient();
+            $this->handler->save($result);
 
-            $resource = imagecreatefromstring(file_get_contents($this->handler->getResource()));
+            $resource = imagecreatefromjpeg($result);
             $point    = imagecolorat($resource, 0, 0);
             $rgb      = imagecolorsforindex($resource, $point);
 
-            $this->handler->save($result);
-
             $this->assertSame(['red' => 62, 'green' => 62, 'blue' => 62, 'alpha' => 0], $rgb);
         }
     }
@@ -477,13 +434,12 @@ public function testImageReorientPortrait(): void
 
             $this->handler->withFile($source);
             $this->handler->reorient();
+            $this->handler->save($result);
 
-            $resource = imagecreatefromstring(file_get_contents($this->handler->getResource()));
+            $resource = imagecreatefromjpeg($result);
             $point    = imagecolorat($resource, 0, 0);
             $rgb      = imagecolorsforindex($resource, $point);
 
-            $this->handler->save($result);
-
             $this->assertSame(['red' => 62, 'green' => 62, 'blue' => 62, 'alpha' => 0], $rgb);
         }
     }
@@ -548,4 +504,40 @@ public function testCommandInjectionPreventionWithText(): void
             }
         }
     }
+
+    public function testClearMetadataEnsuresResource(): void
+    {
+        $this->expectException(ImageException::class);
+        $this->handler->clearMetadata();
+    }
+
+    public function testClearMetadataReturnsSelf(): void
+    {
+        $this->handler->withFile($this->path);
+
+        $result = $this->handler->clearMetadata();
+
+        $this->assertSame($this->handler, $result);
+    }
+
+    public function testClearMetadata(): void
+    {
+        $this->handler->withFile($this->origin . 'Steveston_dusk.JPG');
+        /** @var Imagick $imagick */
+        $imagick = $this->handler->getResource();
+        $before  = $imagick->getImageProperties();
+
+        $this->assertGreaterThan(40, count($before));
+
+        $this->handler
+            ->clearMetadata()
+            ->save($this->root . 'exif-info-no-metadata.jpg');
+
+        $this->handler->withFile($this->root . 'exif-info-no-metadata.jpg');
+        /** @var Imagick $imagick */
+        $imagick = $this->handler->getResource();
+        $after   = $imagick->getImageProperties();
+
+        $this->assertLessThanOrEqual(5, count($after));
+    }
 }
diff --git a/tests/system/Validation/FormatRulesTest.php b/tests/system/Validation/FormatRulesTest.php
index 7849ad3afd4c..6311c352baaf 100644
--- a/tests/system/Validation/FormatRulesTest.php
+++ b/tests/system/Validation/FormatRulesTest.php
@@ -86,6 +86,48 @@ public function testRegexMatchFalse(): void
         $this->assertFalse($this->validation->run($data));
     }
 
+    /**
+     * @see https://github.com/codeigniter4/CodeIgniter4/issues/9596
+     */
+    public function testRegexMatchWithArrayData(): void
+    {
+        $data = [
+            ['uid' => '2025/06/000001'],
+            ['uid' => '2025/06/000002'],
+            ['uid' => '2025/06/000003'],
+            ['uid' => '2025/06/000004'],
+            ['uid' => '2025/06/000005'],
+        ];
+
+        $this->validation->setRules([
+            '*.uid' => 'regex_match[/^(\d{4})\/(0[1-9]|1[0-2])\/\d{6}$/]',
+        ]);
+
+        $this->assertTrue($this->validation->run($data));
+    }
+
+    public function testRegexMatchWithPlaceholder(): void
+    {
+        $data = [
+            'code'       => 'ABC1234',
+            'phone'      => '1234567890',
+            'prefix'     => 'ABC',
+            'min_digits' => 10,
+            'max_digits' => 15,
+        ];
+
+        $this->validation->setRules([
+            'prefix'     => 'required|string',
+            'min_digits' => 'required|integer',
+            'max_digits' => 'required|integer',
+            'code'       => 'required|regex_match[/^{{prefix}}\d{4}$/]',
+            'phone'      => 'required|regex_match[/^\d{{{min_digits}},{{max_digits}}}$/]',
+        ]);
+
+        $result = $this->validation->run($data);
+        $this->assertTrue($result);
+    }
+
     #[DataProvider('provideValidUrl')]
     public function testValidURL(?string $url, bool $isLoose, bool $isStrict): void
     {
diff --git a/user_guide_src/source/changelogs/index.rst b/user_guide_src/source/changelogs/index.rst
index 248594e298c0..443e0fdce364 100644
--- a/user_guide_src/source/changelogs/index.rst
+++ b/user_guide_src/source/changelogs/index.rst
@@ -12,6 +12,7 @@ See all the changes.
 .. toctree::
     :titlesonly:
 
+    v4.7.0
     v4.6.3
     v4.6.2
     v4.6.1
diff --git a/user_guide_src/source/changelogs/v4.7.0.rst b/user_guide_src/source/changelogs/v4.7.0.rst
new file mode 100644
index 000000000000..f8ad0f446f13
--- /dev/null
+++ b/user_guide_src/source/changelogs/v4.7.0.rst
@@ -0,0 +1,122 @@
+#############
+Version 4.7.0
+#############
+
+Release Date: Unreleased
+
+**4.7.0 release of CodeIgniter4**
+
+.. contents::
+    :local:
+    :depth: 3
+
+**********
+Highlights
+**********
+
+- TBD
+
+********
+BREAKING
+********
+
+Behavior Changes
+================
+
+Validation Rules
+----------------
+
+Placeholders in the ``regex_match`` validation rule must now use double curly braces.
+If you previously used single braces like ``regex_match[/^{placeholder}$/]``, you must
+update it to use double braces: ``regex_match[/^{{placeholder}}$/]``.
+
+This change was introduced to avoid ambiguity with regular expression syntax,
+where single curly braces (e.g., ``{1,3}``) are used for quantifiers.
+
+Interface Changes
+=================
+
+- **Images:** The ``ImageHandlerInterface`` now includes a new method: ``clearMetadata()``. If you've implemented your own handler from scratch, you will need to provide an implementation for this method to ensure compatibility.
+
+Method Signature Changes
+========================
+
+Removed Deprecated Items
+========================
+
+- **Text Helper:** The deprecated types in ``random_string()`` function: ``basic``, ``md5``, and ``sha1`` has been removed.
+
+************
+Enhancements
+************
+
+Libraries
+=========
+
+- **CURLRequest:** Added ``shareConnection`` config item to change default share connection.
+- **CURLRequest:** Added ``dns_cache_timeout`` option to change default DNS cache timeout.
+- **CURLRequest:** Added ``fresh_connect`` options to enable/disabled request fresh connection.
+- **Email:** Added support for choosing the SMTP authorization method. You can change it via ``Config\Email::$SMTPAuthMethod`` option.
+- **Image:** The ``ImageMagickHandler`` has been rewritten to rely solely on the PHP ``imagick`` extension.
+- **Image:** Added ``ImageMagickHandler::clearMetadata()`` method to remove image metadata for privacy protection.
+- **Time:** added methods ``Time::addCalendarMonths()`` and ``Time::subCalendarMonths()``
+
+Commands
+========
+
+Testing
+=======
+
+Database
+========
+
+Query Builder
+-------------
+
+Forge
+-----
+
+Others
+------
+
+Model
+=====
+
+Helpers and Functions
+=====================
+
+Others
+======
+
+***************
+Message Changes
+***************
+
+- Added ``Email.invalidSMTPAuthMethod`` and ``Email.failureSMTPAuthMethod``
+- Deprecated ``Email.failedSMTPLogin`` and ``Image.libPathInvalid``
+
+*******
+Changes
+*******
+
+- **Cookie:** The ``CookieInterface::EXPIRES_FORMAT`` has been changed to ``D, d M Y H:i:s T`` to follow the recommended format in RFC 7231.
+- **Format:** Added support for configuring ``json_encode()`` maximum depth via ``Config\Format::$jsonEncodeDepth``.
+- **Paths:** Added support for changing the location of the ``.env`` file via the ``Paths::$envDirectory`` property.
+
+************
+Deprecations
+************
+
+- **Image:**
+    - The config property ``Config\Image::libraryPath`` has been deprecated. No longer used.
+    - The exception method ``CodeIgniter\Images\Exceptions\ImageException::forInvalidImageLibraryPath`` has been deprecated. No longer used.
+
+**********
+Bugs Fixed
+**********
+
+- **Cookie:** The ``CookieInterface::SAMESITE_STRICT``, ``CookieInterface::SAMESITE_LAX``, and ``CookieInterface::SAMESITE_NONE`` constants are now written in ucfirst style to be consistent with usage in the rest of the framework.
+
+See the repo's
+`CHANGELOG.md <https://github.com/codeigniter4/CodeIgniter4/blob/develop/CHANGELOG.md>`_
+for a complete list of bugs fixed.
diff --git a/user_guide_src/source/general/managing_apps.rst b/user_guide_src/source/general/managing_apps.rst
index 35a30153d034..5e71840a98bf 100644
--- a/user_guide_src/source/general/managing_apps.rst
+++ b/user_guide_src/source/general/managing_apps.rst
@@ -97,3 +97,25 @@ of those:
 .. literalinclude:: managing_apps/004.php
 
 Only when you change the Application Directory, see :ref:`renaming-app-directory` and modify the paths in the **index.php** and **spark**.
+
+Changing the Location of the .env File
+======================================
+
+If necessary, you can change the location of the ``.env`` file by adjusting the ``$envDirectory``
+property in ``app/Config/Paths.php``.
+
+By default, the framework loads environment settings from a ``.env`` file located one level above
+the ``app/`` directory (in the ``ROOTPATH``). This is a safe location when your domain is correctly
+pointed to the ``public/`` directory, as recommended.
+
+In practice, however, some applications are served from a subdirectory (e.g., ``http://example.com/myapp``)
+rather than from the main domain. In such cases, placing the ``.env`` file within the ``ROOTPATH`` may expose
+sensitive configuration data if ``.htaccess`` or other protections are misconfigured.
+
+To avoid this risk in such setups, it is recommended that you ensure the ``.env`` file is located outside any web-accessible directories.
+
+.. warning::
+
+   If you change the location of the ``.env`` file, make absolutely sure it is not publicly accessible.
+   Exposure of this file could lead to compromised credentials and access to critical services, such as your
+   database, mail server, or third-party APIs.
diff --git a/user_guide_src/source/helpers/text_helper.rst b/user_guide_src/source/helpers/text_helper.rst
index d5966347f2ef..5a0fe51d9967 100644
--- a/user_guide_src/source/helpers/text_helper.rst
+++ b/user_guide_src/source/helpers/text_helper.rst
@@ -30,21 +30,13 @@ The following functions are available:
     Generates a random string based on the type and length you specify.
     Useful for creating passwords or generating random hashes.
 
-    .. warning:: For types: **basic**, **md5**, and **sha1**, generated strings
-        are not cryptographically secure. Therefore, these types cannot be used
-        for cryptographic purposes or purposes requiring unguessable return values.
-        Since v4.3.3, these types are deprecated.
-
     The first parameter specifies the type of string, the second parameter
     specifies the length. The following choices are available:
 
     - **alpha**: A string with lower and uppercase letters only.
     - **alnum**: Alphanumeric string with lower and uppercase characters.
-    - **basic**: [deprecated] A random number based on ``mt_rand()`` (length ignored).
     - **numeric**: Numeric string.
     - **nozero**: Numeric string with no zeros.
-    - **md5**: [deprecated] An encrypted random number based on ``md5()`` (fixed length of 32).
-    - **sha1**: [deprecated] An encrypted random number based on ``sha1()`` (fixed length of 40).
     - **crypto**: A random string based on ``random_bytes()``.
 
     .. note:: When you use **crypto**, you must set an even number to the second parameter.
diff --git a/user_guide_src/source/installation/upgrade_470.rst b/user_guide_src/source/installation/upgrade_470.rst
new file mode 100644
index 000000000000..2882870a18f4
--- /dev/null
+++ b/user_guide_src/source/installation/upgrade_470.rst
@@ -0,0 +1,55 @@
+#############################
+Upgrading from 4.6.x to 4.7.0
+#############################
+
+Please refer to the upgrade instructions corresponding to your installation method.
+
+- :ref:`Composer Installation App Starter Upgrading <app-starter-upgrading>`
+- :ref:`Composer Installation Adding CodeIgniter4 to an Existing Project Upgrading <adding-codeigniter4-upgrading>`
+- :ref:`Manual Installation Upgrading <installing-manual-upgrading>`
+
+.. contents::
+    :local:
+    :depth: 2
+
+**********************
+Mandatory File Changes
+**********************
+
+****************
+Breaking Changes
+****************
+
+*********************
+Breaking Enhancements
+*********************
+
+*************
+Project Files
+*************
+
+Some files in the **project space** (root, app, public, writable) received updates. Due to
+these files being outside of the **system** scope they will not be changed without your intervention.
+
+.. note:: There are some third-party CodeIgniter modules available to assist
+    with merging changes to the project space:
+    `Explore on Packagist <https://packagist.org/explore/?query=codeigniter4%20updates>`_.
+
+Content Changes
+===============
+
+The following files received significant changes (including deprecations or visual adjustments)
+and it is recommended that you merge the updated versions with your application:
+
+Config
+------
+
+- @TODO
+
+All Changes
+===========
+
+This is a list of all files in the **project space** that received changes;
+many will be simple comments or formatting that have no effect on the runtime:
+
+- @TODO
diff --git a/user_guide_src/source/installation/upgrading.rst b/user_guide_src/source/installation/upgrading.rst
index c03b6d0b8c7f..a7630713e070 100644
--- a/user_guide_src/source/installation/upgrading.rst
+++ b/user_guide_src/source/installation/upgrading.rst
@@ -16,6 +16,7 @@ See also :doc:`./backward_compatibility_notes`.
 
     backward_compatibility_notes
 
+    upgrade_470
     upgrade_463
     upgrade_462
     upgrade_461
diff --git a/user_guide_src/source/libraries/cookies/004.php b/user_guide_src/source/libraries/cookies/004.php
index e39854766db1..d68d97081713 100644
--- a/user_guide_src/source/libraries/cookies/004.php
+++ b/user_guide_src/source/libraries/cookies/004.php
@@ -23,7 +23,7 @@
 $cookie->getPrefix();           // '__Secure-'
 $cookie->getPrefixedName();     // '__Secure-remember_token'
 $cookie->getExpiresTimestamp(); // UNIX timestamp
-$cookie->getExpiresString();    // 'Fri, 14-Feb-2025 00:00:00 GMT'
+$cookie->getExpiresString();    // 'Fri, 14 Feb 2025 00:00:00 GMT'
 $cookie->isExpired();           // false
 $cookie->getMaxAge();           // the difference from time() to expires
 $cookie->isRaw();               // false
diff --git a/user_guide_src/source/libraries/cookies/006.php b/user_guide_src/source/libraries/cookies/006.php
index 1bf6c732b71a..512d3e8fefe0 100644
--- a/user_guide_src/source/libraries/cookies/006.php
+++ b/user_guide_src/source/libraries/cookies/006.php
@@ -2,6 +2,6 @@
 
 use CodeIgniter\Cookie\Cookie;
 
-Cookie::SAMESITE_LAX;       // 'lax'
-Cookie::SAMESITE_STRICT;    // 'strict'
-Cookie::SAMESITE_NONE;      // 'none'
+Cookie::SAMESITE_LAX;    // 'Lax'
+Cookie::SAMESITE_STRICT; // 'Strict'
+Cookie::SAMESITE_NONE;   // 'None'
diff --git a/user_guide_src/source/libraries/curlrequest.rst b/user_guide_src/source/libraries/curlrequest.rst
index 49540613e1cc..c1bbc4eccdfd 100644
--- a/user_guide_src/source/libraries/curlrequest.rst
+++ b/user_guide_src/source/libraries/curlrequest.rst
@@ -23,6 +23,23 @@ to change very little to move over to use Guzzle.
 Config for CURLRequest
 **********************
 
+.. _curlrequest-sharing-connection:
+
+Sharing Connection
+==================
+
+.. versionadded:: 4.7.0
+
+By default, this option is enabled with the constants ``CURL_LOCK_DATA_CONNECT`` and ``CURL_LOCK_DATA_DNS``.
+
+If you want to share connection between requests, set ``$shareConnectionOptions`` with array constant `CURL_LOCK_DATA_* <https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect>`_ in **app/Config/CURLRequest.php**:
+
+.. literalinclude:: curlrequest/039.php
+
+or when you want to disable it, just change to empty array:
+
+.. literalinclude:: curlrequest/040.php
+
 .. _curlrequest-sharing-options:
 
 Sharing Options
@@ -251,6 +268,18 @@ Allows you to pause a number of milliseconds before sending the request:
 
 .. literalinclude:: curlrequest/023.php
 
+dns_cache_timeout
+=================
+
+.. versionadded:: 4.7.0
+
+By default, CodeIgniter does not change the DNS Cache Timeout value (``120`` seconds). If you need to
+modify this value, you can do so by passing an amount of time in seconds with the ``dns_cache_timeout`` option.
+
+.. literalinclude:: curlrequest/037.php
+
+.. note:: Based on the `libcurl <https://curl.se/libcurl/c/CURLOPT_DNS_CACHE_TIMEOUT.html>`__ documentation, you can set to zero (``0``) to completely disable caching, or set to ``-1`` to make the cached entries remain forever.
+
 form_params
 ===========
 
@@ -266,6 +295,15 @@ if it's not already set:
 
 .. _curlrequest-request-options-headers:
 
+fresh_connect
+=============
+
+.. versionadded:: 4.7.0
+
+By default, the request is sent using a fresh connection. You can disable this behavior using the ``fresh_connect`` option:
+
+.. literalinclude:: curlrequest/038.php
+
 headers
 =======
 
diff --git a/user_guide_src/source/libraries/curlrequest/037.php b/user_guide_src/source/libraries/curlrequest/037.php
new file mode 100644
index 000000000000..7d71d90bec70
--- /dev/null
+++ b/user_guide_src/source/libraries/curlrequest/037.php
@@ -0,0 +1,4 @@
+<?php
+
+// Modify default DNS Cache Timeout
+$client->request('GET', '/', ['dns_cache_timeout' => 360]); // seconds
diff --git a/user_guide_src/source/libraries/curlrequest/038.php b/user_guide_src/source/libraries/curlrequest/038.php
new file mode 100644
index 000000000000..83e971d2782a
--- /dev/null
+++ b/user_guide_src/source/libraries/curlrequest/038.php
@@ -0,0 +1,4 @@
+<?php
+
+$client->request('GET', 'http://example.com', ['fresh_connect' => true]);
+$client->request('GET', 'http://example.com', ['fresh_connect' => false]);
diff --git a/user_guide_src/source/libraries/curlrequest/039.php b/user_guide_src/source/libraries/curlrequest/039.php
new file mode 100644
index 000000000000..9dda44d9312c
--- /dev/null
+++ b/user_guide_src/source/libraries/curlrequest/039.php
@@ -0,0 +1,26 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class CURLRequest extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * CURLRequest Share Connection Options
+     * --------------------------------------------------------------------------
+     *
+     * Share connection options between requests.
+     *
+     * @var list<int>
+     *
+     * @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect
+     */
+    public array $shareConnection = [
+        CURL_LOCK_DATA_CONNECT,
+        CURL_LOCK_DATA_DNS,
+    ];
+
+    // ...
+}
diff --git a/user_guide_src/source/libraries/curlrequest/040.php b/user_guide_src/source/libraries/curlrequest/040.php
new file mode 100644
index 000000000000..bd68f56a04b8
--- /dev/null
+++ b/user_guide_src/source/libraries/curlrequest/040.php
@@ -0,0 +1,23 @@
+<?php
+
+namespace Config;
+
+use CodeIgniter\Config\BaseConfig;
+
+class CURLRequest extends BaseConfig
+{
+    /**
+     * --------------------------------------------------------------------------
+     * CURLRequest Share Connection Options
+     * --------------------------------------------------------------------------
+     *
+     * Share connection options between requests.
+     *
+     * @var list<int>
+     *
+     * @see https://www.php.net/manual/en/curl.constants.php#constant.curl-lock-data-connect
+     */
+    public array $shareConnection = [];
+
+    // ...
+}
diff --git a/user_guide_src/source/libraries/email.rst b/user_guide_src/source/libraries/email.rst
index ec21b79ddcf2..b89d764e50f9 100644
--- a/user_guide_src/source/libraries/email.rst
+++ b/user_guide_src/source/libraries/email.rst
@@ -39,7 +39,7 @@ Here is a basic example demonstrating how you might send email:
 Setting Email Preferences
 =========================
 
-There are 21 different preferences available to tailor how your email
+There are 22 different preferences available to tailor how your email
 messages are sent. You can either set them manually as described here,
 or automatically via preferences stored in your config file, described
 in `Email Preferences`_.
@@ -120,6 +120,7 @@ Preference          Default Value       Options                      Description
                                         or ``smtp``
 **mailPath**        /usr/sbin/sendmail                               The server path to Sendmail.
 **SMTPHost**                                                         SMTP Server Hostname.
+**SMTPAuthMethod**  login               ``login``, ``plain``         SMTP Authentication Method. (Available since 4.7.0)
 **SMTPUser**                                                         SMTP Username.
 **SMTPPass**                                                         SMTP Password.
 **SMTPPort**        25                                               SMTP Port. (If set to ``465``, TLS will be used for the connection
diff --git a/user_guide_src/source/libraries/images.rst b/user_guide_src/source/libraries/images.rst
index a439c3321a0b..b2b6b0238942 100644
--- a/user_guide_src/source/libraries/images.rst
+++ b/user_guide_src/source/libraries/images.rst
@@ -36,9 +36,6 @@ The available Handlers are as follows:
 - ``gd``      The GD/GD2 image library
 - ``imagick`` The ImageMagick library.
 
-If using the ImageMagick library, you must set the path to the library on your
-server in **app/Config/Images.php**.
-
 .. note:: The ImageMagick handler requires the imagick extension.
 
 *******************
@@ -264,5 +261,17 @@ The possible options that are recognized are as follows:
 - ``fontPath``        The full server path to the TTF font you wish to use. System font will be used if none is given.
 - ``fontSize``        The font size to use. When using the GD handler with the system font, valid values are between ``1`` to ``5``.
 
-.. note:: The ImageMagick driver does not recognize full server path for fontPath. Instead, simply provide the
-        name of one of the installed system fonts that you wish to use, i.e., Calibri.
+Clearing Image Metadata
+=======================
+
+This method removes metadata (EXIF, XMP, ICC, IPTC, comments, etc.) from an image.
+
+.. important:: The GD image library automatically strips all metadata during processing,
+        so this method has no additional effect when using the GD handler.
+        This behavior is built into GD itself and cannot be modified.
+
+Some essential technical metadata (dimensions, color depth) will be regenerated during save operations
+as they're required for image display. However, all privacy-sensitive information such as GPS location,
+camera details, and timestamps will be completely removed.
+
+.. literalinclude:: images/015.php
diff --git a/user_guide_src/source/libraries/images/015.php b/user_guide_src/source/libraries/images/015.php
new file mode 100644
index 000000000000..446deab2d35f
--- /dev/null
+++ b/user_guide_src/source/libraries/images/015.php
@@ -0,0 +1,6 @@
+<?php
+
+service('image', 'imagick')
+    ->withFile('/path/to/image/mypic.jpg')
+    ->clearMetadata()
+    ->save('/path/to/new/image.jpg');
diff --git a/user_guide_src/source/libraries/time.rst b/user_guide_src/source/libraries/time.rst
index 17af069c8c5f..c3ddb3c16d39 100644
--- a/user_guide_src/source/libraries/time.rst
+++ b/user_guide_src/source/libraries/time.rst
@@ -325,6 +325,24 @@ modify the existing Time instance, but will return a new instance.
 
 .. literalinclude:: time/031.php
 
+addCalendarMonths() / subCalendarMonths()
+-----------------------------------------
+
+Modifies the current Time by adding or subtracting whole calendar months. These methods can be useful if you 
+require no calendar months are skipped in recurring dates. Refer to the table below for a comparison between 
+``addMonths()`` and ``addCalendarMonths()`` for an initial date of ``2025-01-31``.
+
+======= =========== =================== 
+$months addMonths() addCalendarMonths()
+======= =========== ===================
+1       2025-03-03  2025-02-28          
+2       2025-03-31  2025-03-31          
+3       2025-05-01  2025-04-30       
+4       2025-05-31  2025-05-31
+5       2025-07-01  2025-06-30
+6       2025-07-31  2025-07-31
+======= =========== ===================
+
 Comparing Two Times
 ===================
 
diff --git a/user_guide_src/source/libraries/time/031.php b/user_guide_src/source/libraries/time/031.php
index 3737ae67a3c1..914ff279871d 100644
--- a/user_guide_src/source/libraries/time/031.php
+++ b/user_guide_src/source/libraries/time/031.php
@@ -5,6 +5,7 @@
 $time = $time->addHours(12);
 $time = $time->addDays(21);
 $time = $time->addMonths(14);
+$time = $time->addCalendarMonths(2);
 $time = $time->addYears(5);
 
 $time = $time->subSeconds(23);
@@ -12,4 +13,5 @@
 $time = $time->subHours(12);
 $time = $time->subDays(21);
 $time = $time->subMonths(14);
+$time = $time->subCalendarMonths(2);
 $time = $time->subYears(5);
diff --git a/user_guide_src/source/libraries/validation.rst b/user_guide_src/source/libraries/validation.rst
index 96416355bb22..a6e9e2947762 100644
--- a/user_guide_src/source/libraries/validation.rst
+++ b/user_guide_src/source/libraries/validation.rst
@@ -976,7 +976,10 @@ numeric                 No         Fails if field contains anything other than
 permit_empty            No         Allows the field to receive an empty array,
                                    empty string, null or false.
 regex_match             Yes        Fails if field does not match the regular     ``regex_match[/regex/]``
-                                   expression.
+                                   expression. **Note:** Since v4.7.0, if
+                                   you're using a placeholder with this rule,
+                                   you must use double braces ``{{...}}``
+                                   instead of single ones ``{...}``.
 required                No         Fails if the field is an empty array, empty
                                    string, null or false.
 required_with           Yes        The field is required when any of the other   ``required_with[field1,field2]``
diff --git a/user_guide_src/source/outgoing/api_responses.rst b/user_guide_src/source/outgoing/api_responses.rst
index 8b5564aa1e98..b47c516c59e5 100644
--- a/user_guide_src/source/outgoing/api_responses.rst
+++ b/user_guide_src/source/outgoing/api_responses.rst
@@ -49,6 +49,8 @@ format both XML and JSON responses:
 
 .. literalinclude:: api_responses/003.php
 
+.. note:: Since ``v4.7.0``, you can change the default JSON encoding depth by editing **app/Config/Format.php** file. The ``$jsonEncodeDepth`` value defines the maximum depth, with a default of ``512``.
+
 This is the array that is used during :doc:`Content Negotiation </incoming/content_negotiation>` to determine which
 type of response to return. If no matches are found between what the client requested and what you support, the first
 format in this array is what will be returned.
diff --git a/utils/phpstan-baseline/argument.type.neon b/utils/phpstan-baseline/argument.type.neon
index 9fe27bf262a7..10aa72d7c547 100644
--- a/utils/phpstan-baseline/argument.type.neon
+++ b/utils/phpstan-baseline/argument.type.neon
@@ -1,4 +1,4 @@
-# total 90 errors
+# total 88 errors
 
 parameters:
     ignoreErrors:
@@ -192,11 +192,6 @@ parameters:
             count: 2
             path: ../../tests/system/Images/GDHandlerTest.php
 
-        -
-            message: '#^Parameter \#1 \$filename of function file_get_contents expects string, resource given\.$#'
-            count: 2
-            path: ../../tests/system/Images/ImageMagickHandlerTest.php
-
         -
             message: '#^Parameter \#2 \$message of method CodeIgniter\\Log\\Handlers\\ChromeLoggerHandler\:\:handle\(\) expects string, stdClass given\.$#'
             count: 1
diff --git a/utils/phpstan-baseline/empty.notAllowed.neon b/utils/phpstan-baseline/empty.notAllowed.neon
index 9bdf7e5f5720..9e31db0650ba 100644
--- a/utils/phpstan-baseline/empty.notAllowed.neon
+++ b/utils/phpstan-baseline/empty.notAllowed.neon
@@ -1,4 +1,4 @@
-# total 239 errors
+# total 231 errors
 
 parameters:
     ignoreErrors:
@@ -272,11 +272,6 @@ parameters:
             count: 2
             path: ../../system/Images/Handlers/BaseHandler.php
 
-        -
-            message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#'
-            count: 8
-            path: ../../system/Images/Handlers/ImageMagickHandler.php
-
         -
             message: '#^Construct empty\(\) is not allowed\. Use more strict comparison\.$#'
             count: 1
diff --git a/utils/phpstan-baseline/loader.neon b/utils/phpstan-baseline/loader.neon
index cbafddf4c147..51e85d7e11f0 100644
--- a/utils/phpstan-baseline/loader.neon
+++ b/utils/phpstan-baseline/loader.neon
@@ -1,4 +1,4 @@
-# total 2865 errors
+# total 2846 errors
 includes:
     - argument.type.neon
     - assign.propertyType.neon
@@ -24,3 +24,4 @@ includes:
     - property.phpDocType.neon
     - staticMethod.notFound.neon
     - ternary.shortNotAllowed.neon
+    - varTag.type.neon
diff --git a/utils/phpstan-baseline/method.alreadyNarrowedType.neon b/utils/phpstan-baseline/method.alreadyNarrowedType.neon
index f980738a073a..f3c373da1f4a 100644
--- a/utils/phpstan-baseline/method.alreadyNarrowedType.neon
+++ b/utils/phpstan-baseline/method.alreadyNarrowedType.neon
@@ -1,4 +1,4 @@
-# total 23 errors
+# total 22 errors
 
 parameters:
     ignoreErrors:
@@ -54,7 +54,7 @@ parameters:
 
         -
             message: '#^Call to method PHPUnit\\Framework\\Assert\:\:assertIsString\(\) with string will always evaluate to true\.$#'
-            count: 2
+            count: 1
             path: ../../tests/system/Helpers/TextHelperTest.php
 
         -
diff --git a/utils/phpstan-baseline/method.childReturnType.neon b/utils/phpstan-baseline/method.childReturnType.neon
index c48fbca29d99..adf2f2999aaf 100644
--- a/utils/phpstan-baseline/method.childReturnType.neon
+++ b/utils/phpstan-baseline/method.childReturnType.neon
@@ -1,4 +1,4 @@
-# total 30 errors
+# total 29 errors
 
 parameters:
     ignoreErrors:
@@ -142,11 +142,6 @@ parameters:
             count: 1
             path: ../../system/Images/Handlers/ImageMagickHandler.php
 
-        -
-            message: '#^Return type \(bool\|CodeIgniter\\Images\\Handlers\\ImageMagickHandler\) of method CodeIgniter\\Images\\Handlers\\ImageMagickHandler\:\:_crop\(\) should be covariant with return type \(\$this\(CodeIgniter\\Images\\Handlers\\BaseHandler\)\) of method CodeIgniter\\Images\\Handlers\\BaseHandler\:\:_crop\(\)$#'
-            count: 1
-            path: ../../system/Images/Handlers/ImageMagickHandler.php
-
         -
             message: '#^Return type \(array\|bool\|float\|int\|object\|string\|null\) of method CodeIgniter\\Model\:\:__call\(\) should be covariant with return type \(\$this\(CodeIgniter\\BaseModel\)\|null\) of method CodeIgniter\\BaseModel\:\:__call\(\)$#'
             count: 1
diff --git a/utils/phpstan-baseline/missingType.iterableValue.neon b/utils/phpstan-baseline/missingType.iterableValue.neon
index f8a533b18a39..84c8bce4de6a 100644
--- a/utils/phpstan-baseline/missingType.iterableValue.neon
+++ b/utils/phpstan-baseline/missingType.iterableValue.neon
@@ -1,4 +1,4 @@
-# total 1435 errors
+# total 1434 errors
 
 parameters:
     ignoreErrors:
@@ -4147,11 +4147,6 @@ parameters:
             count: 1
             path: ../../system/Images/Handlers/BaseHandler.php
 
-        -
-            message: '#^Method CodeIgniter\\Images\\Handlers\\ImageMagickHandler\:\:process\(\) return type has no value type specified in iterable type array\.$#'
-            count: 1
-            path: ../../system/Images/Handlers/ImageMagickHandler.php
-
         -
             message: '#^Method CodeIgniter\\Images\\Image\:\:getProperties\(\) return type has no value type specified in iterable type array\.$#'
             count: 1
diff --git a/utils/phpstan-baseline/nullCoalesce.property.neon b/utils/phpstan-baseline/nullCoalesce.property.neon
index 88cdd2e7254e..f641d464825d 100644
--- a/utils/phpstan-baseline/nullCoalesce.property.neon
+++ b/utils/phpstan-baseline/nullCoalesce.property.neon
@@ -1,4 +1,4 @@
-# total 19 errors
+# total 11 errors
 
 parameters:
     ignoreErrors:
@@ -22,16 +22,6 @@ parameters:
             count: 1
             path: ../../system/HTTP/URI.php
 
-        -
-            message: '#^Property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$height \(int\) on left side of \?\? is not nullable\.$#'
-            count: 4
-            path: ../../system/Images/Handlers/ImageMagickHandler.php
-
-        -
-            message: '#^Property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$width \(int\) on left side of \?\? is not nullable\.$#'
-            count: 4
-            path: ../../system/Images/Handlers/ImageMagickHandler.php
-
         -
             message: '#^Property CodeIgniter\\Throttle\\Throttler\:\:\$testTime \(int\) on left side of \?\? is not nullable\.$#'
             count: 1
diff --git a/utils/phpstan-baseline/property.phpDocType.neon b/utils/phpstan-baseline/property.phpDocType.neon
index dda215c49436..6e00157b9feb 100644
--- a/utils/phpstan-baseline/property.phpDocType.neon
+++ b/utils/phpstan-baseline/property.phpDocType.neon
@@ -163,7 +163,7 @@ parameters:
             path: ../../system/HTTP/IncomingRequest.php
 
         -
-            message: '#^PHPDoc type string\|null of property CodeIgniter\\Images\\Handlers\\ImageMagickHandler\:\:\$resource is not the same as PHPDoc type resource\|null of overridden property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$resource\.$#'
+            message: '#^PHPDoc type Imagick\|null of property CodeIgniter\\Images\\Handlers\\ImageMagickHandler\:\:\$resource is not the same as PHPDoc type resource\|null of overridden property CodeIgniter\\Images\\Handlers\\BaseHandler\:\:\$resource\.$#'
             count: 1
             path: ../../system/Images/Handlers/ImageMagickHandler.php
 
diff --git a/utils/phpstan-baseline/varTag.type.neon b/utils/phpstan-baseline/varTag.type.neon
new file mode 100644
index 000000000000..d2244f5a31ab
--- /dev/null
+++ b/utils/phpstan-baseline/varTag.type.neon
@@ -0,0 +1,8 @@
+# total 2 errors
+
+parameters:
+    ignoreErrors:
+        -
+            message: '#^PHPDoc tag @var with type Imagick is not subtype of type resource\.$#'
+            count: 2
+            path: ../../tests/system/Images/ImageMagickHandlerTest.php
