Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/media/Bitmap.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Generic handler for bitmap images.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Media
22
 */
23
24
/**
25
 * Generic handler for bitmap images
26
 *
27
 * @ingroup Media
28
 */
29
class BitmapHandler extends TransformationalImageHandler {
30
31
	/**
32
	 * Returns which scaler type should be used. Creates parent directories
33
	 * for $dstPath and returns 'client' on error
34
	 *
35
	 * @param string $dstPath
36
	 * @param bool $checkDstPath
37
	 * @return string|Callable One of client, im, custom, gd, imext or an array( object, method )
38
	 */
39
	protected function getScalerType( $dstPath, $checkDstPath = true ) {
40
		global $wgUseImageResize, $wgUseImageMagick, $wgCustomConvertCommand;
41
42
		if ( !$dstPath && $checkDstPath ) {
43
			# No output path available, client side scaling only
44
			$scaler = 'client';
45
		} elseif ( !$wgUseImageResize ) {
46
			$scaler = 'client';
47
		} elseif ( $wgUseImageMagick ) {
48
			$scaler = 'im';
49
		} elseif ( $wgCustomConvertCommand ) {
50
			$scaler = 'custom';
51
		} elseif ( function_exists( 'imagecreatetruecolor' ) ) {
52
			$scaler = 'gd';
53
		} elseif ( class_exists( 'Imagick' ) ) {
54
			$scaler = 'imext';
55
		} else {
56
			$scaler = 'client';
57
		}
58
59
		return $scaler;
60
	}
61
62 View Code Duplication
	public function makeParamString( $params ) {
63
		$res = parent::makeParamString( $params );
64
		if ( isset( $params['interlace'] ) && $params['interlace'] ) {
65
			return "interlaced-{$res}";
66
		} else {
67
			return $res;
68
		}
69
	}
70
71
	public function parseParamString( $str ) {
72
		$remainder = preg_replace( '/^interlaced-/', '', $str );
73
		$params = parent::parseParamString( $remainder );
74
		if ( $params === false ) {
75
			return false;
76
		}
77
		$params['interlace'] = $str !== $remainder;
78
		return $params;
79
	}
80
81
	public function validateParam( $name, $value ) {
82
		if ( $name === 'interlace' ) {
83
			return $value === false || $value === true;
84
		} else {
85
			return parent::validateParam( $name, $value );
86
		}
87
	}
88
89
	/**
90
	 * @param File $image
91
	 * @param array $params
92
	 * @return bool
93
	 */
94
	function normaliseParams( $image, &$params ) {
95
		global $wgMaxInterlacingAreas;
96
		if ( !parent::normaliseParams( $image, $params ) ) {
97
			return false;
98
		}
99
		$mimeType = $image->getMimeType();
100
		$interlace = isset( $params['interlace'] ) && $params['interlace']
101
			&& isset( $wgMaxInterlacingAreas[$mimeType] )
102
			&& $this->getImageArea( $image ) <= $wgMaxInterlacingAreas[$mimeType];
103
		$params['interlace'] = $interlace;
104
		return true;
105
	}
106
107
	/**
108
	 * Get ImageMagick subsampling factors for the target JPEG pixel format.
109
	 *
110
	 * @param string $pixelFormat one of 'yuv444', 'yuv422', 'yuv420'
111
	 * @return array of string keys
112
	 */
113
	protected function imageMagickSubsampling( $pixelFormat ) {
114
		switch ( $pixelFormat ) {
115
		case 'yuv444':
116
			return [ '1x1', '1x1', '1x1' ];
117
		case 'yuv422':
118
			return [ '2x1', '1x1', '1x1' ];
119
		case 'yuv420':
120
			return [ '2x2', '1x1', '1x1' ];
121
		default:
122
			throw new MWException( 'Invalid pixel format for JPEG output' );
123
		}
124
	}
125
126
	/**
127
	 * Transform an image using ImageMagick
128
	 *
129
	 * @param File $image File associated with this thumbnail
130
	 * @param array $params Array with scaler params
131
	 *
132
	 * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
133
	 */
134
	protected function transformImageMagick( $image, $params ) {
135
		# use ImageMagick
136
		global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
137
			$wgImageMagickTempDir, $wgImageMagickConvertCommand, $wgJpegPixelFormat;
138
139
		$quality = [];
140
		$sharpen = [];
141
		$scene = false;
142
		$animation_pre = [];
143
		$animation_post = [];
144
		$decoderHint = [];
145
		$subsampling = [];
146
147
		if ( $params['mimeType'] == 'image/jpeg' ) {
148
			$qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
149
			$quality = [ '-quality', $qualityVal ?: '80' ]; // 80%
150
			if ( $params['interlace'] ) {
151
				$animation_post = [ '-interlace', 'JPEG' ];
152
			}
153
			# Sharpening, see bug 6193
154
			if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
155
				/ ( $params['srcWidth'] + $params['srcHeight'] )
156
				< $wgSharpenReductionThreshold
157
			) {
158
				$sharpen = [ '-sharpen', $wgSharpenParameter ];
159
			}
160
			if ( version_compare( $this->getMagickVersion(), "6.5.6" ) >= 0 ) {
161
				// JPEG decoder hint to reduce memory, available since IM 6.5.6-2
162
				$decoderHint = [ '-define', "jpeg:size={$params['physicalDimensions']}" ];
163
			}
164
			if ( $wgJpegPixelFormat ) {
165
				$factors = $this->imageMagickSubsampling( $wgJpegPixelFormat );
166
				$subsampling = [ '-sampling-factor', implode( ',', $factors ) ];
167
			}
168
		} elseif ( $params['mimeType'] == 'image/png' ) {
169
			$quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
170
			if ( $params['interlace'] ) {
171
				$animation_post = [ '-interlace', 'PNG' ];
172
			}
173
		} elseif ( $params['mimeType'] == 'image/webp' ) {
174
			$quality = [ '-quality', '95' ]; // zlib 9, adaptive filtering
175
		} elseif ( $params['mimeType'] == 'image/gif' ) {
176
			if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
177
				// Extract initial frame only; we're so big it'll
178
				// be a total drag. :P
179
				$scene = 0;
180
			} elseif ( $this->isAnimatedImage( $image ) ) {
181
				// Coalesce is needed to scale animated GIFs properly (bug 1017).
182
				$animation_pre = [ '-coalesce' ];
183
				// We optimize the output, but -optimize is broken,
184
				// use optimizeTransparency instead (bug 11822)
185
				if ( version_compare( $this->getMagickVersion(), "6.3.5" ) >= 0 ) {
186
					$animation_post = [ '-fuzz', '5%', '-layers', 'optimizeTransparency' ];
187
				}
188
			}
189
			if ( $params['interlace'] && version_compare( $this->getMagickVersion(), "6.3.4" ) >= 0
190
				&& !$this->isAnimatedImage( $image ) ) { // interlacing animated GIFs is a bad idea
191
				$animation_post[] = '-interlace';
192
				$animation_post[] = 'GIF';
193
			}
194
		} elseif ( $params['mimeType'] == 'image/x-xcf' ) {
195
			// Before merging layers, we need to set the background
196
			// to be transparent to preserve alpha, as -layers merge
197
			// merges all layers on to a canvas filled with the
198
			// background colour. After merging we reset the background
199
			// to be white for the default background colour setting
200
			// in the PNG image (which is used in old IE)
201
			$animation_pre = [
202
				'-background', 'transparent',
203
				'-layers', 'merge',
204
				'-background', 'white',
205
			];
206
			MediaWiki\suppressWarnings();
207
			$xcfMeta = unserialize( $image->getMetadata() );
208
			MediaWiki\restoreWarnings();
209
			if ( $xcfMeta
210
				&& isset( $xcfMeta['colorType'] )
211
				&& $xcfMeta['colorType'] === 'greyscale-alpha'
212
				&& version_compare( $this->getMagickVersion(), "6.8.9-3" ) < 0
213
			) {
214
				// bug 66323 - Greyscale images not rendered properly.
215
				// So only take the "red" channel.
216
				$channelOnly = [ '-channel', 'R', '-separate' ];
217
				$animation_pre = array_merge( $animation_pre, $channelOnly );
218
			}
219
		}
