From a8fd670237e4983b08a2f92d492946d570d8238b Mon Sep 17 00:00:00 2001 From: Anthony Burchell Date: Wed, 15 Feb 2023 21:30:20 +0000 Subject: [PATCH] Media: Add `setImagickTimeLimit()` function to avoid timeout in Imagick operations. Previously, Imagick operations could silently error by timeout and produce unexpected results. The new `setImagickTimeLimit()` function will better handle garbage collection in these cases as well as better align Imagick's timeout with PHP timeout, assuming it is set. Props drzraf, audrasjb, costdev. Fixes #52569. git-svn-id: https://develop.svn.wordpress.org/branches/6.1@55348 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-admin/includes/class-wp-debug-data.php | 2 + .../class-wp-image-editor-imagick.php | 46 +++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index 67d79b9369..d8b06f18a8 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -580,6 +580,7 @@ class WP_Debug_Data { 'map' => ( defined( 'imagick::RESOURCETYPE_MAP' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_MAP ) ) : $not_available ), 'memory' => ( defined( 'imagick::RESOURCETYPE_MEMORY' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_MEMORY ) ) : $not_available ), 'thread' => ( defined( 'imagick::RESOURCETYPE_THREAD' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_THREAD ) : $not_available ), + 'time' => ( defined( 'imagick::RESOURCETYPE_TIME' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_TIME ) : $not_available ), ); $limits_debug = array( @@ -589,6 +590,7 @@ class WP_Debug_Data { 'imagick::RESOURCETYPE_MAP' => ( defined( 'imagick::RESOURCETYPE_MAP' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_MAP ) ) : 'not available' ), 'imagick::RESOURCETYPE_MEMORY' => ( defined( 'imagick::RESOURCETYPE_MEMORY' ) ? size_format( $imagick->getResourceLimit( imagick::RESOURCETYPE_MEMORY ) ) : 'not available' ), 'imagick::RESOURCETYPE_THREAD' => ( defined( 'imagick::RESOURCETYPE_THREAD' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_THREAD ) : 'not available' ), + 'imagick::RESOURCETYPE_TIME' => ( defined( 'imagick::RESOURCETYPE_TIME' ) ? $imagick->getResourceLimit( imagick::RESOURCETYPE_TIME ) : 'not available' ), ); $info['wp-media']['fields']['imagick_limits'] = array( diff --git a/src/wp-includes/class-wp-image-editor-imagick.php b/src/wp-includes/class-wp-image-editor-imagick.php index f413387233..71e4fb0392 100644 --- a/src/wp-includes/class-wp-image-editor-imagick.php +++ b/src/wp-includes/class-wp-image-editor-imagick.php @@ -253,6 +253,48 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor { return parent::update_size( $width, $height ); } + /** + * Depending on configuration, Imagick processing may take time. + * + * Multiple problems exist if PHP timeouts before ImageMagick completed: + * 1. Temporary files aren't cleaned by ImageMagick garbage collection. + * 2. No clear error is provided. + * 3. The cause of such timeout can be hard to pinpoint. + * + * This function, which is expected to be run before heavy image routines, resolves + * point 1 above by aligning Imagick's timeout with PHP's timeout, assuming it's set. + * + * Note: Imagick resource exhaustion does not issue catchable exceptions (yet) + * See https://github.com/Imagick/imagick/issues/333 + * Note: The resource limit isn't saved/restored. It applies to + * subsequent image operations within the time of the HTTP request. + * + * @since 6.2.0 + * + * @return int|null Whether the new limit or null if none was set + */ + public static function setImagickTimeLimit() { + if ( ! defined( 'Imagick::RESOURCETYPE_TIME' ) ) { + return null; + } + + // Returns PHP_FLOAT_MAX if unset. + $imagick_timeout = Imagick::getResourceLimit( Imagick::RESOURCETYPE_TIME ); + + // Convert to an integer, keeping in mind that: 0 === (int) PHP_FLOAT_MAX. + $imagick_timeout = $imagick_timeout > PHP_INT_MAX ? PHP_INT_MAX : (int) $imagick_timeout; + + $php_timeout = intval( ini_get( 'max_execution_time' ) ); + + if ( $php_timeout > 1 && $php_timeout < $imagick_timeout ) { + // $limit = $php_timeout - 1; + $limit = floatval( 0.8 * $php_timeout ); + Imagick::setResourceLimit( Imagick::RESOURCETYPE_TIME, $limit ); + + return $limit; + } + } + /** * Resizes current image. * @@ -279,6 +321,8 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor { list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims; + self::setImagickTimeLimit(); + if ( $crop ) { return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h ); } @@ -545,6 +589,8 @@ class WP_Image_Editor_Imagick extends WP_Image_Editor { $src_h -= $src_y; } + self::setImagickTimeLimit(); + try { $this->image->cropImage( $src_w, $src_h, $src_x, $src_y ); $this->image->setImagePage( $src_w, $src_h, 0, 0 );