LocalFile   F
last analyzed

Complexity

Total Complexity 220

Size/Duplication

Total Lines 2020
Duplicated Lines 4.9 %

Coupling/Cohesion

Components 1
Dependencies 44

Importance

Changes 0
Metric Value
dl 99
loc 2020
rs 0.5217
c 0
b 0
f 0
wmc 220
lcom 1
cbo 44

65 Methods

Rating   Name   Duplication   Size   Complexity  
A newFromTitle() 0 3 1
A newFromRow() 7 7 1
A newFromKey() 15 15 3
A selectFields() 0 18 1
A __construct() 0 12 1
A publish() 0 3 1
A getCacheKey() 0 3 1
C loadFromCache() 0 60 11
A invalidateCache() 0 13 2
A loadFromFile() 0 4 1
A getCacheFields() 7 20 4
A getLazyCacheFields() 7 18 4
A loadFromDB() 0 20 3
A loadExtraFromDB() 0 19 4
A loadFieldsWithTimestamp() 0 22 3
A unprefixRow() 0 16 3
B decodeRow() 0 29 4
A loadFromRow() 0 13 2
B load() 0 14 5
D maybeUpgradeRow() 0 35 10
A getUpgraded() 0 3 1
B upgradeRow() 0 44 3
B setProps() 0 19 5
A isMissing() 0 8 2
A getWidth() 20 20 4
A getHeight() 20 20 4
A getUser() 0 9 2
A getDescriptionShortUrl() 6 11 3
A getMetadata() 0 4 1
A getBitDepth() 0 5 1
A getSize() 0 5 1
A getMimeType() 0 5 1
A getMediaType() 0 5 1
A exists() 0 5 1
A getThumbnails() 0 19 4
A purgeMetadataCache() 0 3 1
A purgeCache() 0 13 1
A purgeOldThumbnails() 0 18 2
B purgeThumbnails() 0 27 4
B prerenderThumbnails() 0 21 5
B purgeThumbList() 0 23 4
F getHistory() 0 41 9
B nextHistoryLine() 0 34 4
A resetHistory() 0 7 2
B upload() 5 55 9
A recordUpload() 0 19 4
F recordUpload2() 0 307 20
D publishTo() 0 43 9
B move() 0 45 4
B delete() 0 42 5
B deleteOld() 0 24 3
B restore() 0 24 4
A getDescriptionUrl() 0 3 1
A getDescriptionText() 0 13 3
B getDescription() 12 12 5
A getTimestamp() 0 5 1
A getDescriptionTouched() 0 15 3
B getSha1() 0 21 5
A isCacheable() 0 7 2
A acquireFileLock() 0 5 1
A releaseFileLock() 0 5 1
B lock() 0 36 4
A unlock() 0 10 3
A readOnlyFatalStatus() 0 4 1
A __destruct() 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 LocalFile 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 LocalFile, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Local file in the wiki's own database.
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 FileAbstraction
22
 */
23
24
use \MediaWiki\Logger\LoggerFactory;
25
26
/**
27
 * Class to represent a local file in the wiki's own database
28
 *
29
 * Provides methods to retrieve paths (physical, logical, URL),
30
 * to generate image thumbnails or for uploading.
31
 *
32
 * Note that only the repo object knows what its file class is called. You should
33
 * never name a file class explictly outside of the repo class. Instead use the
34
 * repo's factory functions to generate file objects, for example:
35
 *
36
 * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
37
 *
38
 * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
39
 * in most cases.
40
 *
41
 * @ingroup FileAbstraction
42
 */
43
class LocalFile extends File {
44
	const VERSION = 10; // cache version
45
46
	const CACHE_FIELD_MAX_LEN = 1000;
47
48
	/** @var bool Does the file exist on disk? (loadFromXxx) */
49
	protected $fileExists;
50
51
	/** @var int Image width */
52
	protected $width;
53
54
	/** @var int Image height */
55
	protected $height;
56
57
	/** @var int Returned by getimagesize (loadFromXxx) */
58
	protected $bits;
59
60
	/** @var string MEDIATYPE_xxx (bitmap, drawing, audio...) */
61
	protected $media_type;
62
63
	/** @var string MIME type, determined by MimeMagic::guessMimeType */
64
	protected $mime;
65
66
	/** @var int Size in bytes (loadFromXxx) */
67
	protected $size;
68
69
	/** @var string Handler-specific metadata */
70
	protected $metadata;
71
72
	/** @var string SHA-1 base 36 content hash */
73
	protected $sha1;
74
75
	/** @var bool Whether or not core data has been loaded from the database (loadFromXxx) */
76
	protected $dataLoaded;
77
78
	/** @var bool Whether or not lazy-loaded data has been loaded from the database */
79
	protected $extraDataLoaded;
80
81
	/** @var int Bitfield akin to rev_deleted */
82
	protected $deleted;
83
84
	/** @var string */
85
	protected $repoClass = 'LocalRepo';
86
87
	/** @var int Number of line to return by nextHistoryLine() (constructor) */
88
	private $historyLine;
89
90
	/** @var int Result of the query for the file's history (nextHistoryLine) */
91
	private $historyRes;
92
93
	/** @var string Major MIME type */
94
	private $major_mime;
95
96
	/** @var string Minor MIME type */
97
	private $minor_mime;
98
99
	/** @var string Upload timestamp */
100
	private $timestamp;
101
102
	/** @var int User ID of uploader */
103
	private $user;
104
105
	/** @var string User name of uploader */
106
	private $user_text;
107
108
	/** @var string Description of current revision of the file */
109
	private $description;
110
111
	/** @var string TS_MW timestamp of the last change of the file description */
112
	private $descriptionTouched;
113
114
	/** @var bool Whether the row was upgraded on load */
115
	private $upgraded;
116
117
	/** @var bool Whether the row was scheduled to upgrade on load */
118
	private $upgrading;
119
120
	/** @var bool True if the image row is locked */
121
	private $locked;
122
123
	/** @var bool True if the image row is locked with a lock initiated transaction */
124
	private $lockedOwnTrx;
125
126
	/** @var bool True if file is not present in file system. Not to be cached in memcached */
127
	private $missing;
128
129
	// @note: higher than IDBAccessObject constants
130
	const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
131
132
	const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
133
134
	/**
135
	 * Create a LocalFile from a title
136
	 * Do not call this except from inside a repo class.
137
	 *
138
	 * Note: $unused param is only here to avoid an E_STRICT
139
	 *
140
	 * @param Title $title
141
	 * @param FileRepo $repo
142
	 * @param null $unused
143
	 *
144
	 * @return LocalFile
145
	 */
146
	static function newFromTitle( $title, $repo, $unused = null ) {
147
		return new self( $title, $repo );
148
	}
149
150
	/**
151
	 * Create a LocalFile from a title
152
	 * Do not call this except from inside a repo class.
153
	 *
154
	 * @param stdClass $row
155
	 * @param FileRepo $repo
156
	 *
157
	 * @return LocalFile
158
	 */
159 View Code Duplication
	static function newFromRow( $row, $repo ) {
160
		$title = Title::makeTitle( NS_FILE, $row->img_name );
161
		$file = new self( $title, $repo );
162
		$file->loadFromRow( $row );
163
164
		return $file;
165
	}
166
167
	/**
168
	 * Create a LocalFile from a SHA-1 key
169
	 * Do not call this except from inside a repo class.
170
	 *
171
	 * @param string $sha1 Base-36 SHA-1
172
	 * @param LocalRepo $repo
173
	 * @param string|bool $timestamp MW_timestamp (optional)
174
	 * @return bool|LocalFile
175
	 */
176 View Code Duplication
	static function newFromKey( $sha1, $repo, $timestamp = false ) {
177
		$dbr = $repo->getSlaveDB();
178
179
		$conds = [ 'img_sha1' => $sha1 ];
180
		if ( $timestamp ) {
181
			$conds['img_timestamp'] = $dbr->timestamp( $timestamp );
182
		}
183
184
		$row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
185
		if ( $row ) {
186
			return self::newFromRow( $row, $repo );
187
		} else {
188
			return false;
189
		}
190
	}
191
192
	/**
193
	 * Fields in the image table
194
	 * @return array
195
	 */
196
	static function selectFields() {
197
		return [
198
			'img_name',
199
			'img_size',
200
			'img_width',
201
			'img_height',
202
			'img_metadata',
203
			'img_bits',
204
			'img_media_type',
205
			'img_major_mime',
206
			'img_minor_mime',
207
			'img_description',
208
			'img_user',
209
			'img_user_text',
210
			'img_timestamp',
211
			'img_sha1',
212
		];
213
	}
214
215
	/**
216
	 * Constructor.
217
	 * Do not call this except from inside a repo class.
218
	 * @param Title $title
219
	 * @param FileRepo $repo
220
	 */
221
	function __construct( $title, $repo ) {
222
		parent::__construct( $title, $repo );
223
224
		$this->metadata = '';
225
		$this->historyLine = 0;
226
		$this->historyRes = null;
227
		$this->dataLoaded = false;
228
		$this->extraDataLoaded = false;
229
230
		$this->assertRepoDefined();
231
		$this->assertTitleDefined();
232
	}
233
234
	/**
235
	 * Get the memcached key for the main data for this file, or false if
236
	 * there is no access to the shared cache.
237
	 * @return string|bool
238
	 */
239
	function getCacheKey() {
240
		return $this->repo->getSharedCacheKey( 'file', sha1( $this->getName() ) );
241
	}
242
243
	/**
244
	 * Try to load file metadata from memcached, falling back to the database
245
	 */
246
	private function loadFromCache() {
247
		$this->dataLoaded = false;
248
		$this->extraDataLoaded = false;
249
250
		$key = $this->getCacheKey();
251
		if ( !$key ) {
252
			$this->loadFromDB( self::READ_NORMAL );
253
254
			return;
255
		}
256
257
		$cache = ObjectCache::getMainWANInstance();
258
		$cachedValues = $cache->getWithSetCallback(
259
			$key,
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 250 can also be of type boolean; however, WANObjectCache::getWithSetCallback() 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...
260
			$cache::TTL_WEEK,
261
			function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
262
				$setOpts += Database::getCacheSetOptions( $this->repo->getSlaveDB() );
0 ignored issues
show
Bug introduced by
The method getSlaveDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
263
264
				$this->loadFromDB( self::READ_NORMAL );
265
266
				$fields = $this->getCacheFields( '' );
267
				$cacheVal['fileExists'] = $this->fileExists;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$cacheVal was never initialized. Although not strictly required by PHP, it is generally a good practice to add $cacheVal = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
268
				if ( $this->fileExists ) {
269
					foreach ( $fields as $field ) {
270
						$cacheVal[$field] = $this->$field;
271
					}
272
				}
273
				// Strip off excessive entries from the subset of fields that can become large.
274
				// If the cache value gets to large it will not fit in memcached and nothing will
275
				// get cached at all, causing master queries for any file access.
276
				foreach ( $this->getLazyCacheFields( '' ) as $field ) {
277
					if ( isset( $cacheVal[$field] )
278
						&& strlen( $cacheVal[$field] ) > 100 * 1024
279
					) {
280
						unset( $cacheVal[$field] ); // don't let the value get too big
281
					}
282
				}
283
284
				if ( $this->fileExists ) {
285
					$ttl = $cache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->timestamp ), $ttl );
286
				} else {
287
					$ttl = $cache::TTL_DAY;
288
				}
289
290
				return $cacheVal;
291
			},
292
			[ 'version' => self::VERSION ]
293
		);
294
295
		$this->fileExists = $cachedValues['fileExists'];
296
		if ( $this->fileExists ) {
297
			$this->setProps( $cachedValues );
298
		}
299
300
		$this->dataLoaded = true;
301
		$this->extraDataLoaded = true;
302
		foreach ( $this->getLazyCacheFields( '' ) as $field ) {
303
			$this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
304
		}
305
	}
306
307
	/**
308
	 * Purge the file object/metadata cache
309
	 */
310
	public function invalidateCache() {
311
		$key = $this->getCacheKey();
312
		if ( !$key ) {
313
			return;
314
		}
315
316
		$this->repo->getMasterDB()->onTransactionPreCommitOrIdle(
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
317
			function () use ( $key ) {
318
				ObjectCache::getMainWANInstance()->delete( $key );
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 311 can also be of type boolean; however, WANObjectCache::delete() 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...
319
			},
320
			__METHOD__
321
		);
322
	}
323
324
	/**
325
	 * Load metadata from the file itself
326
	 */
327
	function loadFromFile() {
328
		$props = $this->repo->getFileProps( $this->getVirtualUrl() );
329
		$this->setProps( $props );
330
	}
331
332
	/**
333
	 * @param string $prefix
334
	 * @return array
335
	 */
336
	function getCacheFields( $prefix = 'img_' ) {
337
		static $fields = [ 'size', 'width', 'height', 'bits', 'media_type',
338
			'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
339
			'user_text', 'description' ];
340
		static $results = [];
341
342
		if ( $prefix == '' ) {
343
			return $fields;
344
		}
345
346 View Code Duplication
		if ( !isset( $results[$prefix] ) ) {
347
			$prefixedFields = [];
348
			foreach ( $fields as $field ) {
349
				$prefixedFields[] = $prefix . $field;
350
			}
351
			$results[$prefix] = $prefixedFields;
352
		}
353
354
		return $results[$prefix];
355
	}
356
357
	/**
358
	 * @param string $prefix
359
	 * @return array
360
	 */
361
	function getLazyCacheFields( $prefix = 'img_' ) {
362
		static $fields = [ 'metadata' ];
363
		static $results = [];
364
365
		if ( $prefix == '' ) {
366
			return $fields;
367
		}
368
369 View Code Duplication
		if ( !isset( $results[$prefix] ) ) {
370
			$prefixedFields = [];
371
			foreach ( $fields as $field ) {
372
				$prefixedFields[] = $prefix . $field;
373
			}
374
			$results[$prefix] = $prefixedFields;
375
		}
376
377
		return $results[$prefix];
378
	}