220
221
		// Use one thread only, to avoid deadlock bugs on OOM
222
		$env = [ 'OMP_NUM_THREADS' => 1 ];
223
		if ( strval( $wgImageMagickTempDir ) !== '' ) {
224
			$env['MAGICK_TMPDIR'] = $wgImageMagickTempDir;
225
		}
226
227
		$rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
228
		list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
229
230
		$cmd = call_user_func_array( 'wfEscapeShellArg', array_merge(
231
			[ $wgImageMagickConvertCommand ],
232
			$quality,
233
			// Specify white background color, will be used for transparent images
234
			// in Internet Explorer/Windows instead of default black.
235
			[ '-background', 'white' ],
236
			$decoderHint,
237
			[ $this->escapeMagickInput( $params['srcPath'], $scene ) ],
0 ignored issues
show
It seems like $scene defined by 0 on line 179 can also be of type integer; however, TransformationalImageHandler::escapeMagickInput() does only seem to accept boolean|string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
238
			$animation_pre,
239
			// For the -thumbnail option a "!" is needed to force exact size,
240
			// or ImageMagick may decide your ratio is wrong and slice off
241
			// a pixel.
242
			[ '-thumbnail', "{$width}x{$height}!" ],
243
			// Add the source url as a comment to the thumb, but don't add the flag if there's no comment
244
			( $params['comment'] !== ''
245
				? [ '-set', 'comment', $this->escapeMagickProperty( $params['comment'] ) ]
246
				: [] ),
247
			// T108616: Avoid exposure of local file path
248
			[ '+set', 'Thumb::URI' ],
249
			[ '-depth', 8 ],
250
			$sharpen,
251
			[ '-rotate', "-$rotation" ],
252
			$subsampling,
253
			$animation_post,
254
			[ $this->escapeMagickOutput( $params['dstPath'] ) ] ) );
