WordPress.org

Make WordPress Core

Changes between Initial Version and Version 2 of Ticket #39262


Ignore:
Timestamp:
12/14/2016 01:49:11 AM (5 years ago)
Author:
dd32
Comment:

Just a note that i've removed the contents of the diff from the ticket description to make it easier to read.

Hi and welcome back to Trac.

Just to note that there was some discussion related to the usage of the external commands in 33:ticket:6821 of the original ticket. My concerns still remain from that ticket - in that I don't think WordPress should be shelling out to perform commands.

Quick look at the patch points to issue in update_size() - not escaping the width/height variables, and shell_exec() doesn't throw exceptions.

I think this would be great as a plugin myself.

Legend:

Unmodified
Added
Removed
Modified
  • Ticket #39262 – Description

    initial v2  
    1 The patch allows WordPress to fall back to the ImageMagick command line when the imagic pecl is not available on the server. Patch attached. Here's the .diff:
    2 
    3 
    4 {{{
    5 diff --git a/wp-includes/class-wp-image-editor-imagick-external.php b/wp-includes/class-wp-image-editor-imagick-external.php
    6 new file mode 100644
    7 index 0000000..5f61280
    8 --- /dev/null
    9 +++ b/wp-includes/class-wp-image-editor-imagick-external.php
    10 @@ -0,0 +1,407 @@
    11 +<?php
    12 +/**
    13 + * WordPress Imagick Image Editor
    14 + *
    15 + * @package WordPress
    16 + * @subpackage Image_Editor
    17 + */
    18 +
    19 +/**
    20 + * WordPress Image Editor Class for Image Manipulation through Imagick command line utilities
    21 + *
    22 + * @since 3.5.0
    23 + * @package WordPress
    24 + * @subpackage Image_Editor
    25 + * @uses WP_Image_Editor Extends class
    26 + */
    27 +class WP_Image_Editor_Imagick_External extends WP_Image_Editor {
    28 +       /**
    29 +        * Imagick object.
    30 +        *
    31 +        * @access protected
    32 +        * @var Imagick
    33 +        */
    34 +       protected $image;
    35 +       public static $prog_convert  = '/usr/bin/convert';
    36 +       public static $prog_identify = '/usr/bin/identify';
    37 +
    38 +       public function __destruct() {
    39 +       }
    40 +
    41 +       /**
    42 +        * Checks to see if current environment supports Imagick.
    43 +        *
    44 +        * We require Imagick 2.2.0 or greater, based on whether the queryFormats()
    45 +        * method can be called statically.
    46 +        *
    47 +        * @since 3.5.0
    48 +        *
    49 +        * @static
    50 +        * @access public
    51 +        *
    52 +        * @param array $args
    53 +        * @return bool
    54 +        */
    55 +       public static function test( $args = array() ) {
    56 +               return is_executable( self::$prog_convert ) && is_executable( self::$prog_identify );
    57 +       }
    58 +
    59 +       /**
    60 +        * Checks to see if editor supports the mime-type specified.
    61 +        *
    62 +        * @since 3.5.0
    63 +        *
    64 +        * @static
    65 +        * @access public
    66 +        *
    67 +        * @param string $mime_type
    68 +        * @return bool
    69 +        */
    70 +       public static function supports_mime_type( $mime_type ) {
    71 +               $imagick_extension = strtoupper( self::get_extension( $mime_type ) );
    72 +
    73 +               if ( ! $imagick_extension )
    74 +                       return false;
    75 +
    76 +               if ( $imagick_extension === 'PDF' )
    77 +                       return true;
    78 +
    79 +               return false;
    80 +       }
    81 +
    82 +       /**
    83 +        * Loads image from $this->file into new Imagick Object.
    84 +        *
    85 +        * @since 3.5.0
    86 +        * @access protected
    87 +        *
    88 +        * @return true|WP_Error True if loaded; WP_Error on failure.
    89 +        */
    90 +       public function load() {
    91 +               if ( $this->image )
    92 +                       return true;
    93 +
    94 +               $this->image = $this->file;
    95 +
    96 +               if ( ! is_file( $this->file ) )
    97 +                       return new WP_Error( 'error_loading_image', __('File doesn&#8217;t exist?'), $this->file );
    98 +
    99 +               try {
    100 +                       $filename = $this->file;
    101 +
    102 +                       list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename );
    103 +                       $this->mime_type = $mime_type;
    104 +               }
    105 +               catch ( Exception $e ) {
    106 +                       return new WP_Error( 'invalid_image', $e->getMessage(), $this->file );
    107 +               }
    108 +
    109 +               $updated_size = $this->update_size();
    110 +               if ( is_wp_error( $updated_size ) ) {
    111 +                       return $updated_size;
    112 +               }
    113 +
    114 +               return $this->set_quality();
    115 +       }
    116 +
    117 +       /**
    118 +        * Sets Image Compression quality on a 1-100% scale.
    119 +        *
    120 +        * @since 3.5.0
    121 +        * @access public
    122 +        *
    123 +        * @param int $quality Compression Quality. Range: [1,100]
    124 +        * @return true|WP_Error True if set successfully; WP_Error on failure.
    125 +        */
    126 +       public function set_quality( $quality = null ) {
    127 +               $quality_result = parent::set_quality( $quality );
    128 +               if ( is_wp_error( $quality_result ) ) {
    129 +                       return $quality_result;
    130 +               } else {
    131 +                       $quality = $this->get_quality();
    132 +               }
    133 +               return true;
    134 +       }
    135 +
    136 +       /**
    137 +        * Sets or updates current image size.
    138 +        *
    139 +        * @since 3.5.0
    140 +        * @access protected
    141 +        *
    142 +        * @param int $width
    143 +        * @param int $height
    144 +        *
    145 +        * @return true|WP_Error
    146 +        */
    147 +       protected function update_size( $width = null, $height = null ) {
    148 +               $size = null;
    149 +               if ( !$width || !$height ) {
    150 +                       try {
    151 +                               $ret = shell_exec( self::$prog_identify . " -format '%[width] %[height]' " .  escapeshellarg( $this->file ) . " 2>/dev/null" );
    152 +                               list( $width, $height ) = explode( " ", trim( $ret ) );
    153 +                       }
    154 +                       catch ( Exception $e ) {
    155 +                               return new WP_Error( 'invalid_image', __( 'Could not read image size.' ), $this->file );
    156 +                       }
    157 +               }
    158 +               if ( ! $width )
    159 +                       $width = $size['width'];
    160 +               if ( ! $height )
    161 +                       $height = $size['height'];
    162 +
    163 +               return parent::update_size( $width, $height );
    164 +       }
    165 +
    166 +       /**
    167 +        * Resizes current image.
    168 +        *
    169 +        * At minimum, either a height or width must be provided.
    170 +        * If one of the two is set to null, the resize will
    171 +        * maintain aspect ratio according to the provided dimension.
    172 +        *
    173 +        * @since 3.5.0
    174 +        * @access public
    175 +        *
    176 +        * @param  int|null $max_w Image width.
    177 +        * @param  int|null $max_h Image height.
    178 +        * @param  bool     $crop
    179 +        * @return bool|WP_Error
    180 +        */
    181 +       public function resize( $max_w, $max_h, $crop = false ) {
    182 +               if ( ( $this->size['width'] == $max_w ) && ( $this->size['height'] == $max_h ) )
    183 +                       return true;
    184 +
    185 +               $dims = image_resize_dimensions( $this->size['width'], $this->size['height'], $max_w, $max_h, $crop );
    186 +               if ( ! $dims )
    187 +                       return new WP_Error( 'error_getting_dimensions', __('Could not calculate resized image dimensions') );
    188 +               list( $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h ) = $dims;
    189 +
    190 +               if ( $crop ) {
    191 +                       return $this->crop( $src_x, $src_y, $src_w, $src_h, $dst_w, $dst_h );
    192 +               }
    193 +
    194 +               // Execute the resize
    195 +               $thumb_result = $this->thumbnail_image( $dst_w, $dst_h );
    196 +               if ( is_wp_error( $thumb_result ) ) {
    197 +                       return $thumb_result;
    198 +               }
    199 +
    200 +               return $this->update_size( $dst_w, $dst_h );
    201 +       }
    202 +
    203 +       /**
    204 +        * Efficiently resize the current image
    205 +        *
    206 +        * This is a WordPress specific implementation of Imagick::thumbnailImage(),
    207 +        * which resizes an image to given dimensions and removes any associated profiles.
    208 +        *
    209 +        * @since 4.5.0
    210 +        * @access protected
    211 +        *
    212 +        * @param int    $dst_w       The destination width.
    213 +        * @param int    $dst_h       The destination height.
    214 +        * @param string $filter_name Optional. The Imagick filter to use when resizing. Default 'FILTER_TRIANGLE'.
    215 +        * @param bool   $strip_meta  Optional. Strip all profiles, excluding color profiles, from the image. Default true.
    216 +        * @return bool|WP_Error
    217 +        */
    218 +       protected function thumbnail_image( $dst_w, $dst_h, $filter_name = 'FILTER_TRIANGLE', $strip_meta = true ) {
    219 +               list( $filename, $extension, $mime_type ) = $this->get_output_format( $this->filename, $this->mime_type );
    220 +               $dst_w = intval( $dst_w );
    221 +               $dst_h = intval( $dst_h );
    222 +               $filename = $this->generate_filename( "${dst_w}x${dst_h}", null, $extension );
    223 +
    224 +               $ret = 0;
    225 +               $cmd = self::$prog_convert .
    226 +                       " " . escapeshellarg( $this->file ) .
    227 +                       " -resize " . escapeshellarg( "${dst_w}x$dst_h" ) .
    228 +                       " -quality " . escapeshellarg( $this->quality ).
    229 +                       " " . escapeshellarg( $filename );
    230 +
    231 +               system( $cmd, $ret );
    232 +               if ( $ret !== 0 )
    233 +                       return new WP_Error( 'image_resize_error', "convert returned error: $ret", $filename );
    234 +
    235 +               return true;
    236 +       }
    237 +
    238 +       /**
    239 +        * Resize multiple images from a single source.
    240 +        *
    241 +        * @since 3.5.0
    242 +        * @access public
    243 +        *
    244 +        * @param array $sizes {
    245 +        *     An array of image size arrays. Default sizes are 'small', 'medium', 'medium_large', 'large'.
    246 +        *
    247 +        *     Either a height or width must be provided.
    248 +        *     If one of the two is set to null, the resize will
    249 +        *     maintain aspect ratio according to the provided dimension.
    250 +        *
    251 +        *     @type array $size {
    252 +        *         Array of height, width values, and whether to crop.
    253 +        *
    254 +        *         @type int  $width  Image width. Optional if `$height` is specified.
    255 +        *         @type int  $height Image height. Optional if `$width` is specified.
    256 +        *         @type bool $crop   Optional. Whether to crop the image. Default false.
    257 +        *     }
    258 +        * }
    259 +        * @return array An array of resized images' metadata by size.
    260 +        */
    261 +       public function multi_resize( $sizes ) {
    262 +               $metadata = array();
    263 +               $orig_size = $this->size;
    264 +               $orig_image = $this->image;
    265 +
    266 +               foreach ( $sizes as $size => $size_data ) {
    267 +                       if ( ! $this->image )
    268 +                               $this->image = $orig_image;
    269 +
    270 +                       if ( ! isset( $size_data['width'] ) && ! isset( $size_data['height'] ) ) {
    271 +                               continue;
    272 +                       }
    273 +
    274 +                       if ( ! isset( $size_data['width'] ) ) {
    275 +                               $size_data['width'] = null;
    276 +                       }
    277 +                       if ( ! isset( $size_data['height'] ) ) {
    278 +                               $size_data['height'] = null;
    279 +                       }
    280 +
    281 +                       if ( ! isset( $size_data['crop'] ) ) {
    282 +                               $size_data['crop'] = false;
    283 +                       }
    284 +
    285 +                       $resize_result = $this->resize( $size_data['width'], $size_data['height'], $size_data['crop'] );
    286 +
    287 +                       $duplicate = ( ( $orig_size['width'] == $size_data['width'] ) && ( $orig_size['height'] == $size_data['height'] ) );
    288 +
    289 +                       if ( ! is_wp_error( $resize_result ) && ! $duplicate ) {
    290 +                               $resized = $this->_save( $this->image );
    291 +                               $this->image = null;
    292 +                               if ( ! is_wp_error( $resized ) && $resized ) {
    293 +                                       unset( $resized['path'] );
    294 +                                       $metadata[$size] = $resized;
    295 +                               }
    296 +                       }
    297 +
    298 +                       $this->size = $orig_size;
    299 +               }
    300 +
    301 +               $this->image = $orig_image;
    302 +
    303 +               return $metadata;
    304 +       }
    305 +
    306 +       /**
    307 +        * Crops Image.
    308 +        *
    309 +        * @since 3.5.0
    310 +        * @access public
    311 +        *
    312 +        * @param int  $src_x The start x position to crop from.
    313 +        * @param int  $src_y The start y position to crop from.
    314 +        * @param int  $src_w The width to crop.
    315 +        * @param int  $src_h The height to crop.
    316 +        * @param int  $dst_w Optional. The destination width.
    317 +        * @param int  $dst_h Optional. The destination height.
    318 +        * @param bool $src_abs Optional. If the source crop points are absolute.
    319 +        * @return bool|WP_Error
    320 +        */
    321 +       public function crop( $src_x, $src_y, $src_w, $src_h, $dst_w = null, $dst_h = null, $src_abs = false ) {
    322 +               return new WP_Error( 'image_crop_error', 'Unsupported operation' );
    323 +       }
    324 +
    325 +       /**
    326 +        * Rotates current image counter-clockwise by $angle.
    327 +        *
    328 +        * @since 3.5.0
    329 +        * @access public
    330 +        *
    331 +        * @param float $angle
    332 +        * @return true|WP_Error
    333 +        */
    334 +       public function rotate( $angle ) {
    335 +               return new WP_Error( 'image_rotate_error', 'Unsupported operation' );
    336 +       }
    337 +
    338 +       /**
    339 +        * Flips current image.
    340 +        *
    341 +        * @since 3.5.0
    342 +        * @access public
    343 +        *
    344 +        * @param bool $horz Flip along Horizontal Axis
    345 +        * @param bool $vert Flip along Vertical Axis
    346 +        * @return true|WP_Error
    347 +        */
    348 +       public function flip( $horz, $vert ) {
    349 +               return new WP_Error( 'image_flip_error', 'Unsupported operation' );
    350 +       }
    351 +
    352 +       /**
    353 +        * Saves current image to file.
    354 +        *
    355 +        * @since 3.5.0
    356 +        * @access public
    357 +        *
    358 +        * @param string $destfilename
    359 +        * @param string $mime_type
    360 +        * @return array|WP_Error {'path'=>string, 'file'=>string, 'width'=>int, 'height'=>int, 'mime-type'=>string}
    361 +        */
    362 +       public function save( $destfilename = null, $mime_type = null ) {
    363 +               $saved = $this->_save( $this->image, $destfilename, $mime_type );
    364 +
    365 +               if ( ! is_wp_error( $saved ) ) {
    366 +                       $this->file = $saved['path'];
    367 +                       $this->mime_type = $saved['mime-type'];
    368 +               }
    369 +
    370 +               return $saved;
    371 +       }
    372 +
    373 +       /**
    374 +        *
    375 +        * @param Imagick $image
    376 +        * @param string $filename
    377 +        * @param string $mime_type
    378 +        * @return array|WP_Error
    379 +        */
    380 +       protected function _save( $image, $filename = null, $mime_type = null ) {
    381 +               list( $filename, $extension, $mime_type ) = $this->get_output_format( $filename, $mime_type );
    382 +
    383 +               if ( ! $filename )
    384 +                       $filename = $this->generate_filename( null, null, $extension );
    385 +
    386 +               $ret = 0;
    387 +               $cmd = self::$prog_convert .
    388 +                       " " . escapeshellarg( $this->file ) .
    389 +                       " -quality " . escapeshellarg($this->quality) .
    390 +                       " +repage" .
    391 +                       " " . escapeshellarg( $filename );
    392 +               system( $cmd, $ret );
    393 +               if ( $ret !== 0 )
    394 +                       return new WP_Error( 'image_save_error', "convert returned error: $ret", $filename );
    395 +
    396 +               // Set correct file permissions
    397 +               $stat = stat( dirname( $filename ) );
    398 +               $perms = $stat['mode'] & 0000666; //same permissions as parent folder, strip off the executable bits
    399 +               @ chmod( $filename, $perms );
    400 +
    401 +               /** This filter is documented in wp-includes/class-wp-image-editor-gd.php */
    402 +               return array(
    403 +                       'path'      => $filename,
    404 +                       'file'      => wp_basename( apply_filters( 'image_make_intermediate_size', $filename ) ),
    405 +                       'width'     => $this->size['width'],
    406 +                       'height'    => $this->size['height'],
    407 +                       'mime-type' => $mime_type,
    408 +               );
    409 +       }
    410 +
    411 +       public function stream( $mime_type = null ) {
    412 +               header( "Content-Type: $mime_type" );
    413 +               readfile( $this->file );
    414 +               return;
    415 +       }
    416 +
    417 +}
    418 diff --git a/wp-includes/media.php b/wp-includes/media.php
    419 index ba52555..a7aa3ad 100644
    420 --- a/wp-includes/media.php
    421 +++ b/wp-includes/media.php
    422 @@ -2928,15 +2928,17 @@ function _wp_image_editor_choose( $args = array() ) {
    423         require_once ABSPATH . WPINC . '/class-wp-image-editor.php';
    424         require_once ABSPATH . WPINC . '/class-wp-image-editor-gd.php';
    425         require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick.php';
    426 +       require_once ABSPATH . WPINC . '/class-wp-image-editor-imagick-external.php';
    427         /**
    428          * Filters the list of image editing library classes.
    429          *
    430          * @since 3.5.0
    431          *
    432          * @param array $image_editors List of available image editors. Defaults are
    433 -        *                             'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD'.
    434 +        *                             'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD', 'WP_Image_Editor_Imagick_External'
    435          */
    436 -       $implementations = apply_filters( 'wp_image_editors', array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD' ) );
    437 +       $implementations = apply_filters( 'wp_image_editors', array( 'WP_Image_Editor_Imagick', 'WP_Image_Editor_GD',
    438 +               'WP_Image_Editor_Imagick_External' ) );
    439  
    440         foreach ( $implementations as $implementation ) {
    441                 if ( ! call_user_func( array( $implementation, 'test' ), $args ) )
    442 
    443 }}}
     1The patch allows WordPress to fall back to the ImageMagick command line when the imagic pecl is not available on the server. Patch attached.