379
380
	/**
381
	 * Load file metadata from the DB
382
	 * @param int $flags
383
	 */
384
	function loadFromDB( $flags = 0 ) {
385
		$fname = get_class( $this ) . '::' . __FUNCTION__;
386
387
		# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
388
		$this->dataLoaded = true;
389
		$this->extraDataLoaded = true;
390
391
		$dbr = ( $flags & self::READ_LATEST )
392
			? $this->repo->getMasterDB()
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
393
			: $this->repo->getSlaveDB();
0 ignored issues
show
Bug introduced by
The method getSlaveDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
394
395
		$row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
396
			[ 'img_name' => $this->getName() ], $fname );
397
398
		if ( $row ) {
399
			$this->loadFromRow( $row );
400
		} else {
401
			$this->fileExists = false;
402
		}
403
	}
404
405
	/**
406
	 * Load lazy file metadata from the DB.
407
	 * This covers fields that are sometimes not cached.
408
	 */
409
	protected function loadExtraFromDB() {
410
		$fname = get_class( $this ) . '::' . __FUNCTION__;
411
412
		# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
413
		$this->extraDataLoaded = true;
414
415
		$fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getSlaveDB(), $fname );
0 ignored issues
show
Bug introduced by
The method getSlaveDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
416
		if ( !$fieldMap ) {
417
			$fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getMasterDB(), $fname );
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
418
		}
419
420
		if ( $fieldMap ) {
421
			foreach ( $fieldMap as $name => $value ) {
422
				$this->$name = $value;
423
			}
424
		} else {
425
			throw new MWException( "Could not find data for image '{$this->getName()}'." );
426
		}
427
	}
428
429
	/**
430
	 * @param IDatabase $dbr
431
	 * @param string $fname
432
	 * @return array|bool
433
	 */
434
	private function loadFieldsWithTimestamp( $dbr, $fname ) {
435
		$fieldMap = false;
436
437
		$row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ), [
438
				'img_name' => $this->getName(),
439
				'img_timestamp' => $dbr->timestamp( $this->getTimestamp() )
440
			], $fname );
441
		if ( $row ) {
442
			$fieldMap = $this->unprefixRow( $row, 'img_' );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $dbr->selectRow('image',...tTimestamp())), $fname) on line 437 can also be of type boolean; however, LocalFile::unprefixRow() does only seem to accept array|object, 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...
443
		} else {
444
			# File may have been uploaded over in the meantime; check the old versions
445
			$row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ), [
446
					'oi_name' => $this->getName(),
447
					'oi_timestamp' => $dbr->timestamp( $this->getTimestamp() )
448
				], $fname );
449
			if ( $row ) {
450
				$fieldMap = $this->unprefixRow( $row, 'oi_' );
451
			}
452
		}
453
454
		return $fieldMap;
455
	}
456
457
	/**
458
	 * @param array|object $row
459
	 * @param string $prefix
460
	 * @throws MWException
461
	 * @return array
462
	 */
463
	protected function unprefixRow( $row, $prefix = 'img_' ) {
464
		$array = (array)$row;
465
		$prefixLength = strlen( $prefix );
466
467
		// Sanity check prefix once
468
		if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
469
			throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
470
		}
471
472
		$decoded = [];
473
		foreach ( $array as $name => $value ) {
474
			$decoded[substr( $name, $prefixLength )] = $value;
475
		}
476
477
		return $decoded;
478
	}
479
480
	/**
481
	 * Decode a row from the database (either object or array) to an array
482
	 * with timestamps and MIME types decoded, and the field prefix removed.
483
	 * @param object $row
484
	 * @param string $prefix
485
	 * @throws MWException
486
	 * @return array
487
	 */
488
	function decodeRow( $row, $prefix = 'img_' ) {
489
		$decoded = $this->unprefixRow( $row, $prefix );
490
491
		$decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
492
493
		$decoded['metadata'] = $this->repo->getSlaveDB()->decodeBlob( $decoded['metadata'] );
0 ignored issues
show
Bug introduced by
The method getSlaveDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
494
495
		if ( empty( $decoded['major_mime'] ) ) {
496
			$decoded['mime'] = 'unknown/unknown';
497
		} else {
498
			if ( !$decoded['minor_mime'] ) {
499
				$decoded['minor_mime'] = 'unknown';
500
			}
501
			$decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
502
		}
503
504
		// Trim zero padding from char/binary field
505
		$decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
506
507
		// Normalize some fields to integer type, per their database definition.
508
		// Use unary + so that overflows will be upgraded to double instead of
509
		// being trucated as with intval(). This is important to allow >2GB
510
		// files on 32-bit systems.
511
		foreach ( [ 'size', 'width', 'height', 'bits' ] as $field ) {
512
			$decoded[$field] = +$decoded[$field];
513
		}
514
515
		return $decoded;
516
	}
517
518
	/**
519
	 * Load file metadata from a DB result row
520
	 *
521
	 * @param object $row
522
	 * @param string $prefix
523
	 */
524
	function loadFromRow( $row, $prefix = 'img_' ) {
525
		$this->dataLoaded = true;
526
		$this->extraDataLoaded = true;
527
528
		$array = $this->decodeRow( $row, $prefix );
529
530
		foreach ( $array as $name => $value ) {
531
			$this->$name = $value;
532
		}
533
534
		$this->fileExists = true;
535
		$this->maybeUpgradeRow();
536
	}
537
538
	/**
539
	 * Load file metadata from cache or DB, unless already loaded
540
	 * @param int $flags
541
	 */
542
	function load( $flags = 0 ) {
543
		if ( !$this->dataLoaded ) {
544
			if ( $flags & self::READ_LATEST ) {
545
				$this->loadFromDB( $flags );
546
			} else {
547
				$this->loadFromCache();
548
			}
549
		}
550
551
		if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
552
			// @note: loads on name/timestamp to reduce race condition problems
553
			$this->loadExtraFromDB();
554
		}
555
	}
556
557
	/**
558
	 * Upgrade a row if it needs it
559
	 */
560
	function maybeUpgradeRow() {
561
		global $wgUpdateCompatibleMetadata;
562
563
		if ( wfReadOnly() || $this->upgrading ) {
564
			return;
565
		}
566
567
		$upgrade = false;
568
		if ( is_null( $this->media_type ) || $this->mime == 'image/svg' ) {
569
			$upgrade = true;
570
		} else {
571
			$handler = $this->getHandler();
572
			if ( $handler ) {
573
				$validity = $handler->isMetadataValid( $this, $this->getMetadata() );
574
				if ( $validity === MediaHandler::METADATA_BAD ) {
575
					$upgrade = true;
576
				} elseif ( $validity === MediaHandler::METADATA_COMPATIBLE ) {
577
					$upgrade = $wgUpdateCompatibleMetadata;
578
				}
579
			}
580
		}
581
582
		if ( $upgrade ) {
583
			$this->upgrading = true;
584
			// Defer updates unless in auto-commit CLI mode
585
			DeferredUpdates::addCallableUpdate( function() {
586
				$this->upgrading = false; // avoid duplicate updates
587
				try {
588
					$this->upgradeRow();
589
				} catch ( LocalFileLockError $e ) {
590
					// let the other process handle it (or do it next time)
591
				}
592
			} );
593
		}
594
	}
595
596
	/**
597
	 * @return bool Whether upgradeRow() ran for this object
598
	 */
599
	function getUpgraded() {
600
		return $this->upgraded;
601
	}
602
603
	/**
604
	 * Fix assorted version-related problems with the image row by reloading it from the file
605
	 */
606
	function upgradeRow() {
607
		$this->lock(); // begin
608
609
		$this->loadFromFile();
610
611
		# Don't destroy file info of missing files
612
		if ( !$this->fileExists ) {
613
			$this->unlock();
614
			wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
615
616
			return;
617
		}
618
619
		$dbw = $this->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
620
		list( $major, $minor ) = self::splitMime( $this->mime );
621
622
		if ( wfReadOnly() ) {
623
			$this->unlock();
624
625
			return;
626
		}
627
		wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
628
629
		$dbw->update( 'image',
630
			[
631
				'img_size' => $this->size, // sanity
632
				'img_width' => $this->width,
633
				'img_height' => $this->height,
634
				'img_bits' => $this->bits,
635
				'img_media_type' => $this->media_type,
636
				'img_major_mime' => $major,
637
				'img_minor_mime' => $minor,
638
				'img_metadata' => $dbw->encodeBlob( $this->metadata ),
639
				'img_sha1' => $this->sha1,
640
			],
641
			[ 'img_name' => $this->getName() ],
642
			__METHOD__
643
		);
644
645
		$this->invalidateCache();
646
647
		$this->unlock(); // done
648
		$this->upgraded = true; // avoid rework/retries
649
	}
650
651
	/**
652
	 * Set properties in this object to be equal to those given in the
653
	 * associative array $info. Only cacheable fields can be set.
654
	 * All fields *must* be set in $info except for getLazyCacheFields().
655
	 *
656
	 * If 'mime' is given, it will be split into major_mime/minor_mime.
657
	 * If major_mime/minor_mime are given, $this->mime will also be set.
658
	 *
659
	 * @param array $info
660
	 */
661
	function setProps( $info ) {
662
		$this->dataLoaded = true;
663
		$fields = $this->getCacheFields( '' );
664
		$fields[] = 'fileExists';
665
666
		foreach ( $fields as $field ) {
667
			if ( isset( $info[$field] ) ) {
668
				$this->$field = $info[$field];
669
			}
670
		}
671
672
		// Fix up mime fields
673
		if ( isset( $info['major_mime'] ) ) {
674
			$this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
675
		} elseif ( isset( $info['mime'] ) ) {
676
			$this->mime = $info['mime'];
677
			list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
678
		}
679
	}
680
681
	/** splitMime inherited */
682
	/** getName inherited */
683
	/** getTitle inherited */
684
	/** getURL inherited */
685
	/** getViewURL inherited */
686
	/** getPath inherited */
687
	/** isVisible inherited */
688
689
	/**
690
	 * @return bool
691
	 */
692
	function isMissing() {
693
		if ( $this->missing === null ) {
694
			list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
695
			$this->missing = !$fileExists;
696
		}
697
698
		return $this->missing;
699
	}
700
701
	/**
702
	 * Return the width of the image
703
	 *
704
	 * @param int $page
705
	 * @return int
706
	 */
707 View Code Duplication
	public function getWidth( $page = 1 ) {
708
		$this->load();
709
710
		if ( $this->isMultipage() ) {
711
			$handler = $this->getHandler();
712
			if ( !$handler ) {
713
				return 0;
714
			}
715
			$dim = $handler->getPageDimensions( $this, $page );
716
			if ( $dim ) {
717
				return $dim['width'];
718
			} else {
719
				// For non-paged media, the false goes through an
720
				// intval, turning failure into 0, so do same here.
721
				return 0;
722
			}
723
		} else {
724
			return $this->width;
725
		}
726
	}
727
728
	/**
729
	 * Return the height of the image
730
	 *
731
	 * @param int $page
732
	 * @return int
733
	 */
734 View Code Duplication
	public function getHeight( $page = 1 ) {
735
		$this->load();
736
737
		if ( $this->isMultipage() ) {
738
			$handler = $this->getHandler();
739
			if ( !$handler ) {
740
				return 0;
741
			}
742
			$dim = $handler->getPageDimensions( $this, $page );
743
			if ( $dim ) {
744
				return $dim['height'];
745
			} else {
746
				// For non-paged media, the false goes through an
747
				// intval, turning failure into 0, so do same here.
748
				return 0;
749
			}
750
		} else {
751
			return $this->height;
752
		}
753
	}
754
755
	/**
756
	 * Returns ID or name of user who uploaded the file
757
	 *
758
	 * @param string $type 'text' or 'id'
759
	 * @return int|string
760
	 */
761
	function getUser( $type = 'text' ) {
762
		$this->load();
763
764
		if ( $type == 'text' ) {
765
			return $this->user_text;
766
		} else { // id
767
			return (int)$this->user;
768
		}
769
	}
770
771
	/**
772
	 * Get short description URL for a file based on the page ID.
773
	 *
774
	 * @return string|null
775
	 * @throws MWException
776
	 * @since 1.27
777
	 */
778
	public function getDescriptionShortUrl() {
779
		$pageId = $this->title->getArticleID();
780
781 View Code Duplication
		if ( $pageId !== null ) {
782
			$url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
783
			if ( $url !== false ) {
784
				return $url;
785
			}
786
		}
787
		return null;
788
	}
789
790
	/**
791
	 * Get handler-specific metadata
792
	 * @return string
793
	 */
794
	function getMetadata() {
795
		$this->load( self::LOAD_ALL ); // large metadata is loaded in another step
796
		return $this->metadata;
797
	}
798
799
	/**
800
	 * @return int
801
	 */
802
	function getBitDepth() {
803
		$this->load();
804
805
		return (int)$this->bits;
806
	}
807
808
	/**
809
	 * Returns the size of the image file, in bytes
810
	 * @return int
811
	 */
812
	public function getSize() {
813
		$this->load();
814
815
		return $this->size;
816
	}
817
818
	/**
819
	 * Returns the MIME type of the file.
820
	 * @return string
821
	 */
822
	function getMimeType() {
823
		$this->load();
824
825
		return $this->mime;
826
	}
827
828
	/**
829
	 * Returns the type of the media in the file.
830
	 * Use the value returned by this function with the MEDIATYPE_xxx constants.
831
	 * @return string
832
	 */
833
	function getMediaType() {
834
		$this->load();
835
836
		return $this->media_type;
837
	}
