Completed
Branch master (b92a94)
by
unknown
34:34
created

LocalFileDeleteBatch::doDBInserts()   D

Complexity

Conditions 9
Paths 48

Size

Total Lines 106
Code Lines 84

Duplication

Lines 10
Ratio 9.43 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 84
c 1
b 0
f 0
nc 48
nop 0
dl 10
loc 106
rs 4.8196

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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
 * Bump this number when serialized cache records may be incompatible.
28
 */
29
define( 'MW_FILE_VERSION', 9 );
30
31
/**
32
 * Class to represent a local file in the wiki's own database
33
 *
34
 * Provides methods to retrieve paths (physical, logical, URL),
35
 * to generate image thumbnails or for uploading.
36
 *
37
 * Note that only the repo object knows what its file class is called. You should
38
 * never name a file class explictly outside of the repo class. Instead use the
39
 * repo's factory functions to generate file objects, for example:
40
 *
41
 * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
42
 *
43
 * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
44
 * in most cases.
45
 *
46
 * @ingroup FileAbstraction
47
 */
48
class LocalFile extends File {
49
	const CACHE_FIELD_MAX_LEN = 1000;
50
51
	/** @var bool Does the file exist on disk? (loadFromXxx) */
52
	protected $fileExists;
53
54
	/** @var int Image width */
55
	protected $width;
56
57
	/** @var int Image height */
58
	protected $height;
59
60
	/** @var int Returned by getimagesize (loadFromXxx) */
61
	protected $bits;
62
63
	/** @var string MEDIATYPE_xxx (bitmap, drawing, audio...) */
64
	protected $media_type;
65
66
	/** @var string MIME type, determined by MimeMagic::guessMimeType */
67
	protected $mime;
68
69
	/** @var int Size in bytes (loadFromXxx) */
70
	protected $size;
71
72
	/** @var string Handler-specific metadata */
73
	protected $metadata;
74
75
	/** @var string SHA-1 base 36 content hash */
76
	protected $sha1;
77
78
	/** @var bool Whether or not core data has been loaded from the database (loadFromXxx) */
79
	protected $dataLoaded;
80
81
	/** @var bool Whether or not lazy-loaded data has been loaded from the database */
82
	protected $extraDataLoaded;
83
84
	/** @var int Bitfield akin to rev_deleted */
85
	protected $deleted;
86
87
	/** @var string */
88
	protected $repoClass = 'LocalRepo';
89
90
	/** @var int Number of line to return by nextHistoryLine() (constructor) */
91
	private $historyLine;
92
93
	/** @var int Result of the query for the file's history (nextHistoryLine) */
94
	private $historyRes;
95
96
	/** @var string Major MIME type */
97
	private $major_mime;
98
99
	/** @var string Minor MIME type */
100
	private $minor_mime;
101
102
	/** @var string Upload timestamp */
103
	private $timestamp;
104
105
	/** @var int User ID of uploader */
106
	private $user;
107
108
	/** @var string User name of uploader */
109
	private $user_text;
110
111
	/** @var string Description of current revision of the file */
112
	private $description;
113
114
	/** @var string TS_MW timestamp of the last change of the file description */
115
	private $descriptionTouched;
116
117
	/** @var bool Whether the row was upgraded on load */
118
	private $upgraded;
119
120
	/** @var bool Whether the row was scheduled to upgrade on load */
121
	private $upgrading;
122
123
	/** @var bool True if the image row is locked */
124
	private $locked;
125
126
	/** @var bool True if the image row is locked with a lock initiated transaction */
127
	private $lockedOwnTrx;
128
129
	/** @var bool True if file is not present in file system. Not to be cached in memcached */
130
	private $missing;
131
132
	// @note: higher than IDBAccessObject constants
133
	const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
134
135
	const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
136
137
	/**
138
	 * Create a LocalFile from a title
139
	 * Do not call this except from inside a repo class.
140
	 *
141
	 * Note: $unused param is only here to avoid an E_STRICT
142
	 *
143
	 * @param Title $title
144
	 * @param FileRepo $repo
145
	 * @param null $unused
146
	 *
147
	 * @return LocalFile
148
	 */
149
	static function newFromTitle( $title, $repo, $unused = null ) {
150
		return new self( $title, $repo );
151
	}
152
153
	/**
154
	 * Create a LocalFile from a title
155
	 * Do not call this except from inside a repo class.
156
	 *
157
	 * @param stdClass $row
158
	 * @param FileRepo $repo
159
	 *
160
	 * @return LocalFile
161
	 */
162 View Code Duplication
	static function newFromRow( $row, $repo ) {
163
		$title = Title::makeTitle( NS_FILE, $row->img_name );
164
		$file = new self( $title, $repo );
165
		$file->loadFromRow( $row );
166
167
		return $file;
168
	}
169
170
	/**
171
	 * Create a LocalFile from a SHA-1 key
172
	 * Do not call this except from inside a repo class.
173
	 *
174
	 * @param string $sha1 Base-36 SHA-1
175
	 * @param LocalRepo $repo
176
	 * @param string|bool $timestamp MW_timestamp (optional)
177
	 * @return bool|LocalFile
178
	 */
179 View Code Duplication
	static function newFromKey( $sha1, $repo, $timestamp = false ) {
180
		$dbr = $repo->getSlaveDB();
181
182
		$conds = [ 'img_sha1' => $sha1 ];
183
		if ( $timestamp ) {
184
			$conds['img_timestamp'] = $dbr->timestamp( $timestamp );
185
		}
186
187
		$row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
188
		if ( $row ) {
189
			return self::newFromRow( $row, $repo );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $dbr->selectRow('image',...(), $conds, __METHOD__) on line 187 can also be of type boolean; however, LocalFile::newFromRow() does only seem to accept object<stdClass>, 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...
190
		} else {
191
			return false;
192
		}
193
	}
194
195
	/**
196
	 * Fields in the image table
197
	 * @return array
198
	 */
199
	static function selectFields() {
200
		return [
201
			'img_name',
202
			'img_size',
203
			'img_width',
204
			'img_height',
205
			'img_metadata',
206
			'img_bits',
207
			'img_media_type',
208
			'img_major_mime',
209
			'img_minor_mime',
210
			'img_description',
211
			'img_user',
212
			'img_user_text',
213
			'img_timestamp',
214
			'img_sha1',
215
		];
216
	}
217
218
	/**
219
	 * Constructor.
220
	 * Do not call this except from inside a repo class.
221
	 * @param Title $title
222
	 * @param FileRepo $repo
223
	 */
224
	function __construct( $title, $repo ) {
225
		parent::__construct( $title, $repo );
226
227
		$this->metadata = '';
228
		$this->historyLine = 0;
229
		$this->historyRes = null;
230
		$this->dataLoaded = false;
231
		$this->extraDataLoaded = false;
232
233
		$this->assertRepoDefined();
234
		$this->assertTitleDefined();
235
	}
236
237
	/**
238
	 * Get the memcached key for the main data for this file, or false if
239
	 * there is no access to the shared cache.
240
	 * @return string|bool
241
	 */
242
	function getCacheKey() {
243
		$hashedName = md5( $this->getName() );
244
245
		return $this->repo->getSharedCacheKey( 'file', $hashedName );
246
	}
247
248
	/**
249
	 * Try to load file metadata from memcached. Returns true on success.
250
	 * @return bool
251
	 */
252
	private function loadFromCache() {
253
		$this->dataLoaded = false;
254
		$this->extraDataLoaded = false;
255
		$key = $this->getCacheKey();
256
257
		if ( !$key ) {
258
			return false;
259
		}
260
261
		$cache = ObjectCache::getMainWANInstance();
262
		$cachedValues = $cache->get( $key );
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 255 can also be of type boolean; however, WANObjectCache::get() 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...
263
264
		// Check if the key existed and belongs to this version of MediaWiki
265
		if ( is_array( $cachedValues ) && $cachedValues['version'] == MW_FILE_VERSION ) {
266
			$this->fileExists = $cachedValues['fileExists'];
267
			if ( $this->fileExists ) {
268
				$this->setProps( $cachedValues );
269
			}
270
			$this->dataLoaded = true;
271
			$this->extraDataLoaded = true;
272
			foreach ( $this->getLazyCacheFields( '' ) as $field ) {
273
				$this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
274
			}
275
		}
276
277
		return $this->dataLoaded;
278
	}
279
280
	/**
281
	 * Save the file metadata to memcached
282
	 */
283
	private function saveToCache() {
284
		$this->load();
285
286
		$key = $this->getCacheKey();
287
		if ( !$key ) {
288
			return;
289
		}
290
291
		$fields = $this->getCacheFields( '' );
292
		$cacheVal = [ 'version' => MW_FILE_VERSION ];
293
		$cacheVal['fileExists'] = $this->fileExists;
294
295
		if ( $this->fileExists ) {
296
			foreach ( $fields as $field ) {
297
				$cacheVal[$field] = $this->$field;
298
			}
299
		}
300
301
		// Strip off excessive entries from the subset of fields that can become large.
302
		// If the cache value gets to large it will not fit in memcached and nothing will
303
		// get cached at all, causing master queries for any file access.
304
		foreach ( $this->getLazyCacheFields( '' ) as $field ) {
305
			if ( isset( $cacheVal[$field] ) && strlen( $cacheVal[$field] ) > 100 * 1024 ) {
306
				unset( $cacheVal[$field] ); // don't let the value get too big
307
			}
308
		}
309
310
		// Cache presence for 1 week and negatives for 1 day
311
		$ttl = $this->fileExists ? 86400 * 7 : 86400;
312
		$opts = 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...
313
		ObjectCache::getMainWANInstance()->set( $key, $cacheVal, $ttl, $opts );
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 286 can also be of type boolean; however, WANObjectCache::set() 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...
314
	}
315
316
	/**
317
	 * Purge the file object/metadata cache
318
	 */
319
	public function invalidateCache() {
320
		$key = $this->getCacheKey();
321
		if ( !$key ) {
322
			return;
323
		}
324
325
		$this->repo->getMasterDB()->onTransactionPreCommitOrIdle( function() use ( $key ) {
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...
326
			ObjectCache::getMainWANInstance()->delete( $key );
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 320 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...
327
		} );
328
	}
329
330
	/**
331
	 * Load metadata from the file itself
332
	 */
333
	function loadFromFile() {
334
		$props = $this->repo->getFileProps( $this->getVirtualUrl() );
335
		$this->setProps( $props );
336
	}
337
338
	/**
339
	 * @param string $prefix
340
	 * @return array
341
	 */
342
	function getCacheFields( $prefix = 'img_' ) {
343
		static $fields = [ 'size', 'width', 'height', 'bits', 'media_type',
344
			'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
345
			'user_text', 'description' ];
346
		static $results = [];
347
348
		if ( $prefix == '' ) {
349
			return $fields;
350
		}
351
352 View Code Duplication
		if ( !isset( $results[$prefix] ) ) {
353
			$prefixedFields = [];
354
			foreach ( $fields as $field ) {
355
				$prefixedFields[] = $prefix . $field;
356
			}
357
			$results[$prefix] = $prefixedFields;
358
		}
359
360
		return $results[$prefix];
361
	}
362
363
	/**
364
	 * @param string $prefix
365
	 * @return array
366
	 */
367
	function getLazyCacheFields( $prefix = 'img_' ) {
368
		static $fields = [ 'metadata' ];
369
		static $results = [];
370
371
		if ( $prefix == '' ) {
372
			return $fields;
373
		}
374
375 View Code Duplication
		if ( !isset( $results[$prefix] ) ) {
376
			$prefixedFields = [];
377
			foreach ( $fields as $field ) {
378
				$prefixedFields[] = $prefix . $field;
379
			}
380
			$results[$prefix] = $prefixedFields;
381
		}
382
383
		return $results[$prefix];
384
	}
385
386
	/**
387
	 * Load file metadata from the DB
388
	 * @param int $flags
389
	 */
390
	function loadFromDB( $flags = 0 ) {
391
		$fname = get_class( $this ) . '::' . __FUNCTION__;
392
393
		# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
394
		$this->dataLoaded = true;
395
		$this->extraDataLoaded = true;
396
397
		$dbr = ( $flags & self::READ_LATEST )
398
			? $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...
399
			: $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...
400
401
		$row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
402
			[ 'img_name' => $this->getName() ], $fname );
403
404
		if ( $row ) {
405
			$this->loadFromRow( $row );
406
		} else {
407
			$this->fileExists = false;
408
		}
409
	}