255
256
		wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
257
		$retval = 0;
258
		$err = wfShellExecWithStderr( $cmd, $retval, $env );
259
260
		if ( $retval !== 0 ) {
261
			$this->logErrorForExternalProcess( $retval, $err, $cmd );
262
263
			return $this->getMediaTransformError( $params, "$err\nError code: $retval" );
264
		}
265
266
		return false; # No error
267
	}
268
269
	/**
270
	 * Transform an image using the Imagick PHP extension
271
	 *
272
	 * @param File $image File associated with this thumbnail
273
	 * @param array $params Array with scaler params
274
	 *
275
	 * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
276
	 */
277
	protected function transformImageMagickExt( $image, $params ) {
278
		global $wgSharpenReductionThreshold, $wgSharpenParameter, $wgMaxAnimatedGifArea,
279
			$wgJpegPixelFormat;
280
281
		try {
282
			$im = new Imagick();
283
			$im->readImage( $params['srcPath'] );
284
285
			if ( $params['mimeType'] == 'image/jpeg' ) {
286
				// Sharpening, see bug 6193
287
				if ( ( $params['physicalWidth'] + $params['physicalHeight'] )
288
					/ ( $params['srcWidth'] + $params['srcHeight'] )
289
					< $wgSharpenReductionThreshold
290
				) {
291
					// Hack, since $wgSharpenParameter is written specifically for the command line convert
292
					list( $radius, $sigma ) = explode( 'x', $wgSharpenParameter );
293
					$im->sharpenImage( $radius, $sigma );
294
				}
295
				$qualityVal = isset( $params['quality'] ) ? (string)$params['quality'] : null;
296
				$im->setCompressionQuality( $qualityVal ?: 80 );
297
				if ( $params['interlace'] ) {
298
					$im->setInterlaceScheme( Imagick::INTERLACE_JPEG );
299
				}
300
				if ( $wgJpegPixelFormat ) {
301
					$factors = $this->imageMagickSubsampling( $wgJpegPixelFormat );
302
					$im->setSamplingFactors( $factors );
303
				}
304
			} elseif ( $params['mimeType'] == 'image/png' ) {
305
				$im->setCompressionQuality( 95 );
306
				if ( $params['interlace'] ) {
307
					$im->setInterlaceScheme( Imagick::INTERLACE_PNG );
308
				}
309
			} elseif ( $params['mimeType'] == 'image/gif' ) {
310
				if ( $this->getImageArea( $image ) > $wgMaxAnimatedGifArea ) {
311
					// Extract initial frame only; we're so big it'll
312
					// be a total drag. :P
313
					$im->setImageScene( 0 );
314
				} elseif ( $this->isAnimatedImage( $image ) ) {
315
					// Coalesce is needed to scale animated GIFs properly (bug 1017).
316
					$im = $im->coalesceImages();
317
				}
318
				// GIF interlacing is only available since 6.3.4
319
				$v = Imagick::getVersion();
320
				preg_match( '/ImageMagick ([0-9]+\.[0-9]+\.[0-9]+)/', $v['versionString'], $v );
321
322
				if ( $params['interlace'] && version_compare( $v[1], '6.3.4' ) >= 0 ) {
323
					$im->setInterlaceScheme( Imagick::INTERLACE_GIF );
324
				}
325
			}
326
327
			$rotation = isset( $params['disableRotation'] ) ? 0 : $this->getRotation( $image );
328
			list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
329
330
			$im->setImageBackgroundColor( new ImagickPixel( 'white' ) );
331
332
			// Call Imagick::thumbnailImage on each frame
333
			foreach ( $im as $i => $frame ) {
334
				if ( !$frame->thumbnailImage( $width, $height, /* fit */ false ) ) {
335
					return $this->getMediaTransformError( $params, "Error scaling frame $i" );
336
				}
337
			}
338
			$im->setImageDepth( 8 );
339
340
			if ( $rotation ) {
341
				if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
342
					return $this->getMediaTransformError( $params, "Error rotating $rotation degrees" );
343
				}
344
			}
345
346
			if ( $this->isAnimatedImage( $image ) ) {
347
				wfDebug( __METHOD__ . ": Writing animated thumbnail\n" );
348
				// This is broken somehow... can't find out how to fix it
349
				$result = $im->writeImages( $params['dstPath'], true );
350
			} else {
351
				$result = $im->writeImage( $params['dstPath'] );
352
			}
353
			if ( !$result ) {
354
				return $this->getMediaTransformError( $params,
355
					"Unable to write thumbnail to {$params['dstPath']}" );
356
			}
357
		} catch ( ImagickException $e ) {
358
			return $this->getMediaTransformError( $params, $e->getMessage() );
359
		}