838
839
	/** canRender inherited */
840
	/** mustRender inherited */
841
	/** allowInlineDisplay inherited */
842
	/** isSafeFile inherited */
843
	/** isTrustedFile inherited */
844
845
	/**
846
	 * Returns true if the file exists on disk.
847
	 * @return bool Whether file exist on disk.
848
	 */
849
	public function exists() {
850
		$this->load();
851
852
		return $this->fileExists;
853
	}
854
855
	/** getTransformScript inherited */
856
	/** getUnscaledThumb inherited */
857
	/** thumbName inherited */
858
	/** createThumb inherited */
859
	/** transform inherited */
860
861
	/** getHandler inherited */
862
	/** iconThumb inherited */
863
	/** getLastError inherited */
864
865
	/**
866
	 * Get all thumbnail names previously generated for this file
867
	 * @param string|bool $archiveName Name of an archive file, default false
868
	 * @return array First element is the base dir, then files in that base dir.
869
	 */
870
	function getThumbnails( $archiveName = false ) {
871
		if ( $archiveName ) {
872
			$dir = $this->getArchiveThumbPath( $archiveName );
0 ignored issues
show
Bug introduced by
It seems like $archiveName defined by parameter $archiveName on line 870 can also be of type boolean; however, File::getArchiveThumbPath() does only seem to accept string, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and 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...
873
		} else {
874
			$dir = $this->getThumbPath();
875
		}
876
877
		$backend = $this->repo->getBackend();
878
		$files = [ $dir ];
879
		try {
880
			$iterator = $backend->getFileList( [ 'dir' => $dir ] );
881
			foreach ( $iterator as $file ) {
882
				$files[] = $file;
883
			}
884
		} catch ( FileBackendError $e ) {
885
		} // suppress (bug 54674)
886
887
		return $files;
888
	}
889
890
	/**
891
	 * Refresh metadata in memcached, but don't touch thumbnails or CDN
892
	 */
893
	function purgeMetadataCache() {
894
		$this->invalidateCache();
895
	}
896
897
	/**
898
	 * Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
899
	 *
900
	 * @param array $options An array potentially with the key forThumbRefresh.
901
	 *
902
	 * @note This used to purge old thumbnails by default as well, but doesn't anymore.
903
	 */
904
	function purgeCache( $options = [] ) {
905
		// Refresh metadata cache
906
		$this->purgeMetadataCache();
907
908
		// Delete thumbnails
909
		$this->purgeThumbnails( $options );
910
911
		// Purge CDN cache for this file
912
		DeferredUpdates::addUpdate(
913
			new CdnCacheUpdate( [ $this->getUrl() ] ),
914
			DeferredUpdates::PRESEND
915
		);
916
	}
917
918
	/**
919
	 * Delete cached transformed files for an archived version only.
920
	 * @param string $archiveName Name of the archived file
921
	 */
922
	function purgeOldThumbnails( $archiveName ) {
923
		// Get a list of old thumbnails and URLs
924
		$files = $this->getThumbnails( $archiveName );
925
926
		// Purge any custom thumbnail caches
927
		Hooks::run( 'LocalFilePurgeThumbnails', [ $this, $archiveName ] );
928
929
		// Delete thumbnails
930
		$dir = array_shift( $files );
931
		$this->purgeThumbList( $dir, $files );
932
933
		// Purge the CDN
934
		$urls = [];
935
		foreach ( $files as $file ) {
936
			$urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
937
		}
938
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
939
	}
940
941
	/**
942
	 * Delete cached transformed files for the current version only.
943
	 * @param array $options
944
	 */
945
	public function purgeThumbnails( $options = [] ) {
946
		$files = $this->getThumbnails();
947
		// Always purge all files from CDN regardless of handler filters
948
		$urls = [];
949
		foreach ( $files as $file ) {
950
			$urls[] = $this->getThumbUrl( $file );
951
		}
952
		array_shift( $urls ); // don't purge directory
953
954
		// Give media handler a chance to filter the file purge list
955
		if ( !empty( $options['forThumbRefresh'] ) ) {
956
			$handler = $this->getHandler();
957
			if ( $handler ) {
958
				$handler->filterThumbnailPurgeList( $files, $options );
959
			}
960
		}
961
962
		// Purge any custom thumbnail caches
963
		Hooks::run( 'LocalFilePurgeThumbnails', [ $this, false ] );
964
965
		// Delete thumbnails
966
		$dir = array_shift( $files );
967
		$this->purgeThumbList( $dir, $files );
968
969
		// Purge the CDN
970
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
971
	}
972
973
	/**
974
	 * Prerenders a configurable set of thumbnails
975
	 *
976
	 * @since 1.28
977
	 */
978
	public function prerenderThumbnails() {
979
		global $wgUploadThumbnailRenderMap;
980
981
		$jobs = [];
982
983
		$sizes = $wgUploadThumbnailRenderMap;
984
		rsort( $sizes );
985
986
		foreach ( $sizes as $size ) {
987
			if ( $this->isVectorized() || $this->getWidth() > $size ) {
988
				$jobs[] = new ThumbnailRenderJob(
989
					$this->getTitle(),
990
					[ 'transformParams' => [ 'width' => $size ] ]
991
				);
992
			}
993
		}
994
995
		if ( $jobs ) {
996
			JobQueueGroup::singleton()->lazyPush( $jobs );
997
		}
998
	}
999
1000
	/**
1001
	 * Delete a list of thumbnails visible at urls
1002
	 * @param string $dir Base dir of the files.
1003
	 * @param array $files Array of strings: relative filenames (to $dir)
1004
	 */
1005
	protected function purgeThumbList( $dir, $files ) {
1006
		$fileListDebug = strtr(
1007
			var_export( $files, true ),
1008
			[ "\n" => '' ]
1009
		);
1010
		wfDebug( __METHOD__ . ": $fileListDebug\n" );
1011
1012
		$purgeList = [];
1013
		foreach ( $files as $file ) {
1014
			# Check that the base file name is part of the thumb name
1015
			# This is a basic sanity check to avoid erasing unrelated directories
1016
			if ( strpos( $file, $this->getName() ) !== false
1017
				|| strpos( $file, "-thumbnail" ) !== false // "short" thumb name
1018
			) {
1019
				$purgeList[] = "{$dir}/{$file}";
1020
			}
1021
		}
1022
1023
		# Delete the thumbnails
1024
		$this->repo->quickPurgeBatch( $purgeList );
1025
		# Clear out the thumbnail directory if empty
1026
		$this->repo->quickCleanDir( $dir );
1027
	}
1028
1029
	/** purgeDescription inherited */
1030
	/** purgeEverything inherited */
1031
1032
	/**
1033
	 * @param int $limit Optional: Limit to number of results
1034
	 * @param int $start Optional: Timestamp, start from
1035
	 * @param int $end Optional: Timestamp, end at
1036
	 * @param bool $inc
1037
	 * @return OldLocalFile[]
1038
	 */
1039
	function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1040
		$dbr = $this->repo->getSlaveDB();
0 ignored issues
show
Bug introduced by
The method getSlaveDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1041
		$tables = [ 'oldimage' ];
1042
		$fields = OldLocalFile::selectFields();
1043
		$conds = $opts = $join_conds = [];
1044
		$eq = $inc ? '=' : '';
1045
		$conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
1046
1047
		if ( $start ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $start of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1048
			$conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
1049
		}
1050
1051
		if ( $end ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $end of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1052
			$conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
1053
		}
1054
1055
		if ( $limit ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $limit of type integer|null is loosely compared to true; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1056
			$opts['LIMIT'] = $limit;
1057
		}
1058
1059
		// Search backwards for time > x queries
1060
		$order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
0 ignored issues
show
Bug Best Practice introduced by
The expression $start of type integer|null is loosely compared to false; this is ambiguous if the integer can be zero. 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 integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
1061
		$opts['ORDER BY'] = "oi_timestamp $order";
1062
		$opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1063
1064
		Hooks::run( 'LocalFile::getHistory', [ &$this, &$tables, &$fields,
1065
			&$conds, &$opts, &$join_conds ] );
1066
1067
		$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1068
		$r = [];
1069
1070
		foreach ( $res as $row ) {
1071
			$r[] = $this->repo->newFileFromRow( $row );
0 ignored issues
show
Bug introduced by
The method newFileFromRow does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1072
		}
1073
1074
		if ( $order == 'ASC' ) {
1075
			$r = array_reverse( $r ); // make sure it ends up descending
1076
		}
1077
1078
		return $r;
1079
	}
1080
1081
	/**
1082
	 * Returns the history of this file, line by line.
1083
	 * starts with current version, then old versions.
1084
	 * uses $this->historyLine to check which line to return:
1085
	 *  0      return line for current version
1086
	 *  1      query for old versions, return first one
1087
	 *  2, ... return next old version from above query
1088
	 * @return bool
1089
	 */
1090
	public function nextHistoryLine() {
1091
		# Polymorphic function name to distinguish foreign and local fetches
1092
		$fname = get_class( $this ) . '::' . __FUNCTION__;
1093
1094
		$dbr = $this->repo->getSlaveDB();
0 ignored issues
show
Bug introduced by
The method getSlaveDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1095
1096
		if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1097
			$this->historyRes = $dbr->select( 'image',
1098
				[
1099
					'*',
1100
					"'' AS oi_archive_name",
1101
					'0 as oi_deleted',
1102
					'img_sha1'
1103
				],
1104
				[ 'img_name' => $this->title->getDBkey() ],
1105
				$fname
1106
			);
1107
1108
			if ( 0 == $dbr->numRows( $this->historyRes ) ) {
1109
				$this->historyRes = null;
1110
1111
				return false;
1112
			}
1113
		} elseif ( $this->historyLine == 1 ) {
1114
			$this->historyRes = $dbr->select( 'oldimage', '*',
1115
				[ 'oi_name' => $this->title->getDBkey() ],
1116
				$fname,
1117
				[ 'ORDER BY' => 'oi_timestamp DESC' ]
1118
			);
1119
		}
1120
		$this->historyLine++;
1121
1122
		return $dbr->fetchObject( $this->historyRes );
1123
	}
1124
1125
	/**
1126
	 * Reset the history pointer to the first element of the history
1127
	 */
1128
	public function resetHistory() {
1129
		$this->historyLine = 0;
1130
1131
		if ( !is_null( $this->historyRes ) ) {
1132
			$this->historyRes = null;
1133
		}
1134
	}
1135
1136
	/** getHashPath inherited */
1137
	/** getRel inherited */
1138
	/** getUrlRel inherited */
1139
	/** getArchiveRel inherited */
1140
	/** getArchivePath inherited */
1141
	/** getThumbPath inherited */
1142
	/** getArchiveUrl inherited */
1143
	/** getThumbUrl inherited */
1144
	/** getArchiveVirtualUrl inherited */
1145
	/** getThumbVirtualUrl inherited */
1146
	/** isHashed inherited */
1147
1148
	/**
1149
	 * Upload a file and record it in the DB
1150
	 * @param string|FSFile $src Source storage path, virtual URL, or filesystem path
1151
	 * @param string $comment Upload description
1152
	 * @param string $pageText Text to use for the new description page,
1153
	 *   if a new description page is created
1154
	 * @param int|bool $flags Flags for publish()
1155
	 * @param array|bool $props File properties, if known. This can be used to
1156
	 *   reduce the upload time when uploading virtual URLs for which the file
1157
	 *   info is already known
1158
	 * @param string|bool $timestamp Timestamp for img_timestamp, or false to use the
1159
	 *   current time
1160
	 * @param User|null $user User object or null to use $wgUser
1161
	 * @param string[] $tags Change tags to add to the log entry and page revision.
1162
	 *   (This doesn't check $user's permissions.)
1163
	 * @return Status On success, the value member contains the
1164
	 *     archive name, or an empty string if it was a new file.
1165
	 */
1166
	function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1167
		$timestamp = false, $user = null, $tags = []
1168
	) {
1169
		global $wgContLang;
1170
1171
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1172
			return $this->readOnlyFatalStatus();
1173
		}
1174
1175
		$srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1176
		if ( !$props ) {
1177
			if ( $this->repo->isVirtualUrl( $srcPath )
1178
				|| FileBackend::isStoragePath( $srcPath )
1179
			) {
1180
				$props = $this->repo->getFileProps( $srcPath );
1181
			} else {
1182
				$mwProps = new MWFileProps( MimeMagic::singleton() );
1183
				$props = $mwProps->getPropsFromPath( $srcPath, true );
1184
			}
1185
		}
1186
1187
		$options = [];
1188
		$handler = MediaHandler::getHandler( $props['mime'] );
1189 View Code Duplication
		if ( $handler ) {
1190
			$options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
1191
		} else {
1192
			$options['headers'] = [];
1193
		}
1194
1195
		// Trim spaces on user supplied text
1196
		$comment = trim( $comment );
1197
1198
		// Truncate nicely or the DB will do it for us
1199
		// non-nicely (dangling multi-byte chars, non-truncated version in cache).
1200
		$comment = $wgContLang->truncate( $comment, 255 );
1201
		$this->lock(); // begin
1202
		$status = $this->publish( $src, $flags, $options );
1203
1204
		if ( $status->successCount >= 2 ) {
1205
			// There will be a copy+(one of move,copy,store).
1206
			// The first succeeding does not commit us to updating the DB
1207
			// since it simply copied the current version to a timestamped file name.
1208
			// It is only *preferable* to avoid leaving such files orphaned.
1209
			// Once the second operation goes through, then the current version was
1210
			// updated and we must therefore update the DB too.
1211
			$oldver = $status->value;
1212
			if ( !$this->recordUpload2( $oldver, $comment, $pageText, $props, $timestamp, $user, $tags ) ) {
1213
				$status->fatal( 'filenotfound', $srcPath );
1214
			}
1215
		}
