Completed
Branch master (939199)
by
unknown
39:35
created

includes/media/MediaTransformOutput.php (1 issue)

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
 * Base class for the output of file transformation methods.
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
 * Base class for the output of MediaHandler::doTransform() and File::transform().
26
 *
27
 * @ingroup Media
28
 */
29
abstract class MediaTransformOutput {
30
	/** @var array Associative array mapping optional supplementary image files
31
	 *  from pixel density (eg 1.5 or 2) to additional URLs.
32
	 */
33
	public $responsiveUrls = [];
34
35
	/** @var File */
36
	protected $file;
37
38
	/** @var int Image width */
39
	protected $width;
40
41
	/** @var int Image height */
42
	protected $height;
43
44
	/** @var string URL path to the thumb */
45
	protected $url;
46
47
	/** @var bool|string */
48
	protected $page;
49
50
	/** @var bool|string Filesystem path to the thumb  */
51
	protected $path;
52
53
	/** @var bool|string Language code, false if not set */
54
	protected $lang;
55
56
	/** @var bool|string Permanent storage path  */
57
	protected $storagePath = false;
58
59
	/**
60
	 * @return int Width of the output box
61
	 */
62
	public function getWidth() {
63
		return $this->width;
64
	}
65
66
	/**
67
	 * @return int Height of the output box
68
	 */
69
	public function getHeight() {
70
		return $this->height;
71
	}
72
73
	/**
74
	 * @return File
75
	 */
76
	public function getFile() {
77
		return $this->file;
78
	}
79
80
	/**
81
	 * Get the final extension of the thumbnail.
82
	 * Returns false for scripted transformations.
83
	 * @return string|bool
84
	 */
85
	public function getExtension() {
86
		return $this->path ? FileBackend::extensionFromPath( $this->path ) : false;
87
	}
88
89
	/**
90
	 * @return string|bool The thumbnail URL
91
	 */
92
	public function getUrl() {
93
		return $this->url;
94
	}
95
96
	/**
97
	 * @return string|bool The permanent thumbnail storage path
98
	 */
99
	public function getStoragePath() {
100
		return $this->storagePath;
101
	}
102
103
	/**
104
	 * @param string $storagePath The permanent storage path
105
	 * @return void
106
	 */
107
	public function setStoragePath( $storagePath ) {
108
		$this->storagePath = $storagePath;
109
		if ( $this->path === false ) {
110
			$this->path = $storagePath;
111
		}
112
	}
113
114
	/**
115
	 * Fetch HTML for this transform output
116
	 *
117
	 * @param array $options Associative array of options. Boolean options
118
	 *     should be indicated with a value of true for true, and false or
119
	 *     absent for false.
120
	 *
121
	 *     alt          Alternate text or caption
122
	 *     desc-link    Boolean, show a description link
123
	 *     file-link    Boolean, show a file download link
124
	 *     custom-url-link    Custom URL to link to
125
	 *     custom-title-link  Custom Title object to link to
126
	 *     valign       vertical-align property, if the output is an inline element
127
	 *     img-class    Class applied to the "<img>" tag, if there is such a tag
128
	 *
129
	 * For images, desc-link and file-link are implemented as a click-through. For
130
	 * sounds and videos, they may be displayed in other ways.
131
	 *
132
	 * @return string
133
	 */
134
	abstract public function toHtml( $options = [] );
135
136
	/**
137
	 * This will be overridden to return true in error classes
138
	 * @return bool
139
	 */
140
	public function isError() {
141
		return false;
142
	}
143
144
	/**
145
	 * Check if an output thumbnail file actually exists.
146
	 *
147
	 * This will return false if there was an error, the
148
	 * thumbnail is to be handled client-side only, or if
149
	 * transformation was deferred via TRANSFORM_LATER.
150
	 * This file may exist as a new file in /tmp, a file
151
	 * in permanent storage, or even refer to the original.
152
	 *
153
	 * @return bool
154
	 */
155
	public function hasFile() {
156
		// If TRANSFORM_LATER, $this->path will be false.
157
		// Note: a null path means "use the source file".
158
		return ( !$this->isError() && ( $this->path || $this->path === null ) );
159
	}
160
161
	/**
162
	 * Check if the output thumbnail is the same as the source.
163
	 * This can occur if the requested width was bigger than the source.
164
	 *
165
	 * @return bool
166
	 */
167
	public function fileIsSource() {
168
		return ( !$this->isError() && $this->path === null );
169
	}
170
171
	/**
172
	 * Get the path of a file system copy of the thumbnail.
173
	 * Callers should never write to this path.
174
	 *
175
	 * @return string|bool Returns false if there isn't one
176
	 */
177
	public function getLocalCopyPath() {
178
		if ( $this->isError() ) {
179
			return false;
180
		} elseif ( $this->path === null ) {
181
			return $this->file->getLocalRefPath(); // assume thumb was not scaled
182
		} elseif ( FileBackend::isStoragePath( $this->path ) ) {
183
			$be = $this->file->getRepo()->getBackend();
184
			// The temp file will be process cached by FileBackend
185
			$fsFile = $be->getLocalReference( [ 'src' => $this->path ] );
186
187
			return $fsFile ? $fsFile->getPath() : false;
188
		} else {
189
			return $this->path; // may return false
190
		}
191
	}
192
193
	/**
194
	 * Stream the file if there were no errors
195
	 *
196
	 * @param array $headers Additional HTTP headers to send on success
197
	 * @return Status
198
	 * @since 1.27
199
	 */
200
	public function streamFileWithStatus( $headers = [] ) {
201
		if ( !$this->path ) {
202
			return Status::newFatal( 'backend-fail-stream', '<no path>' );
203
		} elseif ( FileBackend::isStoragePath( $this->path ) ) {
204
			$be = $this->file->getRepo()->getBackend();
205
			return $be->streamFile( [ 'src' => $this->path, 'headers' => $headers ] );
206
		} else { // FS-file
207
			$success = StreamFile::stream( $this->getLocalCopyPath(), $headers );
208
			return $success ? Status::newGood() : Status::newFatal( 'backend-fail-stream', $this->path );
209
		}
210
	}
211
212
	/**
213
	 * Stream the file if there were no errors
214
	 *
215
	 * @deprecated since 1.26, use streamFileWithStatus
216
	 * @param array $headers Additional HTTP headers to send on success
217
	 * @return bool Success
218
	 */
219
	public function streamFile( $headers = [] ) {
220
		$this->streamFileWithStatus( $headers )->isOK();
221
	}
222
223
	/**
224
	 * Wrap some XHTML text in an anchor tag with the given attributes
225
	 *
226
	 * @param array $linkAttribs
227
	 * @param string $contents
228
	 * @return string
229
	 */
230
	protected function linkWrap( $linkAttribs, $contents ) {
231
		if ( $linkAttribs ) {
232
			return Xml::tags( 'a', $linkAttribs, $contents );
233
		} else {
234
			return $contents;
235
		}
236
	}
237
238
	/**
239
	 * @param string $title
240
	 * @param string|array $params Query parameters to add
241
	 * @return array
242
	 */
243
	public function getDescLinkAttribs( $title = null, $params = [] ) {
244
		if ( is_array( $params ) ) {
245
			$query = $params;
246
		} else {
247
			$query = [];
248
		}
249
		if ( $this->page && $this->page !== 1 ) {
250
			$query['page'] = $this->page;
251
		}
252
		if ( $this->lang ) {
253
			$query['lang'] = $this->lang;
254
		}
255
256
		if ( is_string( $params ) && $params !== '' ) {
257
			$query = $params . '&' . wfArrayToCgi( $query );
258
		}
259
260
		$attribs = [
261
			'href' => $this->file->getTitle()->getLocalURL( $query ),
262
			'class' => 'image',
263
		];
264
		if ( $title ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $title of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
265
			$attribs['title'] = $title;
266
		}
267
268
		return $attribs;
269
	}
270
}
271
272
/**
273
 * Media transform output for images
274
 *
275
 * @ingroup Media
276
 */