360
361
		return false;
362
	}
363
364
	/**
365
	 * Transform an image using a custom command
366
	 *
367
	 * @param File $image File associated with this thumbnail
368
	 * @param array $params Array with scaler params
369
	 *
370
	 * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
371
	 */
372
	protected function transformCustom( $image, $params ) {
373
		# Use a custom convert command
374
		global $wgCustomConvertCommand;
375
376
		# Variables: %s %d %w %h
377
		$src = wfEscapeShellArg( $params['srcPath'] );
378
		$dst = wfEscapeShellArg( $params['dstPath'] );
379
		$cmd = $wgCustomConvertCommand;
380
		$cmd = str_replace( '%s', $src, str_replace( '%d', $dst, $cmd ) ); # Filenames
381
		$cmd = str_replace( '%h', wfEscapeShellArg( $params['physicalHeight'] ),
382
			str_replace( '%w', wfEscapeShellArg( $params['physicalWidth'] ), $cmd ) ); # Size
383
		wfDebug( __METHOD__ . ": Running custom convert command $cmd\n" );
384
		$retval = 0;
385
		$err = wfShellExecWithStderr( $cmd, $retval );
386
387
		if ( $retval !== 0 ) {
388
			$this->logErrorForExternalProcess( $retval, $err, $cmd );
389
390
			return $this->getMediaTransformError( $params, $err );
391
		}
392
393
		return false; # No error
394
	}