1216
1217
		$this->unlock(); // done
1218
1219
		return $status;
1220
	}
1221
1222
	/**
1223
	 * Record a file upload in the upload log and the image table
1224
	 * @param string $oldver
1225
	 * @param string $desc
1226
	 * @param string $license
1227
	 * @param string $copyStatus
1228
	 * @param string $source
1229
	 * @param bool $watch
1230
	 * @param string|bool $timestamp
1231
	 * @param User|null $user User object or null to use $wgUser
1232
	 * @return bool
1233
	 */
1234
	function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
1235
		$watch = false, $timestamp = false, User $user = null ) {
1236
		if ( !$user ) {
1237
			global $wgUser;
1238
			$user = $wgUser;
1239
		}
1240
1241
		$pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
1242
1243
		if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
1244
			return false;
1245
		}
1246
1247
		if ( $watch ) {
1248
			$user->addWatch( $this->getTitle() );
1249
		}
1250
1251
		return true;
1252
	}
1253
1254
	/**
1255
	 * Record a file upload in the upload log and the image table
1256
	 * @param string $oldver
1257
	 * @param string $comment
1258
	 * @param string $pageText
1259
	 * @param bool|array $props
1260
	 * @param string|bool $timestamp
1261
	 * @param null|User $user
1262
	 * @param string[] $tags
1263
	 * @return bool
1264
	 */
1265
	function recordUpload2(
1266
		$oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
1267
	) {
1268
		if ( is_null( $user ) ) {
1269
			global $wgUser;
1270
			$user = $wgUser;
1271
		}
1272
1273
		$dbw = $this->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1274
1275
		# Imports or such might force a certain timestamp; otherwise we generate
1276
		# it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1277
		if ( $timestamp === false ) {
1278
			$timestamp = $dbw->timestamp();
1279
			$allowTimeKludge = true;
1280
		} else {
1281
			$allowTimeKludge = false;
1282
		}
1283
1284
		$props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1285
		$props['description'] = $comment;
1286
		$props['user'] = $user->getId();
1287
		$props['user_text'] = $user->getName();
1288
		$props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1289
		$this->setProps( $props );
1290
1291
		# Fail now if the file isn't there
1292
		if ( !$this->fileExists ) {
1293
			wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
1294
1295
			return false;
1296
		}
1297
1298
		$dbw->startAtomic( __METHOD__ );
1299
1300
		# Test to see if the row exists using INSERT IGNORE
1301
		# This avoids race conditions by locking the row until the commit, and also
1302
		# doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1303
		$dbw->insert( 'image',
1304
			[
1305
				'img_name' => $this->getName(),
1306
				'img_size' => $this->size,
1307
				'img_width' => intval( $this->width ),
1308
				'img_height' => intval( $this->height ),
1309
				'img_bits' => $this->bits,
1310
				'img_media_type' => $this->media_type,
1311
				'img_major_mime' => $this->major_mime,
1312
				'img_minor_mime' => $this->minor_mime,
1313
				'img_timestamp' => $timestamp,
1314
				'img_description' => $comment,
1315
				'img_user' => $user->getId(),
1316
				'img_user_text' => $user->getName(),
1317
				'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1318
				'img_sha1' => $this->sha1
1319
			],
1320
			__METHOD__,
1321
			'IGNORE'
1322
		);
1323
1324
		$reupload = ( $dbw->affectedRows() == 0 );
1325
		if ( $reupload ) {
1326
			if ( $allowTimeKludge ) {
1327
				# Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1328
				$ltimestamp = $dbw->selectField(
1329
					'image',
1330
					'img_timestamp',
1331
					[ 'img_name' => $this->getName() ],
1332
					__METHOD__,
1333
					[ 'LOCK IN SHARE MODE' ]
1334
				);
1335
				$lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
1336
				# Avoid a timestamp that is not newer than the last version
1337
				# TODO: the image/oldimage tables should be like page/revision with an ID field
1338
				if ( $lUnixtime && wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $lUnixtime of type string|false is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false 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...
1339
					sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1340
					$timestamp = $dbw->timestamp( $lUnixtime + 1 );
1341
					$this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
0 ignored issues
show
Documentation Bug introduced by
It seems like wfTimestamp(TS_MW, $timestamp) can also be of type false. However, the property $timestamp is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1342
				}
1343
			}
1344
1345
			# (bug 34993) Note: $oldver can be empty here, if the previous
1346
			# version of the file was broken. Allow registration of the new
1347
			# version to continue anyway, because that's better than having
1348
			# an image that's not fixable by user operations.
1349
			# Collision, this is an update of a file
1350
			# Insert previous contents into oldimage
1351
			$dbw->insertSelect( 'oldimage', 'image',
1352
				[
1353
					'oi_name' => 'img_name',
1354
					'oi_archive_name' => $dbw->addQuotes( $oldver ),
1355
					'oi_size' => 'img_size',
1356
					'oi_width' => 'img_width',
1357
					'oi_height' => 'img_height',
1358
					'oi_bits' => 'img_bits',
1359
					'oi_timestamp' => 'img_timestamp',
1360
					'oi_description' => 'img_description',
1361
					'oi_user' => 'img_user',
1362
					'oi_user_text' => 'img_user_text',
1363
					'oi_metadata' => 'img_metadata',
1364
					'oi_media_type' => 'img_media_type',
1365
					'oi_major_mime' => 'img_major_mime',
1366
					'oi_minor_mime' => 'img_minor_mime',
1367
					'oi_sha1' => 'img_sha1'
1368
				],
1369
				[ 'img_name' => $this->getName() ],
1370
				__METHOD__
1371
			);
1372
1373
			# Update the current image row
1374
			$dbw->update( 'image',
1375
				[
1376
					'img_size' => $this->size,
1377
					'img_width' => intval( $this->width ),
1378
					'img_height' => intval( $this->height ),
1379
					'img_bits' => $this->bits,
1380
					'img_media_type' => $this->media_type,
1381
					'img_major_mime' => $this->major_mime,
1382
					'img_minor_mime' => $this->minor_mime,
1383
					'img_timestamp' => $timestamp,
1384
					'img_description' => $comment,
1385
					'img_user' => $user->getId(),
1386
					'img_user_text' => $user->getName(),
1387
					'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1388
					'img_sha1' => $this->sha1
1389
				],
1390
				[ 'img_name' => $this->getName() ],
1391
				__METHOD__
1392
			);
1393
		}
1394
1395
		$descTitle = $this->getTitle();
1396
		$descId = $descTitle->getArticleID();
1397
		$wikiPage = new WikiFilePage( $descTitle );
1398
		$wikiPage->setFile( $this );
1399
1400
		// Add the log entry...
1401
		$logEntry = new ManualLogEntry( 'upload', $reupload ? 'overwrite' : 'upload' );
1402
		$logEntry->setTimestamp( $this->timestamp );
0 ignored issues
show
Security Bug introduced by
It seems like $this->timestamp can also be of type false; however, ManualLogEntry::setTimestamp() does only seem to accept string, did you maybe forget to handle an error condition?
Loading history...
1403
		$logEntry->setPerformer( $user );
1404
		$logEntry->setComment( $comment );
1405
		$logEntry->setTarget( $descTitle );
1406
		// Allow people using the api to associate log entries with the upload.
1407
		// Log has a timestamp, but sometimes different from upload timestamp.
1408
		$logEntry->setParameters(
1409
			[
1410
				'img_sha1' => $this->sha1,
1411
				'img_timestamp' => $timestamp,
1412
			]
1413
		);
1414
		// Note we keep $logId around since during new image
1415
		// creation, page doesn't exist yet, so log_page = 0
1416
		// but we want it to point to the page we're making,
1417
		// so we later modify the log entry.
1418
		// For a similar reason, we avoid making an RC entry
1419
		// now and wait until the page exists.
1420
		$logId = $logEntry->insert();
1421
1422
		if ( $descTitle->exists() ) {
1423
			// Use own context to get the action text in content language
1424
			$formatter = LogFormatter::newFromEntry( $logEntry );
1425
			$formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1426
			$editSummary = $formatter->getPlainActionText();
1427
1428
			$nullRevision = Revision::newNullRevision(
1429
				$dbw,
1430
				$descId,
1431
				$editSummary,
1432
				false,
1433
				$user
1434
			);
1435
			if ( $nullRevision ) {
1436
				$nullRevision->insertOn( $dbw );
1437
				Hooks::run(
1438
					'NewRevisionFromEditComplete',
1439
					[ $wikiPage, $nullRevision, $nullRevision->getParentId(), $user ]
1440
				);
1441
				$wikiPage->updateRevisionOn( $dbw, $nullRevision );
1442
				// Associate null revision id
1443
				$logEntry->setAssociatedRevId( $nullRevision->getId() );
1444
			}
1445
1446
			$newPageContent = null;
1447
		} else {
1448
			// Make the description page and RC log entry post-commit
1449
			$newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1450
		}
1451
1452
		# Defer purges, page creation, and link updates in case they error out.
1453
		# The most important thing is that files and the DB registry stay synced.
1454
		$dbw->endAtomic( __METHOD__ );
1455
1456
		# Do some cache purges after final commit so that:
1457
		# a) Changes are more likely to be seen post-purge
1458
		# b) They won't cause rollback of the log publish/update above
1459
		DeferredUpdates::addUpdate(
1460
			new AutoCommitUpdate(
1461
				$dbw,
1462
				__METHOD__,
1463
				function () use (
1464
					$reupload, $wikiPage, $newPageContent, $comment, $user,
1465
					$logEntry, $logId, $descId, $tags
1466
				) {
1467
					# Update memcache after the commit
1468
					$this->invalidateCache();
1469
1470
					$updateLogPage = false;
1471
					if ( $newPageContent ) {
1472
						# New file page; create the description page.
1473
						# There's already a log entry, so don't make a second RC entry
1474
						# CDN and file cache for the description page are purged by doEditContent.
1475
						$status = $wikiPage->doEditContent(
1476
							$newPageContent,
1477
							$comment,
1478
							EDIT_NEW | EDIT_SUPPRESS_RC,
1479
							false,
1480
							$user
1481
						);
1482
1483
						if ( isset( $status->value['revision'] ) ) {
1484
							/** @var $rev Revision */
1485
							$rev = $status->value['revision'];
1486
							// Associate new page revision id
1487
							$logEntry->setAssociatedRevId( $rev->getId() );
1488
						}
1489
						// This relies on the resetArticleID() call in WikiPage::insertOn(),
1490
						// which is triggered on $descTitle by doEditContent() above.
1491
						if ( isset( $status->value['revision'] ) ) {
1492
							/** @var $rev Revision */
1493
							$rev = $status->value['revision'];
1494
							$updateLogPage = $rev->getPage();
1495
						}
1496
					} else {
1497
						# Existing file page: invalidate description page cache
1498
						$wikiPage->getTitle()->invalidateCache();
1499
						$wikiPage->getTitle()->purgeSquid();
1500
						# Allow the new file version to be patrolled from the page footer
1501
						Article::purgePatrolFooterCache( $descId );
1502
					}
1503
1504
					# Update associated rev id. This should be done by $logEntry->insert() earlier,
1505
					# but setAssociatedRevId() wasn't called at that point yet...
1506
					$logParams = $logEntry->getParameters();
1507
					$logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
1508
					$update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
1509
					if ( $updateLogPage ) {
1510
						# Also log page, in case where we just created it above
1511
						$update['log_page'] = $updateLogPage;
1512
					}
1513
					$this->getRepo()->getMasterDB()->update(
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class FileRepo as the method getMasterDB() does only exist in the following sub-classes of FileRepo: ForeignDBRepo, ForeignDBViaLBRepo, LocalRepo. 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...
1514
						'logging',
1515
						$update,
1516
						[ 'log_id' => $logId ],
1517
						__METHOD__
1518
					);
1519
					$this->getRepo()->getMasterDB()->insert(
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class FileRepo as the method getMasterDB() does only exist in the following sub-classes of FileRepo: ForeignDBRepo, ForeignDBViaLBRepo, LocalRepo. 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...
1520
						'log_search',
1521
						[
1522
							'ls_field' => 'associated_rev_id',
1523
							'ls_value' => $logEntry->getAssociatedRevId(),
1524
							'ls_log_id' => $logId,
1525
						],
1526
						__METHOD__
1527
					);
1528
1529
					# Add change tags, if any
1530
					if ( $tags ) {
1531
						$logEntry->setTags( $tags );
1532
					}
1533
1534
					# Uploads can be patrolled
1535
					$logEntry->setIsPatrollable( true );
1536
1537
					# Now that the log entry is up-to-date, make an RC entry.
1538
					$logEntry->publish( $logId );
1539
1540
					# Run hook for other updates (typically more cache purging)
1541
					Hooks::run( 'FileUpload', [ $this, $reupload, !$newPageContent ] );
1542
1543
					if ( $reupload ) {
1544
						# Delete old thumbnails
1545
						$this->purgeThumbnails();
1546
						# Remove the old file from the CDN cache
1547
						DeferredUpdates::addUpdate(
1548
							new CdnCacheUpdate( [ $this->getUrl() ] ),
1549
							DeferredUpdates::PRESEND
1550
						);
1551
					} else {
1552
						# Update backlink pages pointing to this title if created
1553
						LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
1554
					}
1555
1556
					$this->prerenderThumbnails();
1557
				}
1558
			),
1559
			DeferredUpdates::PRESEND
1560
		);
1561
1562
		if ( !$reupload ) {
1563
			# This is a new file, so update the image count
1564
			DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
1565
		}
1566
1567
		# Invalidate cache for all pages using this file
1568
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ) );
1569
1570
		return true;
1571
	}