277
class ThumbnailImage extends MediaTransformOutput {
278
	/**
279
	 * Get a thumbnail object from a file and parameters.
280
	 * If $path is set to null, the output file is treated as a source copy.
281
	 * If $path is set to false, no output file will be created.
282
	 * $parameters should include, as a minimum, (file) 'width' and 'height'.
283
	 * It may also include a 'page' parameter for multipage files.
284
	 *
285
	 * @param File $file
286
	 * @param string $url URL path to the thumb
287
	 * @param string|bool $path Filesystem path to the thumb
288
	 * @param array $parameters Associative array of parameters
289
	 */
290
	function __construct( $file, $url, $path = false, $parameters = [] ) {
291
		# Previous parameters:
292
		#   $file, $url, $width, $height, $path = false, $page = false
293
294
		$defaults = [
295
			'page' => false,
296
			'lang' => false
297
		];
298
299
		if ( is_array( $parameters ) ) {
300
			$actualParams = $parameters + $defaults;
301
		} else {
302
			# Using old format, should convert. Later a warning could be added here.
303
			$numArgs = func_num_args();
304
			$actualParams = [
305
				'width' => $path,
306
				'height' => $parameters,
307
				'page' => ( $numArgs > 5 ) ? func_get_arg( 5 ) : false
308
			] + $defaults;
309
			$path = ( $numArgs > 4 ) ? func_get_arg( 4 ) : false;
310
		}
311
312
		$this->file = $file;
313
		$this->url = $url;
314
		$this->path = $path;
315
316
		# These should be integers when they get here.
317
		# If not, there's a bug somewhere.  But let's at
318
		# least produce valid HTML code regardless.
319
		$this->width = round( $actualParams['width'] );
320
		$this->height = round( $actualParams['height'] );
321
322
		$this->page = $actualParams['page'];
323
		$this->lang = $actualParams['lang'];
324
	}
325
326
	/**
327
	 * Return HTML <img ... /> tag for the thumbnail, will include
328
	 * width and height attributes and a blank alt text (as required).
329
	 *
330
	 * @param array $options Associative array of options. Boolean options
331
	 *     should be indicated with a value of true for true, and false or
332
	 *     absent for false.
333
	 *
334
	 *     alt          HTML alt attribute
335
	 *     title        HTML title attribute
336
	 *     desc-link    Boolean, show a description link
337
	 *     file-link    Boolean, show a file download link
338
	 *     valign       vertical-align property, if the output is an inline element
339
	 *     img-class    Class applied to the \<img\> tag, if there is such a tag
340
	 *     desc-query   String, description link query params
341
	 *     override-width     Override width attribute. Should generally not set
342
	 *     override-height    Override height attribute. Should generally not set
343
	 *     no-dimensions      Boolean, skip width and height attributes (useful if
344
	 *                        set in CSS)
345
	 *     custom-url-link    Custom URL to link to
346
	 *     custom-title-link  Custom Title object to link to
347
	 *     custom target-link Value of the target attribute, for custom-target-link
348
	 *     parser-extlink-*   Attributes added by parser for external links:
349
	 *          parser-extlink-rel: add rel="nofollow"
350
	 *          parser-extlink-target: link target, but overridden by custom-target-link
351
	 *
352
	 * For images, desc-link and file-link are implemented as a click-through. For
353
	 * sounds and videos, they may be displayed in other ways.
354
	 *
355
	 * @throws MWException
356
	 * @return string
357
	 */
358
	function toHtml( $options = [] ) {
359
		if ( count( func_get_args() ) == 2 ) {
360
			throw new MWException( __METHOD__ . ' called in the old style' );
361
		}
362
363
		$alt = isset( $options['alt'] ) ? $options['alt'] : '';
364
365
		$query = isset( $options['desc-query'] ) ? $options['desc-query'] : '';
366
367
		$attribs = [
368
			'alt' => $alt,
369
			'src' => $this->url,
370
		];
371
372
		if ( !empty( $options['custom-url-link'] ) ) {
373
			$linkAttribs = [ 'href' => $options['custom-url-link'] ];
374
			if ( !empty( $options['title'] ) ) {
375
				$linkAttribs['title'] = $options['title'];
376
			}
377
			if ( !empty( $options['custom-target-link'] ) ) {
378
				$linkAttribs['target'] = $options['custom-target-link'];
379
			} elseif ( !empty( $options['parser-extlink-target'] ) ) {
380
				$linkAttribs['target'] = $options['parser-extlink-target'];
381
			}
382
			if ( !empty( $options['parser-extlink-rel'] ) ) {
383
				$linkAttribs['rel'] = $options['parser-extlink-rel'];
384
			}
385
		} elseif ( !empty( $options['custom-title-link'] ) ) {
386
			/** @var Title $title */
387
			$title = $options['custom-title-link'];
388
			$linkAttribs = [
389
				'href' => $title->getLinkURL(),
390
				'title' => empty( $options['title'] ) ? $title->getFullText() : $options['title']
391
			];
392
		} elseif ( !empty( $options['desc-link'] ) ) {
393
			$linkAttribs = $this->getDescLinkAttribs(
394
				empty( $options['title'] ) ? null : $options['title'],
395
				$query
396
			);
397
		} elseif ( !empty( $options['file-link'] ) ) {
398
			$linkAttribs = [ 'href' => $this->file->getUrl() ];
399
		} else {
400
			$linkAttribs = false;
401
			if ( !empty( $options['title'] ) ) {
402
				$attribs['title'] = $options['title'];
403
			}
404
		}
405
406
		if ( empty( $options['no-dimensions'] ) ) {
407
			$attribs['width'] = $this->width;
408
			$attribs['height'] = $this->height;
409
		}
410
		if ( !empty( $options['valign'] ) ) {
411
			$attribs['style'] = "vertical-align: {$options['valign']}";
412
		}
413
		if ( !empty( $options['img-class'] ) ) {
414
			$attribs['class'] = $options['img-class'];
415
		}
416
		if ( isset( $options['override-height'] ) ) {
417
			$attribs['height'] = $options['override-height'];
418
		}
419
		if ( isset( $options['override-width'] ) ) {
420
			$attribs['width'] = $options['override-width'];
421
		}
422
423
		// Additional densities for responsive images, if specified.
424
		// If any of these urls is the same as src url, it'll be excluded.
425
		$responsiveUrls = array_diff( $this->responsiveUrls, [ $this->url ] );
426
		if ( !empty( $responsiveUrls ) ) {
427
			$attribs['srcset'] = Html::srcSet( $responsiveUrls );
428
		}
429
430
		Hooks::run( 'ThumbnailBeforeProduceHTML', [ $this, &$attribs, &$linkAttribs ] );
431
432
		return $this->linkWrap( $linkAttribs, Xml::element( 'img', $attribs ) );
433
	}
434
}
435
436
/**
437
 * Basic media transform error class
438
 *
439
 * @ingroup Media
440
 */