395
396
	/**
397
	 * Transform an image using the built in GD library
398
	 *
399
	 * @param File $image File associated with this thumbnail
400
	 * @param array $params Array with scaler params
401
	 *
402
	 * @return MediaTransformError Error object if error occurred, false (=no error) otherwise
403
	 */
404
	protected function transformGd( $image, $params ) {
405
		# Use PHP's builtin GD library functions.
406
		# First find out what kind of file this is, and select the correct
407
		# input routine for this.
408
409
		$typemap = [
410
			'image/gif' => [ 'imagecreatefromgif', 'palette', false, 'imagegif' ],
411
			'image/jpeg' => [ 'imagecreatefromjpeg', 'truecolor', true,
412
				[ __CLASS__, 'imageJpegWrapper' ] ],
413
			'image/png' => [ 'imagecreatefrompng', 'bits', false, 'imagepng' ],
414
			'image/vnd.wap.wbmp' => [ 'imagecreatefromwbmp', 'palette', false, 'imagewbmp' ],
415
			'image/xbm' => [ 'imagecreatefromxbm', 'palette', false, 'imagexbm' ],
416
		];
417
418
		if ( !isset( $typemap[$params['mimeType']] ) ) {
419
			$err = 'Image type not supported';
420
			wfDebug( "$err\n" );
421
			$errMsg = wfMessage( 'thumbnail_image-type' )->text();
422
423
			return $this->getMediaTransformError( $params, $errMsg );
424
		}
425
		list( $loader, $colorStyle, $useQuality, $saveType ) = $typemap[$params['mimeType']];
426
427
		if ( !function_exists( $loader ) ) {
428
			$err = "Incomplete GD library configuration: missing function $loader";
429
			wfDebug( "$err\n" );
430
			$errMsg = wfMessage( 'thumbnail_gd-library', $loader )->text();
431
432
			return $this->getMediaTransformError( $params, $errMsg );
433
		}
434
435
		if ( !file_exists( $params['srcPath'] ) ) {
436
			$err = "File seems to be missing: {$params['srcPath']}";
437
			wfDebug( "$err\n" );
438
			$errMsg = wfMessage( 'thumbnail_image-missing', $params['srcPath'] )->text();
439
440
			return $this->getMediaTransformError( $params, $errMsg );
441
		}
442
443
		$src_image = call_user_func( $loader, $params['srcPath'] );
444
445
		$rotation = function_exists( 'imagerotate' ) && !isset( $params['disableRotation'] ) ?
446
			$this->getRotation( $image ) :
447
			0;
448
		list( $width, $height ) = $this->extractPreRotationDimensions( $params, $rotation );
449
		$dst_image = imagecreatetruecolor( $width, $height );
450
451
		// Initialise the destination image to transparent instead of
452
		// the default solid black, to support PNG and GIF transparency nicely
453
		$background = imagecolorallocate( $dst_image, 0, 0, 0 );
454
		imagecolortransparent( $dst_image, $background );
455
		imagealphablending( $dst_image, false );
456
457
		if ( $colorStyle == 'palette' ) {
458
			// Don't resample for paletted GIF images.
459
			// It may just uglify them, and completely breaks transparency.
460
			imagecopyresized( $dst_image, $src_image,
461
				0, 0, 0, 0,
462
				$width, $height,
463
				imagesx( $src_image ), imagesy( $src_image ) );
464
		} else {
465
			imagecopyresampled( $dst_image, $src_image,
466
				0, 0, 0, 0,
467
				$width, $height,
468
				imagesx( $src_image ), imagesy( $src_image ) );
469
		}
470
471
		if ( $rotation % 360 != 0 && $rotation % 90 == 0 ) {
472
			$rot_image = imagerotate( $dst_image, $rotation, 0 );
473
			imagedestroy( $dst_image );
474
			$dst_image = $rot_image;
475
		}
476
477
		imagesavealpha( $dst_image, true );
478
479
		$funcParams = [ $dst_image, $params['dstPath'] ];
480
		if ( $useQuality && isset( $params['quality'] ) ) {
481
			$funcParams[] = $params['quality'];
482
		}
483
		call_user_func_array( $saveType, $funcParams );
484
485
		imagedestroy( $dst_image );
486
		imagedestroy( $src_image );
487
488
		return false; # No error
489
	}