1572
1573
	/**
1574
	 * Move or copy a file to its public location. If a file exists at the
1575
	 * destination, move it to an archive. Returns a FileRepoStatus object with
1576
	 * the archive name in the "value" member on success.
1577
	 *
1578
	 * The archive name should be passed through to recordUpload for database
1579
	 * registration.
1580
	 *
1581
	 * @param string|FSFile $src Local filesystem path or virtual URL to the source image
1582
	 * @param int $flags A bitwise combination of:
1583
	 *     File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
1584
	 * @param array $options Optional additional parameters
1585
	 * @return Status On success, the value member contains the
1586
	 *     archive name, or an empty string if it was a new file.
1587
	 */
1588
	function publish( $src, $flags = 0, array $options = [] ) {
1589
		return $this->publishTo( $src, $this->getRel(), $flags, $options );
1590
	}
1591
1592
	/**
1593
	 * Move or copy a file to a specified location. Returns a FileRepoStatus
1594
	 * object with the archive name in the "value" member on success.
1595
	 *
1596
	 * The archive name should be passed through to recordUpload for database
1597
	 * registration.
1598
	 *
1599
	 * @param string|FSFile $src Local filesystem path or virtual URL to the source image
1600
	 * @param string $dstRel Target relative path
1601
	 * @param int $flags A bitwise combination of:
1602
	 *     File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
1603
	 * @param array $options Optional additional parameters
1604
	 * @return Status On success, the value member contains the
1605
	 *     archive name, or an empty string if it was a new file.
1606
	 */
1607
	function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
1608
		$srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1609
1610
		$repo = $this->getRepo();
1611
		if ( $repo->getReadOnlyReason() !== false ) {
1612
			return $this->readOnlyFatalStatus();
1613
		}
1614
1615
		$this->lock(); // begin
1616
1617
		$archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
1618
		$archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
1619
1620
		if ( $repo->hasSha1Storage() ) {
1621
			$sha1 = $repo->isVirtualUrl( $srcPath )
1622
				? $repo->getFileSha1( $srcPath )
1623
				: FSFile::getSha1Base36FromPath( $srcPath );
1624
			/** @var FileBackendDBRepoWrapper $wrapperBackend */
1625
			$wrapperBackend = $repo->getBackend();
1626
			$dst = $wrapperBackend->getPathForSHA1( $sha1 );
1627
			$status = $repo->quickImport( $src, $dst );
1628
			if ( $flags & File::DELETE_SOURCE ) {
1629
				unlink( $srcPath );
1630
			}
1631
1632
			if ( $this->exists() ) {
1633
				$status->value = $archiveName;
1634
			}
1635
		} else {
1636
			$flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
1637
			$status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
1638
1639
			if ( $status->value == 'new' ) {
1640
				$status->value = '';
1641
			} else {
1642
				$status->value = $archiveName;
1643
			}
1644
		}
1645
1646
		$this->unlock(); // done
1647
1648
		return $status;
1649
	}
1650
1651
	/** getLinksTo inherited */
1652
	/** getExifData inherited */
1653
	/** isLocal inherited */
1654
	/** wasDeleted inherited */
1655
1656
	/**
1657
	 * Move file to the new title
1658
	 *
1659
	 * Move current, old version and all thumbnails
1660
	 * to the new filename. Old file is deleted.
1661
	 *
1662
	 * Cache purging is done; checks for validity
1663
	 * and logging are caller's responsibility
1664
	 *
1665
	 * @param Title $target New file name
1666
	 * @return Status
1667
	 */
1668
	function move( $target ) {
1669
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1670
			return $this->readOnlyFatalStatus();
1671
		}
1672
1673
		wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1674
		$batch = new LocalFileMoveBatch( $this, $target );
1675
1676
		$this->lock(); // begin
1677
		$batch->addCurrent();
1678
		$archiveNames = $batch->addOlds();
1679
		$status = $batch->execute();
1680
		$this->unlock(); // done
1681
1682
		wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1683
1684
		// Purge the source and target files...
1685
		$oldTitleFile = wfLocalFile( $this->title );
1686
		$newTitleFile = wfLocalFile( $target );
1687
		// To avoid slow purges in the transaction, move them outside...
1688
		DeferredUpdates::addUpdate(
1689
			new AutoCommitUpdate(
1690
				$this->getRepo()->getMasterDB(),
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class FileRepo as the method getMasterDB() does only exist in the following sub-classes of FileRepo: ForeignDBRepo, ForeignDBViaLBRepo, LocalRepo. 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...
1691
				__METHOD__,
1692
				function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
1693
					$oldTitleFile->purgeEverything();
1694
					foreach ( $archiveNames as $archiveName ) {
1695
						$oldTitleFile->purgeOldThumbnails( $archiveName );
1696
					}
1697
					$newTitleFile->purgeEverything();
1698
				}
1699
			),
1700
			DeferredUpdates::PRESEND
1701
		);
1702
1703
		if ( $status->isOK() ) {
1704
			// Now switch the object
1705
			$this->title = $target;
1706
			// Force regeneration of the name and hashpath
1707
			unset( $this->name );
1708
			unset( $this->hashPath );
1709
		}
1710
1711
		return $status;
1712
	}
1713
1714
	/**
1715
	 * Delete all versions of the file.
1716
	 *
1717
	 * Moves the files into an archive directory (or deletes them)
1718
	 * and removes the database rows.
1719
	 *
1720
	 * Cache purging is done; logging is caller's responsibility.
1721
	 *
1722
	 * @param string $reason
1723
	 * @param bool $suppress
1724
	 * @param User|null $user
1725
	 * @return Status
1726
	 */
1727
	function delete( $reason, $suppress = false, $user = null ) {
1728
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1729
			return $this->readOnlyFatalStatus();
1730
		}
1731
1732
		$batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1733
1734
		$this->lock(); // begin
1735
		$batch->addCurrent();
1736
		// Get old version relative paths
1737
		$archiveNames = $batch->addOlds();
1738
		$status = $batch->execute();
1739
		$this->unlock(); // done
1740
1741
		if ( $status->isOK() ) {
1742
			DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
1743
		}
1744
1745
		// To avoid slow purges in the transaction, move them outside...
1746
		DeferredUpdates::addUpdate(
1747
			new AutoCommitUpdate(
1748
				$this->getRepo()->getMasterDB(),
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class FileRepo as the method getMasterDB() does only exist in the following sub-classes of FileRepo: ForeignDBRepo, ForeignDBViaLBRepo, LocalRepo. 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...
1749
				__METHOD__,
1750
				function () use ( $archiveNames ) {
1751
					$this->purgeEverything();
1752
					foreach ( $archiveNames as $archiveName ) {
1753
						$this->purgeOldThumbnails( $archiveName );
1754
					}
1755
				}
1756
			),
1757
			DeferredUpdates::PRESEND
1758
		);
1759
1760
		// Purge the CDN
1761
		$purgeUrls = [];
1762
		foreach ( $archiveNames as $archiveName ) {
1763
			$purgeUrls[] = $this->getArchiveUrl( $archiveName );
1764
		}
1765
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $purgeUrls ), DeferredUpdates::PRESEND );
1766
1767
		return $status;
1768
	}
1769
1770
	/**
1771
	 * Delete an old version of the file.
1772
	 *
1773
	 * Moves the file into an archive directory (or deletes it)
1774
	 * and removes the database row.
1775
	 *
1776
	 * Cache purging is done; logging is caller's responsibility.
1777
	 *
1778
	 * @param string $archiveName
1779
	 * @param string $reason
1780
	 * @param bool $suppress
1781
	 * @param User|null $user
1782
	 * @throws MWException Exception on database or file store failure
1783
	 * @return Status
1784
	 */
1785
	function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
1786
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1787
			return $this->readOnlyFatalStatus();
1788
		}
1789
1790
		$batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1791
1792
		$this->lock(); // begin
1793
		$batch->addOld( $archiveName );
1794
		$status = $batch->execute();
1795
		$this->unlock(); // done
1796
1797
		$this->purgeOldThumbnails( $archiveName );
1798
		if ( $status->isOK() ) {
1799
			$this->purgeDescription();
1800
		}
1801
1802
		DeferredUpdates::addUpdate(
1803
			new CdnCacheUpdate( [ $this->getArchiveUrl( $archiveName ) ] ),
1804
			DeferredUpdates::PRESEND
1805
		);
1806
1807
		return $status;
1808
	}
1809
1810
	/**
1811
	 * Restore all or specified deleted revisions to the given file.
1812
	 * Permissions and logging are left to the caller.
1813
	 *
1814
	 * May throw database exceptions on error.
1815
	 *
1816
	 * @param array $versions Set of record ids of deleted items to restore,
1817
	 *   or empty to restore all revisions.
1818
	 * @param bool $unsuppress
1819
	 * @return Status
1820
	 */
1821
	function restore( $versions = [], $unsuppress = false ) {
1822
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1823
			return $this->readOnlyFatalStatus();
1824
		}
1825
1826
		$batch = new LocalFileRestoreBatch( $this, $unsuppress );
1827
1828
		$this->lock(); // begin
1829
		if ( !$versions ) {
1830
			$batch->addAll();
1831
		} else {
1832
			$batch->addIds( $versions );
1833
		}
1834
		$status = $batch->execute();
1835
		if ( $status->isGood() ) {
1836
			$cleanupStatus = $batch->cleanup();
1837
			$cleanupStatus->successCount = 0;
1838
			$cleanupStatus->failCount = 0;
1839
			$status->merge( $cleanupStatus );
1840
		}
1841
		$this->unlock(); // done
1842
1843
		return $status;
1844
	}
1845
1846
	/** isMultipage inherited */
1847
	/** pageCount inherited */
1848
	/** scaleHeight inherited */
1849
	/** getImageSize inherited */
1850
1851
	/**
1852
	 * Get the URL of the file description page.
1853
	 * @return string
1854
	 */
1855
	function getDescriptionUrl() {
1856
		return $this->title->getLocalURL();
1857
	}
1858
1859
	/**
1860
	 * Get the HTML text of the description page
1861
	 * This is not used by ImagePage for local files, since (among other things)
1862
	 * it skips the parser cache.
1863
	 *
1864
	 * @param Language $lang What language to get description in (Optional)
1865
	 * @return bool|mixed
1866
	 */
1867
	function getDescriptionText( $lang = null ) {
1868
		$revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
1869
		if ( !$revision ) {
1870
			return false;
1871
		}
1872
		$content = $revision->getContent();
1873
		if ( !$content ) {
1874
			return false;
1875
		}
1876
		$pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
1877
1878
		return $pout->getText();
1879
	}
1880
1881
	/**
1882
	 * @param int $audience
1883
	 * @param User $user
1884
	 * @return string
1885
	 */
1886 View Code Duplication
	function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
1887
		$this->load();
1888
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
1889
			return '';
1890
		} elseif ( $audience == self::FOR_THIS_USER
1891
			&& !$this->userCan( self::DELETED_COMMENT, $user )
1892
		) {
1893
			return '';
1894
		} else {
1895
			return $this->description;
1896
		}
1897
	}
1898
1899
	/**
1900
	 * @return bool|string
1901
	 */
1902
	function getTimestamp() {
1903
		$this->load();
1904
1905
		return $this->timestamp;
1906
	}
1907
1908
	/**
1909
	 * @return bool|string
1910
	 */
1911
	public function getDescriptionTouched() {
1912
		// The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
1913
		// itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
1914
		// need to differentiate between null (uninitialized) and false (failed to load).
1915
		if ( $this->descriptionTouched === null ) {
1916
			$cond = [
1917
				'page_namespace' => $this->title->getNamespace(),
1918
				'page_title' => $this->title->getDBkey()
1919
			];
1920
			$touched = $this->repo->getSlaveDB()->selectField( 'page', 'page_touched', $cond, __METHOD__ );
0 ignored issues
show
Bug introduced by
The method getSlaveDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1921
			$this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
0 ignored issues
show
Documentation Bug introduced by
It seems like $touched ? wfTimestamp(TS_MW, $touched) : false can also be of type false. However, the property $descriptionTouched is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
1922
		}
1923
1924
		return $this->descriptionTouched;
1925
	}
1926
1927
	/**
1928
	 * @return string
1929
	 */
1930
	function getSha1() {
1931
		$this->load();
1932
		// Initialise now if necessary
1933
		if ( $this->sha1 == '' && $this->fileExists ) {
1934
			$this->lock(); // begin
1935
1936
			$this->sha1 = $this->repo->getFileSha1( $this->getPath() );
0 ignored issues
show
Bug introduced by
It seems like $this->getPath() targeting File::getPath() can also be of type boolean; however, FileRepo::getFileSha1() 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...
1937
			if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1938
				$dbw = $this->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1939
				$dbw->update( 'image',
1940
					[ 'img_sha1' => $this->sha1 ],
1941
					[ 'img_name' => $this->getName() ],
1942
					__METHOD__ );
1943
				$this->invalidateCache();
1944
			}
1945
1946
			$this->unlock(); // done
1947
		}
1948
1949
		return $this->sha1;
1950
	}
1951
1952
	/**
1953
	 * @return bool Whether to cache in RepoGroup (this avoids OOMs)
1954
	 */
1955
	function isCacheable() {
1956
		$this->load();
1957
1958
		// If extra data (metadata) was not loaded then it must have been large
1959
		return $this->extraDataLoaded
1960
		&& strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
1961
	}
1962
1963
	/**
1964
	 * @return Status
1965
	 * @since 1.28
1966
	 */
1967
	public function acquireFileLock() {
1968
		return $this->getRepo()->getBackend()->lockFiles(
1969
			[ $this->getPath() ], LockManager::LOCK_EX, 10
1970
		);
1971
	}
1972
1973
	/**
1974
	 * @return Status
1975
	 * @since 1.28
1976
	 */
