MediaTransformOutput::streamFileWithStatus()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 1
dl 0
loc 11
rs 9.2
c 0
b 0
f 0
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;
0 ignored issues
show
Bug introduced by
It seems like $this->path can also be of type boolean; however, FileBackend::extensionFromPath() does only seem to accept 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...
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 ) ) {
0 ignored issues
show
Bug introduced by
It seems like $this->path can also be of type boolean; however, FileBackend::isStoragePath() does only seem to accept 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...
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 ) ) {
0 ignored issues
show
Bug introduced by
It seems like $this->path can also be of type boolean; however, FileBackend::isStoragePath() does only seem to accept 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...
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 );
0 ignored issues
show
Bug introduced by
It seems like $this->getLocalCopyPath() targeting MediaTransformOutput::getLocalCopyPath() can also be of type boolean; however, StreamFile::stream() does only seem to accept string, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
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();
0 ignored issues
show
Unused Code introduced by
The call to the method Status::isOK() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
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'] );
0 ignored issues
show
Documentation Bug introduced by
The property $width was declared of type integer, but round($actualParams['width']) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
320
		$this->height = round( $actualParams['height'] );
0 ignored issues
show
Documentation Bug introduced by
The property $height was declared of type integer, but round($actualParams['height']) is of type double. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
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 ) );
0 ignored issues
show
Security Bug introduced by
It seems like $linkAttribs defined by false on line 400 can also be of type false; however, MediaTransformOutput::linkWrap() does only seem to accept array, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
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;
0 ignored issues
show
Documentation Bug introduced by
The property $url was declared of type string, but false is of type false. Maybe add a type cast?

This check looks for assignments to scalar types that may be of the wrong type.

To ensure the code behaves as expected, it may be a good idea to add an explicit type cast.

$answer = 42;

$correct = false;

$correct = (bool) $answer;
Loading history...
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