410
411
	/**
412
	 * Load lazy file metadata from the DB.
413
	 * This covers fields that are sometimes not cached.
414
	 */
415
	protected function loadExtraFromDB() {
416
		$fname = get_class( $this ) . '::' . __FUNCTION__;
417
418
		# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
419
		$this->extraDataLoaded = true;
420
421
		$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...
422
		if ( !$fieldMap ) {
423
			$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...
424
		}
425
426
		if ( $fieldMap ) {
427
			foreach ( $fieldMap as $name => $value ) {
428
				$this->$name = $value;
429
			}
430
		} else {
431
			throw new MWException( "Could not find data for image '{$this->getName()}'." );
432
		}
433
	}
434
435
	/**
436
	 * @param IDatabase $dbr
437
	 * @param string $fname
438
	 * @return array|bool
439
	 */
440
	private function loadFieldsWithTimestamp( $dbr, $fname ) {
441
		$fieldMap = false;
442
443
		$row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
444
			[ 'img_name' => $this->getName(), 'img_timestamp' => $this->getTimestamp() ],
445
			$fname );
446
		if ( $row ) {
447
			$fieldMap = $this->unprefixRow( $row, 'img_' );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $dbr->selectRow('image',...etTimestamp()), $fname) on line 443 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...
448
		} else {
449
			# File may have been uploaded over in the meantime; check the old versions
450
			$row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
451
				[ 'oi_name' => $this->getName(), 'oi_timestamp' => $this->getTimestamp() ],
452
				$fname );
453
			if ( $row ) {
454
				$fieldMap = $this->unprefixRow( $row, 'oi_' );
455
			}
456
		}
457
458
		return $fieldMap;
459
	}
460
461
	/**
462
	 * @param array|object $row
463
	 * @param string $prefix
464
	 * @throws MWException
465
	 * @return array
466
	 */
467
	protected function unprefixRow( $row, $prefix = 'img_' ) {
468
		$array = (array)$row;
469
		$prefixLength = strlen( $prefix );
470
471
		// Sanity check prefix once
472
		if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
473
			throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
474
		}
475
476
		$decoded = [];
477
		foreach ( $array as $name => $value ) {
478
			$decoded[substr( $name, $prefixLength )] = $value;
479
		}
480
481
		return $decoded;
482
	}
483
484
	/**
485
	 * Decode a row from the database (either object or array) to an array
486
	 * with timestamps and MIME types decoded, and the field prefix removed.
487
	 * @param object $row
488
	 * @param string $prefix
489
	 * @throws MWException
490
	 * @return array
491
	 */
492
	function decodeRow( $row, $prefix = 'img_' ) {
493
		$decoded = $this->unprefixRow( $row, $prefix );
494
495
		$decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
496
497
		$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...
498
499
		if ( empty( $decoded['major_mime'] ) ) {
500
			$decoded['mime'] = 'unknown/unknown';
501
		} else {
502
			if ( !$decoded['minor_mime'] ) {
503
				$decoded['minor_mime'] = 'unknown';
504
			}
505
			$decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
506
		}
507
508
		// Trim zero padding from char/binary field
509
		$decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
510
511
		// Normalize some fields to integer type, per their database definition.
512
		// Use unary + so that overflows will be upgraded to double instead of
513
		// being trucated as with intval(). This is important to allow >2GB
514
		// files on 32-bit systems.
515
		foreach ( [ 'size', 'width', 'height', 'bits' ] as $field ) {
516
			$decoded[$field] = +$decoded[$field];
517
		}
518
519
		return $decoded;
520
	}
521
522
	/**
523
	 * Load file metadata from a DB result row
524
	 *
525
	 * @param object $row
526
	 * @param string $prefix
527
	 */
528
	function loadFromRow( $row, $prefix = 'img_' ) {
529
		$this->dataLoaded = true;
530
		$this->extraDataLoaded = true;
531
532
		$array = $this->decodeRow( $row, $prefix );
533
534
		foreach ( $array as $name => $value ) {
535
			$this->$name = $value;
536
		}
537
538
		$this->fileExists = true;
539
		$this->maybeUpgradeRow();
540
	}
541
542
	/**
543
	 * Load file metadata from cache or DB, unless already loaded
544
	 * @param int $flags
545
	 */
546
	function load( $flags = 0 ) {
547
		if ( !$this->dataLoaded ) {
548
			if ( ( $flags & self::READ_LATEST ) || !$this->loadFromCache() ) {
549
				$this->loadFromDB( $flags );
550
				$this->saveToCache();
551
			}
552
			$this->dataLoaded = true;
553
		}
554
		if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
555
			// @note: loads on name/timestamp to reduce race condition problems
556
			$this->loadExtraFromDB();
557
		}
558
	}
559
560
	/**
561
	 * Upgrade a row if it needs it
562
	 */
563
	function maybeUpgradeRow() {
564
		global $wgUpdateCompatibleMetadata;
565
566
		if ( wfReadOnly() || $this->upgrading ) {
567
			return;
568
		}
569
570
		$upgrade = false;
571
		if ( is_null( $this->media_type ) || $this->mime == 'image/svg' ) {
572
			$upgrade = true;
573
		} else {
574
			$handler = $this->getHandler();
575
			if ( $handler ) {
576
				$validity = $handler->isMetadataValid( $this, $this->getMetadata() );
577
				if ( $validity === MediaHandler::METADATA_BAD ) {
578
					$upgrade = true;
579
				} elseif ( $validity === MediaHandler::METADATA_COMPATIBLE ) {
580
					$upgrade = $wgUpdateCompatibleMetadata;
581
				}
582
			}
583
		}
584
585
		if ( $upgrade ) {
586
			$this->upgrading = true;
587
			// Defer updates unless in auto-commit CLI mode
588
			DeferredUpdates::addCallableUpdate( function() {
589
				$this->upgrading = false; // avoid duplicate updates
590
				try {
591
					$this->upgradeRow();
592
				} catch ( LocalFileLockError $e ) {
593
					// let the other process handle it (or do it next time)
594
				}
595
			} );
596
		}
597
	}
598
599
	/**
600
	 * @return bool Whether upgradeRow() ran for this object
601
	 */
602
	function getUpgraded() {
603
		return $this->upgraded;
604
	}
605
606
	/**
607
	 * Fix assorted version-related problems with the image row by reloading it from the file
608
	 */