1977
	public function releaseFileLock() {
1978
		return $this->getRepo()->getBackend()->unlockFiles(
1979
			[ $this->getPath() ], LockManager::LOCK_EX
1980
		);
1981
	}
1982
1983
	/**
1984
	 * Start an atomic DB section and lock the image for update
1985
	 * or increments a reference counter if the lock is already held
1986
	 *
1987
	 * This method should not be used outside of LocalFile/LocalFile*Batch
1988
	 *
1989
	 * @throws LocalFileLockError Throws an error if the lock was not acquired
1990
	 * @return bool Whether the file lock owns/spawned the DB transaction
1991
	 */
1992
	public function lock() {
1993
		if ( !$this->locked ) {
1994
			$logger = LoggerFactory::getInstance( 'LocalFile' );
1995
1996
			$dbw = $this->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
1997
			$makesTransaction = !$dbw->trxLevel();
1998
			$dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
1999
			// Bug 54736: use simple lock to handle when the file does not exist.
2000
			// SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
2001
			// Also, that would cause contention on INSERT of similarly named rows.
2002
			$status = $this->acquireFileLock(); // represents all versions of the file
2003
			if ( !$status->isGood() ) {
2004
				$dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2005
				$logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
2006
2007
				throw new LocalFileLockError( $status );
2008
			}
2009
			// Release the lock *after* commit to avoid row-level contention.
2010
			// Make sure it triggers on rollback() as well as commit() (T132921).
2011
			$dbw->onTransactionResolution(
2012
				function () use ( $logger ) {
2013
					$status = $this->releaseFileLock();
2014
					if ( !$status->isGood() ) {
2015
						$logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
2016
					}
2017
				},
2018
				__METHOD__
2019
			);
2020
			// Callers might care if the SELECT snapshot is safely fresh
2021
			$this->lockedOwnTrx = $makesTransaction;
2022
		}
2023
2024
		$this->locked++;
2025
2026
		return $this->lockedOwnTrx;
2027
	}
2028
2029
	/**
2030
	 * Decrement the lock reference count and end the atomic section if it reaches zero
2031
	 *
2032
	 * This method should not be used outside of LocalFile/LocalFile*Batch
2033
	 *
2034
	 * The commit and loc release will happen when no atomic sections are active, which
2035
	 * may happen immediately or at some point after calling this
2036
	 */
2037
	public function unlock() {
2038
		if ( $this->locked ) {
2039
			--$this->locked;
2040
			if ( !$this->locked ) {
2041
				$dbw = $this->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2042
				$dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2043
				$this->lockedOwnTrx = false;
2044
			}
2045
		}
2046
	}
2047
2048
	/**
2049
	 * @return Status
2050
	 */
2051
	protected function readOnlyFatalStatus() {
2052
		return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
2053
			$this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2054
	}
2055
2056
	/**
2057
	 * Clean up any dangling locks
2058
	 */
2059
	function __destruct() {
2060
		$this->unlock();
2061
	}
2062
} // LocalFile class
2063
2064
# ------------------------------------------------------------------------------
2065
2066
/**
2067
 * Helper class for file deletion
2068
 * @ingroup FileAbstraction
2069
 */
2070
class LocalFileDeleteBatch {
2071
	/** @var LocalFile */
2072
	private $file;
2073
2074
	/** @var string */
2075
	private $reason;
2076
2077
	/** @var array */
2078
	private $srcRels = [];
2079
2080
	/** @var array */
2081
	private $archiveUrls = [];
2082
2083
	/** @var array Items to be processed in the deletion batch */
2084
	private $deletionBatch;
2085
2086
	/** @var bool Whether to suppress all suppressable fields when deleting */
2087
	private $suppress;
2088
2089
	/** @var FileRepoStatus */
2090
	private $status;
2091
2092
	/** @var User */
2093
	private $user;
2094
2095
	/**
2096
	 * @param File $file
2097
	 * @param string $reason
2098
	 * @param bool $suppress
2099
	 * @param User|null $user
2100
	 */
2101
	function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
2102
		$this->file = $file;
0 ignored issues
show
Documentation Bug introduced by
$file is of type object<File>, but the property $file was declared to be of type object<LocalFile>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
2103
		$this->reason = $reason;
2104
		$this->suppress = $suppress;
2105
		if ( $user ) {
2106
			$this->user = $user;
2107
		} else {
2108
			global $wgUser;
2109
			$this->user = $wgUser;
2110
		}
2111
		$this->status = $file->repo->newGood();
2112
	}
2113
2114
	public function addCurrent() {
2115
		$this->srcRels['.'] = $this->file->getRel();
2116
	}
2117
2118
	/**
2119
	 * @param string $oldName
2120
	 */
2121
	public function addOld( $oldName ) {
2122
		$this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
2123
		$this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
2124
	}
2125
2126
	/**
2127
	 * Add the old versions of the image to the batch
2128
	 * @return array List of archive names from old versions
2129
	 */
2130
	public function addOlds() {
2131
		$archiveNames = [];
2132
2133
		$dbw = $this->file->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2134
		$result = $dbw->select( 'oldimage',
2135
			[ 'oi_archive_name' ],
2136
			[ 'oi_name' => $this->file->getName() ],
2137
			__METHOD__
2138
		);
2139
2140
		foreach ( $result as $row ) {
2141
			$this->addOld( $row->oi_archive_name );
2142
			$archiveNames[] = $row->oi_archive_name;
2143
		}
2144
2145
		return $archiveNames;
2146
	}
2147
2148
	/**
2149
	 * @return array
2150
	 */
2151
	protected function getOldRels() {
2152
		if ( !isset( $this->srcRels['.'] ) ) {
2153
			$oldRels =& $this->srcRels;
2154
			$deleteCurrent = false;
2155
		} else {
2156
			$oldRels = $this->srcRels;
2157
			unset( $oldRels['.'] );
2158
			$deleteCurrent = true;
2159
		}
2160
2161
		return [ $oldRels, $deleteCurrent ];
2162
	}
2163
2164
	/**
2165
	 * @return array
2166
	 */
2167
	protected function getHashes() {
2168
		$hashes = [];
2169
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2170
2171
		if ( $deleteCurrent ) {
2172
			$hashes['.'] = $this->file->getSha1();
2173
		}
2174
2175
		if ( count( $oldRels ) ) {
2176
			$dbw = $this->file->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2177
			$res = $dbw->select(
2178
				'oldimage',
2179
				[ 'oi_archive_name', 'oi_sha1' ],
2180
				[ 'oi_archive_name' => array_keys( $oldRels ),
2181
					'oi_name' => $this->file->getName() ], // performance
2182
				__METHOD__
2183
			);
2184
2185
			foreach ( $res as $row ) {
2186
				if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
2187
					// Get the hash from the file
2188
					$oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
2189
					$props = $this->file->repo->getFileProps( $oldUrl );
2190
2191
					if ( $props['fileExists'] ) {
2192
						// Upgrade the oldimage row
2193
						$dbw->update( 'oldimage',
2194
							[ 'oi_sha1' => $props['sha1'] ],
2195
							[ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
2196
							__METHOD__ );
2197
						$hashes[$row->oi_archive_name] = $props['sha1'];
2198
					} else {
2199
						$hashes[$row->oi_archive_name] = false;
2200
					}
2201
				} else {
2202
					$hashes[$row->oi_archive_name] = $row->oi_sha1;
2203
				}
2204
			}
2205
		}
2206
2207
		$missing = array_diff_key( $this->srcRels, $hashes );
2208
2209
		foreach ( $missing as $name => $rel ) {
2210
			$this->status->error( 'filedelete-old-unregistered', $name );
2211
		}
2212
2213
		foreach ( $hashes as $name => $hash ) {
2214
			if ( !$hash ) {
2215
				$this->status->error( 'filedelete-missing', $this->srcRels[$name] );
2216
				unset( $hashes[$name] );
2217
			}
2218
		}
2219
2220
		return $hashes;
2221
	}
2222
2223
	protected function doDBInserts() {
2224
		$now = time();
2225
		$dbw = $this->file->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2226
		$encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
2227
		$encUserId = $dbw->addQuotes( $this->user->getId() );
2228
		$encReason = $dbw->addQuotes( $this->reason );
2229
		$encGroup = $dbw->addQuotes( 'deleted' );
2230
		$ext = $this->file->getExtension();
2231
		$dotExt = $ext === '' ? '' : ".$ext";
2232
		$encExt = $dbw->addQuotes( $dotExt );
2233
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2234
2235
		// Bitfields to further suppress the content
2236
		if ( $this->suppress ) {
2237
			$bitfield = Revision::SUPPRESSED_ALL;
2238
		} else {
2239
			$bitfield = 'oi_deleted';
2240
		}
2241
2242
		if ( $deleteCurrent ) {
2243
			$dbw->insertSelect(
2244
				'filearchive',
2245
				'image',
2246
				[
2247
					'fa_storage_group' => $encGroup,
2248
					'fa_storage_key' => $dbw->conditional(
2249
						[ 'img_sha1' => '' ],
2250
						$dbw->addQuotes( '' ),
2251
						$dbw->buildConcat( [ "img_sha1", $encExt ] )
2252
					),
2253
					'fa_deleted_user' => $encUserId,
2254
					'fa_deleted_timestamp' => $encTimestamp,
2255
					'fa_deleted_reason' => $encReason,
2256
					'fa_deleted' => $this->suppress ? $bitfield : 0,
2257
					'fa_name' => 'img_name',
2258
					'fa_archive_name' => 'NULL',
2259
					'fa_size' => 'img_size',
2260
					'fa_width' => 'img_width',
2261
					'fa_height' => 'img_height',
2262
					'fa_metadata' => 'img_metadata',
2263
					'fa_bits' => 'img_bits',
2264
					'fa_media_type' => 'img_media_type',
2265
					'fa_major_mime' => 'img_major_mime',
2266
					'fa_minor_mime' => 'img_minor_mime',
2267
					'fa_description' => 'img_description',
2268
					'fa_user' => 'img_user',
2269
					'fa_user_text' => 'img_user_text',
2270
					'fa_timestamp' => 'img_timestamp',
2271
					'fa_sha1' => 'img_sha1'
2272
				],
2273
				[ 'img_name' => $this->file->getName() ],
2274
				__METHOD__
2275
			);
2276
		}
2277
2278
		if ( count( $oldRels ) ) {
2279
			$res = $dbw->select(
2280
				'oldimage',
2281
				OldLocalFile::selectFields(),
2282
				[
2283
					'oi_name' => $this->file->getName(),
2284
					'oi_archive_name' => array_keys( $oldRels )
2285
				],
2286
				__METHOD__,
2287
				[ 'FOR UPDATE' ]
2288
			);
2289
			$rowsInsert = [];
2290
			foreach ( $res as $row ) {
2291
				$rowsInsert[] = [
2292
					// Deletion-specific fields
2293
					'fa_storage_group' => 'deleted',
2294
					'fa_storage_key' => ( $row->oi_sha1 === '' )
2295
						? ''
2296
						: "{$row->oi_sha1}{$dotExt}",
2297
					'fa_deleted_user' => $this->user->getId(),
2298
					'fa_deleted_timestamp' => $dbw->timestamp( $now ),
2299
					'fa_deleted_reason' => $this->reason,
2300
					// Counterpart fields
2301
					'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
2302
					'fa_name' => $row->oi_name,
2303
					'fa_archive_name' => $row->oi_archive_name,
2304
					'fa_size' => $row->oi_size,
2305
					'fa_width' => $row->oi_width,
2306
					'fa_height' => $row->oi_height,
2307
					'fa_metadata' => $row->oi_metadata,
2308
					'fa_bits' => $row->oi_bits,
2309
					'fa_media_type' => $row->oi_media_type,
2310
					'fa_major_mime' => $row->oi_major_mime,
2311
					'fa_minor_mime' => $row->oi_minor_mime,
2312
					'fa_description' => $row->oi_description,
2313
					'fa_user' => $row->oi_user,
2314
					'fa_user_text' => $row->oi_user_text,
2315
					'fa_timestamp' => $row->oi_timestamp,
2316
					'fa_sha1' => $row->oi_sha1
2317
				];
2318
			}
2319
2320
			$dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
2321
		}
2322
	}
2323
2324
	function doDBDeletes() {
2325
		$dbw = $this->file->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2326
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2327
2328
		if ( count( $oldRels ) ) {
2329
			$dbw->delete( 'oldimage',
2330
				[
2331
					'oi_name' => $this->file->getName(),
2332
					'oi_archive_name' => array_keys( $oldRels )
2333
				], __METHOD__ );
2334
		}
2335
2336
		if ( $deleteCurrent ) {
2337
			$dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
2338
		}
2339
	}
2340
2341
	/**
2342
	 * Run the transaction
2343
	 * @return Status
2344
	 */