441
class MediaTransformError extends MediaTransformOutput {
442
	/** @var string HTML formatted version of the error */
443
	private $htmlMsg;
444
445
	/** @var string Plain text formatted version of the error */
446
	private $textMsg;
447
448
	function __construct( $msg, $width, $height /*, ... */ ) {
449
		$args = array_slice( func_get_args(), 3 );
450
		$htmlArgs = array_map( 'htmlspecialchars', $args );
451
		$htmlArgs = array_map( 'nl2br', $htmlArgs );
452
453
		$this->htmlMsg = wfMessage( $msg )->rawParams( $htmlArgs )->escaped();
454
		$this->textMsg = wfMessage( $msg )->rawParams( $htmlArgs )->text();
455
		$this->width = intval( $width );
456
		$this->height = intval( $height );
457
		$this->url = false;
458
		$this->path = false;
459
	}
460
461
	function toHtml( $options = [] ) {
462
		return "<div class=\"MediaTransformError\" style=\"" .
463
			"width: {$this->width}px; height: {$this->height}px; display:inline-block;\">" .
464
			$this->htmlMsg .
465
			"</div>";
466
	}
467
468
	function toText() {
469
		return $this->textMsg;
470
	}
471
472
	function getHtmlMsg() {
473
		return $this->htmlMsg;
474
	}
475
476
	function isError() {
477
		return true;
478
	}
479
}
480
481
/**
482
 * Shortcut class for parameter validation errors
483
 *
484
 * @ingroup Media
485
 */
486
class TransformParameterError extends MediaTransformError {
487
	function __construct( $params ) {
488
		parent::__construct( 'thumbnail_error',
489
			max( isset( $params['width'] ) ? $params['width'] : 0, 120 ),
490
			max( isset( $params['height'] ) ? $params['height'] : 0, 120 ),
491
			wfMessage( 'thumbnail_invalid_params' )->text() );
492
	}
493
}
494
495
/**
496
 * Shortcut class for parameter file size errors
497
 *
498
 * @ingroup Media
499
 * @since 1.25
500
 */
501
class TransformTooBigImageAreaError extends MediaTransformError {
502
	function __construct( $params, $maxImageArea ) {
503
		$msg = wfMessage( 'thumbnail_toobigimagearea' );
504
505
		parent::__construct( 'thumbnail_error',
506
			max( isset( $params['width'] ) ? $params['width'] : 0, 120 ),
507
			max( isset( $params['height'] ) ? $params['height'] : 0, 120 ),
508
			$msg->rawParams(
509
				$msg->getLanguage()->formatComputingNumbers(
510
					$maxImageArea, 1000, "size-$1pixel" )
511
				)->text()
512
			);
513
	}
514
}
515