609
	function upgradeRow() {
610
		$this->lock(); // begin
611
612
		$this->loadFromFile();
613
614
		# Don't destroy file info of missing files
615
		if ( !$this->fileExists ) {
616
			$this->unlock();
617
			wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
618
619
			return;
620
		}
621
622
		$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...
623
		list( $major, $minor ) = self::splitMime( $this->mime );
624
625
		if ( wfReadOnly() ) {
626
			$this->unlock();
627
628
			return;
629
		}
630
		wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
631
632
		$dbw->update( 'image',
633
			[
634
				'img_size' => $this->size, // sanity
635
				'img_width' => $this->width,
636
				'img_height' => $this->height,
637
				'img_bits' => $this->bits,
638
				'img_media_type' => $this->media_type,
639
				'img_major_mime' => $major,
640
				'img_minor_mime' => $minor,
641
				'img_metadata' => $dbw->encodeBlob( $this->metadata ),
642
				'img_sha1' => $this->sha1,
643
			],
644
			[ 'img_name' => $this->getName() ],
645
			__METHOD__
646
		);
647
648
		$this->invalidateCache();
649
650
		$this->unlock(); // done
651
		$this->upgraded = true; // avoid rework/retries
652
	}
653
654
	/**
655
	 * Set properties in this object to be equal to those given in the
656
	 * associative array $info. Only cacheable fields can be set.
657
	 * All fields *must* be set in $info except for getLazyCacheFields().
658
	 *
659
	 * If 'mime' is given, it will be split into major_mime/minor_mime.
660
	 * If major_mime/minor_mime are given, $this->mime will also be set.
661
	 *
662
	 * @param array $info
663
	 */
664
	function setProps( $info ) {
665
		$this->dataLoaded = true;
666
		$fields = $this->getCacheFields( '' );
667
		$fields[] = 'fileExists';
668
669
		foreach ( $fields as $field ) {
670
			if ( isset( $info[$field] ) ) {
671
				$this->$field = $info[$field];
672
			}
673
		}
674
675
		// Fix up mime fields
676
		if ( isset( $info['major_mime'] ) ) {
677
			$this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
678
		} elseif ( isset( $info['mime'] ) ) {
679
			$this->mime = $info['mime'];
680
			list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
681
		}
682
	}
683
684
	/** splitMime inherited */
685
	/** getName inherited */
686
	/** getTitle inherited */
687
	/** getURL inherited */
688
	/** getViewURL inherited */
689
	/** getPath inherited */
690
	/** isVisible inherited */
691
692
	/**
693
	 * @return bool
694
	 */
695
	function isMissing() {
696
		if ( $this->missing === null ) {
697
			list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
698
			$this->missing = !$fileExists;
699
		}
700
701
		return $this->missing;
702
	}
703
704
	/**
705
	 * Return the width of the image
706
	 *
707
	 * @param int $page
708
	 * @return int
709
	 */
710 View Code Duplication
	public function getWidth( $page = 1 ) {
711
		$this->load();
712
713
		if ( $this->isMultipage() ) {
714
			$handler = $this->getHandler();
715
			if ( !$handler ) {
716
				return 0;
717
			}
718
			$dim = $handler->getPageDimensions( $this, $page );
719
			if ( $dim ) {
720
				return $dim['width'];
721
			} else {
722
				// For non-paged media, the false goes through an
723
				// intval, turning failure into 0, so do same here.
724
				return 0;
725
			}
726
		} else {
727
			return $this->width;
728
		}
729
	}
730
731
	/**
732
	 * Return the height of the image
733
	 *
734
	 * @param int $page
735
	 * @return int
736
	 */
737 View Code Duplication
	public function getHeight( $page = 1 ) {
738
		$this->load();
739
740
		if ( $this->isMultipage() ) {
741
			$handler = $this->getHandler();
742
			if ( !$handler ) {
743
				return 0;
744
			}
745
			$dim = $handler->getPageDimensions( $this, $page );
746
			if ( $dim ) {
747
				return $dim['height'];
748
			} else {
749
				// For non-paged media, the false goes through an
750
				// intval, turning failure into 0, so do same here.
751
				return 0;
752
			}
753
		} else {
754
			return $this->height;
755
		}
756
	}
757
758
	/**
759
	 * Returns ID or name of user who uploaded the file
760
	 *
761
	 * @param string $type 'text' or 'id'
762
	 * @return int|string
763
	 */
764
	function getUser( $type = 'text' ) {
765
		$this->load();
766
767
		if ( $type == 'text' ) {
768
			return $this->user_text;
769
		} elseif ( $type == 'id' ) {
770
			return (int)$this->user;
771
		}
772
	}
773
774
	/**
775
	 * Get short description URL for a file based on the page ID.
776
	 *
777
	 * @return string|null
778
	 * @throws MWException
779
	 * @since 1.27
780
	 */
781
	public function getDescriptionShortUrl() {
782
		$pageId = $this->title->getArticleID();
783
784 View Code Duplication
		if ( $pageId !== null ) {
785
			$url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
786
			if ( $url !== false ) {
787
				return $url;
788
			}
789
		}
790
		return null;
791
	}
792
793
	/**
794
	 * Get handler-specific metadata
795
	 * @return string
796
	 */
797
	function getMetadata() {
798
		$this->load( self::LOAD_ALL ); // large metadata is loaded in another step
799
		return $this->metadata;
800
	}
801
802
	/**
803
	 * @return int
804
	 */
805
	function getBitDepth() {
806
		$this->load();
807
808
		return (int)$this->bits;
809
	}
810
811
	/**
812
	 * Returns the size of the image file, in bytes
813
	 * @return int
814
	 */
815
	public function getSize() {
816
		$this->load();
817
818
		return $this->size;
819
	}
820
821
	/**
822
	 * Returns the MIME type of the file.
823
	 * @return string
824
	 */
825
	function getMimeType() {
826
		$this->load();
827
828
		return $this->mime;
829
	}
830
831
	/**
832
	 * Returns the type of the media in the file.
833
	 * Use the value returned by this function with the MEDIATYPE_xxx constants.
834
	 * @return string
835
	 */
836
	function getMediaType() {
837
		$this->load();
838
839
		return $this->media_type;
840
	}
841
842
	/** canRender inherited */
843
	/** mustRender inherited */
844
	/** allowInlineDisplay inherited */
845
	/** isSafeFile inherited */
846
	/** isTrustedFile inherited */
847
848
	/**
849
	 * Returns true if the file exists on disk.
850
	 * @return bool Whether file exist on disk.
851
	 */
852
	public function exists() {
853
		$this->load();
854
855
		return $this->fileExists;
856
	}
857
858
	/** getTransformScript inherited */
859
	/** getUnscaledThumb inherited */
860
	/** thumbName inherited */
861
	/** createThumb inherited */
862
	/** transform inherited */
863
864
	/** getHandler inherited */
865
	/** iconThumb inherited */
866
	/** getLastError inherited */
867
868
	/**
869
	 * Get all thumbnail names previously generated for this file
870
	 * @param string|bool $archiveName Name of an archive file, default false
871
	 * @return array First element is the base dir, then files in that base dir.
872
	 */
873
	function getThumbnails( $archiveName = false ) {
874
		if ( $archiveName ) {
875
			$dir = $this->getArchiveThumbPath( $archiveName );
0 ignored issues
show
Bug introduced by
It seems like $archiveName defined by parameter $archiveName on line 873 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...
876
		} else {
877
			$dir = $this->getThumbPath();
878
		}
879
880
		$backend = $this->repo->getBackend();
881
		$files = [ $dir ];
882
		try {
883
			$iterator = $backend->getFileList( [ 'dir' => $dir ] );
884
			foreach ( $iterator as $file ) {
885
				$files[] = $file;
886
			}
887
		} catch ( FileBackendError $e ) {
888
		} // suppress (bug 54674)
889
890
		return $files;
891
	}
892
893
	/**
894
	 * Refresh metadata in memcached, but don't touch thumbnails or CDN
895
	 */
896
	function purgeMetadataCache() {
897
		$this->invalidateCache();
898
	}
899
900
	/**
901
	 * Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
902
	 *
903
	 * @param array $options An array potentially with the key forThumbRefresh.
904
	 *
905
	 * @note This used to purge old thumbnails by default as well, but doesn't anymore.
906
	 */
907
	function purgeCache( $options = [] ) {
908
		// Refresh metadata cache
909
		$this->purgeMetadataCache();
910
911
		// Delete thumbnails
912
		$this->purgeThumbnails( $options );
913
914
		// Purge CDN cache for this file
915
		DeferredUpdates::addUpdate(
916
			new CdnCacheUpdate( [ $this->getUrl() ] ),
917
			DeferredUpdates::PRESEND
918
		);
919
	}
920
921
	/**
922
	 * Delete cached transformed files for an archived version only.
923
	 * @param string $archiveName Name of the archived file
924
	 */
925
	function purgeOldThumbnails( $archiveName ) {
926
		// Get a list of old thumbnails and URLs
927
		$files = $this->getThumbnails( $archiveName );
928
929
		// Purge any custom thumbnail caches
930
		Hooks::run( 'LocalFilePurgeThumbnails', [ $this, $archiveName ] );
931
932
		// Delete thumbnails
933
		$dir = array_shift( $files );
934
		$this->purgeThumbList( $dir, $files );
935
936
		// Purge the CDN
937
		$urls = [];
938
		foreach ( $files as $file ) {
939
			$urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
940
		}
941
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
942
	}
943
944
	/**
945
	 * Delete cached transformed files for the current version only.
946
	 * @param array $options
947
	 */