2345
	public function execute() {
2346
		$repo = $this->file->getRepo();
2347
		$this->file->lock();
2348
2349
		// Prepare deletion batch
2350
		$hashes = $this->getHashes();
2351
		$this->deletionBatch = [];
2352
		$ext = $this->file->getExtension();
2353
		$dotExt = $ext === '' ? '' : ".$ext";
2354
2355
		foreach ( $this->srcRels as $name => $srcRel ) {
2356
			// Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
2357
			if ( isset( $hashes[$name] ) ) {
2358
				$hash = $hashes[$name];
2359
				$key = $hash . $dotExt;
2360
				$dstRel = $repo->getDeletedHashPath( $key ) . $key;
2361
				$this->deletionBatch[$name] = [ $srcRel, $dstRel ];
2362
			}
2363
		}
2364
2365
		if ( !$repo->hasSha1Storage() ) {
2366
			// Removes non-existent file from the batch, so we don't get errors.
2367
			// This also handles files in the 'deleted' zone deleted via revision deletion.
2368
			$checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
2369
			if ( !$checkStatus->isGood() ) {
2370
				$this->status->merge( $checkStatus );
2371
				return $this->status;
2372
			}
2373
			$this->deletionBatch = $checkStatus->value;
0 ignored issues
show
Documentation Bug introduced by
It seems like $checkStatus->value of type * is incompatible with the declared type array of property $deletionBatch.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
2374
2375
			// Execute the file deletion batch
2376
			$status = $this->file->repo->deleteBatch( $this->deletionBatch );
2377
			if ( !$status->isGood() ) {
2378
				$this->status->merge( $status );
2379
			}
2380
		}
2381
2382
		if ( !$this->status->isOK() ) {
2383
			// Critical file deletion error; abort
2384
			$this->file->unlock();
2385
2386
			return $this->status;
2387
		}
2388
2389
		// Copy the image/oldimage rows to filearchive
2390
		$this->doDBInserts();
2391
		// Delete image/oldimage rows
2392
		$this->doDBDeletes();
2393
2394
		// Commit and return
2395
		$this->file->unlock();
2396
2397
		return $this->status;
2398
	}
2399
2400
	/**
2401
	 * Removes non-existent files from a deletion batch.
2402
	 * @param array $batch
2403
	 * @return Status
2404
	 */
2405
	protected function removeNonexistentFiles( $batch ) {
2406
		$files = $newBatch = [];
2407
2408
		foreach ( $batch as $batchItem ) {
2409
			list( $src, ) = $batchItem;
2410
			$files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
2411
		}
2412
2413
		$result = $this->file->repo->fileExistsBatch( $files );
2414 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2415
			return Status::newFatal( 'backend-fail-internal',
2416
				$this->file->repo->getBackend()->getName() );
2417
		}
2418
2419
		foreach ( $batch as $batchItem ) {
2420
			if ( $result[$batchItem[0]] ) {
2421
				$newBatch[] = $batchItem;
2422
			}
2423
		}
2424
2425
		return Status::newGood( $newBatch );
2426
	}
2427
}
2428
2429
# ------------------------------------------------------------------------------
2430
2431
/**
2432
 * Helper class for file undeletion
2433
 * @ingroup FileAbstraction
2434
 */
2435
class LocalFileRestoreBatch {
2436
	/** @var LocalFile */
2437
	private $file;
2438
2439
	/** @var array List of file IDs to restore */
2440
	private $cleanupBatch;
2441
2442
	/** @var array List of file IDs to restore */
2443
	private $ids;
2444
2445
	/** @var bool Add all revisions of the file */
2446
	private $all;
2447
2448
	/** @var bool Whether to remove all settings for suppressed fields */
2449
	private $unsuppress = false;
2450
2451
	/**
2452
	 * @param File $file
2453
	 * @param bool $unsuppress
2454
	 */
2455
	function __construct( File $file, $unsuppress = false ) {
2456
		$this->file = $file;
0 ignored issues
show
Documentation Bug introduced by
$file is of type object<File>, but the property $file was declared to be of type object<LocalFile>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
2457
		$this->cleanupBatch = $this->ids = [];
2458
		$this->ids = [];
2459
		$this->unsuppress = $unsuppress;
2460
	}
2461
2462
	/**
2463
	 * Add a file by ID
2464
	 * @param int $fa_id
2465
	 */
2466
	public function addId( $fa_id ) {
2467
		$this->ids[] = $fa_id;
2468
	}
2469
2470
	/**
2471
	 * Add a whole lot of files by ID
2472
	 * @param int[] $ids
2473
	 */
2474
	public function addIds( $ids ) {
2475
		$this->ids = array_merge( $this->ids, $ids );
2476
	}
2477
2478
	/**
2479
	 * Add all revisions of the file
2480
	 */
2481
	public function addAll() {
2482
		$this->all = true;
2483
	}
2484
2485
	/**
2486
	 * Run the transaction, except the cleanup batch.
2487
	 * The cleanup batch should be run in a separate transaction, because it locks different
2488
	 * rows and there's no need to keep the image row locked while it's acquiring those locks
2489
	 * The caller may have its own transaction open.
2490
	 * So we save the batch and let the caller call cleanup()
2491
	 * @return Status
2492
	 */
2493
	public function execute() {
2494
		/** @var Language */
2495
		global $wgLang;
2496
2497
		$repo = $this->file->getRepo();
2498
		if ( !$this->all && !$this->ids ) {
2499
			// Do nothing
2500
			return $repo->newGood();
2501
		}
2502
2503
		$lockOwnsTrx = $this->file->lock();
2504
2505
		$dbw = $this->file->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2506
		$status = $this->file->repo->newGood();
2507
2508
		$exists = (bool)$dbw->selectField( 'image', '1',
2509
			[ 'img_name' => $this->file->getName() ],
2510
			__METHOD__,
2511
			// The lock() should already prevents changes, but this still may need
2512
			// to bypass any transaction snapshot. However, if lock() started the
2513
			// trx (which it probably did) then snapshot is post-lock and up-to-date.
2514
			$lockOwnsTrx ? [] : [ 'LOCK IN SHARE MODE' ]
2515
		);
2516
2517
		// Fetch all or selected archived revisions for the file,
2518
		// sorted from the most recent to the oldest.
2519
		$conditions = [ 'fa_name' => $this->file->getName() ];
2520
2521
		if ( !$this->all ) {
2522
			$conditions['fa_id'] = $this->ids;
2523
		}
2524
2525
		$result = $dbw->select(
2526
			'filearchive',
2527
			ArchivedFile::selectFields(),
2528
			$conditions,
2529
			__METHOD__,
2530
			[ 'ORDER BY' => 'fa_timestamp DESC' ]
2531
		);
2532
2533
		$idsPresent = [];
2534
		$storeBatch = [];
2535
		$insertBatch = [];
2536
		$insertCurrent = false;
2537
		$deleteIds = [];
2538
		$first = true;
2539
		$archiveNames = [];
2540
2541
		foreach ( $result as $row ) {
2542
			$idsPresent[] = $row->fa_id;
2543
2544
			if ( $row->fa_name != $this->file->getName() ) {
2545
				$status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
2546
				$status->failCount++;
2547
				continue;
2548
			}
2549
2550
			if ( $row->fa_storage_key == '' ) {
2551
				// Revision was missing pre-deletion
2552
				$status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
2553
				$status->failCount++;
2554
				continue;
2555
			}
2556
2557
			$deletedRel = $repo->getDeletedHashPath( $row->fa_storage_key ) .
2558
				$row->fa_storage_key;
2559
			$deletedUrl = $repo->getVirtualUrl() . '/deleted/' . $deletedRel;
2560
2561
			if ( isset( $row->fa_sha1 ) ) {
2562
				$sha1 = $row->fa_sha1;
2563
			} else {
2564
				// old row, populate from key
2565
				$sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
2566
			}
2567
2568
			# Fix leading zero
2569
			if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
2570
				$sha1 = substr( $sha1, 1 );
2571
			}
2572
2573
			if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
2574
				|| is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
2575
				|| is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
2576
				|| is_null( $row->fa_metadata )
2577
			) {
2578
				// Refresh our metadata
2579
				// Required for a new current revision; nice for older ones too. :)
2580
				$props = RepoGroup::singleton()->getFileProps( $deletedUrl );
2581
			} else {
2582
				$props = [
2583
					'minor_mime' => $row->fa_minor_mime,
2584
					'major_mime' => $row->fa_major_mime,
2585
					'media_type' => $row->fa_media_type,
2586
					'metadata' => $row->fa_metadata
2587
				];
2588
			}
2589
2590
			if ( $first && !$exists ) {
2591
				// This revision will be published as the new current version
2592
				$destRel = $this->file->getRel();
2593
				$insertCurrent = [
2594
					'img_name' => $row->fa_name,
2595
					'img_size' => $row->fa_size,
2596
					'img_width' => $row->fa_width,
2597
					'img_height' => $row->fa_height,
2598
					'img_metadata' => $props['metadata'],
2599
					'img_bits' => $row->fa_bits,
2600
					'img_media_type' => $props['media_type'],
2601
					'img_major_mime' => $props['major_mime'],
2602
					'img_minor_mime' => $props['minor_mime'],
2603
					'img_description' => $row->fa_description,
2604
					'img_user' => $row->fa_user,
2605
					'img_user_text' => $row->fa_user_text,
2606
					'img_timestamp' => $row->fa_timestamp,
2607
					'img_sha1' => $sha1
2608
				];
2609
2610
				// The live (current) version cannot be hidden!
2611
				if ( !$this->unsuppress && $row->fa_deleted ) {
2612
					$status->fatal( 'undeleterevdel' );
2613
					$this->file->unlock();
2614
					return $status;
2615
				}
2616
			} else {
2617
				$archiveName = $row->fa_archive_name;
2618
2619
				if ( $archiveName == '' ) {
2620
					// This was originally a current version; we
2621
					// have to devise a new archive name for it.
2622
					// Format is <timestamp of archiving>!<name>
2623
					$timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
2624
2625
					do {
2626
						$archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
2627
						$timestamp++;
2628
					} while ( isset( $archiveNames[$archiveName] ) );
2629
				}
2630
2631
				$archiveNames[$archiveName] = true;
2632
				$destRel = $this->file->getArchiveRel( $archiveName );
2633
				$insertBatch[] = [
2634
					'oi_name' => $row->fa_name,
2635
					'oi_archive_name' => $archiveName,
2636
					'oi_size' => $row->fa_size,
2637
					'oi_width' => $row->fa_width,
2638
					'oi_height' => $row->fa_height,
2639
					'oi_bits' => $row->fa_bits,
2640
					'oi_description' => $row->fa_description,
2641
					'oi_user' => $row->fa_user,
2642
					'oi_user_text' => $row->fa_user_text,
2643
					'oi_timestamp' => $row->fa_timestamp,
2644
					'oi_metadata' => $props['metadata'],
2645
					'oi_media_type' => $props['media_type'],
2646
					'oi_major_mime' => $props['major_mime'],
2647
					'oi_minor_mime' => $props['minor_mime'],
2648
					'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
2649
					'oi_sha1' => $sha1 ];
2650
			}
2651
2652
			$deleteIds[] = $row->fa_id;
2653
2654
			if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
2655
				// private files can stay where they are
2656
				$status->successCount++;
2657
			} else {
2658
				$storeBatch[] = [ $deletedUrl, 'public', $destRel ];
2659
				$this->cleanupBatch[] = $row->fa_storage_key;
2660
			}
2661
2662
			$first = false;
2663
		}
2664
2665
		unset( $result );
2666
2667
		// Add a warning to the status object for missing IDs
2668
		$missingIds = array_diff( $this->ids, $idsPresent );
2669
2670
		foreach ( $missingIds as $id ) {
2671
			$status->error( 'undelete-missing-filearchive', $id );
2672
		}
2673
2674
		if ( !$repo->hasSha1Storage() ) {
2675
			// Remove missing files from batch, so we don't get errors when undeleting them
2676
			$checkStatus = $this->removeNonexistentFiles( $storeBatch );
2677
			if ( !$checkStatus->isGood() ) {
2678
				$status->merge( $checkStatus );
2679
				return $status;
2680
			}
2681
			$storeBatch = $checkStatus->value;
2682
2683
			// Run the store batch
2684
			// Use the OVERWRITE_SAME flag to smooth over a common error
2685
			$storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
2686
			$status->merge( $storeStatus );
2687
2688
			if ( !$status->isGood() ) {
2689
				// Even if some files could be copied, fail entirely as that is the
2690
				// easiest thing to do without data loss
2691
				$this->cleanupFailedBatch( $storeStatus, $storeBatch );
2692
				$status->setOK( false );
2693
				$this->file->unlock();
2694
2695
				return $status;
2696
			}
2697
		}
2698
2699
		// Run the DB updates
2700
		// Because we have locked the image row, key conflicts should be rare.
2701
		// If they do occur, we can roll back the transaction at this time with
2702
		// no data loss, but leaving unregistered files scattered throughout the
2703
		// public zone.
2704
		// This is not ideal, which is why it's important to lock the image row.
2705
		if ( $insertCurrent ) {
2706
			$dbw->insert( 'image', $insertCurrent, __METHOD__ );
2707
		}
2708
2709
		if ( $insertBatch ) {
2710
			$dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
2711
		}
2712
2713
		if ( $deleteIds ) {
2714
			$dbw->delete( 'filearchive',
2715
				[ 'fa_id' => $deleteIds ],
2716
				__METHOD__ );
2717
		}
2718
2719
		// If store batch is empty (all files are missing), deletion is to be considered successful
2720
		if ( $status->successCount > 0 || !$storeBatch || $repo->hasSha1Storage() ) {
2721
			if ( !$exists ) {
2722
				wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
2723
2724
				DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2725
2726
				$this->file->purgeEverything();
2727
			} else {
2728
				wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
2729
				$this->file->purgeDescription();
2730
			}
2731
		}
2732
2733
		$this->file->unlock();
2734
2735
		return $status;
2736
	}
2737
2738
	/**
2739
	 * Removes non-existent files from a store batch.
2740
	 * @param array $triplets
2741
	 * @return Status
2742
	 */
2743
	protected function removeNonexistentFiles( $triplets ) {
2744
		$files = $filteredTriplets = [];
2745
		foreach ( $triplets as $file ) {
2746
			$files[$file[0]] = $file[0];
2747
		}
2748
2749
		$result = $this->file->repo->fileExistsBatch( $files );
2750 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2751
			return Status::newFatal( 'backend-fail-internal',
2752
				$this->file->repo->getBackend()->getName() );
2753
		}
2754
2755
		foreach ( $triplets as $file ) {
2756
			if ( $result[$file[0]] ) {
2757
				$filteredTriplets[] = $file;
2758
			}
2759
		}