490
491
	/**
492
	 * Callback for transformGd when transforming jpeg images.
493
	 */
494
	// FIXME: transformImageMagick() & transformImageMagickExt() uses JPEG quality 80, here it's 95?
495
	static function imageJpegWrapper( $dst_image, $thumbPath, $quality = 95 ) {
496
		imageinterlace( $dst_image );
497
		imagejpeg( $dst_image, $thumbPath, $quality );
498
	}
499
500
	/**
501
	 * Returns whether the current scaler supports rotation (im and gd do)
502
	 *
503
	 * @return bool
504
	 */
505
	public function canRotate() {
506
		$scaler = $this->getScalerType( null, false );
507
		switch ( $scaler ) {
508
			case 'im':
509
				# ImageMagick supports autorotation
510
				return true;
511
			case 'imext':
512
				# Imagick::rotateImage
513
				return true;
514
			case 'gd':
515
				# GD's imagerotate function is used to rotate images, but not
516
				# all precompiled PHP versions have that function
517
				return function_exists( 'imagerotate' );
518
			default:
519
				# Other scalers don't support rotation
520
				return false;
521
		}
522
	}
523
524
	/**
525
	 * @see $wgEnableAutoRotation
526
	 * @return bool Whether auto rotation is enabled
527
	 */
528
	public function autoRotateEnabled() {
529
		global $wgEnableAutoRotation;
530
531
		if ( $wgEnableAutoRotation === null ) {
532
			// Only enable auto-rotation when we actually can
533
			return $this->canRotate();
534
		}
535
536
		return $wgEnableAutoRotation;
537
	}
538
539
	/**
540
	 * @param File $file
541
	 * @param array $params Rotate parameters.
542
	 *   'rotation' clockwise rotation in degrees, allowed are multiples of 90
543
	 * @since 1.21
544
	 * @return bool
545
	 */
546
	public function rotate( $file, $params ) {
547
		global $wgImageMagickConvertCommand;
548
549
		$rotation = ( $params['rotation'] + $this->getRotation( $file ) ) % 360;
550
		$scene = false;
551
552
		$scaler = $this->getScalerType( null, false );
553
		switch ( $scaler ) {
554
			case 'im':
555
				$cmd = wfEscapeShellArg( $wgImageMagickConvertCommand ) . " " .
556
					wfEscapeShellArg( $this->escapeMagickInput( $params['srcPath'], $scene ) ) .
557
					" -rotate " . wfEscapeShellArg( "-$rotation" ) . " " .
558
					wfEscapeShellArg( $this->escapeMagickOutput( $params['dstPath'] ) );
559
				wfDebug( __METHOD__ . ": running ImageMagick: $cmd\n" );
560
				$retval = 0;
561
				$err = wfShellExecWithStderr( $cmd, $retval );
562 View Code Duplication
				if ( $retval !== 0 ) {
563
					$this->logErrorForExternalProcess( $retval, $err, $cmd );
564
565
					return new MediaTransformError( 'thumbnail_error', 0, 0, $err );
566
				}
567
568
				return false;
569
			case 'imext':
570
				$im = new Imagick();
571
				$im->readImage( $params['srcPath'] );
572
				if ( !$im->rotateImage( new ImagickPixel( 'white' ), 360 - $rotation ) ) {
573
					return new MediaTransformError( 'thumbnail_error', 0, 0,
574
						"Error rotating $rotation degrees" );
575
				}
576
				$result = $im->writeImage( $params['dstPath'] );
577
				if ( !$result ) {
578
					return new MediaTransformError( 'thumbnail_error', 0, 0,
579
						"Unable to write image to {$params['dstPath']}" );
580
				}
581
582
				return false;
583
			default:
584
				return new MediaTransformError( 'thumbnail_error', 0, 0,
585
					"$scaler rotation not implemented" );
586
		}
587
	}
588
}
589