948
	public function purgeThumbnails( $options = [] ) {
949
		$files = $this->getThumbnails();
950
		// Always purge all files from CDN regardless of handler filters
951
		$urls = [];
952
		foreach ( $files as $file ) {
953
			$urls[] = $this->getThumbUrl( $file );
954
		}
955
		array_shift( $urls ); // don't purge directory
956
957
		// Give media handler a chance to filter the file purge list
958
		if ( !empty( $options['forThumbRefresh'] ) ) {
959
			$handler = $this->getHandler();
960
			if ( $handler ) {
961
				$handler->filterThumbnailPurgeList( $files, $options );
962
			}
963
		}
964
965
		// Purge any custom thumbnail caches
966
		Hooks::run( 'LocalFilePurgeThumbnails', [ $this, false ] );
967
968
		// Delete thumbnails
969
		$dir = array_shift( $files );
970
		$this->purgeThumbList( $dir, $files );
971
972
		// Purge the CDN
973
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
974
	}
975
976
	/**
977
	 * Prerenders a configurable set of thumbnails
978
	 *
979
	 * @since 1.28
980
	 */
981
	public function prerenderThumbnails() {
982
		global $wgUploadThumbnailRenderMap;
983
984
		$jobs = [];
985
986
		$sizes = $wgUploadThumbnailRenderMap;
987
		rsort( $sizes );
988
989
		foreach ( $sizes as $size ) {
990
			if ( $this->isVectorized() || $this->getWidth() > $size ) {
991
				$jobs[] = new ThumbnailRenderJob(
992
					$this->getTitle(),
993
					[ 'transformParams' => [ 'width' => $size ] ]
994
				);
995
			}
996
		}
997
998
		if ( $jobs ) {
999
			JobQueueGroup::singleton()->lazyPush( $jobs );
1000
		}
1001
	}
1002
1003
	/**
1004
	 * Delete a list of thumbnails visible at urls
1005
	 * @param string $dir Base dir of the files.
1006
	 * @param array $files Array of strings: relative filenames (to $dir)
1007
	 */
1008
	protected function purgeThumbList( $dir, $files ) {
1009
		$fileListDebug = strtr(
1010
			var_export( $files, true ),
1011
			[ "\n" => '' ]
1012
		);
1013
		wfDebug( __METHOD__ . ": $fileListDebug\n" );
1014
1015
		$purgeList = [];
1016
		foreach ( $files as $file ) {
1017
			# Check that the base file name is part of the thumb name
1018
			# This is a basic sanity check to avoid erasing unrelated directories
1019
			if ( strpos( $file, $this->getName() ) !== false
1020
				|| strpos( $file, "-thumbnail" ) !== false // "short" thumb name
1021
			) {
1022
				$purgeList[] = "{$dir}/{$file}";
1023
			}
1024
		}
1025
1026
		# Delete the thumbnails
1027
		$this->repo->quickPurgeBatch( $purgeList );
1028
		# Clear out the thumbnail directory if empty
1029
		$this->repo->quickCleanDir( $dir );
1030
	}
1031
1032
	/** purgeDescription inherited */
1033
	/** purgeEverything inherited */
1034
1035
	/**
1036
	 * @param int $limit Optional: Limit to number of results
1037
	 * @param int $start Optional: Timestamp, start from
1038
	 * @param int $end Optional: Timestamp, end at
1039
	 * @param bool $inc
1040
	 * @return OldLocalFile[]
1041
	 */
1042
	function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1043
		$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...
1044
		$tables = [ 'oldimage' ];
1045
		$fields = OldLocalFile::selectFields();
1046
		$conds = $opts = $join_conds = [];
1047
		$eq = $inc ? '=' : '';
1048
		$conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
1049
1050
		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...
1051
			$conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
1052
		}
1053
1054
		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...
1055
			$conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
1056
		}
1057
1058
		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...
1059
			$opts['LIMIT'] = $limit;
1060
		}
1061
1062
		// Search backwards for time > x queries
1063
		$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...
1064
		$opts['ORDER BY'] = "oi_timestamp $order";
1065
		$opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1066
1067
		Hooks::run( 'LocalFile::getHistory', [ &$this, &$tables, &$fields,
1068
			&$conds, &$opts, &$join_conds ] );
1069
1070
		$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1071
		$r = [];
1072
1073
		foreach ( $res as $row ) {
1074
			$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...
1075
		}
1076
1077
		if ( $order == 'ASC' ) {
1078
			$r = array_reverse( $r ); // make sure it ends up descending
1079
		}
1080
1081
		return $r;
1082
	}
1083
1084
	/**
1085
	 * Returns the history of this file, line by line.
1086
	 * starts with current version, then old versions.
1087
	 * uses $this->historyLine to check which line to return:
1088
	 *  0      return line for current version
1089
	 *  1      query for old versions, return first one
1090
	 *  2, ... return next old version from above query
1091
	 * @return bool
1092
	 */
1093
	public function nextHistoryLine() {
1094
		# Polymorphic function name to distinguish foreign and local fetches
1095
		$fname = get_class( $this ) . '::' . __FUNCTION__;
1096
1097
		$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...
1098
1099
		if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1100
			$this->historyRes = $dbr->select( 'image',
1101
				[
1102
					'*',
1103
					"'' AS oi_archive_name",
1104
					'0 as oi_deleted',
1105
					'img_sha1'
1106
				],
1107
				[ 'img_name' => $this->title->getDBkey() ],
1108
				$fname
1109
			);
1110
1111
			if ( 0 == $dbr->numRows( $this->historyRes ) ) {
1112
				$this->historyRes = null;
1113
1114
				return false;
1115
			}
1116
		} elseif ( $this->historyLine == 1 ) {
1117
			$this->historyRes = $dbr->select( 'oldimage', '*',
1118
				[ 'oi_name' => $this->title->getDBkey() ],
1119
				$fname,
1120
				[ 'ORDER BY' => 'oi_timestamp DESC' ]
1121
			);
1122
		}
1123
		$this->historyLine++;
1124
1125
		return $dbr->fetchObject( $this->historyRes );
1126
	}
1127
1128
	/**
1129
	 * Reset the history pointer to the first element of the history
1130
	 */
1131
	public function resetHistory() {
1132
		$this->historyLine = 0;
1133
1134
		if ( !is_null( $this->historyRes ) ) {
1135
			$this->historyRes = null;
1136
		}
1137
	}
1138
1139
	/** getHashPath inherited */
1140
	/** getRel inherited */
1141
	/** getUrlRel inherited */
1142
	/** getArchiveRel inherited */
1143
	/** getArchivePath inherited */
1144
	/** getThumbPath inherited */
1145
	/** getArchiveUrl inherited */
1146
	/** getThumbUrl inherited */
1147
	/** getArchiveVirtualUrl inherited */
1148
	/** getThumbVirtualUrl inherited */
1149
	/** isHashed inherited */
1150
1151
	/**
1152
	 * Upload a file and record it in the DB
1153
	 * @param string|FSFile $src Source storage path, virtual URL, or filesystem path
1154
	 * @param string $comment Upload description
1155
	 * @param string $pageText Text to use for the new description page,
1156
	 *   if a new description page is created
1157
	 * @param int|bool $flags Flags for publish()
1158
	 * @param array|bool $props File properties, if known. This can be used to
1159
	 *   reduce the upload time when uploading virtual URLs for which the file
1160
	 *   info is already known
1161
	 * @param string|bool $timestamp Timestamp for img_timestamp, or false to use the
1162
	 *   current time
1163
	 * @param User|null $user User object or null to use $wgUser
1164
	 * @param string[] $tags Change tags to add to the log entry and page revision.
1165
	 *   (This doesn't check $user's permissions.)
1166
	 * @return FileRepoStatus On success, the value member contains the
1167
	 *     archive name, or an empty string if it was a new file.
1168
	 */
1169
	function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1170
		$timestamp = false, $user = null, $tags = []
1171
	) {
1172
		global $wgContLang;
1173
1174
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1175
			return $this->readOnlyFatalStatus();
1176
		}
1177
1178
		$srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1179
		if ( !$props ) {
1180
			if ( $this->repo->isVirtualUrl( $srcPath )
1181
				|| FileBackend::isStoragePath( $srcPath )
1182
			) {
1183
				$props = $this->repo->getFileProps( $srcPath );
1184
			} else {
1185
				$props = FSFile::getPropsFromPath( $srcPath );
1186
			}
1187
		}
1188
1189
		$options = [];
1190
		$handler = MediaHandler::getHandler( $props['mime'] );
1191 View Code Duplication
		if ( $handler ) {
1192
			$options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
1193
		} else {
1194
			$options['headers'] = [];
1195
		}
1196
1197
		// Trim spaces on user supplied text
1198
		$comment = trim( $comment );
1199
1200
		// Truncate nicely or the DB will do it for us
1201
		// non-nicely (dangling multi-byte chars, non-truncated version in cache).
1202
		$comment = $wgContLang->truncate( $comment, 255 );
1203
		$this->lock(); // begin
1204
		$status = $this->publish( $src, $flags, $options );
1205
1206
		if ( $status->successCount >= 2 ) {
1207
			// There will be a copy+(one of move,copy,store).
1208
			// The first succeeding does not commit us to updating the DB
1209
			// since it simply copied the current version to a timestamped file name.
1210
			// It is only *preferable* to avoid leaving such files orphaned.
1211
			// Once the second operation goes through, then the current version was
1212
			// updated and we must therefore update the DB too.
1213
			$oldver = $status->value;
1214
			if ( !$this->recordUpload2( $oldver, $comment, $pageText, $props, $timestamp, $user, $tags ) ) {
1215
				$status->fatal( 'filenotfound', $srcPath );
1216
			}
1217
		}
1218
1219
		$this->unlock(); // done
1220
1221
		return $status;
1222
	}
