ApiQueryImageInfo   F
last analyzed

Complexity

Total Complexity 122

Size/Duplication

Total Lines 776
Duplicated Lines 6.96 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 54
loc 776
rs 2.6666
wmc 122
lcom 1
cbo 13

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 2
F execute() 54 175 30
A getScale() 0 21 4
C mergeThumbParams() 0 61 13
A checkParameterNormalise() 0 13 3
F getInfo() 0 217 54
A getTransformCount() 0 3 1
A processMetaData() 0 20 4
A getCacheMode() 0 7 2
A getContinueStr() 0 7 2
A getAllowedParams() 0 61 1
A getPropertyNames() 0 3 1
B getPropertyMessages() 0 26 1
B getProperties() 0 29 1
A getPropertyDescriptions() 0 6 1
A getExamplesMessages() 0 9 1
A getHelpUrls() 0 3 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ApiQueryImageInfo often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ApiQueryImageInfo, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 *
4
 *
5
 * Created on July 6, 2007
6
 *
7
 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
8
 *
9
 * This program is free software; you can redistribute it and/or modify
10
 * it under the terms of the GNU General Public License as published by
11
 * the Free Software Foundation; either version 2 of the License, or
12
 * (at your option) any later version.
13
 *
14
 * This program is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 *
19
 * You should have received a copy of the GNU General Public License along
20
 * with this program; if not, write to the Free Software Foundation, Inc.,
21
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22
 * http://www.gnu.org/copyleft/gpl.html
23
 *
24
 * @file
25
 */
26
27
/**
28
 * A query action to get image information and upload history.
29
 *
30
 * @ingroup API
31
 */