2760
2761
		return Status::newGood( $filteredTriplets );
2762
	}
2763
2764
	/**
2765
	 * Removes non-existent files from a cleanup batch.
2766
	 * @param array $batch
2767
	 * @return array
2768
	 */
2769
	protected function removeNonexistentFromCleanup( $batch ) {
2770
		$files = $newBatch = [];
2771
		$repo = $this->file->repo;
2772
2773
		foreach ( $batch as $file ) {
2774
			$files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
2775
				rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
2776
		}
2777
2778
		$result = $repo->fileExistsBatch( $files );
2779
2780
		foreach ( $batch as $file ) {
2781
			if ( $result[$file] ) {
2782
				$newBatch[] = $file;
2783
			}
2784
		}
2785
2786
		return $newBatch;
2787
	}
2788
2789
	/**
2790
	 * Delete unused files in the deleted zone.
2791
	 * This should be called from outside the transaction in which execute() was called.
2792
	 * @return Status
2793
	 */
2794
	public function cleanup() {
2795
		if ( !$this->cleanupBatch ) {
2796
			return $this->file->repo->newGood();
2797
		}
2798
2799
		$this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
2800
2801
		$status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
2802
2803
		return $status;
2804
	}
2805
2806
	/**
2807
	 * Cleanup a failed batch. The batch was only partially successful, so
2808
	 * rollback by removing all items that were succesfully copied.
2809
	 *
2810
	 * @param Status $storeStatus
2811
	 * @param array $storeBatch
2812
	 */
2813
	protected function cleanupFailedBatch( $storeStatus, $storeBatch ) {
2814
		$cleanupBatch = [];
2815
2816
		foreach ( $storeStatus->success as $i => $success ) {
2817
			// Check if this item of the batch was successfully copied
2818
			if ( $success ) {
2819
				// Item was successfully copied and needs to be removed again
2820
				// Extract ($dstZone, $dstRel) from the batch
2821
				$cleanupBatch[] = [ $storeBatch[$i][1], $storeBatch[$i][2] ];
2822
			}
2823
		}
2824
		$this->file->repo->cleanupBatch( $cleanupBatch );
2825
	}
2826
}
2827
2828
# ------------------------------------------------------------------------------
2829
2830
/**
2831
 * Helper class for file movement
2832
 * @ingroup FileAbstraction
2833
 */
2834
class LocalFileMoveBatch {
2835
	/** @var LocalFile */
2836
	protected $file;
2837
2838
	/** @var Title */
2839
	protected $target;
2840
2841
	protected $cur;
2842
2843
	protected $olds;
2844
2845
	protected $oldCount;
2846
2847
	protected $archive;
2848
2849
	/** @var IDatabase */
2850
	protected $db;
2851
2852
	/**
2853
	 * @param File $file
2854
	 * @param Title $target
2855
	 */
2856
	function __construct( File $file, Title $target ) {
2857
		$this->file = $file;
0 ignored issues
show
Documentation Bug introduced by
$file is of type object<File>, but the property $file was declared to be of type object<LocalFile>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
2858
		$this->target = $target;
2859
		$this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
0 ignored issues
show
Bug introduced by
The property oldHash does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2860
		$this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
0 ignored issues
show
Bug introduced by
The property newHash does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2861
		$this->oldName = $this->file->getName();
0 ignored issues
show
Bug introduced by
The property oldName does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2862
		$this->newName = $this->file->repo->getNameFromTitle( $this->target );
0 ignored issues
show
Bug introduced by
The property newName does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2863
		$this->oldRel = $this->oldHash . $this->oldName;
0 ignored issues
show
Bug introduced by
The property oldRel does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2864
		$this->newRel = $this->newHash . $this->newName;
0 ignored issues
show
Bug introduced by
The property newRel does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2865
		$this->db = $file->getRepo()->getMasterDB();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class FileRepo as the method getMasterDB() does only exist in the following sub-classes of FileRepo: ForeignDBRepo, ForeignDBViaLBRepo, LocalRepo. 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...
2866
	}
2867
2868
	/**
2869
	 * Add the current image to the batch
2870
	 */
2871
	public function addCurrent() {
2872
		$this->cur = [ $this->oldRel, $this->newRel ];
2873
	}
2874
2875
	/**
2876
	 * Add the old versions of the image to the batch
2877
	 * @return array List of archive names from old versions
2878
	 */
2879
	public function addOlds() {
2880
		$archiveBase = 'archive';
2881
		$this->olds = [];
2882
		$this->oldCount = 0;
2883
		$archiveNames = [];
2884
2885
		$result = $this->db->select( 'oldimage',
2886
			[ 'oi_archive_name', 'oi_deleted' ],
2887
			[ 'oi_name' => $this->oldName ],
2888
			__METHOD__,
2889
			[ 'LOCK IN SHARE MODE' ] // ignore snapshot
2890
		);
2891
2892
		foreach ( $result as $row ) {
0 ignored issues
show
Bug introduced by
The expression $result of type object<ResultWrapper>|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...
2893
			$archiveNames[] = $row->oi_archive_name;
2894
			$oldName = $row->oi_archive_name;
2895
			$bits = explode( '!', $oldName, 2 );
2896
2897
			if ( count( $bits ) != 2 ) {
2898
				wfDebug( "Old file name missing !: '$oldName' \n" );
2899
				continue;
2900
			}
2901
2902
			list( $timestamp, $filename ) = $bits;
2903
2904
			if ( $this->oldName != $filename ) {
2905
				wfDebug( "Old file name doesn't match: '$oldName' \n" );
2906
				continue;
2907
			}
2908
2909
			$this->oldCount++;
2910
2911
			// Do we want to add those to oldCount?
2912
			if ( $row->oi_deleted & File::DELETED_FILE ) {
2913
				continue;
2914
			}
2915
2916
			$this->olds[] = [
2917
				"{$archiveBase}/{$this->oldHash}{$oldName}",
2918
				"{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
2919
			];
2920
		}
2921
2922
		return $archiveNames;
2923
	}
2924
2925
	/**
2926
	 * Perform the move.
2927
	 * @return Status
2928
	 */
2929
	public function execute() {
2930
		$repo = $this->file->repo;
2931
		$status = $repo->newGood();
2932
		$destFile = wfLocalFile( $this->target );
2933
2934
		$this->file->lock(); // begin
2935
		$destFile->lock(); // quickly fail if destination is not available
2936
2937
		$triplets = $this->getMoveTriplets();
2938
		$checkStatus = $this->removeNonexistentFiles( $triplets );
2939
		if ( !$checkStatus->isGood() ) {
2940
			$destFile->unlock();
2941
			$this->file->unlock();
2942
			$status->merge( $checkStatus ); // couldn't talk to file backend
2943
			return $status;
2944
		}
2945
		$triplets = $checkStatus->value;
2946
2947
		// Verify the file versions metadata in the DB.
2948
		$statusDb = $this->verifyDBUpdates();
2949
		if ( !$statusDb->isGood() ) {
2950
			$destFile->unlock();
2951
			$this->file->unlock();
2952
			$statusDb->setOK( false );
2953
2954
			return $statusDb;
2955
		}
2956
2957
		if ( !$repo->hasSha1Storage() ) {
2958
			// Copy the files into their new location.
2959
			// If a prior process fataled copying or cleaning up files we tolerate any
2960
			// of the existing files if they are identical to the ones being stored.
2961
			$statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
2962
			wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
2963
				"{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
2964
			if ( !$statusMove->isGood() ) {
2965
				// Delete any files copied over (while the destination is still locked)
2966
				$this->cleanupTarget( $triplets );
2967
				$destFile->unlock();
2968
				$this->file->unlock();
2969
				wfDebugLog( 'imagemove', "Error in moving files: "
2970
					. $statusMove->getWikiText( false, false, 'en' ) );
2971
				$statusMove->setOK( false );
2972
2973
				return $statusMove;
2974
			}
2975
			$status->merge( $statusMove );
2976
		}
2977
2978
		// Rename the file versions metadata in the DB.
2979
		$this->doDBUpdates();
2980
2981
		wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
2982
			"{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
2983
2984
		$destFile->unlock();
2985
		$this->file->unlock(); // done
2986
2987
		// Everything went ok, remove the source files
2988
		$this->cleanupSource( $triplets );
2989
2990
		$status->merge( $statusDb );
2991
2992
		return $status;
2993
	}
2994
2995
	/**
2996
	 * Verify the database updates and return a new FileRepoStatus indicating how
2997
	 * many rows would be updated.
2998
	 *
2999
	 * @return Status
3000
	 */
3001
	protected function verifyDBUpdates() {
3002
		$repo = $this->file->repo;
3003
		$status = $repo->newGood();
3004
		$dbw = $this->db;
3005
3006
		$hasCurrent = $dbw->selectField(
3007
			'image',
3008
			'1',
3009
			[ 'img_name' => $this->oldName ],
3010
			__METHOD__,
3011
			[ 'FOR UPDATE' ]
3012
		);
3013
		$oldRowCount = $dbw->selectField(
3014
			'oldimage',
3015
			'COUNT(*)',
3016
			[ 'oi_name' => $this->oldName ],
3017
			__METHOD__,
3018
			[ 'FOR UPDATE' ]
3019
		);
3020
3021
		if ( $hasCurrent ) {
3022
			$status->successCount++;
3023
		} else {
3024
			$status->failCount++;
3025
		}
3026
		$status->successCount += $oldRowCount;
3027
		// Bug 34934: oldCount is based on files that actually exist.
3028
		// There may be more DB rows than such files, in which case $affected
3029
		// can be greater than $total. We use max() to avoid negatives here.
3030
		$status->failCount += max( 0, $this->oldCount - $oldRowCount );
3031
		if ( $status->failCount ) {
3032
			$status->error( 'imageinvalidfilename' );
3033
		}
3034
3035
		return $status;
3036
	}
3037
3038
	/**
3039
	 * Do the database updates and return a new FileRepoStatus indicating how
3040
	 * many rows where updated.
3041
	 */
3042
	protected function doDBUpdates() {
3043
		$dbw = $this->db;
3044
3045
		// Update current image
3046
		$dbw->update(
3047
			'image',
3048
			[ 'img_name' => $this->newName ],
3049
			[ 'img_name' => $this->oldName ],
3050
			__METHOD__
3051
		);
3052
		// Update old images
3053
		$dbw->update(
3054
			'oldimage',
3055
			[
3056
				'oi_name' => $this->newName,
3057
				'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
3058
					$dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
3059
			],
3060
			[ 'oi_name' => $this->oldName ],
3061
			__METHOD__
3062
		);
3063
	}
3064
3065
	/**
3066
	 * Generate triplets for FileRepo::storeBatch().
3067
	 * @return array
3068
	 */
3069
	protected function getMoveTriplets() {
3070
		$moves = array_merge( [ $this->cur ], $this->olds );
3071
		$triplets = []; // The format is: (srcUrl, destZone, destUrl)
3072
3073
		foreach ( $moves as $move ) {
3074
			// $move: (oldRelativePath, newRelativePath)
3075
			$srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
3076
			$triplets[] = [ $srcUrl, 'public', $move[1] ];
3077
			wfDebugLog(
3078
				'imagemove',
3079
				"Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
3080
			);
3081
		}
3082
3083
		return $triplets;
3084
	}
3085
3086
	/**
3087
	 * Removes non-existent files from move batch.
3088
	 * @param array $triplets
3089
	 * @return Status
3090
	 */
3091
	protected function removeNonexistentFiles( $triplets ) {
3092
		$files = [];
3093
3094
		foreach ( $triplets as $file ) {
3095
			$files[$file[0]] = $file[0];
3096
		}
3097
3098
		$result = $this->file->repo->fileExistsBatch( $files );
3099 View Code Duplication
		if ( in_array( null, $result, true ) ) {
3100
			return Status::newFatal( 'backend-fail-internal',
3101
				$this->file->repo->getBackend()->getName() );
3102
		}
3103
3104
		$filteredTriplets = [];
3105
		foreach ( $triplets as $file ) {
3106
			if ( $result[$file[0]] ) {
3107
				$filteredTriplets[] = $file;
3108
			} else {
3109
				wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
3110
			}
3111
		}
3112
3113
		return Status::newGood( $filteredTriplets );
3114
	}
3115
3116
	/**
3117
	 * Cleanup a partially moved array of triplets by deleting the target
3118
	 * files. Called if something went wrong half way.
3119
	 * @param array $triplets
3120
	 */
3121
	protected function cleanupTarget( $triplets ) {
3122
		// Create dest pairs from the triplets
3123
		$pairs = [];
3124
		foreach ( $triplets as $triplet ) {
3125
			// $triplet: (old source virtual URL, dst zone, dest rel)
3126
			$pairs[] = [ $triplet[1], $triplet[2] ];
3127
		}
3128
3129
		$this->file->repo->cleanupBatch( $pairs );
3130
	}
3131
3132
	/**
3133
	 * Cleanup a fully moved array of triplets by deleting the source files.
3134
	 * Called at the end of the move process if everything else went ok.
3135
	 * @param array $triplets
3136
	 */
3137
	protected function cleanupSource( $triplets ) {
3138
		// Create source file names from the triplets
3139
		$files = [];
3140
		foreach ( $triplets as $triplet ) {
3141
			$files[] = $triplet[0];
3142
		}
3143
3144
		$this->file->repo->cleanupBatch( $files );
3145
	}
3146
}
3147
3148
class LocalFileLockError extends ErrorPageError {
3149
	public function __construct( Status $status ) {
3150
		parent::__construct(
3151
			'actionfailed',
3152
			$status->getMessage()
3153
		);
3154
	}
3155
3156
	public function report() {
3157
		global $wgOut;
3158
		$wgOut->setStatusCode( 429 );
3159
		parent::report();
3160
	}
3161
}
3162