1223
1224
	/**
1225
	 * Record a file upload in the upload log and the image table
1226
	 * @param string $oldver
1227
	 * @param string $desc
1228
	 * @param string $license
1229
	 * @param string $copyStatus
1230
	 * @param string $source
1231
	 * @param bool $watch
1232
	 * @param string|bool $timestamp
1233
	 * @param User|null $user User object or null to use $wgUser
1234
	 * @return bool
1235
	 */
1236
	function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
1237
		$watch = false, $timestamp = false, User $user = null ) {
1238
		if ( !$user ) {
1239
			global $wgUser;
1240
			$user = $wgUser;
1241
		}
1242
1243
		$pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
1244
1245
		if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
1246
			return false;
1247
		}
1248
1249
		if ( $watch ) {
1250
			$user->addWatch( $this->getTitle() );
1251
		}
1252
1253
		return true;
1254
	}
1255
1256
	/**
1257
	 * Record a file upload in the upload log and the image table
1258
	 * @param string $oldver
1259
	 * @param string $comment
1260
	 * @param string $pageText
1261
	 * @param bool|array $props
1262
	 * @param string|bool $timestamp
1263
	 * @param null|User $user
1264
	 * @param string[] $tags
1265
	 * @return bool
1266
	 */
1267
	function recordUpload2(
1268
		$oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
1269
	) {
1270
		if ( is_null( $user ) ) {
1271
			global $wgUser;
1272
			$user = $wgUser;
1273
		}
1274
1275
		$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...
1276
1277
		# Imports or such might force a certain timestamp; otherwise we generate
1278
		# it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1279
		if ( $timestamp === false ) {
1280
			$timestamp = $dbw->timestamp();
1281
			$allowTimeKludge = true;
1282
		} else {
1283
			$allowTimeKludge = false;
1284
		}
1285
1286
		$props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1287
		$props['description'] = $comment;
1288
		$props['user'] = $user->getId();
1289
		$props['user_text'] = $user->getName();
1290
		$props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1291
		$this->setProps( $props );
1292
1293
		# Fail now if the file isn't there
1294
		if ( !$this->fileExists ) {
1295
			wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
1296
1297
			return false;
1298
		}
1299
1300
		$dbw->startAtomic( __METHOD__ );
1301
1302
		# Test to see if the row exists using INSERT IGNORE
1303
		# This avoids race conditions by locking the row until the commit, and also
1304
		# doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1305
		$dbw->insert( 'image',
1306
			[
1307
				'img_name' => $this->getName(),
1308
				'img_size' => $this->size,
1309
				'img_width' => intval( $this->width ),
1310
				'img_height' => intval( $this->height ),
1311
				'img_bits' => $this->bits,
1312
				'img_media_type' => $this->media_type,
1313
				'img_major_mime' => $this->major_mime,
1314
				'img_minor_mime' => $this->minor_mime,
1315
				'img_timestamp' => $timestamp,
1316
				'img_description' => $comment,
1317
				'img_user' => $user->getId(),
1318
				'img_user_text' => $user->getName(),
1319
				'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1320
				'img_sha1' => $this->sha1
1321
			],
1322
			__METHOD__,
1323
			'IGNORE'
1324
		);
1325
1326
		$reupload = ( $dbw->affectedRows() == 0 );
1327
		if ( $reupload ) {
1328
			if ( $allowTimeKludge ) {
1329
				# Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1330
				$ltimestamp = $dbw->selectField(
1331
					'image',
1332
					'img_timestamp',
1333
					[ 'img_name' => $this->getName() ],
1334
					__METHOD__,
1335
					[ 'LOCK IN SHARE MODE' ]
1336
				);
1337
				$lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
1338
				# Avoid a timestamp that is not newer than the last version
1339
				# TODO: the image/oldimage tables should be like page/revision with an ID field
1340
				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...
1341
					sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1342
					$timestamp = $dbw->timestamp( $lUnixtime + 1 );
1343
					$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...
1344
				}
1345
			}
1346
1347
			# (bug 34993) Note: $oldver can be empty here, if the previous
1348
			# version of the file was broken. Allow registration of the new
1349
			# version to continue anyway, because that's better than having
1350
			# an image that's not fixable by user operations.
1351
			# Collision, this is an update of a file
1352
			# Insert previous contents into oldimage
1353
			$dbw->insertSelect( 'oldimage', 'image',
1354
				[
1355
					'oi_name' => 'img_name',
1356
					'oi_archive_name' => $dbw->addQuotes( $oldver ),
1357
					'oi_size' => 'img_size',
1358
					'oi_width' => 'img_width',
1359
					'oi_height' => 'img_height',
1360
					'oi_bits' => 'img_bits',
1361
					'oi_timestamp' => 'img_timestamp',
1362
					'oi_description' => 'img_description',
1363
					'oi_user' => 'img_user',
1364
					'oi_user_text' => 'img_user_text',
1365
					'oi_metadata' => 'img_metadata',
1366
					'oi_media_type' => 'img_media_type',
1367
					'oi_major_mime' => 'img_major_mime',
1368
					'oi_minor_mime' => 'img_minor_mime',
1369
					'oi_sha1' => 'img_sha1'
1370
				],
1371
				[ 'img_name' => $this->getName() ],
1372
				__METHOD__
1373
			);
1374
1375
			# Update the current image row
1376
			$dbw->update( 'image',
1377
				[
1378
					'img_size' => $this->size,
1379
					'img_width' => intval( $this->width ),
1380
					'img_height' => intval( $this->height ),
1381
					'img_bits' => $this->bits,
1382
					'img_media_type' => $this->media_type,
1383
					'img_major_mime' => $this->major_mime,
1384
					'img_minor_mime' => $this->minor_mime,
1385
					'img_timestamp' => $timestamp,
1386
					'img_description' => $comment,
1387
					'img_user' => $user->getId(),
1388
					'img_user_text' => $user->getName(),
1389
					'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1390
					'img_sha1' => $this->sha1
1391
				],
1392
				[ 'img_name' => $this->getName() ],
1393
				__METHOD__
1394
			);
1395
		}
1396
1397
		$descTitle = $this->getTitle();
1398
		$descId = $descTitle->getArticleID();
1399
		$wikiPage = new WikiFilePage( $descTitle );
1400
		$wikiPage->setFile( $this );
1401
1402
		// Add the log entry...
1403
		$logEntry = new ManualLogEntry( 'upload', $reupload ? 'overwrite' : 'upload' );
1404
		$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...
1405
		$logEntry->setPerformer( $user );
1406
		$logEntry->setComment( $comment );
1407
		$logEntry->setTarget( $descTitle );
1408
		// Allow people using the api to associate log entries with the upload.
1409
		// Log has a timestamp, but sometimes different from upload timestamp.
1410
		$logEntry->setParameters(
1411
			[
1412
				'img_sha1' => $this->sha1,
1413
				'img_timestamp' => $timestamp,
1414
			]
1415
		);
1416
		// Note we keep $logId around since during new image
1417
		// creation, page doesn't exist yet, so log_page = 0
1418
		// but we want it to point to the page we're making,
1419
		// so we later modify the log entry.
1420
		// For a similar reason, we avoid making an RC entry
1421
		// now and wait until the page exists.
1422
		$logId = $logEntry->insert();
1423
1424
		if ( $descTitle->exists() ) {
1425
			// Use own context to get the action text in content language
1426
			$formatter = LogFormatter::newFromEntry( $logEntry );
1427
			$formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1428
			$editSummary = $formatter->getPlainActionText();
1429
1430
			$nullRevision = Revision::newNullRevision(
1431
				$dbw,
1432
				$descId,
1433
				$editSummary,
1434
				false,
1435
				$user
1436
			);
1437
			if ( $nullRevision ) {
1438
				$nullRevision->insertOn( $dbw );
1439
				Hooks::run(
1440
					'NewRevisionFromEditComplete',
1441
					[ $wikiPage, $nullRevision, $nullRevision->getParentId(), $user ]
1442
				);
1443
				$wikiPage->updateRevisionOn( $dbw, $nullRevision );
1444
				// Associate null revision id
1445
				$logEntry->setAssociatedRevId( $nullRevision->getId() );
1446
			}
1447
1448
			$newPageContent = null;
1449
		} else {
1450
			// Make the description page and RC log entry post-commit
1451
			$newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1452
		}
1453
1454
		# Defer purges, page creation, and link updates in case they error out.
1455
		# The most important thing is that files and the DB registry stay synced.
1456
		$dbw->endAtomic( __METHOD__ );
1457
1458
		# Do some cache purges after final commit so that:
1459
		# a) Changes are more likely to be seen post-purge
1460
		# b) They won't cause rollback of the log publish/update above