32
class ApiQueryImageInfo extends ApiQueryBase {
33
	const TRANSFORM_LIMIT = 50;
34
	private static $transformCount = 0;
35
36
	public function __construct( ApiQuery $query, $moduleName, $prefix = 'ii' ) {
37
		// We allow a subclass to override the prefix, to create a related API
38
		// module. Some other parts of MediaWiki construct this with a null
39
		// $prefix, which used to be ignored when this only took two arguments
40
		if ( is_null( $prefix ) ) {
41
			$prefix = 'ii';
42
		}
43
		parent::__construct( $query, $moduleName, $prefix );
44
	}
45
46
	public function execute() {
47
		$params = $this->extractRequestParams();
48
49
		$prop = array_flip( $params['prop'] );
50
51
		$scale = $this->getScale( $params );
52
53
		$opts = [
54
			'version' => $params['metadataversion'],
55
			'language' => $params['extmetadatalanguage'],
56
			'multilang' => $params['extmetadatamultilang'],
57
			'extmetadatafilter' => $params['extmetadatafilter'],
58
			'revdelUser' => $this->getUser(),
59
		];
60
61
		$pageIds = $this->getPageSet()->getGoodAndMissingTitlesByNamespace();
62
		if ( !empty( $pageIds[NS_FILE] ) ) {
63
			$titles = array_keys( $pageIds[NS_FILE] );
64
			asort( $titles ); // Ensure the order is always the same
65
66
			$fromTitle = null;
67
			if ( !is_null( $params['continue'] ) ) {
68
				$cont = explode( '|', $params['continue'] );
69
				$this->dieContinueUsageIf( count( $cont ) != 2 );
70
				$fromTitle = strval( $cont[0] );
71
				$fromTimestamp = $cont[1];
72
				// Filter out any titles before $fromTitle
73
				foreach ( $titles as $key => $title ) {
74
					if ( $title < $fromTitle ) {
75
						unset( $titles[$key] );
76
					} else {
77
						break;
78
					}
79
				}
80
			}
81
82
			$user = $this->getUser();
83
			$findTitles = array_map( function ( $title ) use ( $user ) {
84
				return [
85
					'title' => $title,
86
					'private' => $user,
87
				];
88
			}, $titles );
89
90 View Code Duplication
			if ( $params['localonly'] ) {
91
				$images = RepoGroup::singleton()->getLocalRepo()->findFiles( $findTitles );
92
			} else {
93
				$images = RepoGroup::singleton()->findFiles( $findTitles );
94
			}
95
96
			$result = $this->getResult();
97
			foreach ( $titles as $title ) {
98
				$pageId = $pageIds[NS_FILE][$title];
99
				$start = $title === $fromTitle ? $fromTimestamp : $params['start'];
0 ignored issues
show
Bug introduced by
The variable $fromTimestamp does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
100
101
				if ( !isset( $images[$title] ) ) {
102
					if ( isset( $prop['uploadwarning'] ) ) {
103
						// Uploadwarning needs info about non-existing files
104
						$images[$title] = wfLocalFile( $title );
105
					} else {
106
						$result->addValue(
107
							[ 'query', 'pages', intval( $pageId ) ],
108
							'imagerepository', ''
109
						);
110
						// The above can't fail because it doesn't increase the result size
111
						continue;
112
					}
113
				}
114
115
				/** @var $img File */
116
				$img = $images[$title];
117
118 View Code Duplication
				if ( self::getTransformCount() >= self::TRANSFORM_LIMIT ) {
119
					if ( count( $pageIds[NS_FILE] ) == 1 ) {
120
						// See the 'the user is screwed' comment below
121
						$this->setContinueEnumParameter( 'start',
122
							$start !== null ? $start : wfTimestamp( TS_ISO_8601, $img->getTimestamp() )
123
						);
124
					} else {
125
						$this->setContinueEnumParameter( 'continue',
126
							$this->getContinueStr( $img, $start ) );
127
					}
128
					break;
129
				}
130
131
				$fit = $result->addValue(
132
					[ 'query', 'pages', intval( $pageId ) ],
133
					'imagerepository', $img->getRepoName()
134
				);
135 View Code Duplication
				if ( !$fit ) {
136
					if ( count( $pageIds[NS_FILE] ) == 1 ) {
137
						// The user is screwed. imageinfo can't be solely
138
						// responsible for exceeding the limit in this case,
139
						// so set a query-continue that just returns the same
140
						// thing again. When the violating queries have been
141
						// out-continued, the result will get through
142
						$this->setContinueEnumParameter( 'start',
143
							$start !== null ? $start : wfTimestamp( TS_ISO_8601, $img->getTimestamp() )
144
						);
145
					} else {
146
						$this->setContinueEnumParameter( 'continue',
147
							$this->getContinueStr( $img, $start ) );
148
					}
149
					break;
150
				}
151
152
				// Check if we can make the requested thumbnail, and get transform parameters.
153
				$finalThumbParams = $this->mergeThumbParams( $img, $scale, $params['urlparam'] );
0 ignored issues
show
Bug introduced by
It seems like $scale defined by $this->getScale($params) on line 51 can also be of type null; however, ApiQueryImageInfo::mergeThumbParams() does only seem to accept array, 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...
154
155
				// Get information about the current version first
156
				// Check that the current version is within the start-end boundaries
157
				$gotOne = false;
158
				if (
159
					( is_null( $start ) || $img->getTimestamp() <= $start ) &&
160
					( is_null( $params['end'] ) || $img->getTimestamp() >= $params['end'] )
161
				) {
162
					$gotOne = true;
163
164
					$fit = $this->addPageSubItem( $pageId,
165
						static::getInfo( $img, $prop, $result,
166
							$finalThumbParams, $opts
167
						)
168
					);
169 View Code Duplication
					if ( !$fit ) {
170
						if ( count( $pageIds[NS_FILE] ) == 1 ) {
171
							// See the 'the user is screwed' comment above
172
							$this->setContinueEnumParameter( 'start',
173
								wfTimestamp( TS_ISO_8601, $img->getTimestamp() ) );
0 ignored issues
show
Security Bug introduced by
It seems like wfTimestamp(TS_ISO_8601, $img->getTimestamp()) targeting wfTimestamp() can also be of type false; however, ApiQueryBase::setContinueEnumParameter() does only seem to accept string|array, did you maybe forget to handle an error condition?
Loading history...
174
						} else {
175
							$this->setContinueEnumParameter( 'continue',
176
								$this->getContinueStr( $img ) );
177
						}
178
						break;
179
					}
180
				}
181
182
				// Now get the old revisions
183
				// Get one more to facilitate query-continue functionality
184
				$count = ( $gotOne ? 1 : 0 );
185
				$oldies = $img->getHistory( $params['limit'] - $count + 1, $start, $params['end'] );
186
				/** @var $oldie File */
187
				foreach ( $oldies as $oldie ) {
188
					if ( ++$count > $params['limit'] ) {
189
						// We've reached the extra one which shows that there are
190
						// additional pages to be had. Stop here...
191
						// Only set a query-continue if there was only one title
192
						if ( count( $pageIds[NS_FILE] ) == 1 ) {
193
							$this->setContinueEnumParameter( 'start',
194
								wfTimestamp( TS_ISO_8601, $oldie->getTimestamp() ) );
0 ignored issues
show
Security Bug introduced by
It seems like wfTimestamp(TS_ISO_8601, $oldie->getTimestamp()) targeting wfTimestamp() can also be of type false; however, ApiQueryBase::setContinueEnumParameter() does only seem to accept string|array, did you maybe forget to handle an error condition?
Loading history...
195
						}
196
						break;
197
					}
198
					$fit = self::getTransformCount() < self::TRANSFORM_LIMIT &&
199
						$this->addPageSubItem( $pageId,
200
							static::getInfo( $oldie, $prop, $result,
201
								$finalThumbParams, $opts
202
							)
203
						);
204 View Code Duplication
					if ( !$fit ) {
205
						if ( count( $pageIds[NS_FILE] ) == 1 ) {
206
							$this->setContinueEnumParameter( 'start',
207
								wfTimestamp( TS_ISO_8601, $oldie->getTimestamp() ) );
0 ignored issues
show
Security Bug introduced by
It seems like wfTimestamp(TS_ISO_8601, $oldie->getTimestamp()) targeting wfTimestamp() can also be of type false; however, ApiQueryBase::setContinueEnumParameter() does only seem to accept string|array, did you maybe forget to handle an error condition?
Loading history...
208
						} else {
209
							$this->setContinueEnumParameter( 'continue',
210
								$this->getContinueStr( $oldie ) );
211
						}
212
						break;
213
					}
214
				}
215
				if ( !$fit ) {
216
					break;
217
				}
218
			}
219
		}
220
	}
221
222
	/**
223
	 * From parameters, construct a 'scale' array
224
	 * @param array $params Parameters passed to api.
225
	 * @return array|null Key-val array of 'width' and 'height', or null
226
	 */
227
	public function getScale( $params ) {
228
		if ( $params['urlwidth'] != -1 ) {
229
			$scale = [];
230
			$scale['width'] = $params['urlwidth'];
231
			$scale['height'] = $params['urlheight'];
232
		} elseif ( $params['urlheight'] != -1 ) {
233
			// Height is specified but width isn't
234
			// Don't set $scale['width']; this signals mergeThumbParams() to fill it with the image's width
235
			$scale = [];
236
			$scale['height'] = $params['urlheight'];
237
		} else {
238
			if ( $params['urlparam'] ) {
239
				// Audio files might not have a width/height.
240
				$scale = [];
241
			} else {
242
				$scale = null;
243
			}
244
		}
245
246
		return $scale;
247
	}
248
249
	/** Validate and merge scale parameters with handler thumb parameters, give error if invalid.
250
	 *
251
	 * We do this later than getScale, since we need the image
252
	 * to know which handler, since handlers can make their own parameters.
253
	 * @param File $image Image that params are for.
254
	 * @param array $thumbParams Thumbnail parameters from getScale
255
	 * @param string $otherParams String of otherParams (iiurlparam).
256
	 * @return array Array of parameters for transform.
257
	 */
258
	protected function mergeThumbParams( $image, $thumbParams, $otherParams ) {
259
		if ( $thumbParams === null ) {
260
			// No scaling requested
261
			return null;
262
		}
263
		if ( !isset( $thumbParams['width'] ) && isset( $thumbParams['height'] ) ) {
264
			// We want to limit only by height in this situation, so pass the
265
			// image's full width as the limiting width. But some file types
266
			// don't have a width of their own, so pick something arbitrary so
267
			// thumbnailing the default icon works.
268
			if ( $image->getWidth() <= 0 ) {
269
				$thumbParams['width'] = max( $this->getConfig()->get( 'ThumbLimits' ) );
270
			} else {
271
				$thumbParams['width'] = $image->getWidth();
272
			}
273
		}
274
275
		if ( !$otherParams ) {
276
			$this->checkParameterNormalise( $image, $thumbParams );
277
			return $thumbParams;
278
		}
279
		$p = $this->getModulePrefix();
280
281
		$h = $image->getHandler();
282
		if ( !$h ) {
283
			$this->setWarning( 'Could not create thumbnail because ' .
284
				$image->getName() . ' does not have an associated image handler' );
285
286
			return $thumbParams;
287
		}
288
289
		$paramList = $h->parseParamString( $otherParams );
290
		if ( !$paramList ) {
291
			// Just set a warning (instead of dieUsage), as in many cases
292
			// we could still render the image using width and height parameters,
293
			// and this type of thing could happen between different versions of
294
			// handlers.
295
			$this->setWarning( "Could not parse {$p}urlparam for " . $image->getName()
296
				. '. Using only width and height' );
297
			$this->checkParameterNormalise( $image, $thumbParams );
298
			return $thumbParams;
299
		}
300
301
		if ( isset( $paramList['width'] ) && isset( $thumbParams['width'] ) ) {
302
			if ( intval( $paramList['width'] ) != intval( $thumbParams['width'] ) ) {
303
				$this->setWarning( "Ignoring width value set in {$p}urlparam ({$paramList['width']}) "
304
					. "in favor of width value derived from {$p}urlwidth/{$p}urlheight "
305
					. "({$thumbParams['width']})" );
306
			}
307
		}
308
309
		foreach ( $paramList as $name => $value ) {
0 ignored issues
show
Bug introduced by
The expression $paramList of type array|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
310
			if ( !$h->validateParam( $name, $value ) ) {
311
				$this->dieUsage( "Invalid value for {$p}urlparam ($name=$value)", 'urlparam' );
312
			}
313
		}
314
315
		$finalParams = $thumbParams + $paramList;
316
		$this->checkParameterNormalise( $image, $finalParams );
317
		return $finalParams;
318
	}
319
320
	/**
321
	 * Verify that the final image parameters can be normalised.
322
	 *
323
	 * This doesn't use the normalised parameters, since $file->transform
324
	 * expects the pre-normalised parameters, but doing the normalisation
325
	 * allows us to catch certain error conditions early (such as missing
326
	 * required parameter).
327
	 *
328
	 * @param File $image
329
	 * @param array $finalParams List of parameters to transform image with
330
	 */
331
	protected function checkParameterNormalise( $image, $finalParams ) {
332
		$h = $image->getHandler();
333
		if ( !$h ) {
334
			return;
335
		}
336
		// Note: normaliseParams modifies the array in place, but we aren't interested
337
		// in the actual normalised version, only if we can actually normalise them,
338
		// so we use the functions scope to throw away the normalisations.
339
		if ( !$h->normaliseParams( $image, $finalParams ) ) {
340
			$this->dieUsage( 'Could not normalise image parameters for ' .
341
				$image->getName(), 'urlparamnormal' );
342
		}
343
	}
344
345
	/**
346
	 * Get result information for an image revision
347
	 *
348
	 * @param File $file
349
	 * @param array $prop Array of properties to get (in the keys)
350
	 * @param ApiResult $result
351
	 * @param array $thumbParams Containing 'width' and 'height' items, or null
352
	 * @param array|bool|string $opts Options for data fetching.
353
	 *   This is an array consisting of the keys:
354
	 *    'version': The metadata version for the metadata option
355
	 *    'language': The language for extmetadata property
356
	 *    'multilang': Return all translations in extmetadata property
357
	 *    'revdelUser': User to use when checking whether to show revision-deleted fields.
358
	 * @return array Result array
359
	 */
360
	public static function getInfo( $file, $prop, $result, $thumbParams = null, $opts = false ) {
361
		global $wgContLang;
362
363
		$anyHidden = false;
364
365
		if ( !$opts || is_string( $opts ) ) {
366
			$opts = [
367
				'version' => $opts ?: 'latest',
368
				'language' => $wgContLang,
369
				'multilang' => false,
370
				'extmetadatafilter' => [],
371
				'revdelUser' => null,
372
			];
373
		}
374
		$version = $opts['version'];
375
		$vals = [
376
			ApiResult::META_TYPE => 'assoc',
377
		];
378
		// Timestamp is shown even if the file is revdelete'd in interface
379
		// so do same here.
380
		if ( isset( $prop['timestamp'] ) ) {
381
			$vals['timestamp'] = wfTimestamp( TS_ISO_8601, $file->getTimestamp() );
382
		}
383
384
		// Handle external callers who don't pass revdelUser
385
		if ( isset( $opts['revdelUser'] ) && $opts['revdelUser'] ) {
386
			$revdelUser = $opts['revdelUser'];
387
			$canShowField = function ( $field ) use ( $file, $revdelUser ) {
388
				return $file->userCan( $field, $revdelUser );
389
			};
390
		} else {
391
			$canShowField = function ( $field ) use ( $file ) {
392
				return !$file->isDeleted( $field );
393
			};
394
		}
395
396
		$user = isset( $prop['user'] );
397
		$userid = isset( $prop['userid'] );
398
399
		if ( $user || $userid ) {
400
			if ( $file->isDeleted( File::DELETED_USER ) ) {
401
				$vals['userhidden'] = true;
402
				$anyHidden = true;
403
			}
404
			if ( $canShowField( File::DELETED_USER ) ) {
405
				if ( $user ) {
406
					$vals['user'] = $file->getUser();
407
				}
408
				if ( $userid ) {
409
					$vals['userid'] = $file->getUser( 'id' );
410
				}
411
				if ( !$file->getUser( 'id' ) ) {
412
					$vals['anon'] = true;
413
				}
414
			}
415
		}
416
417
		// This is shown even if the file is revdelete'd in interface
418
		// so do same here.
419
		if ( isset( $prop['size'] ) || isset( $prop['dimensions'] ) ) {
420
			$vals['size'] = intval( $file->getSize() );
421
			$vals['width'] = intval( $file->getWidth() );
422
			$vals['height'] = intval( $file->getHeight() );
423
424
			$pageCount = $file->pageCount();
425
			if ( $pageCount !== false ) {
426
				$vals['pagecount'] = $pageCount;
427
			}
428
429
			// length as in how many seconds long a video is.
430
			$length = $file->getLength();
431
			if ( $length ) {
432
				// Call it duration, because "length" can be ambiguous.
433
				$vals['duration'] = (float)$length;
434
			}
435
		}
436
437
		$pcomment = isset( $prop['parsedcomment'] );
438
		$comment = isset( $prop['comment'] );
439
440
		if ( $pcomment || $comment ) {
441
			if ( $file->isDeleted( File::DELETED_COMMENT ) ) {
442
				$vals['commenthidden'] = true;
443
				$anyHidden = true;
444
			}
445
			if ( $canShowField( File::DELETED_COMMENT ) ) {
446
				if ( $pcomment ) {
447
					$vals['parsedcomment'] = Linker::formatComment(
448
						$file->getDescription( File::RAW ), $file->getTitle() );
449
				}
450
				if ( $comment ) {
451
					$vals['comment'] = $file->getDescription( File::RAW );
452
				}
453
			}
454
		}
455
456
		$canonicaltitle = isset( $prop['canonicaltitle'] );
457
		$url = isset( $prop['url'] );
458
		$sha1 = isset( $prop['sha1'] );
459
		$meta = isset( $prop['metadata'] );
460
		$extmetadata = isset( $prop['extmetadata'] );
461
		$commonmeta = isset( $prop['commonmetadata'] );
462
		$mime = isset( $prop['mime'] );
463
		$mediatype = isset( $prop['mediatype'] );
464
		$archive = isset( $prop['archivename'] );
465
		$bitdepth = isset( $prop['bitdepth'] );
466
		$uploadwarning = isset( $prop['uploadwarning'] );
467
468
		if ( $uploadwarning ) {
469
			$vals['html'] = SpecialUpload::getExistsWarning( UploadBase::getExistsWarning( $file ) );
0 ignored issues
show
Security Bug introduced by
It seems like \UploadBase::getExistsWarning($file) targeting UploadBase::getExistsWarning() can also be of type false; however, SpecialUpload::getExistsWarning() does only seem to accept array, did you maybe forget to handle an error condition?
Loading history...
470
		}
471
472
		if ( $file->isDeleted( File::DELETED_FILE ) ) {
473
			$vals['filehidden'] = true;
474
			$anyHidden = true;
475
		}
476
477
		if ( $anyHidden && $file->isDeleted( File::DELETED_RESTRICTED ) ) {
478
			$vals['suppressed'] = true;
479
		}
480
481
		if ( !$canShowField( File::DELETED_FILE ) ) {
482
			// Early return, tidier than indenting all following things one level
483
			return $vals;
484
		}
485
486
		if ( $canonicaltitle ) {
487
			$vals['canonicaltitle'] = $file->getTitle()->getPrefixedText();
488
		}
489
490
		if ( $url ) {
491
			if ( !is_null( $thumbParams ) ) {
492
				$mto = $file->transform( $thumbParams );
493
				self::$transformCount++;
494
				if ( $mto && !$mto->isError() ) {
495
					$vals['thumburl'] = wfExpandUrl( $mto->getUrl(), PROTO_CURRENT );
496
497
					// bug 23834 - If the URL's are the same, we haven't resized it, so shouldn't give the wanted
498
					// thumbnail sizes for the thumbnail actual size
499
					if ( $mto->getUrl() !== $file->getUrl() ) {
500
						$vals['thumbwidth'] = intval( $mto->getWidth() );
501
						$vals['thumbheight'] = intval( $mto->getHeight() );
502
					} else {
503
						$vals['thumbwidth'] = intval( $file->getWidth() );
504
						$vals['thumbheight'] = intval( $file->getHeight() );
505
					}
506
507
					if ( isset( $prop['thumbmime'] ) && $file->getHandler() ) {
508
						list( , $mime ) = $file->getHandler()->getThumbType(
509
							$mto->getExtension(), $file->getMimeType(), $thumbParams );
510
						$vals['thumbmime'] = $mime;
511
					}
512
				} elseif ( $mto && $mto->isError() ) {
513
					$vals['thumberror'] = $mto->toText();
514
				}
515
			}
516
			$vals['url'] = wfExpandUrl( $file->getFullUrl(), PROTO_CURRENT );
0 ignored issues
show
Security Bug introduced by
It seems like $file->getFullUrl() targeting File::getFullUrl() can also be of type false; however, wfExpandUrl() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
517
			$vals['descriptionurl'] = wfExpandUrl( $file->getDescriptionUrl(), PROTO_CURRENT );
518
519
			$shortDescriptionUrl = $file->getDescriptionShortUrl();
520
			if ( $shortDescriptionUrl !== null ) {
521
				$vals['descriptionshorturl'] = wfExpandUrl( $shortDescriptionUrl, PROTO_CURRENT );
522
			}
523
		}
524
525
		if ( $sha1 ) {
526
			$vals['sha1'] = Wikimedia\base_convert( $file->getSha1(), 36, 16, 40 );
527
		}
528
529
		if ( $meta ) {
530
			MediaWiki\suppressWarnings();
531
			$metadata = unserialize( $file->getMetadata() );
532
			MediaWiki\restoreWarnings();
533
			if ( $metadata && $version !== 'latest' ) {
534
				$metadata = $file->convertMetadataVersion( $metadata, $version );
535
			}
536
			$vals['metadata'] = $metadata ? static::processMetaData( $metadata, $result ) : null;
537
		}
538
		if ( $commonmeta ) {
539
			$metaArray = $file->getCommonMetaArray();
540
			$vals['commonmetadata'] = $metaArray ? static::processMetaData( $metaArray, $result ) : [];
0 ignored issues
show
Bug introduced by
It seems like $metaArray defined by $file->getCommonMetaArray() on line 539 can also be of type boolean; however, ApiQueryImageInfo::processMetaData() does only seem to accept array, 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...
541
		}
542
543
		if ( $extmetadata ) {
544
			// Note, this should return an array where all the keys
545
			// start with a letter, and all the values are strings.
546
			// Thus there should be no issue with format=xml.
547
			$format = new FormatMetadata;
548
			$format->setSingleLanguage( !$opts['multilang'] );
549
			$format->getContext()->setLanguage( $opts['language'] );
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface IContextSource as the method setLanguage() does only exist in the following implementations of said interface: DerivativeContext, RequestContext.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
550
			$extmetaArray = $format->fetchExtendedMetadata( $file );
551
			if ( $opts['extmetadatafilter'] ) {
552
				$extmetaArray = array_intersect_key(
553
					$extmetaArray, array_flip( $opts['extmetadatafilter'] )
554
				);
555
			}
556
			$vals['extmetadata'] = $extmetaArray;
557
		}
558
559
		if ( $mime ) {
560
			$vals['mime'] = $file->getMimeType();
561
		}
562
563
		if ( $mediatype ) {
564
			$vals['mediatype'] = $file->getMediaType();
565
		}
566
567
		if ( $archive && $file->isOld() ) {
568
			$vals['archivename'] = $file->getArchiveName();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class File as the method getArchiveName() does only exist in the following sub-classes of File: OldLocalFile. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
569
		}
570
571
		if ( $bitdepth ) {
572
			$vals['bitdepth'] = $file->getBitDepth();
573
		}
574
575
		return $vals;
576
	}
577
578
	/**
579
	 * Get the count of image transformations performed
580
	 *
581
	 * If this is >= TRANSFORM_LIMIT, you should probably stop processing images.
582
	 *
583
	 * @return int Count
584
	 */
585
	static function getTransformCount() {
586
		return self::$transformCount;
587
	}
588
589
	/**
590
	 *
591
	 * @param array $metadata
592
	 * @param ApiResult $result
593
	 * @return array
594
	 */
595
	public static function processMetaData( $metadata, $result ) {
596
		$retval = [];
597
		if ( is_array( $metadata ) ) {
598
			foreach ( $metadata as $key => $value ) {
599
				$r = [
600
					'name' => $key,
601
					ApiResult::META_BC_BOOLS => [ 'value' ],
602
				];
603
				if ( is_array( $value ) ) {
604
					$r['value'] = static::processMetaData( $value, $result );
605
				} else {
606
					$r['value'] = $value;
607
				}
608
				$retval[] = $r;
609
			}
610
		}
611
		ApiResult::setIndexedTagName( $retval, 'metadata' );
612
613
		return $retval;
614
	}
615
616
	public function getCacheMode( $params ) {
617
		if ( $this->userCanSeeRevDel() ) {
618
			return 'private';
619
		}
620
621
		return 'public';
622
	}
623
624
	/**
625
	 * @param File $img
626
	 * @param null|string $start
627
	 * @return string
628
	 */
629
	protected function getContinueStr( $img, $start = null ) {
630
		if ( $start === null ) {
631
			$start = $img->getTimestamp();
632
		}
633
634
		return $img->getOriginalTitle()->getDBkey() . '|' . $start;
635
	}
636
637
	public function getAllowedParams() {
638
		global $wgContLang;
639
640
		return [
641
			'prop' => [
642
				ApiBase::PARAM_ISMULTI => true,
643
				ApiBase::PARAM_DFLT => 'timestamp|user',
644
				ApiBase::PARAM_TYPE => static::getPropertyNames(),
645
				ApiBase::PARAM_HELP_MSG_PER_VALUE => static::getPropertyMessages(),
646
			],
647
			'limit' => [
648
				ApiBase::PARAM_TYPE => 'limit',
649
				ApiBase::PARAM_DFLT => 1,
650
				ApiBase::PARAM_MIN => 1,
651
				ApiBase::PARAM_MAX => ApiBase::LIMIT_BIG1,
652
				ApiBase::PARAM_MAX2 => ApiBase::LIMIT_BIG2
653
			],
654
			'start' => [
655
				ApiBase::PARAM_TYPE => 'timestamp'
656
			],
657
			'end' => [
658
				ApiBase::PARAM_TYPE => 'timestamp'
659
			],
660
			'urlwidth' => [
661
				ApiBase::PARAM_TYPE => 'integer',
662
				ApiBase::PARAM_DFLT => -1,
663
				ApiBase::PARAM_HELP_MSG => [
664
					'apihelp-query+imageinfo-param-urlwidth',
665
					ApiQueryImageInfo::TRANSFORM_LIMIT,
666
				],
667
			],
668
			'urlheight' => [
669
				ApiBase::PARAM_TYPE => 'integer',
670
				ApiBase::PARAM_DFLT => -1
671
			],
672
			'metadataversion' => [
673
				ApiBase::PARAM_TYPE => 'string',
674
				ApiBase::PARAM_DFLT => '1',
675
			],
676
			'extmetadatalanguage' => [
677
				ApiBase::PARAM_TYPE => 'string',
678
				ApiBase::PARAM_DFLT => $wgContLang->getCode(),
679
			],
680
			'extmetadatamultilang' => [
681
				ApiBase::PARAM_TYPE => 'boolean',
682
				ApiBase::PARAM_DFLT => false,
683
			],
684
			'extmetadatafilter' => [
685
				ApiBase::PARAM_TYPE => 'string',
686
				ApiBase::PARAM_ISMULTI => true,
687
			],
688
			'urlparam' => [
689
				ApiBase::PARAM_DFLT => '',
690
				ApiBase::PARAM_TYPE => 'string',
691
			],
692
			'continue' => [
693
				ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
694
			],
695
			'localonly' => false,
696
		];
697
	}
698
699
	/**
700
	 * Returns all possible parameters to iiprop
701
	 *
702
	 * @param array $filter List of properties to filter out
703
	 * @return array
704
	 */
705
	public static function getPropertyNames( $filter = [] ) {
706
		return array_keys( static::getPropertyMessages( $filter ) );
707
	}
708
709
	/**
710
	 * Returns messages for all possible parameters to iiprop
711
	 *
712
	 * @param array $filter List of properties to filter out
713
	 * @return array
714
	 */
715
	public static function getPropertyMessages( $filter = [] ) {
716
		return array_diff_key(
717
			[
718
				'timestamp' => 'apihelp-query+imageinfo-paramvalue-prop-timestamp',
719
				'user' => 'apihelp-query+imageinfo-paramvalue-prop-user',
720
				'userid' => 'apihelp-query+imageinfo-paramvalue-prop-userid',
721
				'comment' => 'apihelp-query+imageinfo-paramvalue-prop-comment',
722
				'parsedcomment' => 'apihelp-query+imageinfo-paramvalue-prop-parsedcomment',
723
				'canonicaltitle' => 'apihelp-query+imageinfo-paramvalue-prop-canonicaltitle',
724
				'url' => 'apihelp-query+imageinfo-paramvalue-prop-url',
725
				'size' => 'apihelp-query+imageinfo-paramvalue-prop-size',
726
				'dimensions' => 'apihelp-query+imageinfo-paramvalue-prop-dimensions',
727
				'sha1' => 'apihelp-query+imageinfo-paramvalue-prop-sha1',
728
				'mime' => 'apihelp-query+imageinfo-paramvalue-prop-mime',
729
				'thumbmime' => 'apihelp-query+imageinfo-paramvalue-prop-thumbmime',
730
				'mediatype' => 'apihelp-query+imageinfo-paramvalue-prop-mediatype',
731
				'metadata' => 'apihelp-query+imageinfo-paramvalue-prop-metadata',
732
				'commonmetadata' => 'apihelp-query+imageinfo-paramvalue-prop-commonmetadata',
733
				'extmetadata' => 'apihelp-query+imageinfo-paramvalue-prop-extmetadata',
734
				'archivename' => 'apihelp-query+imageinfo-paramvalue-prop-archivename',
735
				'bitdepth' => 'apihelp-query+imageinfo-paramvalue-prop-bitdepth',
736
				'uploadwarning' => 'apihelp-query+imageinfo-paramvalue-prop-uploadwarning',
737
			],
738
			array_flip( $filter )
739
		);
740
	}
741
742
	/**
743
	 * Returns array key value pairs of properties and their descriptions
744
	 *
745
	 * @deprecated since 1.25
746
	 * @param string $modulePrefix
747
	 * @return array
748
	 */
749
	private static function getProperties( $modulePrefix = '' ) {
750
		return [
751
			'timestamp' =>      ' timestamp     - Adds timestamp for the uploaded version',
752
			'user' =>           ' user          - Adds the user who uploaded the image version',
753
			'userid' =>         ' userid        - Add the user ID that uploaded the image version',
754
			'comment' =>        ' comment       - Comment on the version',
755
			'parsedcomment' =>  ' parsedcomment - Parse the comment on the version',
756
			'canonicaltitle' => ' canonicaltitle - Adds the canonical title of the image file',
757
			'url' =>            ' url           - Gives URL to the image and the description page',
758
			'size' =>           ' size          - Adds the size of the image in bytes, ' .
759
				'its height and its width. Page count and duration are added if applicable',
760
			'dimensions' =>     ' dimensions    - Alias for size', // B/C with Allimages
761
			'sha1' =>           ' sha1          - Adds SHA-1 hash for the image',
762
			'mime' =>           ' mime          - Adds MIME type of the image',
763
			'thumbmime' =>      ' thumbmime     - Adds MIME type of the image thumbnail' .
764
				' (requires url and param ' . $modulePrefix . 'urlwidth)',
765
			'mediatype' =>      ' mediatype     - Adds the media type of the image',
766
			'metadata' =>       ' metadata      - Lists Exif metadata for the version of the image',
767
			'commonmetadata' => ' commonmetadata - Lists file format generic metadata ' .
768
				'for the version of the image',
769
			'extmetadata' =>    ' extmetadata   - Lists formatted metadata combined ' .
770
				'from multiple sources. Results are HTML formatted.',
771
			'archivename' =>    ' archivename   - Adds the file name of the archive ' .
772
				'version for non-latest versions',
773
			'bitdepth' =>       ' bitdepth      - Adds the bit depth of the version',
774
			'uploadwarning' =>  ' uploadwarning - Used by the Special:Upload page to ' .
775
				'get information about an existing file. Not intended for use outside MediaWiki core',
776
		];
777
	}
778
779
	/**
780
	 * Returns the descriptions for the properties provided by getPropertyNames()
781
	 *
782
	 * @deprecated since 1.25
783
	 * @param array $filter List of properties to filter out
784
	 * @param string $modulePrefix
785
	 * @return array
786
	 */
787
	public static function getPropertyDescriptions( $filter = [], $modulePrefix = '' ) {
788
		return array_merge(
789
			[ 'What image information to get:' ],
790
			array_values( array_diff_key( static::getProperties( $modulePrefix ), array_flip( $filter ) ) )
0 ignored issues
show
Bug introduced by
Since getProperties() is declared private, calling it with static will lead to errors in possible sub-classes. You can either use self, or increase the visibility of getProperties() to at least protected.

Let’s assume you have a class which uses late-static binding:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
}

public static function getSomeVariable()
{
    return static::getTemperature();
}

}

The code above will run fine in your PHP runtime. However, if you now create a sub-class and call the getSomeVariable() on that sub-class, you will receive a runtime error:

class YourSubClass extends YourClass {
      private static function getTemperature() {
        return "-182 °C";
    }
}

print YourSubClass::getSomeVariable(); // Will cause an access error.

In the case above, it makes sense to update SomeClass to use self instead:

class YourClass
{
    private static function getTemperature() {
        return "3422 °C";
    }

    public static function getSomeVariable()
    {
        return self::getTemperature();
    }
}
Loading history...
791
		);
792
	}
793
794
	protected function getExamplesMessages() {
795
		return [
796
			'action=query&titles=File:Albert%20Einstein%20Head.jpg&prop=imageinfo'
797
				=> 'apihelp-query+imageinfo-example-simple',
798
			'action=query&titles=File:Test.jpg&prop=imageinfo&iilimit=50&' .
799
				'iiend=2007-12-31T23:59:59Z&iiprop=timestamp|user|url'
800
				=> 'apihelp-query+imageinfo-example-dated',
801
		];
802
	}
803
804
	public function getHelpUrls() {
805
		return 'https://www.mediawiki.org/wiki/API:Imageinfo';
806
	}
807
}
808