1461
		DeferredUpdates::addUpdate(
1462
			new AutoCommitUpdate(
1463
				$dbw,
1464
				__METHOD__,
1465
				function () use (
1466
					$reupload, $wikiPage, $newPageContent, $comment, $user,
1467
					$logEntry, $logId, $descId, $tags
1468
				) {
1469
					# Update memcache after the commit
1470
					$this->invalidateCache();
1471
1472
					$updateLogPage = false;
1473
					if ( $newPageContent ) {
1474
						# New file page; create the description page.
1475
						# There's already a log entry, so don't make a second RC entry
1476
						# CDN and file cache for the description page are purged by doEditContent.
1477
						$status = $wikiPage->doEditContent(
1478
							$newPageContent,
1479
							$comment,
1480
							EDIT_NEW | EDIT_SUPPRESS_RC,
1481
							false,
1482
							$user
1483
						);
1484
1485
						if ( isset( $status->value['revision'] ) ) {
1486
							// Associate new page revision id
1487
							$logEntry->setAssociatedRevId( $status->value['revision']->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 FileRepoStatus 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 FileRepoStatus 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
			$dst = $repo->getBackend()->getPathForSHA1( $sha1 );
1625
			$status = $repo->quickImport( $src, $dst );
1626
			if ( $flags & File::DELETE_SOURCE ) {
1627
				unlink( $srcPath );
1628
			}
1629
1630
			if ( $this->exists() ) {
1631
				$status->value = $archiveName;
1632
			}
1633
		} else {
1634
			$flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
1635
			$status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
1636
1637
			if ( $status->value == 'new' ) {
1638
				$status->value = '';
1639
			} else {
1640
				$status->value = $archiveName;
1641
			}
1642
		}
1643
1644
		$this->unlock(); // done
1645
1646
		return $status;
1647
	}
1648
1649
	/** getLinksTo inherited */
1650
	/** getExifData inherited */
1651
	/** isLocal inherited */
1652
	/** wasDeleted inherited */
1653
1654
	/**
1655
	 * Move file to the new title
1656
	 *
1657
	 * Move current, old version and all thumbnails
1658
	 * to the new filename. Old file is deleted.
1659
	 *
1660
	 * Cache purging is done; checks for validity
1661
	 * and logging are caller's responsibility
1662
	 *
1663
	 * @param Title $target New file name
1664
	 * @return FileRepoStatus
1665
	 */
1666
	function move( $target ) {
1667
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1668
			return $this->readOnlyFatalStatus();
1669
		}
1670
1671
		wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1672
		$batch = new LocalFileMoveBatch( $this, $target );
1673
1674
		$this->lock(); // begin
1675
		$batch->addCurrent();
1676
		$archiveNames = $batch->addOlds();
1677
		$status = $batch->execute();
1678
		$this->unlock(); // done
1679
1680
		wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1681
1682
		// Purge the source and target files...
1683
		$oldTitleFile = wfLocalFile( $this->title );
1684
		$newTitleFile = wfLocalFile( $target );
1685
		// To avoid slow purges in the transaction, move them outside...
1686
		DeferredUpdates::addUpdate(
1687
			new AutoCommitUpdate(
1688
				$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...
1689
				__METHOD__,
1690
				function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
1691
					$oldTitleFile->purgeEverything();
1692
					foreach ( $archiveNames as $archiveName ) {
1693
						$oldTitleFile->purgeOldThumbnails( $archiveName );
1694
					}
1695
					$newTitleFile->purgeEverything();
1696
				}
1697
			),
1698
			DeferredUpdates::PRESEND
1699
		);
1700
1701
		if ( $status->isOK() ) {
1702
			// Now switch the object
1703
			$this->title = $target;
1704
			// Force regeneration of the name and hashpath
1705
			unset( $this->name );
1706
			unset( $this->hashPath );
1707
		}
1708
1709
		return $status;
1710
	}
1711
1712
	/**
1713
	 * Delete all versions of the file.
1714
	 *
1715
	 * Moves the files into an archive directory (or deletes them)
1716
	 * and removes the database rows.
1717
	 *
1718
	 * Cache purging is done; logging is caller's responsibility.
1719
	 *
1720
	 * @param string $reason
1721
	 * @param bool $suppress
1722
	 * @param User|null $user
1723
	 * @return FileRepoStatus
1724
	 */
1725
	function delete( $reason, $suppress = false, $user = null ) {
1726
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1727
			return $this->readOnlyFatalStatus();
1728
		}
1729
1730
		$batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1731
1732
		$this->lock(); // begin
1733
		$batch->addCurrent();
1734
		// Get old version relative paths
1735
		$archiveNames = $batch->addOlds();
1736
		$status = $batch->execute();
1737
		$this->unlock(); // done
1738
1739
		if ( $status->isOK() ) {
1740
			DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
1741
		}
1742
1743
		// To avoid slow purges in the transaction, move them outside...
1744
		DeferredUpdates::addUpdate(
1745
			new AutoCommitUpdate(
1746
				$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...
1747
				__METHOD__,
1748
				function () use ( $archiveNames ) {
1749
					$this->purgeEverything();
1750
					foreach ( $archiveNames as $archiveName ) {
1751
						$this->purgeOldThumbnails( $archiveName );
1752
					}
1753
				}
1754
			),
1755
			DeferredUpdates::PRESEND
1756
		);
1757
1758
		// Purge the CDN
1759
		$purgeUrls = [];
1760
		foreach ( $archiveNames as $archiveName ) {
1761
			$purgeUrls[] = $this->getArchiveUrl( $archiveName );
1762
		}
1763
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $purgeUrls ), DeferredUpdates::PRESEND );
1764
1765
		return $status;
1766
	}
1767
1768
	/**
1769
	 * Delete an old version of the file.
1770
	 *
1771
	 * Moves the file into an archive directory (or deletes it)
1772
	 * and removes the database row.
1773
	 *
1774
	 * Cache purging is done; logging is caller's responsibility.
1775
	 *
1776
	 * @param string $archiveName
1777
	 * @param string $reason
1778
	 * @param bool $suppress
1779
	 * @param User|null $user
1780
	 * @throws MWException Exception on database or file store failure
1781
	 * @return FileRepoStatus
1782
	 */
1783
	function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
1784
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1785
			return $this->readOnlyFatalStatus();
1786
		}
1787
1788
		$batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1789
1790
		$this->lock(); // begin
1791
		$batch->addOld( $archiveName );
1792
		$status = $batch->execute();
1793
		$this->unlock(); // done
1794
1795
		$this->purgeOldThumbnails( $archiveName );
1796
		if ( $status->isOK() ) {
1797
			$this->purgeDescription();
1798
		}
1799
1800
		DeferredUpdates::addUpdate(
1801
			new CdnCacheUpdate( [ $this->getArchiveUrl( $archiveName ) ] ),
1802
			DeferredUpdates::PRESEND
1803
		);
1804
1805
		return $status;
1806
	}
1807
1808
	/**
1809
	 * Restore all or specified deleted revisions to the given file.
1810
	 * Permissions and logging are left to the caller.
1811
	 *
1812
	 * May throw database exceptions on error.
1813
	 *
1814
	 * @param array $versions Set of record ids of deleted items to restore,
1815
	 *   or empty to restore all revisions.
1816
	 * @param bool $unsuppress
1817
	 * @return FileRepoStatus
1818
	 */
1819
	function restore( $versions = [], $unsuppress = false ) {
1820
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1821
			return $this->readOnlyFatalStatus();
1822
		}
1823
1824
		$batch = new LocalFileRestoreBatch( $this, $unsuppress );
1825
1826
		$this->lock(); // begin
1827
		if ( !$versions ) {
1828
			$batch->addAll();
1829
		} else {
1830
			$batch->addIds( $versions );
1831
		}
1832
		$status = $batch->execute();
1833
		if ( $status->isGood() ) {
1834
			$cleanupStatus = $batch->cleanup();
1835
			$cleanupStatus->successCount = 0;
1836
			$cleanupStatus->failCount = 0;
1837
			$status->merge( $cleanupStatus );
1838
		}
1839
		$this->unlock(); // done
1840
1841
		return $status;
1842
	}
1843
1844
	/** isMultipage inherited */
1845
	/** pageCount inherited */
1846
	/** scaleHeight inherited */
1847
	/** getImageSize inherited */
1848
1849
	/**
1850
	 * Get the URL of the file description page.
1851
	 * @return string
1852
	 */
1853
	function getDescriptionUrl() {
1854
		return $this->title->getLocalURL();
1855
	}
1856
1857
	/**
1858
	 * Get the HTML text of the description page
1859
	 * This is not used by ImagePage for local files, since (among other things)
1860
	 * it skips the parser cache.
1861
	 *
1862
	 * @param Language $lang What language to get description in (Optional)
1863
	 * @return bool|mixed
1864
	 */
1865
	function getDescriptionText( $lang = null ) {
1866
		$revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
1867
		if ( !$revision ) {
1868
			return false;
1869
		}
1870
		$content = $revision->getContent();
1871
		if ( !$content ) {
1872
			return false;
1873
		}
1874
		$pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
1875
1876
		return $pout->getText();
1877
	}
1878
1879
	/**
1880
	 * @param int $audience
1881
	 * @param User $user
1882
	 * @return string
1883
	 */
1884 View Code Duplication
	function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
1885
		$this->load();
1886
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
1887
			return '';
1888
		} elseif ( $audience == self::FOR_THIS_USER
1889
			&& !$this->userCan( self::DELETED_COMMENT, $user )
1890
		) {
1891
			return '';
1892
		} else {
1893
			return $this->description;
1894
		}
1895
	}
1896
1897
	/**
1898
	 * @return bool|string
1899
	 */
1900
	function getTimestamp() {
1901
		$this->load();
1902
1903
		return $this->timestamp;
1904
	}
1905
1906
	/**
1907
	 * @return bool|string
1908
	 */
1909
	public function getDescriptionTouched() {
1910
		// The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
1911
		// itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
1912
		// need to differentiate between null (uninitialized) and false (failed to load).
1913
		if ( $this->descriptionTouched === null ) {
1914
			$cond = [
1915
				'page_namespace' => $this->title->getNamespace(),
1916
				'page_title' => $this->title->getDBkey()
1917
			];
1918
			$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...
1919
			$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...
1920
		}
1921
1922
		return $this->descriptionTouched;
1923
	}
1924
1925
	/**
1926
	 * @return string
1927
	 */
1928
	function getSha1() {
1929
		$this->load();
1930
		// Initialise now if necessary
1931
		if ( $this->sha1 == '' && $this->fileExists ) {
1932
			$this->lock(); // begin
1933
1934
			$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...
1935
			if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1936
				$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...
1937
				$dbw->update( 'image',
1938
					[ 'img_sha1' => $this->sha1 ],
1939
					[ 'img_name' => $this->getName() ],
1940
					__METHOD__ );
1941
				$this->invalidateCache();
1942
			}
1943
1944
			$this->unlock(); // done
1945
		}
1946
1947
		return $this->sha1;
1948
	}
1949
1950
	/**
1951
	 * @return bool Whether to cache in RepoGroup (this avoids OOMs)
1952
	 */
1953
	function isCacheable() {
1954
		$this->load();
1955
1956
		// If extra data (metadata) was not loaded then it must have been large
1957
		return $this->extraDataLoaded
1958
		&& strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
1959
	}
1960
1961
	/**
1962
	 * @return Status
1963
	 * @since 1.28
1964
	 */
1965
	public function acquireFileLock() {
1966
		return $this->getRepo()->getBackend()->lockFiles(
1967
			[ $this->getPath() ], LockManager::LOCK_EX, 10
1968
		);
1969
	}
1970
1971
	/**
1972
	 * @return Status
1973
	 * @since 1.28
1974
	 */
1975
	public function releaseFileLock() {
1976
		return $this->getRepo()->getBackend()->unlockFiles(
1977
			[ $this->getPath() ], LockManager::LOCK_EX
1978
		);
1979
	}
1980
1981
	/**
1982
	 * Start an atomic DB section and lock the image for update
1983
	 * or increments a reference counter if the lock is already held
1984
	 *
1985
	 * This method should not be used outside of LocalFile/LocalFile*Batch
1986
	 *
1987
	 * @throws LocalFileLockError Throws an error if the lock was not acquired
1988
	 * @return bool Whether the file lock owns/spawned the DB transaction
1989
	 */
1990
	public function lock() {
1991
		if ( !$this->locked ) {
1992
			$logger = LoggerFactory::getInstance( 'LocalFile' );
1993
1994
			$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...
1995
			$makesTransaction = !$dbw->trxLevel();
1996
			$dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
1997
			// Bug 54736: use simple lock to handle when the file does not exist.
1998
			// SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
1999
			// Also, that would cause contention on INSERT of similarly named rows.
2000
			$status = $this->acquireFileLock(); // represents all versions of the file
2001
			if ( !$status->isGood() ) {
2002
				$dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2003
				$logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
2004
2005
				throw new LocalFileLockError( $status );
2006
			}
2007
			// Release the lock *after* commit to avoid row-level contention.
2008
			// Make sure it triggers on rollback() as well as commit() (T132921).
2009
			$dbw->onTransactionResolution( function () use ( $logger ) {
2010
				$status = $this->releaseFileLock();
2011
				if ( !$status->isGood() ) {
2012
					$logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
2013
				}
2014
			} );
2015
			// Callers might care if the SELECT snapshot is safely fresh
2016
			$this->lockedOwnTrx = $makesTransaction;
2017
		}
2018
2019
		$this->locked++;
2020
2021
		return $this->lockedOwnTrx;
2022
	}
2023
2024
	/**
2025
	 * Decrement the lock reference count and end the atomic section if it reaches zero
2026
	 *
2027
	 * This method should not be used outside of LocalFile/LocalFile*Batch
2028
	 *
2029
	 * The commit and loc release will happen when no atomic sections are active, which
2030
	 * may happen immediately or at some point after calling this
2031
	 */
2032
	public function unlock() {
2033
		if ( $this->locked ) {
2034
			--$this->locked;
2035
			if ( !$this->locked ) {
2036
				$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...
2037
				$dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2038
				$this->lockedOwnTrx = false;
2039
			}
2040
		}
2041
	}
2042
2043
	/**
2044
	 * @return Status
2045
	 */
2046
	protected function readOnlyFatalStatus() {
2047
		return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
2048
			$this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2049
	}
2050
2051
	/**
2052
	 * Clean up any dangling locks
2053
	 */
2054
	function __destruct() {
2055
		$this->unlock();
2056
	}
2057
} // LocalFile class
2058
2059
# ------------------------------------------------------------------------------
2060
2061
/**
2062
 * Helper class for file deletion
2063
 * @ingroup FileAbstraction
2064
 */
2065
class LocalFileDeleteBatch {
2066
	/** @var LocalFile */
2067
	private $file;
2068
2069
	/** @var string */
2070
	private $reason;
2071
2072
	/** @var array */
2073
	private $srcRels = [];
2074
2075
	/** @var array */
2076
	private $archiveUrls = [];
2077
2078
	/** @var array Items to be processed in the deletion batch */
2079
	private $deletionBatch;
2080
2081
	/** @var bool Whether to suppress all suppressable fields when deleting */
2082
	private $suppress;
2083
2084
	/** @var FileRepoStatus */
2085
	private $status;
2086
2087
	/** @var User */
2088
	private $user;
2089
2090
	/**
2091
	 * @param File $file
2092
	 * @param string $reason
2093
	 * @param bool $suppress
2094
	 * @param User|null $user
2095
	 */
2096
	function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
2097
		$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...
2098
		$this->reason = $reason;
2099
		$this->suppress = $suppress;
2100
		if ( $user ) {
2101
			$this->user = $user;
2102
		} else {
2103
			global $wgUser;
2104
			$this->user = $wgUser;
2105
		}
2106
		$this->status = $file->repo->newGood();
2107
	}
2108
2109
	public function addCurrent() {
2110
		$this->srcRels['.'] = $this->file->getRel();
2111
	}
2112
2113
	/**
2114
	 * @param string $oldName
2115
	 */
2116
	public function addOld( $oldName ) {
2117
		$this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
2118
		$this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
2119
	}
2120
2121
	/**
2122
	 * Add the old versions of the image to the batch
2123
	 * @return array List of archive names from old versions
2124
	 */
2125
	public function addOlds() {
2126
		$archiveNames = [];
2127
2128
		$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...
2129
		$result = $dbw->select( 'oldimage',
2130
			[ 'oi_archive_name' ],
2131
			[ 'oi_name' => $this->file->getName() ],
2132
			__METHOD__
2133
		);
2134
2135
		foreach ( $result as $row ) {
2136
			$this->addOld( $row->oi_archive_name );
2137
			$archiveNames[] = $row->oi_archive_name;
2138
		}
2139
2140
		return $archiveNames;
2141
	}
2142
2143
	/**
2144
	 * @return array
2145
	 */
2146
	protected function getOldRels() {
2147
		if ( !isset( $this->srcRels['.'] ) ) {
2148
			$oldRels =& $this->srcRels;
2149
			$deleteCurrent = false;
2150
		} else {
2151
			$oldRels = $this->srcRels;
2152
			unset( $oldRels['.'] );
2153
			$deleteCurrent = true;
2154
		}
2155
2156
		return [ $oldRels, $deleteCurrent ];
2157
	}
2158
2159
	/**
2160
	 * @return array
2161
	 */
2162
	protected function getHashes() {
2163
		$hashes = [];
2164
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2165
2166
		if ( $deleteCurrent ) {
2167
			$hashes['.'] = $this->file->getSha1();
2168
		}
2169
2170
		if ( count( $oldRels ) ) {
2171
			$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...
2172
			$res = $dbw->select(
2173
				'oldimage',
2174
				[ 'oi_archive_name', 'oi_sha1' ],
2175
				[ 'oi_archive_name' => array_keys( $oldRels ),
2176
					'oi_name' => $this->file->getName() ], // performance
2177
				__METHOD__
2178
			);
2179
2180
			foreach ( $res as $row ) {
2181
				if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
2182
					// Get the hash from the file
2183
					$oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
2184
					$props = $this->file->repo->getFileProps( $oldUrl );
2185
2186
					if ( $props['fileExists'] ) {
2187
						// Upgrade the oldimage row
2188
						$dbw->update( 'oldimage',
2189
							[ 'oi_sha1' => $props['sha1'] ],
2190
							[ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
2191
							__METHOD__ );
2192
						$hashes[$row->oi_archive_name] = $props['sha1'];
2193
					} else {
2194
						$hashes[$row->oi_archive_name] = false;
2195
					}
2196
				} else {
2197
					$hashes[$row->oi_archive_name] = $row->oi_sha1;
2198
				}
2199
			}
2200
		}
2201
2202
		$missing = array_diff_key( $this->srcRels, $hashes );
2203
2204
		foreach ( $missing as $name => $rel ) {
2205
			$this->status->error( 'filedelete-old-unregistered', $name );
2206
		}
2207
2208
		foreach ( $hashes as $name => $hash ) {
2209
			if ( !$hash ) {
2210
				$this->status->error( 'filedelete-missing', $this->srcRels[$name] );
2211
				unset( $hashes[$name] );
2212
			}
2213
		}
2214
2215
		return $hashes;
2216
	}
2217
2218
	protected function doDBInserts() {
2219
		$now = time();
2220
		$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...
2221
		$encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
2222
		$encUserId = $dbw->addQuotes( $this->user->getId() );
2223
		$encReason = $dbw->addQuotes( $this->reason );
2224
		$encGroup = $dbw->addQuotes( 'deleted' );
2225
		$ext = $this->file->getExtension();
2226
		$dotExt = $ext === '' ? '' : ".$ext";
2227
		$encExt = $dbw->addQuotes( $dotExt );
2228
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2229
2230
		// Bitfields to further suppress the content
2231 View Code Duplication
		if ( $this->suppress ) {
2232
			$bitfield = 0;
2233
			// This should be 15...
2234
			$bitfield |= Revision::DELETED_TEXT;
2235
			$bitfield |= Revision::DELETED_COMMENT;
2236
			$bitfield |= Revision::DELETED_USER;
2237
			$bitfield |= Revision::DELETED_RESTRICTED;
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
2258
					'fa_name' => 'img_name',
2259
					'fa_archive_name' => 'NULL',
2260
					'fa_size' => 'img_size',
2261
					'fa_width' => 'img_width',
2262
					'fa_height' => 'img_height',
2263
					'fa_metadata' => 'img_metadata',
2264
					'fa_bits' => 'img_bits',
2265
					'fa_media_type' => 'img_media_type',
2266
					'fa_major_mime' => 'img_major_mime',
2267
					'fa_minor_mime' => 'img_minor_mime',
2268
					'fa_description' => 'img_description',
2269
					'fa_user' => 'img_user',
2270
					'fa_user_text' => 'img_user_text',
2271
					'fa_timestamp' => 'img_timestamp',
2272
					'fa_sha1' => 'img_sha1'
2273
				],
2274
				[ 'img_name' => $this->file->getName() ],
2275
				__METHOD__
2276
			);
2277
		}
2278
2279
		if ( count( $oldRels ) ) {
2280
			$res = $dbw->select(
2281
				'oldimage',
2282
				OldLocalFile::selectFields(),
2283
				[
2284
					'oi_name' => $this->file->getName(),
2285
					'oi_archive_name' => array_keys( $oldRels )
2286
				],
2287
				__METHOD__,
2288
				[ 'FOR UPDATE' ]
2289
			);
2290
			$rowsInsert = [];
2291
			foreach ( $res as $row ) {
2292
				$rowsInsert[] = [
2293
					// Deletion-specific fields
2294
					'fa_storage_group' => 'deleted',
2295
					'fa_storage_key' => ( $row->oi_sha1 === '' )
2296
						? ''
2297
						: "{$row->oi_sha1}{$dotExt}",
2298
					'fa_deleted_user' => $this->user->getId(),
2299
					'fa_deleted_timestamp' => $dbw->timestamp( $now ),
2300
					'fa_deleted_reason' => $this->reason,
2301
					// Counterpart fields
2302
					'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
2303
					'fa_name' => $row->oi_name,
2304
					'fa_archive_name' => $row->oi_archive_name,
2305
					'fa_size' => $row->oi_size,
2306
					'fa_width' => $row->oi_width,
2307
					'fa_height' => $row->oi_height,
2308
					'fa_metadata' => $row->oi_metadata,
2309
					'fa_bits' => $row->oi_bits,
2310
					'fa_media_type' => $row->oi_media_type,
2311
					'fa_major_mime' => $row->oi_major_mime,
2312
					'fa_minor_mime' => $row->oi_minor_mime,
2313
					'fa_description' => $row->oi_description,
2314
					'fa_user' => $row->oi_user,
2315
					'fa_user_text' => $row->oi_user_text,
2316
					'fa_timestamp' => $row->oi_timestamp,
2317
					'fa_sha1' => $row->oi_sha1
2318
				];
2319
			}
2320
2321
			$dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
2322
		}
2323
	}
2324
2325
	function doDBDeletes() {
2326
		$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...
2327
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2328
2329
		if ( count( $oldRels ) ) {
2330
			$dbw->delete( 'oldimage',
2331
				[
2332
					'oi_name' => $this->file->getName(),
2333
					'oi_archive_name' => array_keys( $oldRels )
2334
				], __METHOD__ );
2335
		}
2336
2337
		if ( $deleteCurrent ) {
2338
			$dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
2339
		}
2340
	}
2341
2342
	/**
2343
	 * Run the transaction
2344
	 * @return FileRepoStatus
2345
	 */
2346
	public function execute() {
2347
		$repo = $this->file->getRepo();
2348
		$this->file->lock();
2349
2350
		// Prepare deletion batch
2351
		$hashes = $this->getHashes();
2352
		$this->deletionBatch = [];
2353
		$ext = $this->file->getExtension();
2354
		$dotExt = $ext === '' ? '' : ".$ext";
2355
2356
		foreach ( $this->srcRels as $name => $srcRel ) {
2357
			// Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
2358
			if ( isset( $hashes[$name] ) ) {
2359
				$hash = $hashes[$name];
2360
				$key = $hash . $dotExt;
2361
				$dstRel = $repo->getDeletedHashPath( $key ) . $key;
2362
				$this->deletionBatch[$name] = [ $srcRel, $dstRel ];
2363
			}
2364
		}
2365
2366
		if ( !$repo->hasSha1Storage() ) {
2367
			// Removes non-existent file from the batch, so we don't get errors.
2368
			// This also handles files in the 'deleted' zone deleted via revision deletion.
2369
			$checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
2370
			if ( !$checkStatus->isGood() ) {
2371
				$this->status->merge( $checkStatus );
2372
				return $this->status;
2373
			}
2374
			$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...
2375
2376
			// Execute the file deletion batch
2377
			$status = $this->file->repo->deleteBatch( $this->deletionBatch );
2378
			if ( !$status->isGood() ) {
2379
				$this->status->merge( $status );
2380
			}
2381
		}
2382
2383
		if ( !$this->status->isOK() ) {
2384
			// Critical file deletion error; abort
2385
			$this->file->unlock();
2386
2387
			return $this->status;
2388
		}
2389
2390
		// Copy the image/oldimage rows to filearchive
2391
		$this->doDBInserts();
2392
		// Delete image/oldimage rows
2393
		$this->doDBDeletes();
2394
2395
		// Commit and return
2396
		$this->file->unlock();
2397
2398
		return $this->status;
2399
	}
2400
2401
	/**
2402
	 * Removes non-existent files from a deletion batch.
2403
	 * @param array $batch
2404
	 * @return Status
2405
	 */
2406
	protected function removeNonexistentFiles( $batch ) {
2407
		$files = $newBatch = [];
2408
2409
		foreach ( $batch as $batchItem ) {
2410
			list( $src, ) = $batchItem;
2411
			$files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
2412
		}
2413
2414
		$result = $this->file->repo->fileExistsBatch( $files );
2415 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2416
			return Status::newFatal( 'backend-fail-internal',
2417
				$this->file->repo->getBackend()->getName() );
2418
		}
2419
2420
		foreach ( $batch as $batchItem ) {
2421
			if ( $result[$batchItem[0]] ) {
2422
				$newBatch[] = $batchItem;
2423
			}
2424
		}
2425
2426
		return Status::newGood( $newBatch );
2427
	}
2428
}
2429
2430
# ------------------------------------------------------------------------------
2431
2432
/**
2433
 * Helper class for file undeletion
2434
 * @ingroup FileAbstraction
2435
 */
2436
class LocalFileRestoreBatch {
2437
	/** @var LocalFile */
2438
	private $file;
2439
2440
	/** @var array List of file IDs to restore */
2441
	private $cleanupBatch;
2442
2443
	/** @var array List of file IDs to restore */
2444
	private $ids;
2445
2446
	/** @var bool Add all revisions of the file */
2447
	private $all;
2448
2449
	/** @var bool Whether to remove all settings for suppressed fields */
2450
	private $unsuppress = false;
2451
2452
	/**
2453
	 * @param File $file
2454
	 * @param bool $unsuppress
2455
	 */
2456
	function __construct( File $file, $unsuppress = false ) {
2457
		$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...
2458
		$this->cleanupBatch = $this->ids = [];
2459
		$this->ids = [];
2460
		$this->unsuppress = $unsuppress;
2461
	}
2462
2463
	/**
2464
	 * Add a file by ID
2465
	 * @param int $fa_id
2466
	 */
2467
	public function addId( $fa_id ) {
2468
		$this->ids[] = $fa_id;
2469
	}
2470
2471
	/**
2472
	 * Add a whole lot of files by ID
2473
	 * @param int[] $ids
2474
	 */
2475
	public function addIds( $ids ) {
2476
		$this->ids = array_merge( $this->ids, $ids );
2477
	}
2478
2479
	/**
2480
	 * Add all revisions of the file
2481
	 */
2482
	public function addAll() {
2483
		$this->all = true;
2484
	}
2485
2486
	/**
2487
	 * Run the transaction, except the cleanup batch.
2488
	 * The cleanup batch should be run in a separate transaction, because it locks different
2489
	 * rows and there's no need to keep the image row locked while it's acquiring those locks
2490
	 * The caller may have its own transaction open.
2491
	 * So we save the batch and let the caller call cleanup()
2492
	 * @return FileRepoStatus
2493
	 */
2494
	public function execute() {
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->ok = 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 FileRepoStatus
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 DatabaseBase */
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 FileRepoStatus
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->ok = false;
0 ignored issues
show
Documentation introduced by
The property ok does not exist on object<FileRepoStatus>. Since you implemented __set, maybe consider adding a @property annotation.

Since your code implements the magic setter _set, this function will be called for any write access on an undefined variable. You can add the @property annotation to your class or interface to document the existence of this variable.

<?php

/**
 * @property int $x
 * @property int $y
 * @property string $text
 */
class MyLabel
{
    private $properties;

    private $allowedProperties = array('x', 'y', 'text');

    public function __get($name)
    {
        if (isset($properties[$name]) && in_array($name, $this->allowedProperties)) {
            return $properties[$name];
        } else {
            return null;
        }
    }

    public function __set($name, $value)
    {
        if (in_array($name, $this->allowedProperties)) {
            $properties[$name] = $value;
        } else {
            throw new \LogicException("Property $name is not defined.");
        }
    }

}

Since the property has write access only, you can use the @property-write annotation instead.

Of course, you may also just have mistyped another name, in which case you should fix the error.

See also the PhpDoc documentation for @property.

Loading history...
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->ok = 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 FileRepoStatus
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