Completed
Branch master (420c52)
by
unknown
26:22
created

LocalFileMoveBatch::verifyDBUpdates()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 36
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 25
nc 4
nop 0
dl 0
loc 36
rs 8.8571
c 1
b 0
f 0
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 True if the image row is locked */
121
	private $locked;
122
123
	/** @var bool True if the image row is locked with a lock initiated transaction */
124
	private $lockedOwnTrx;
125
126
	/** @var bool True if file is not present in file system. Not to be cached in memcached */
127
	private $missing;
128
129
	// @note: higher than IDBAccessObject constants
130
	const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
131
132
	const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
133
134
	/**
135
	 * Create a LocalFile from a title
136
	 * Do not call this except from inside a repo class.
137
	 *
138
	 * Note: $unused param is only here to avoid an E_STRICT
139
	 *
140
	 * @param Title $title
141
	 * @param FileRepo $repo
142
	 * @param null $unused
143
	 *
144
	 * @return LocalFile
145
	 */
146
	static function newFromTitle( $title, $repo, $unused = null ) {
147
		return new self( $title, $repo );
148
	}
149
150
	/**
151
	 * Create a LocalFile from a title
152
	 * Do not call this except from inside a repo class.
153
	 *
154
	 * @param stdClass $row
155
	 * @param FileRepo $repo
156
	 *
157
	 * @return LocalFile
158
	 */
159 View Code Duplication
	static function newFromRow( $row, $repo ) {
160
		$title = Title::makeTitle( NS_FILE, $row->img_name );
161
		$file = new self( $title, $repo );
162
		$file->loadFromRow( $row );
163
164
		return $file;
165
	}
166
167
	/**
168
	 * Create a LocalFile from a SHA-1 key
169
	 * Do not call this except from inside a repo class.
170
	 *
171
	 * @param string $sha1 Base-36 SHA-1
172
	 * @param LocalRepo $repo
173
	 * @param string|bool $timestamp MW_timestamp (optional)
174
	 * @return bool|LocalFile
175
	 */
176 View Code Duplication
	static function newFromKey( $sha1, $repo, $timestamp = false ) {
177
		$dbr = $repo->getSlaveDB();
178
179
		$conds = [ 'img_sha1' => $sha1 ];
180
		if ( $timestamp ) {
181
			$conds['img_timestamp'] = $dbr->timestamp( $timestamp );
182
		}
183
184
		$row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
185
		if ( $row ) {
186
			return self::newFromRow( $row, $repo );
0 ignored issues
show
Bug introduced by
It seems like $row defined by $dbr->selectRow('image',...(), $conds, __METHOD__) on line 184 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...
187
		} else {
188
			return false;
189
		}
190
	}
191
192
	/**
193
	 * Fields in the image table
194
	 * @return array
195
	 */
196
	static function selectFields() {
197
		return [
198
			'img_name',
199
			'img_size',
200
			'img_width',
201
			'img_height',
202
			'img_metadata',
203
			'img_bits',
204
			'img_media_type',
205
			'img_major_mime',
206
			'img_minor_mime',
207
			'img_description',
208
			'img_user',
209
			'img_user_text',
210
			'img_timestamp',
211
			'img_sha1',
212
		];
213
	}
214
215
	/**
216
	 * Constructor.
217
	 * Do not call this except from inside a repo class.
218
	 * @param Title $title
219
	 * @param FileRepo $repo
220
	 */
221
	function __construct( $title, $repo ) {
222
		parent::__construct( $title, $repo );
223
224
		$this->metadata = '';
225
		$this->historyLine = 0;
226
		$this->historyRes = null;
227
		$this->dataLoaded = false;
228
		$this->extraDataLoaded = false;
229
230
		$this->assertRepoDefined();
231
		$this->assertTitleDefined();
232
	}
233
234
	/**
235
	 * Get the memcached key for the main data for this file, or false if
236
	 * there is no access to the shared cache.
237
	 * @return string|bool
238
	 */
239
	function getCacheKey() {
240
		$hashedName = md5( $this->getName() );
241
242
		return $this->repo->getSharedCacheKey( 'file', $hashedName );
243
	}
244
245
	/**
246
	 * Try to load file metadata from memcached. Returns true on success.
247
	 * @return bool
248
	 */
249
	private function loadFromCache() {
250
		$this->dataLoaded = false;
251
		$this->extraDataLoaded = false;
252
		$key = $this->getCacheKey();
253
254
		if ( !$key ) {
255
			return false;
256
		}
257
258
		$cache = ObjectCache::getMainWANInstance();
259
		$cachedValues = $cache->get( $key );
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 252 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...
260
261
		// Check if the key existed and belongs to this version of MediaWiki
262
		if ( is_array( $cachedValues ) && $cachedValues['version'] == MW_FILE_VERSION ) {
263
			$this->fileExists = $cachedValues['fileExists'];
264
			if ( $this->fileExists ) {
265
				$this->setProps( $cachedValues );
266
			}
267
			$this->dataLoaded = true;
268
			$this->extraDataLoaded = true;
269
			foreach ( $this->getLazyCacheFields( '' ) as $field ) {
270
				$this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
271
			}
272
		}
273
274
		return $this->dataLoaded;
275
	}
276
277
	/**
278
	 * Save the file metadata to memcached
279
	 */
280
	private function saveToCache() {
281
		$this->load();
282
283
		$key = $this->getCacheKey();
284
		if ( !$key ) {
285
			return;
286
		}
287
288
		$fields = $this->getCacheFields( '' );
289
		$cacheVal = [ 'version' => MW_FILE_VERSION ];
290
		$cacheVal['fileExists'] = $this->fileExists;
291
292
		if ( $this->fileExists ) {
293
			foreach ( $fields as $field ) {
294
				$cacheVal[$field] = $this->$field;
295
			}
296
		}
297
298
		// Strip off excessive entries from the subset of fields that can become large.
299
		// If the cache value gets to large it will not fit in memcached and nothing will
300
		// get cached at all, causing master queries for any file access.
301
		foreach ( $this->getLazyCacheFields( '' ) as $field ) {
302
			if ( isset( $cacheVal[$field] ) && strlen( $cacheVal[$field] ) > 100 * 1024 ) {
303
				unset( $cacheVal[$field] ); // don't let the value get too big
304
			}
305
		}
306
307
		// Cache presence for 1 week and negatives for 1 day
308
		$ttl = $this->fileExists ? 86400 * 7 : 86400;
309
		$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...
310
		ObjectCache::getMainWANInstance()->set( $key, $cacheVal, $ttl, $opts );
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 283 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...
311
	}
312
313
	/**
314
	 * Purge the file object/metadata cache
315
	 */
316
	public function invalidateCache() {
317
		$key = $this->getCacheKey();
318
		if ( !$key ) {
319
			return;
320
		}
321
322
		$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...
323
			ObjectCache::getMainWANInstance()->delete( $key );
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 317 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...
324
		} );
325
	}
326
327
	/**
328
	 * Load metadata from the file itself
329
	 */
330
	function loadFromFile() {
331
		$props = $this->repo->getFileProps( $this->getVirtualUrl() );
332
		$this->setProps( $props );
333
	}
334
335
	/**
336
	 * @param string $prefix
337
	 * @return array
338
	 */
339
	function getCacheFields( $prefix = 'img_' ) {
340
		static $fields = [ 'size', 'width', 'height', 'bits', 'media_type',
341
			'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
342
			'user_text', 'description' ];
343
		static $results = [];
344
345
		if ( $prefix == '' ) {
346
			return $fields;
347
		}
348
349 View Code Duplication
		if ( !isset( $results[$prefix] ) ) {
350
			$prefixedFields = [];
351
			foreach ( $fields as $field ) {
352
				$prefixedFields[] = $prefix . $field;
353
			}
354
			$results[$prefix] = $prefixedFields;
355
		}
356
357
		return $results[$prefix];
358
	}
359
360
	/**
361
	 * @param string $prefix
362
	 * @return array
363
	 */
364
	function getLazyCacheFields( $prefix = 'img_' ) {
365
		static $fields = [ 'metadata' ];
366
		static $results = [];
367
368
		if ( $prefix == '' ) {
369
			return $fields;
370
		}
371
372 View Code Duplication
		if ( !isset( $results[$prefix] ) ) {
373
			$prefixedFields = [];
374
			foreach ( $fields as $field ) {
375
				$prefixedFields[] = $prefix . $field;
376
			}
377
			$results[$prefix] = $prefixedFields;
378
		}
379
380
		return $results[$prefix];
381
	}
382
383
	/**
384
	 * Load file metadata from the DB
385
	 * @param int $flags
386
	 */
387
	function loadFromDB( $flags = 0 ) {
388
		$fname = get_class( $this ) . '::' . __FUNCTION__;
389
390
		# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
391
		$this->dataLoaded = true;
392
		$this->extraDataLoaded = true;
393
394
		$dbr = ( $flags & self::READ_LATEST )
395
			? $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...
396
			: $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...
397
398
		$row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
399
			[ 'img_name' => $this->getName() ], $fname );
400
401
		if ( $row ) {
402
			$this->loadFromRow( $row );
403
		} else {
404
			$this->fileExists = false;
405
		}
406
	}
407
408
	/**
409
	 * Load lazy file metadata from the DB.
410
	 * This covers fields that are sometimes not cached.
411
	 */
412
	protected function loadExtraFromDB() {
413
		$fname = get_class( $this ) . '::' . __FUNCTION__;
414
415
		# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
416
		$this->extraDataLoaded = true;
417
418
		$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...
419
		if ( !$fieldMap ) {
420
			$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...
421
		}
422
423
		if ( $fieldMap ) {
424
			foreach ( $fieldMap as $name => $value ) {
425
				$this->$name = $value;
426
			}
427
		} else {
428
			throw new MWException( "Could not find data for image '{$this->getName()}'." );
429
		}
430
	}
431
432
	/**
433
	 * @param IDatabase $dbr
434
	 * @param string $fname
435
	 * @return array|bool
436
	 */
437
	private function loadFieldsWithTimestamp( $dbr, $fname ) {
438
		$fieldMap = false;
439
440
		$row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
441
			[ 'img_name' => $this->getName(), 'img_timestamp' => $this->getTimestamp() ],
442
			$fname );
443
		if ( $row ) {
444
			$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 440 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...
445
		} else {
446
			# File may have been uploaded over in the meantime; check the old versions
447
			$row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
448
				[ 'oi_name' => $this->getName(), 'oi_timestamp' => $this->getTimestamp() ],
449
				$fname );
450
			if ( $row ) {
451
				$fieldMap = $this->unprefixRow( $row, 'oi_' );
452
			}
453
		}
454
455
		return $fieldMap;
456
	}
457
458
	/**
459
	 * @param array|object $row
460
	 * @param string $prefix
461
	 * @throws MWException
462
	 * @return array
463
	 */
464
	protected function unprefixRow( $row, $prefix = 'img_' ) {
465
		$array = (array)$row;
466
		$prefixLength = strlen( $prefix );
467
468
		// Sanity check prefix once
469
		if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
470
			throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
471
		}
472
473
		$decoded = [];
474
		foreach ( $array as $name => $value ) {
475
			$decoded[substr( $name, $prefixLength )] = $value;
476
		}
477
478
		return $decoded;
479
	}
480
481
	/**
482
	 * Decode a row from the database (either object or array) to an array
483
	 * with timestamps and MIME types decoded, and the field prefix removed.
484
	 * @param object $row
485
	 * @param string $prefix
486
	 * @throws MWException
487
	 * @return array
488
	 */
489
	function decodeRow( $row, $prefix = 'img_' ) {
490
		$decoded = $this->unprefixRow( $row, $prefix );
491
492
		$decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
493
494
		$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...
495
496
		if ( empty( $decoded['major_mime'] ) ) {
497
			$decoded['mime'] = 'unknown/unknown';
498
		} else {
499
			if ( !$decoded['minor_mime'] ) {
500
				$decoded['minor_mime'] = 'unknown';
501
			}
502
			$decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
503
		}
504
505
		// Trim zero padding from char/binary field
506
		$decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
507
508
		// Normalize some fields to integer type, per their database definition.
509
		// Use unary + so that overflows will be upgraded to double instead of
510
		// being trucated as with intval(). This is important to allow >2GB
511
		// files on 32-bit systems.
512
		foreach ( [ 'size', 'width', 'height', 'bits' ] as $field ) {
513
			$decoded[$field] = +$decoded[$field];
514
		}
515
516
		return $decoded;
517
	}
518
519
	/**
520
	 * Load file metadata from a DB result row
521
	 *
522
	 * @param object $row
523
	 * @param string $prefix
524
	 */
525
	function loadFromRow( $row, $prefix = 'img_' ) {
526
		$this->dataLoaded = true;
527
		$this->extraDataLoaded = true;
528
529
		$array = $this->decodeRow( $row, $prefix );
530
531
		foreach ( $array as $name => $value ) {
532
			$this->$name = $value;
533
		}
534
535
		$this->fileExists = true;
536
		$this->maybeUpgradeRow();
537
	}
538
539
	/**
540
	 * Load file metadata from cache or DB, unless already loaded
541
	 * @param int $flags
542
	 */
543
	function load( $flags = 0 ) {
544
		if ( !$this->dataLoaded ) {
545
			if ( ( $flags & self::READ_LATEST ) || !$this->loadFromCache() ) {
546
				$this->loadFromDB( $flags );
547
				$this->saveToCache();
548
			}
549
			$this->dataLoaded = true;
550
		}
551
		if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
552
			// @note: loads on name/timestamp to reduce race condition problems
553
			$this->loadExtraFromDB();
554
		}
555
	}
556
557
	/**
558
	 * Upgrade a row if it needs it
559
	 */
560
	function maybeUpgradeRow() {
561
		global $wgUpdateCompatibleMetadata;
562
		if ( wfReadOnly() ) {
563
			return;
564
		}
565
566
		$upgrade = false;
567
		if ( is_null( $this->media_type ) ||
568
			$this->mime == 'image/svg'
569
		) {
570
			$upgrade = true;
571
		} else {
572
			$handler = $this->getHandler();
573
			if ( $handler ) {
574
				$validity = $handler->isMetadataValid( $this, $this->getMetadata() );
575
				if ( $validity === MediaHandler::METADATA_BAD
576
					|| ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
577
				) {
578
					$upgrade = true;
579
				}
580
			}
581
		}
582
583
		if ( $upgrade ) {
584
			try {
585
				$this->upgradeRow();
586
			} catch ( LocalFileLockError $e ) {
587
				// let the other process handle it (or do it next time)
588
			}
589
			$this->upgraded = true; // avoid rework/retries
590
		}
591
	}
592
593
	function getUpgraded() {
594
		return $this->upgraded;
595
	}
596
597
	/**
598
	 * Fix assorted version-related problems with the image row by reloading it from the file
599
	 */
600
	function upgradeRow() {
601
		$this->lock(); // begin
602
603
		$this->loadFromFile();
604
605
		# Don't destroy file info of missing files
606
		if ( !$this->fileExists ) {
607
			$this->unlock();
608
			wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
609
610
			return;
611
		}
612
613
		$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...
614
		list( $major, $minor ) = self::splitMime( $this->mime );
615
616
		if ( wfReadOnly() ) {
617
			$this->unlock();
618
619
			return;
620
		}
621
		wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
622
623
		$dbw->update( 'image',
624
			[
625
				'img_size' => $this->size, // sanity
626
				'img_width' => $this->width,
627
				'img_height' => $this->height,
628
				'img_bits' => $this->bits,
629
				'img_media_type' => $this->media_type,
630
				'img_major_mime' => $major,
631
				'img_minor_mime' => $minor,
632
				'img_metadata' => $dbw->encodeBlob( $this->metadata ),
633
				'img_sha1' => $this->sha1,
634
			],
635
			[ 'img_name' => $this->getName() ],
636
			__METHOD__
637
		);
638
639
		$this->invalidateCache();
640
641
		$this->unlock(); // done
642
643
	}
644
645
	/**
646
	 * Set properties in this object to be equal to those given in the
647
	 * associative array $info. Only cacheable fields can be set.
648
	 * All fields *must* be set in $info except for getLazyCacheFields().
649
	 *
650
	 * If 'mime' is given, it will be split into major_mime/minor_mime.
651
	 * If major_mime/minor_mime are given, $this->mime will also be set.
652
	 *
653
	 * @param array $info
654
	 */
655
	function setProps( $info ) {
656
		$this->dataLoaded = true;
657
		$fields = $this->getCacheFields( '' );
658
		$fields[] = 'fileExists';
659
660
		foreach ( $fields as $field ) {
661
			if ( isset( $info[$field] ) ) {
662
				$this->$field = $info[$field];
663
			}
664
		}
665
666
		// Fix up mime fields
667
		if ( isset( $info['major_mime'] ) ) {
668
			$this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
669
		} elseif ( isset( $info['mime'] ) ) {
670
			$this->mime = $info['mime'];
671
			list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
672
		}
673
	}
674
675
	/** splitMime inherited */
676
	/** getName inherited */
677
	/** getTitle inherited */
678
	/** getURL inherited */
679
	/** getViewURL inherited */
680
	/** getPath inherited */
681
	/** isVisible inherited */
682
683
	/**
684
	 * @return bool
685
	 */
686
	function isMissing() {
687
		if ( $this->missing === null ) {
688
			list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
689
			$this->missing = !$fileExists;
690
		}
691
692
		return $this->missing;
693
	}
694
695
	/**
696
	 * Return the width of the image
697
	 *
698
	 * @param int $page
699
	 * @return int
700
	 */
701 View Code Duplication
	public function getWidth( $page = 1 ) {
702
		$this->load();
703
704
		if ( $this->isMultipage() ) {
705
			$handler = $this->getHandler();
706
			if ( !$handler ) {
707
				return 0;
708
			}
709
			$dim = $handler->getPageDimensions( $this, $page );
710
			if ( $dim ) {
711
				return $dim['width'];
712
			} else {
713
				// For non-paged media, the false goes through an
714
				// intval, turning failure into 0, so do same here.
715
				return 0;
716
			}
717
		} else {
718
			return $this->width;
719
		}
720
	}
721
722
	/**
723
	 * Return the height of the image
724
	 *
725
	 * @param int $page
726
	 * @return int
727
	 */
728 View Code Duplication
	public function getHeight( $page = 1 ) {
729
		$this->load();
730
731
		if ( $this->isMultipage() ) {
732
			$handler = $this->getHandler();
733
			if ( !$handler ) {
734
				return 0;
735
			}
736
			$dim = $handler->getPageDimensions( $this, $page );
737
			if ( $dim ) {
738
				return $dim['height'];
739
			} else {
740
				// For non-paged media, the false goes through an
741
				// intval, turning failure into 0, so do same here.
742
				return 0;
743
			}
744
		} else {
745
			return $this->height;
746
		}
747
	}
748
749
	/**
750
	 * Returns ID or name of user who uploaded the file
751
	 *
752
	 * @param string $type 'text' or 'id'
753
	 * @return int|string
754
	 */
755
	function getUser( $type = 'text' ) {
756
		$this->load();
757
758
		if ( $type == 'text' ) {
759
			return $this->user_text;
760
		} elseif ( $type == 'id' ) {
761
			return (int)$this->user;
762
		}
763
	}
764
765
	/**
766
	 * Get short description URL for a file based on the page ID.
767
	 *
768
	 * @return string|null
769
	 * @throws MWException
770
	 * @since 1.27
771
	 */
772
	public function getDescriptionShortUrl() {
773
		$pageId = $this->title->getArticleID();
774
775 View Code Duplication
		if ( $pageId !== null ) {
776
			$url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
777
			if ( $url !== false ) {
778
				return $url;
779
			}
780
		}
781
		return null;
782
	}
783
784
	/**
785
	 * Get handler-specific metadata
786
	 * @return string
787
	 */
788
	function getMetadata() {
789
		$this->load( self::LOAD_ALL ); // large metadata is loaded in another step
790
		return $this->metadata;
791
	}
792
793
	/**
794
	 * @return int
795
	 */
796
	function getBitDepth() {
797
		$this->load();
798
799
		return (int)$this->bits;
800
	}
801
802
	/**
803
	 * Returns the size of the image file, in bytes
804
	 * @return int
805
	 */
806
	public function getSize() {
807
		$this->load();
808
809
		return $this->size;
810
	}
811
812
	/**
813
	 * Returns the MIME type of the file.
814
	 * @return string
815
	 */
816
	function getMimeType() {
817
		$this->load();
818
819
		return $this->mime;
820
	}
821
822
	/**
823
	 * Returns the type of the media in the file.
824
	 * Use the value returned by this function with the MEDIATYPE_xxx constants.
825
	 * @return string
826
	 */
827
	function getMediaType() {
828
		$this->load();
829
830
		return $this->media_type;
831
	}
832
833
	/** canRender inherited */
834
	/** mustRender inherited */
835
	/** allowInlineDisplay inherited */
836
	/** isSafeFile inherited */
837
	/** isTrustedFile inherited */
838
839
	/**
840
	 * Returns true if the file exists on disk.
841
	 * @return bool Whether file exist on disk.
842
	 */
843
	public function exists() {
844
		$this->load();
845
846
		return $this->fileExists;
847
	}
848
849
	/** getTransformScript inherited */
850
	/** getUnscaledThumb inherited */
851
	/** thumbName inherited */
852
	/** createThumb inherited */
853
	/** transform inherited */
854
855
	/** getHandler inherited */
856
	/** iconThumb inherited */
857
	/** getLastError inherited */
858
859
	/**
860
	 * Get all thumbnail names previously generated for this file
861
	 * @param string|bool $archiveName Name of an archive file, default false
862
	 * @return array First element is the base dir, then files in that base dir.
863
	 */
864
	function getThumbnails( $archiveName = false ) {
865
		if ( $archiveName ) {
866
			$dir = $this->getArchiveThumbPath( $archiveName );
0 ignored issues
show
Bug introduced by
It seems like $archiveName defined by parameter $archiveName on line 864 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...
867
		} else {
868
			$dir = $this->getThumbPath();
869
		}
870
871
		$backend = $this->repo->getBackend();
872
		$files = [ $dir ];
873
		try {
874
			$iterator = $backend->getFileList( [ 'dir' => $dir ] );
875
			foreach ( $iterator as $file ) {
876
				$files[] = $file;
877
			}
878
		} catch ( FileBackendError $e ) {
879
		} // suppress (bug 54674)
880
881
		return $files;
882
	}
883
884
	/**
885
	 * Refresh metadata in memcached, but don't touch thumbnails or CDN
886
	 */
887
	function purgeMetadataCache() {
888
		$this->invalidateCache();
889
	}
890
891
	/**
892
	 * Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
893
	 *
894
	 * @param array $options An array potentially with the key forThumbRefresh.
895
	 *
896
	 * @note This used to purge old thumbnails by default as well, but doesn't anymore.
897
	 */
898
	function purgeCache( $options = [] ) {
899
		// Refresh metadata cache
900
		$this->purgeMetadataCache();
901
902
		// Delete thumbnails
903
		$this->purgeThumbnails( $options );
904
905
		// Purge CDN cache for this file
906
		DeferredUpdates::addUpdate(
907
			new CdnCacheUpdate( [ $this->getUrl() ] ),
908
			DeferredUpdates::PRESEND
909
		);
910
	}
911
912
	/**
913
	 * Delete cached transformed files for an archived version only.
914
	 * @param string $archiveName Name of the archived file
915
	 */
916
	function purgeOldThumbnails( $archiveName ) {
917
		// Get a list of old thumbnails and URLs
918
		$files = $this->getThumbnails( $archiveName );
919
920
		// Purge any custom thumbnail caches
921
		Hooks::run( 'LocalFilePurgeThumbnails', [ $this, $archiveName ] );
922
923
		// Delete thumbnails
924
		$dir = array_shift( $files );
925
		$this->purgeThumbList( $dir, $files );
926
927
		// Purge the CDN
928
		$urls = [];
929
		foreach ( $files as $file ) {
930
			$urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
931
		}
932
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
933
	}
934
935
	/**
936
	 * Delete cached transformed files for the current version only.
937
	 * @param array $options
938
	 */
939
	public function purgeThumbnails( $options = [] ) {
940
		$files = $this->getThumbnails();
941
		// Always purge all files from CDN regardless of handler filters
942
		$urls = [];
943
		foreach ( $files as $file ) {
944
			$urls[] = $this->getThumbUrl( $file );
945
		}
946
		array_shift( $urls ); // don't purge directory
947
948
		// Give media handler a chance to filter the file purge list
949
		if ( !empty( $options['forThumbRefresh'] ) ) {
950
			$handler = $this->getHandler();
951
			if ( $handler ) {
952
				$handler->filterThumbnailPurgeList( $files, $options );
953
			}
954
		}
955
956
		// Purge any custom thumbnail caches
957
		Hooks::run( 'LocalFilePurgeThumbnails', [ $this, false ] );
958
959
		// Delete thumbnails
960
		$dir = array_shift( $files );
961
		$this->purgeThumbList( $dir, $files );
962
963
		// Purge the CDN
964
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
965
	}
966
967
	/**
968
	 * Delete a list of thumbnails visible at urls
969
	 * @param string $dir Base dir of the files.
970
	 * @param array $files Array of strings: relative filenames (to $dir)
971
	 */
972
	protected function purgeThumbList( $dir, $files ) {
973
		$fileListDebug = strtr(
974
			var_export( $files, true ),
975
			[ "\n" => '' ]
976
		);
977
		wfDebug( __METHOD__ . ": $fileListDebug\n" );
978
979
		$purgeList = [];
980
		foreach ( $files as $file ) {
981
			# Check that the base file name is part of the thumb name
982
			# This is a basic sanity check to avoid erasing unrelated directories
983
			if ( strpos( $file, $this->getName() ) !== false
984
				|| strpos( $file, "-thumbnail" ) !== false // "short" thumb name
985
			) {
986
				$purgeList[] = "{$dir}/{$file}";
987
			}
988
		}
989
990
		# Delete the thumbnails
991
		$this->repo->quickPurgeBatch( $purgeList );
992
		# Clear out the thumbnail directory if empty
993
		$this->repo->quickCleanDir( $dir );
994
	}
995
996
	/** purgeDescription inherited */
997
	/** purgeEverything inherited */
998
999
	/**
1000
	 * @param int $limit Optional: Limit to number of results
1001
	 * @param int $start Optional: Timestamp, start from
1002
	 * @param int $end Optional: Timestamp, end at
1003
	 * @param bool $inc
1004
	 * @return OldLocalFile[]
1005
	 */
1006
	function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1007
		$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...
1008
		$tables = [ 'oldimage' ];
1009
		$fields = OldLocalFile::selectFields();
1010
		$conds = $opts = $join_conds = [];
1011
		$eq = $inc ? '=' : '';
1012
		$conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
1013
1014
		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...
1015
			$conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
1016
		}
1017
1018
		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...
1019
			$conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
1020
		}
1021
1022
		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...
1023
			$opts['LIMIT'] = $limit;
1024
		}
1025
1026
		// Search backwards for time > x queries
1027
		$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...
1028
		$opts['ORDER BY'] = "oi_timestamp $order";
1029
		$opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1030
1031
		Hooks::run( 'LocalFile::getHistory', [ &$this, &$tables, &$fields,
1032
			&$conds, &$opts, &$join_conds ] );
1033
1034
		$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1035
		$r = [];
1036
1037
		foreach ( $res as $row ) {
1038
			$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...
1039
		}
1040
1041
		if ( $order == 'ASC' ) {
1042
			$r = array_reverse( $r ); // make sure it ends up descending
1043
		}
1044
1045
		return $r;
1046
	}
1047
1048
	/**
1049
	 * Returns the history of this file, line by line.
1050
	 * starts with current version, then old versions.
1051
	 * uses $this->historyLine to check which line to return:
1052
	 *  0      return line for current version
1053
	 *  1      query for old versions, return first one
1054
	 *  2, ... return next old version from above query
1055
	 * @return bool
1056
	 */
1057
	public function nextHistoryLine() {
1058
		# Polymorphic function name to distinguish foreign and local fetches
1059
		$fname = get_class( $this ) . '::' . __FUNCTION__;
1060
1061
		$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...
1062
1063
		if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1064
			$this->historyRes = $dbr->select( 'image',
1065
				[
1066
					'*',
1067
					"'' AS oi_archive_name",
1068
					'0 as oi_deleted',
1069
					'img_sha1'
1070
				],
1071
				[ 'img_name' => $this->title->getDBkey() ],
1072
				$fname
1073
			);
1074
1075
			if ( 0 == $dbr->numRows( $this->historyRes ) ) {
1076
				$this->historyRes = null;
1077
1078
				return false;
1079
			}
1080
		} elseif ( $this->historyLine == 1 ) {
1081
			$this->historyRes = $dbr->select( 'oldimage', '*',
1082
				[ 'oi_name' => $this->title->getDBkey() ],
1083
				$fname,
1084
				[ 'ORDER BY' => 'oi_timestamp DESC' ]
1085
			);
1086
		}
1087
		$this->historyLine++;
1088
1089
		return $dbr->fetchObject( $this->historyRes );
1090
	}
1091
1092
	/**
1093
	 * Reset the history pointer to the first element of the history
1094
	 */
1095
	public function resetHistory() {
1096
		$this->historyLine = 0;
1097
1098
		if ( !is_null( $this->historyRes ) ) {
1099
			$this->historyRes = null;
1100
		}
1101
	}
1102
1103
	/** getHashPath inherited */
1104
	/** getRel inherited */
1105
	/** getUrlRel inherited */
1106
	/** getArchiveRel inherited */
1107
	/** getArchivePath inherited */
1108
	/** getThumbPath inherited */
1109
	/** getArchiveUrl inherited */
1110
	/** getThumbUrl inherited */
1111
	/** getArchiveVirtualUrl inherited */
1112
	/** getThumbVirtualUrl inherited */
1113
	/** isHashed inherited */
1114
1115
	/**
1116
	 * Upload a file and record it in the DB
1117
	 * @param string|FSFile $src Source storage path, virtual URL, or filesystem path
1118
	 * @param string $comment Upload description
1119
	 * @param string $pageText Text to use for the new description page,
1120
	 *   if a new description page is created
1121
	 * @param int|bool $flags Flags for publish()
1122
	 * @param array|bool $props File properties, if known. This can be used to
1123
	 *   reduce the upload time when uploading virtual URLs for which the file
1124
	 *   info is already known
1125
	 * @param string|bool $timestamp Timestamp for img_timestamp, or false to use the
1126
	 *   current time
1127
	 * @param User|null $user User object or null to use $wgUser
1128
	 * @param string[] $tags Change tags to add to the log entry and page revision.
1129
	 *   (This doesn't check $user's permissions.)
1130
	 * @return FileRepoStatus On success, the value member contains the
1131
	 *     archive name, or an empty string if it was a new file.
1132
	 */
1133
	function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1134
		$timestamp = false, $user = null, $tags = []
1135
	) {
1136
		global $wgContLang;
1137
1138
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1139
			return $this->readOnlyFatalStatus();
1140
		}
1141
1142
		$srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1143
		if ( !$props ) {
1144
			if ( $this->repo->isVirtualUrl( $srcPath )
1145
				|| FileBackend::isStoragePath( $srcPath )
1146
			) {
1147
				$props = $this->repo->getFileProps( $srcPath );
1148
			} else {
1149
				$props = FSFile::getPropsFromPath( $srcPath );
1150
			}
1151
		}
1152
1153
		$options = [];
1154
		$handler = MediaHandler::getHandler( $props['mime'] );
1155 View Code Duplication
		if ( $handler ) {
1156
			$options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
1157
		} else {
1158
			$options['headers'] = [];
1159
		}
1160
1161
		// Trim spaces on user supplied text
1162
		$comment = trim( $comment );
1163
1164
		// Truncate nicely or the DB will do it for us
1165
		// non-nicely (dangling multi-byte chars, non-truncated version in cache).
1166
		$comment = $wgContLang->truncate( $comment, 255 );
1167
		$this->lock(); // begin
1168
		$status = $this->publish( $src, $flags, $options );
1169
1170
		if ( $status->successCount >= 2 ) {
1171
			// There will be a copy+(one of move,copy,store).
1172
			// The first succeeding does not commit us to updating the DB
1173
			// since it simply copied the current version to a timestamped file name.
1174
			// It is only *preferable* to avoid leaving such files orphaned.
1175
			// Once the second operation goes through, then the current version was
1176
			// updated and we must therefore update the DB too.
1177
			$oldver = $status->value;
1178
			if ( !$this->recordUpload2( $oldver, $comment, $pageText, $props, $timestamp, $user, $tags ) ) {
1179
				$status->fatal( 'filenotfound', $srcPath );
1180
			}
1181
		}
1182
1183
		$this->unlock(); // done
1184
1185
		return $status;
1186
	}
1187
1188
	/**
1189
	 * Record a file upload in the upload log and the image table
1190
	 * @param string $oldver
1191
	 * @param string $desc
1192
	 * @param string $license
1193
	 * @param string $copyStatus
1194
	 * @param string $source
1195
	 * @param bool $watch
1196
	 * @param string|bool $timestamp
1197
	 * @param User|null $user User object or null to use $wgUser
1198
	 * @return bool
1199
	 */
1200
	function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
1201
		$watch = false, $timestamp = false, User $user = null ) {
1202
		if ( !$user ) {
1203
			global $wgUser;
1204
			$user = $wgUser;
1205
		}
1206
1207
		$pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
1208
1209
		if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
1210
			return false;
1211
		}
1212
1213
		if ( $watch ) {
1214
			$user->addWatch( $this->getTitle() );
1215
		}
1216
1217
		return true;
1218
	}
1219
1220
	/**
1221
	 * Record a file upload in the upload log and the image table
1222
	 * @param string $oldver
1223
	 * @param string $comment
1224
	 * @param string $pageText
1225
	 * @param bool|array $props
1226
	 * @param string|bool $timestamp
1227
	 * @param null|User $user
1228
	 * @param string[] $tags
1229
	 * @return bool
1230
	 */
1231
	function recordUpload2(
1232
		$oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
1233
	) {
1234
		if ( is_null( $user ) ) {
1235
			global $wgUser;
1236
			$user = $wgUser;
1237
		}
1238
1239
		$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...
1240
1241
		# Imports or such might force a certain timestamp; otherwise we generate
1242
		# it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1243
		if ( $timestamp === false ) {
1244
			$timestamp = $dbw->timestamp();
1245
			$allowTimeKludge = true;
1246
		} else {
1247
			$allowTimeKludge = false;
1248
		}
1249
1250
		$props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1251
		$props['description'] = $comment;
1252
		$props['user'] = $user->getId();
1253
		$props['user_text'] = $user->getName();
1254
		$props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1255
		$this->setProps( $props );
1256
1257
		# Fail now if the file isn't there
1258
		if ( !$this->fileExists ) {
1259
			wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
1260
1261
			return false;
1262
		}
1263
1264
		$dbw->startAtomic( __METHOD__ );
1265
1266
		# Test to see if the row exists using INSERT IGNORE
1267
		# This avoids race conditions by locking the row until the commit, and also
1268
		# doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1269
		$dbw->insert( 'image',
1270
			[
1271
				'img_name' => $this->getName(),
1272
				'img_size' => $this->size,
1273
				'img_width' => intval( $this->width ),
1274
				'img_height' => intval( $this->height ),
1275
				'img_bits' => $this->bits,
1276
				'img_media_type' => $this->media_type,
1277
				'img_major_mime' => $this->major_mime,
1278
				'img_minor_mime' => $this->minor_mime,
1279
				'img_timestamp' => $timestamp,
1280
				'img_description' => $comment,
1281
				'img_user' => $user->getId(),
1282
				'img_user_text' => $user->getName(),
1283
				'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1284
				'img_sha1' => $this->sha1
1285
			],
1286
			__METHOD__,
1287
			'IGNORE'
1288
		);
1289
1290
		$reupload = ( $dbw->affectedRows() == 0 );
1291
		if ( $reupload ) {
1292
			if ( $allowTimeKludge ) {
1293
				# Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1294
				$ltimestamp = $dbw->selectField(
1295
					'image',
1296
					'img_timestamp',
1297
					[ 'img_name' => $this->getName() ],
1298
					__METHOD__,
1299
					[ 'LOCK IN SHARE MODE' ]
1300
				);
1301
				$lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
1302
				# Avoid a timestamp that is not newer than the last version
1303
				# TODO: the image/oldimage tables should be like page/revision with an ID field
1304
				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...
1305
					sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1306
					$timestamp = $dbw->timestamp( $lUnixtime + 1 );
1307
					$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...
1308
				}
1309
			}
1310
1311
			# (bug 34993) Note: $oldver can be empty here, if the previous
1312
			# version of the file was broken. Allow registration of the new
1313
			# version to continue anyway, because that's better than having
1314
			# an image that's not fixable by user operations.
1315
			# Collision, this is an update of a file
1316
			# Insert previous contents into oldimage
1317
			$dbw->insertSelect( 'oldimage', 'image',
1318
				[
1319
					'oi_name' => 'img_name',
1320
					'oi_archive_name' => $dbw->addQuotes( $oldver ),
1321
					'oi_size' => 'img_size',
1322
					'oi_width' => 'img_width',
1323
					'oi_height' => 'img_height',
1324
					'oi_bits' => 'img_bits',
1325
					'oi_timestamp' => 'img_timestamp',
1326
					'oi_description' => 'img_description',
1327
					'oi_user' => 'img_user',
1328
					'oi_user_text' => 'img_user_text',
1329
					'oi_metadata' => 'img_metadata',
1330
					'oi_media_type' => 'img_media_type',
1331
					'oi_major_mime' => 'img_major_mime',
1332
					'oi_minor_mime' => 'img_minor_mime',
1333
					'oi_sha1' => 'img_sha1'
1334
				],
1335
				[ 'img_name' => $this->getName() ],
1336
				__METHOD__
1337
			);
1338
1339
			# Update the current image row
1340
			$dbw->update( 'image',
1341
				[
1342
					'img_size' => $this->size,
1343
					'img_width' => intval( $this->width ),
1344
					'img_height' => intval( $this->height ),
1345
					'img_bits' => $this->bits,
1346
					'img_media_type' => $this->media_type,
1347
					'img_major_mime' => $this->major_mime,
1348
					'img_minor_mime' => $this->minor_mime,
1349
					'img_timestamp' => $timestamp,
1350
					'img_description' => $comment,
1351
					'img_user' => $user->getId(),
1352
					'img_user_text' => $user->getName(),
1353
					'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1354
					'img_sha1' => $this->sha1
1355
				],
1356
				[ 'img_name' => $this->getName() ],
1357
				__METHOD__
1358
			);
1359
		}
1360
1361
		$descTitle = $this->getTitle();
1362
		$descId = $descTitle->getArticleID();
1363
		$wikiPage = new WikiFilePage( $descTitle );
1364
		$wikiPage->setFile( $this );
1365
1366
		// Add the log entry...
1367
		$logEntry = new ManualLogEntry( 'upload', $reupload ? 'overwrite' : 'upload' );
1368
		$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...
1369
		$logEntry->setPerformer( $user );
1370
		$logEntry->setComment( $comment );
1371
		$logEntry->setTarget( $descTitle );
1372
		// Allow people using the api to associate log entries with the upload.
1373
		// Log has a timestamp, but sometimes different from upload timestamp.
1374
		$logEntry->setParameters(
1375
			[
1376
				'img_sha1' => $this->sha1,
1377
				'img_timestamp' => $timestamp,
1378
			]
1379
		);
1380
		// Note we keep $logId around since during new image
1381
		// creation, page doesn't exist yet, so log_page = 0
1382
		// but we want it to point to the page we're making,
1383
		// so we later modify the log entry.
1384
		// For a similar reason, we avoid making an RC entry
1385
		// now and wait until the page exists.
1386
		$logId = $logEntry->insert();
1387
1388
		if ( $descTitle->exists() ) {
1389
			// Use own context to get the action text in content language
1390
			$formatter = LogFormatter::newFromEntry( $logEntry );
1391
			$formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1392
			$editSummary = $formatter->getPlainActionText();
1393
1394
			$nullRevision = Revision::newNullRevision(
1395
				$dbw,
1396
				$descId,
1397
				$editSummary,
1398
				false,
1399
				$user
1400
			);
1401
			if ( $nullRevision ) {
1402
				$nullRevision->insertOn( $dbw );
1403
				Hooks::run(
1404
					'NewRevisionFromEditComplete',
1405
					[ $wikiPage, $nullRevision, $nullRevision->getParentId(), $user ]
1406
				);
1407
				$wikiPage->updateRevisionOn( $dbw, $nullRevision );
1408
				// Associate null revision id
1409
				$logEntry->setAssociatedRevId( $nullRevision->getId() );
1410
			}
1411
1412
			$newPageContent = null;
1413
		} else {
1414
			// Make the description page and RC log entry post-commit
1415
			$newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1416
		}
1417
1418
		# Defer purges, page creation, and link updates in case they error out.
1419
		# The most important thing is that files and the DB registry stay synced.
1420
		$dbw->endAtomic( __METHOD__ );
1421
1422
		# Do some cache purges after final commit so that:
1423
		# a) Changes are more likely to be seen post-purge
1424
		# b) They won't cause rollback of the log publish/update above
1425
		DeferredUpdates::addUpdate(
1426
			new AutoCommitUpdate(
1427
				$dbw,
1428
				__METHOD__,
1429
				function () use (
1430
					$reupload, $wikiPage, $newPageContent, $comment, $user,
1431
					$logEntry, $logId, $descId, $tags
1432
				) {
1433
					# Update memcache after the commit
1434
					$this->invalidateCache();
1435
1436
					$updateLogPage = false;
1437
					if ( $newPageContent ) {
1438
						# New file page; create the description page.
1439
						# There's already a log entry, so don't make a second RC entry
1440
						# CDN and file cache for the description page are purged by doEditContent.
1441
						$status = $wikiPage->doEditContent(
1442
							$newPageContent,
1443
							$comment,
1444
							EDIT_NEW | EDIT_SUPPRESS_RC,
1445
							false,
1446
							$user
1447
						);
1448
1449
						if ( isset( $status->value['revision'] ) ) {
1450
							// Associate new page revision id
1451
							$logEntry->setAssociatedRevId( $status->value['revision']->getId() );
1452
						}
1453
						// This relies on the resetArticleID() call in WikiPage::insertOn(),
1454
						// which is triggered on $descTitle by doEditContent() above.
1455
						if ( isset( $status->value['revision'] ) ) {
1456
							/** @var $rev Revision */
1457
							$rev = $status->value['revision'];
1458
							$updateLogPage = $rev->getPage();
1459
						}
1460
					} else {
1461
						# Existing file page: invalidate description page cache
1462
						$wikiPage->getTitle()->invalidateCache();
1463
						$wikiPage->getTitle()->purgeSquid();
1464
						# Allow the new file version to be patrolled from the page footer
1465
						Article::purgePatrolFooterCache( $descId );
1466
					}
1467
1468
					# Update associated rev id. This should be done by $logEntry->insert() earlier,
1469
					# but setAssociatedRevId() wasn't called at that point yet...
1470
					$logParams = $logEntry->getParameters();
1471
					$logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
1472
					$update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
1473
					if ( $updateLogPage ) {
1474
						# Also log page, in case where we just created it above
1475
						$update['log_page'] = $updateLogPage;
1476
					}
1477
					$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...
1478
						'logging',
1479
						$update,
1480
						[ 'log_id' => $logId ],
1481
						__METHOD__
1482
					);
1483
					$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...
1484
						'log_search',
1485
						[
1486
							'ls_field' => 'associated_rev_id',
1487
							'ls_value' => $logEntry->getAssociatedRevId(),
1488
							'ls_log_id' => $logId,
1489
						],
1490
						__METHOD__
1491
					);
1492
1493
					# Add change tags, if any
1494
					if ( $tags ) {
1495
						$logEntry->setTags( $tags );
1496
					}
1497
1498
					# Uploads can be patrolled
1499
					$logEntry->setIsPatrollable( true );
1500
1501
					# Now that the log entry is up-to-date, make an RC entry.
1502
					$logEntry->publish( $logId );
1503
1504
					# Run hook for other updates (typically more cache purging)
1505
					Hooks::run( 'FileUpload', [ $this, $reupload, !$newPageContent ] );
1506
1507
					if ( $reupload ) {
1508
						# Delete old thumbnails
1509
						$this->purgeThumbnails();
1510
						# Remove the old file from the CDN cache
1511
						DeferredUpdates::addUpdate(
1512
							new CdnCacheUpdate( [ $this->getUrl() ] ),
1513
							DeferredUpdates::PRESEND
1514
						);
1515
					} else {
1516
						# Update backlink pages pointing to this title if created
1517
						LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
1518
					}
1519
				}
1520
			)
1521
		);
1522
1523
		if ( !$reupload ) {
1524
			# This is a new file, so update the image count
1525
			DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
1526
		}
1527
1528
		# Invalidate cache for all pages using this file
1529
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ) );
1530
1531
		return true;
1532
	}
1533
1534
	/**
1535
	 * Move or copy a file to its public location. If a file exists at the
1536
	 * destination, move it to an archive. Returns a FileRepoStatus object with
1537
	 * the archive name in the "value" member on success.
1538
	 *
1539
	 * The archive name should be passed through to recordUpload for database
1540
	 * registration.
1541
	 *
1542
	 * @param string|FSFile $src Local filesystem path or virtual URL to the source image
1543
	 * @param int $flags A bitwise combination of:
1544
	 *     File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
1545
	 * @param array $options Optional additional parameters
1546
	 * @return FileRepoStatus On success, the value member contains the
1547
	 *     archive name, or an empty string if it was a new file.
1548
	 */
1549
	function publish( $src, $flags = 0, array $options = [] ) {
1550
		return $this->publishTo( $src, $this->getRel(), $flags, $options );
1551
	}
1552
1553
	/**
1554
	 * Move or copy a file to a specified location. Returns a FileRepoStatus
1555
	 * object with the archive name in the "value" member on success.
1556
	 *
1557
	 * The archive name should be passed through to recordUpload for database
1558
	 * registration.
1559
	 *
1560
	 * @param string|FSFile $src Local filesystem path or virtual URL to the source image
1561
	 * @param string $dstRel Target relative path
1562
	 * @param int $flags A bitwise combination of:
1563
	 *     File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
1564
	 * @param array $options Optional additional parameters
1565
	 * @return FileRepoStatus On success, the value member contains the
1566
	 *     archive name, or an empty string if it was a new file.
1567
	 */
1568
	function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
1569
		$srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1570
1571
		$repo = $this->getRepo();
1572
		if ( $repo->getReadOnlyReason() !== false ) {
1573
			return $this->readOnlyFatalStatus();
1574
		}
1575
1576
		$this->lock(); // begin
1577
1578
		$archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
1579
		$archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
1580
1581
		if ( $repo->hasSha1Storage() ) {
1582
			$sha1 = $repo->isVirtualUrl( $srcPath )
1583
				? $repo->getFileSha1( $srcPath )
1584
				: FSFile::getSha1Base36FromPath( $srcPath );
1585
			$dst = $repo->getBackend()->getPathForSHA1( $sha1 );
1586
			$status = $repo->quickImport( $src, $dst );
1587
			if ( $flags & File::DELETE_SOURCE ) {
1588
				unlink( $srcPath );
1589
			}
1590
1591
			if ( $this->exists() ) {
1592
				$status->value = $archiveName;
1593
			}
1594
		} else {
1595
			$flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
1596
			$status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
1597
1598
			if ( $status->value == 'new' ) {
1599
				$status->value = '';
1600
			} else {
1601
				$status->value = $archiveName;
1602
			}
1603
		}
1604
1605
		$this->unlock(); // done
1606
1607
		return $status;
1608
	}
1609
1610
	/** getLinksTo inherited */
1611
	/** getExifData inherited */
1612
	/** isLocal inherited */
1613
	/** wasDeleted inherited */
1614
1615
	/**
1616
	 * Move file to the new title
1617
	 *
1618
	 * Move current, old version and all thumbnails
1619
	 * to the new filename. Old file is deleted.
1620
	 *
1621
	 * Cache purging is done; checks for validity
1622
	 * and logging are caller's responsibility
1623
	 *
1624
	 * @param Title $target New file name
1625
	 * @return FileRepoStatus
1626
	 */
1627
	function move( $target ) {
1628
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1629
			return $this->readOnlyFatalStatus();
1630
		}
1631
1632
		wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1633
		$batch = new LocalFileMoveBatch( $this, $target );
1634
1635
		$this->lock(); // begin
1636
		$batch->addCurrent();
1637
		$archiveNames = $batch->addOlds();
1638
		$status = $batch->execute();
1639
		$this->unlock(); // done
1640
1641
		wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1642
1643
		// Purge the source and target files...
1644
		$oldTitleFile = wfLocalFile( $this->title );
1645
		$newTitleFile = wfLocalFile( $target );
1646
		// To avoid slow purges in the transaction, move them outside...
1647
		DeferredUpdates::addUpdate(
1648
			new AutoCommitUpdate(
1649
				$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...
1650
				__METHOD__,
1651
				function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
1652
					$oldTitleFile->purgeEverything();
1653
					foreach ( $archiveNames as $archiveName ) {
1654
						$oldTitleFile->purgeOldThumbnails( $archiveName );
1655
					}
1656
					$newTitleFile->purgeEverything();
1657
				}
1658
			),
1659
			DeferredUpdates::PRESEND
1660
		);
1661
1662
		if ( $status->isOK() ) {
1663
			// Now switch the object
1664
			$this->title = $target;
1665
			// Force regeneration of the name and hashpath
1666
			unset( $this->name );
1667
			unset( $this->hashPath );
1668
		}
1669
1670
		return $status;
1671
	}
1672
1673
	/**
1674
	 * Delete all versions of the file.
1675
	 *
1676
	 * Moves the files into an archive directory (or deletes them)
1677
	 * and removes the database rows.
1678
	 *
1679
	 * Cache purging is done; logging is caller's responsibility.
1680
	 *
1681
	 * @param string $reason
1682
	 * @param bool $suppress
1683
	 * @param User|null $user
1684
	 * @return FileRepoStatus
1685
	 */
1686
	function delete( $reason, $suppress = false, $user = null ) {
1687
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1688
			return $this->readOnlyFatalStatus();
1689
		}
1690
1691
		$batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1692
1693
		$this->lock(); // begin
1694
		$batch->addCurrent();
1695
		// Get old version relative paths
1696
		$archiveNames = $batch->addOlds();
1697
		$status = $batch->execute();
1698
		$this->unlock(); // done
1699
1700
		if ( $status->isOK() ) {
1701
			DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
1702
		}
1703
1704
		// To avoid slow purges in the transaction, move them outside...
1705
		DeferredUpdates::addUpdate(
1706
			new AutoCommitUpdate(
1707
				$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...
1708
				__METHOD__,
1709
				function () use ( $archiveNames ) {
1710
					$this->purgeEverything();
1711
					foreach ( $archiveNames as $archiveName ) {
1712
						$this->purgeOldThumbnails( $archiveName );
1713
					}
1714
				}
1715
			),
1716
			DeferredUpdates::PRESEND
1717
		);
1718
1719
		// Purge the CDN
1720
		$purgeUrls = [];
1721
		foreach ( $archiveNames as $archiveName ) {
1722
			$purgeUrls[] = $this->getArchiveUrl( $archiveName );
1723
		}
1724
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $purgeUrls ), DeferredUpdates::PRESEND );
1725
1726
		return $status;
1727
	}
1728
1729
	/**
1730
	 * Delete an old version of the file.
1731
	 *
1732
	 * Moves the file into an archive directory (or deletes it)
1733
	 * and removes the database row.
1734
	 *
1735
	 * Cache purging is done; logging is caller's responsibility.
1736
	 *
1737
	 * @param string $archiveName
1738
	 * @param string $reason
1739
	 * @param bool $suppress
1740
	 * @param User|null $user
1741
	 * @throws MWException Exception on database or file store failure
1742
	 * @return FileRepoStatus
1743
	 */
1744
	function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
1745
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1746
			return $this->readOnlyFatalStatus();
1747
		}
1748
1749
		$batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1750
1751
		$this->lock(); // begin
1752
		$batch->addOld( $archiveName );
1753
		$status = $batch->execute();
1754
		$this->unlock(); // done
1755
1756
		$this->purgeOldThumbnails( $archiveName );
1757
		if ( $status->isOK() ) {
1758
			$this->purgeDescription();
1759
		}
1760
1761
		DeferredUpdates::addUpdate(
1762
			new CdnCacheUpdate( [ $this->getArchiveUrl( $archiveName ) ] ),
1763
			DeferredUpdates::PRESEND
1764
		);
1765
1766
		return $status;
1767
	}
1768
1769
	/**
1770
	 * Restore all or specified deleted revisions to the given file.
1771
	 * Permissions and logging are left to the caller.
1772
	 *
1773
	 * May throw database exceptions on error.
1774
	 *
1775
	 * @param array $versions Set of record ids of deleted items to restore,
1776
	 *   or empty to restore all revisions.
1777
	 * @param bool $unsuppress
1778
	 * @return FileRepoStatus
1779
	 */
1780
	function restore( $versions = [], $unsuppress = false ) {
1781
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1782
			return $this->readOnlyFatalStatus();
1783
		}
1784
1785
		$batch = new LocalFileRestoreBatch( $this, $unsuppress );
1786
1787
		$this->lock(); // begin
1788
		if ( !$versions ) {
1789
			$batch->addAll();
1790
		} else {
1791
			$batch->addIds( $versions );
1792
		}
1793
		$status = $batch->execute();
1794
		if ( $status->isGood() ) {
1795
			$cleanupStatus = $batch->cleanup();
1796
			$cleanupStatus->successCount = 0;
1797
			$cleanupStatus->failCount = 0;
1798
			$status->merge( $cleanupStatus );
1799
		}
1800
		$this->unlock(); // done
1801
1802
		return $status;
1803
	}
1804
1805
	/** isMultipage inherited */
1806
	/** pageCount inherited */
1807
	/** scaleHeight inherited */
1808
	/** getImageSize inherited */
1809
1810
	/**
1811
	 * Get the URL of the file description page.
1812
	 * @return string
1813
	 */
1814
	function getDescriptionUrl() {
1815
		return $this->title->getLocalURL();
1816
	}
1817
1818
	/**
1819
	 * Get the HTML text of the description page
1820
	 * This is not used by ImagePage for local files, since (among other things)
1821
	 * it skips the parser cache.
1822
	 *
1823
	 * @param Language $lang What language to get description in (Optional)
1824
	 * @return bool|mixed
1825
	 */
1826
	function getDescriptionText( $lang = null ) {
1827
		$revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
1828
		if ( !$revision ) {
1829
			return false;
1830
		}
1831
		$content = $revision->getContent();
1832
		if ( !$content ) {
1833
			return false;
1834
		}
1835
		$pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
1836
1837
		return $pout->getText();
1838
	}
1839
1840
	/**
1841
	 * @param int $audience
1842
	 * @param User $user
1843
	 * @return string
1844
	 */
1845 View Code Duplication
	function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
1846
		$this->load();
1847
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
1848
			return '';
1849
		} elseif ( $audience == self::FOR_THIS_USER
1850
			&& !$this->userCan( self::DELETED_COMMENT, $user )
1851
		) {
1852
			return '';
1853
		} else {
1854
			return $this->description;
1855
		}
1856
	}
1857
1858
	/**
1859
	 * @return bool|string
1860
	 */
1861
	function getTimestamp() {
1862
		$this->load();
1863
1864
		return $this->timestamp;
1865
	}
1866
1867
	/**
1868
	 * @return bool|string
1869
	 */
1870
	public function getDescriptionTouched() {
1871
		// The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
1872
		// itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
1873
		// need to differentiate between null (uninitialized) and false (failed to load).
1874
		if ( $this->descriptionTouched === null ) {
1875
			$cond = [
1876
				'page_namespace' => $this->title->getNamespace(),
1877
				'page_title' => $this->title->getDBkey()
1878
			];
1879
			$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...
1880
			$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...
1881
		}
1882
1883
		return $this->descriptionTouched;
1884
	}
1885
1886
	/**
1887
	 * @return string
1888
	 */
1889
	function getSha1() {
1890
		$this->load();
1891
		// Initialise now if necessary
1892
		if ( $this->sha1 == '' && $this->fileExists ) {
1893
			$this->lock(); // begin
1894
1895
			$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...
1896
			if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1897
				$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...
1898
				$dbw->update( 'image',
1899
					[ 'img_sha1' => $this->sha1 ],
1900
					[ 'img_name' => $this->getName() ],
1901
					__METHOD__ );
1902
				$this->invalidateCache();
1903
			}
1904
1905
			$this->unlock(); // done
1906
		}
1907
1908
		return $this->sha1;
1909
	}
1910
1911
	/**
1912
	 * @return bool Whether to cache in RepoGroup (this avoids OOMs)
1913
	 */
1914
	function isCacheable() {
1915
		$this->load();
1916
1917
		// If extra data (metadata) was not loaded then it must have been large
1918
		return $this->extraDataLoaded
1919
		&& strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
1920
	}
1921
1922
	/**
1923
	 * @return Status
1924
	 * @since 1.28
1925
	 */
1926
	public function acquireFileLock() {
1927
		return $this->getRepo()->getBackend()->lockFiles(
1928
			[ $this->getPath() ], LockManager::LOCK_EX, 10
1929
		);
1930
	}
1931
1932
	/**
1933
	 * @return Status
1934
	 * @since 1.28
1935
	 */
1936
	public function releaseFileLock() {
1937
		return $this->getRepo()->getBackend()->unlockFiles(
1938
			[ $this->getPath() ], LockManager::LOCK_EX
1939
		);
1940
	}
1941
1942
	/**
1943
	 * Start an atomic DB section and lock the image for update
1944
	 * or increments a reference counter if the lock is already held
1945
	 *
1946
	 * This method should not be used outside of LocalFile/LocalFile*Batch
1947
	 *
1948
	 * @throws LocalFileLockError Throws an error if the lock was not acquired
1949
	 * @return bool Whether the file lock owns/spawned the DB transaction
1950
	 */
1951
	public function lock() {
1952
		if ( !$this->locked ) {
1953
			$logger = LoggerFactory::getInstance( 'LocalFile' );
1954
1955
			$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...
1956
			$makesTransaction = !$dbw->trxLevel();
1957
			$dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
1958
			// Bug 54736: use simple lock to handle when the file does not exist.
1959
			// SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
1960
			// Also, that would cause contention on INSERT of similarly named rows.
1961
			$status = $this->acquireFileLock(); // represents all versions of the file
1962
			if ( !$status->isGood() ) {
1963
				$dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
1964
				$logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
1965
1966
				throw new LocalFileLockError( $status );
1967
			}
1968
			// Release the lock *after* commit to avoid row-level contention.
1969
			// Make sure it triggers on rollback() as well as commit() (T132921).
1970
			$dbw->onTransactionResolution( function () use ( $logger ) {
1971
				$status = $this->releaseFileLock();
1972
				if ( !$status->isGood() ) {
1973
					$logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
1974
				}
1975
			} );
1976
			// Callers might care if the SELECT snapshot is safely fresh
1977
			$this->lockedOwnTrx = $makesTransaction;
1978
		}
1979
1980
		$this->locked++;
1981
1982
		return $this->lockedOwnTrx;
1983
	}
1984
1985
	/**
1986
	 * Decrement the lock reference count and end the atomic section if it reaches zero
1987
	 *
1988
	 * This method should not be used outside of LocalFile/LocalFile*Batch
1989
	 *
1990
	 * The commit and loc release will happen when no atomic sections are active, which
1991
	 * may happen immediately or at some point after calling this
1992
	 */
1993
	public function unlock() {
1994
		if ( $this->locked ) {
1995
			--$this->locked;
1996
			if ( !$this->locked ) {
1997
				$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...
1998
				$dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
1999
				$this->lockedOwnTrx = false;
2000
			}
2001
		}
2002
	}
2003
2004
	/**
2005
	 * @return Status
2006
	 */
2007
	protected function readOnlyFatalStatus() {
2008
		return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
2009
			$this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2010
	}
2011
2012
	/**
2013
	 * Clean up any dangling locks
2014
	 */
2015
	function __destruct() {
2016
		$this->unlock();
2017
	}
2018
} // LocalFile class
2019
2020
# ------------------------------------------------------------------------------
2021
2022
/**
2023
 * Helper class for file deletion
2024
 * @ingroup FileAbstraction
2025
 */
2026
class LocalFileDeleteBatch {
2027
	/** @var LocalFile */
2028
	private $file;
2029
2030
	/** @var string */
2031
	private $reason;
2032
2033
	/** @var array */
2034
	private $srcRels = [];
2035
2036
	/** @var array */
2037
	private $archiveUrls = [];
2038
2039
	/** @var array Items to be processed in the deletion batch */
2040
	private $deletionBatch;
2041
2042
	/** @var bool Whether to suppress all suppressable fields when deleting */
2043
	private $suppress;
2044
2045
	/** @var FileRepoStatus */
2046
	private $status;
2047
2048
	/** @var User */
2049
	private $user;
2050
2051
	/**
2052
	 * @param File $file
2053
	 * @param string $reason
2054
	 * @param bool $suppress
2055
	 * @param User|null $user
2056
	 */
2057
	function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
2058
		$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...
2059
		$this->reason = $reason;
2060
		$this->suppress = $suppress;
2061
		if ( $user ) {
2062
			$this->user = $user;
2063
		} else {
2064
			global $wgUser;
2065
			$this->user = $wgUser;
2066
		}
2067
		$this->status = $file->repo->newGood();
2068
	}
2069
2070
	public function addCurrent() {
2071
		$this->srcRels['.'] = $this->file->getRel();
2072
	}
2073
2074
	/**
2075
	 * @param string $oldName
2076
	 */
2077
	public function addOld( $oldName ) {
2078
		$this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
2079
		$this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
2080
	}
2081
2082
	/**
2083
	 * Add the old versions of the image to the batch
2084
	 * @return array List of archive names from old versions
2085
	 */
2086
	public function addOlds() {
2087
		$archiveNames = [];
2088
2089
		$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...
2090
		$result = $dbw->select( 'oldimage',
2091
			[ 'oi_archive_name' ],
2092
			[ 'oi_name' => $this->file->getName() ],
2093
			__METHOD__
2094
		);
2095
2096
		foreach ( $result as $row ) {
2097
			$this->addOld( $row->oi_archive_name );
2098
			$archiveNames[] = $row->oi_archive_name;
2099
		}
2100
2101
		return $archiveNames;
2102
	}
2103
2104
	/**
2105
	 * @return array
2106
	 */
2107
	protected function getOldRels() {
2108
		if ( !isset( $this->srcRels['.'] ) ) {
2109
			$oldRels =& $this->srcRels;
2110
			$deleteCurrent = false;
2111
		} else {
2112
			$oldRels = $this->srcRels;
2113
			unset( $oldRels['.'] );
2114
			$deleteCurrent = true;
2115
		}
2116
2117
		return [ $oldRels, $deleteCurrent ];
2118
	}
2119
2120
	/**
2121
	 * @return array
2122
	 */
2123
	protected function getHashes() {
2124
		$hashes = [];
2125
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2126
2127
		if ( $deleteCurrent ) {
2128
			$hashes['.'] = $this->file->getSha1();
2129
		}
2130
2131
		if ( count( $oldRels ) ) {
2132
			$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...
2133
			$res = $dbw->select(
2134
				'oldimage',
2135
				[ 'oi_archive_name', 'oi_sha1' ],
2136
				[ 'oi_archive_name' => array_keys( $oldRels ),
2137
					'oi_name' => $this->file->getName() ], // performance
2138
				__METHOD__
2139
			);
2140
2141
			foreach ( $res as $row ) {
2142
				if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
2143
					// Get the hash from the file
2144
					$oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
2145
					$props = $this->file->repo->getFileProps( $oldUrl );
2146
2147
					if ( $props['fileExists'] ) {
2148
						// Upgrade the oldimage row
2149
						$dbw->update( 'oldimage',
2150
							[ 'oi_sha1' => $props['sha1'] ],
2151
							[ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
2152
							__METHOD__ );
2153
						$hashes[$row->oi_archive_name] = $props['sha1'];
2154
					} else {
2155
						$hashes[$row->oi_archive_name] = false;
2156
					}
2157
				} else {
2158
					$hashes[$row->oi_archive_name] = $row->oi_sha1;
2159
				}
2160
			}
2161
		}
2162
2163
		$missing = array_diff_key( $this->srcRels, $hashes );
2164
2165
		foreach ( $missing as $name => $rel ) {
2166
			$this->status->error( 'filedelete-old-unregistered', $name );
2167
		}
2168
2169
		foreach ( $hashes as $name => $hash ) {
2170
			if ( !$hash ) {
2171
				$this->status->error( 'filedelete-missing', $this->srcRels[$name] );
2172
				unset( $hashes[$name] );
2173
			}
2174
		}
2175
2176
		return $hashes;
2177
	}
2178
2179
	protected function doDBInserts() {
2180
		$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...
2181
		$encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
2182
		$encUserId = $dbw->addQuotes( $this->user->getId() );
2183
		$encReason = $dbw->addQuotes( $this->reason );
2184
		$encGroup = $dbw->addQuotes( 'deleted' );
2185
		$ext = $this->file->getExtension();
2186
		$dotExt = $ext === '' ? '' : ".$ext";
2187
		$encExt = $dbw->addQuotes( $dotExt );
2188
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2189
2190
		// Bitfields to further suppress the content
2191 View Code Duplication
		if ( $this->suppress ) {
2192
			$bitfield = 0;
2193
			// This should be 15...
2194
			$bitfield |= Revision::DELETED_TEXT;
2195
			$bitfield |= Revision::DELETED_COMMENT;
2196
			$bitfield |= Revision::DELETED_USER;
2197
			$bitfield |= Revision::DELETED_RESTRICTED;
2198
		} else {
2199
			$bitfield = 'oi_deleted';
2200
		}
2201
2202
		if ( $deleteCurrent ) {
2203
			$concat = $dbw->buildConcat( [ "img_sha1", $encExt ] );
2204
			$where = [ 'img_name' => $this->file->getName() ];
2205
			$dbw->insertSelect( 'filearchive', 'image',
2206
				[
2207
					'fa_storage_group' => $encGroup,
2208
					'fa_storage_key' => $dbw->conditional(
2209
						[ 'img_sha1' => '' ],
2210
						$dbw->addQuotes( '' ),
2211
						$concat
2212
					),
2213
					'fa_deleted_user' => $encUserId,
2214
					'fa_deleted_timestamp' => $encTimestamp,
2215
					'fa_deleted_reason' => $encReason,
2216
					'fa_deleted' => $this->suppress ? $bitfield : 0,
2217
2218
					'fa_name' => 'img_name',
2219
					'fa_archive_name' => 'NULL',
2220
					'fa_size' => 'img_size',
2221
					'fa_width' => 'img_width',
2222
					'fa_height' => 'img_height',
2223
					'fa_metadata' => 'img_metadata',
2224
					'fa_bits' => 'img_bits',
2225
					'fa_media_type' => 'img_media_type',
2226
					'fa_major_mime' => 'img_major_mime',
2227
					'fa_minor_mime' => 'img_minor_mime',
2228
					'fa_description' => 'img_description',
2229
					'fa_user' => 'img_user',
2230
					'fa_user_text' => 'img_user_text',
2231
					'fa_timestamp' => 'img_timestamp',
2232
					'fa_sha1' => 'img_sha1',
2233
				], $where, __METHOD__ );
2234
		}
2235
2236
		if ( count( $oldRels ) ) {
2237
			$concat = $dbw->buildConcat( [ "oi_sha1", $encExt ] );
2238
			$where = [
2239
				'oi_name' => $this->file->getName(),
2240
				'oi_archive_name' => array_keys( $oldRels ) ];
2241
			$dbw->insertSelect( 'filearchive', 'oldimage',
2242
				[
2243
					'fa_storage_group' => $encGroup,
2244
					'fa_storage_key' => $dbw->conditional(
2245
						[ 'oi_sha1' => '' ],
2246
						$dbw->addQuotes( '' ),
2247
						$concat
2248
					),
2249
					'fa_deleted_user' => $encUserId,
2250
					'fa_deleted_timestamp' => $encTimestamp,
2251
					'fa_deleted_reason' => $encReason,
2252
					'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
2253
2254
					'fa_name' => 'oi_name',
2255
					'fa_archive_name' => 'oi_archive_name',
2256
					'fa_size' => 'oi_size',
2257
					'fa_width' => 'oi_width',
2258
					'fa_height' => 'oi_height',
2259
					'fa_metadata' => 'oi_metadata',
2260
					'fa_bits' => 'oi_bits',
2261
					'fa_media_type' => 'oi_media_type',
2262
					'fa_major_mime' => 'oi_major_mime',
2263
					'fa_minor_mime' => 'oi_minor_mime',
2264
					'fa_description' => 'oi_description',
2265
					'fa_user' => 'oi_user',
2266
					'fa_user_text' => 'oi_user_text',
2267
					'fa_timestamp' => 'oi_timestamp',
2268
					'fa_sha1' => 'oi_sha1',
2269
				], $where, __METHOD__ );
2270
		}
2271
	}
2272
2273
	function doDBDeletes() {
2274
		$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...
2275
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2276
2277
		if ( count( $oldRels ) ) {
2278
			$dbw->delete( 'oldimage',
2279
				[
2280
					'oi_name' => $this->file->getName(),
2281
					'oi_archive_name' => array_keys( $oldRels )
2282
				], __METHOD__ );
2283
		}
2284
2285
		if ( $deleteCurrent ) {
2286
			$dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
2287
		}
2288
	}
2289
2290
	/**
2291
	 * Run the transaction
2292
	 * @return FileRepoStatus
2293
	 */
2294
	public function execute() {
2295
		$repo = $this->file->getRepo();
2296
		$this->file->lock();
2297
2298
		// Prepare deletion batch
2299
		$hashes = $this->getHashes();
2300
		$this->deletionBatch = [];
2301
		$ext = $this->file->getExtension();
2302
		$dotExt = $ext === '' ? '' : ".$ext";
2303
2304
		foreach ( $this->srcRels as $name => $srcRel ) {
2305
			// Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
2306
			if ( isset( $hashes[$name] ) ) {
2307
				$hash = $hashes[$name];
2308
				$key = $hash . $dotExt;
2309
				$dstRel = $repo->getDeletedHashPath( $key ) . $key;
2310
				$this->deletionBatch[$name] = [ $srcRel, $dstRel ];
2311
			}
2312
		}
2313
2314
		if ( !$repo->hasSha1Storage() ) {
2315
			// Removes non-existent file from the batch, so we don't get errors.
2316
			// This also handles files in the 'deleted' zone deleted via revision deletion.
2317
			$checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
2318
			if ( !$checkStatus->isGood() ) {
2319
				$this->status->merge( $checkStatus );
2320
				return $this->status;
2321
			}
2322
			$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...
2323
2324
			// Execute the file deletion batch
2325
			$status = $this->file->repo->deleteBatch( $this->deletionBatch );
2326
			if ( !$status->isGood() ) {
2327
				$this->status->merge( $status );
2328
			}
2329
		}
2330
2331
		if ( !$this->status->isOK() ) {
2332
			// Critical file deletion error; abort
2333
			$this->file->unlock();
2334
2335
			return $this->status;
2336
		}
2337
2338
		// Copy the image/oldimage rows to filearchive
2339
		$this->doDBInserts();
2340
		// Delete image/oldimage rows
2341
		$this->doDBDeletes();
2342
2343
		// Commit and return
2344
		$this->file->unlock();
2345
2346
		return $this->status;
2347
	}
2348
2349
	/**
2350
	 * Removes non-existent files from a deletion batch.
2351
	 * @param array $batch
2352
	 * @return Status
2353
	 */
2354
	protected function removeNonexistentFiles( $batch ) {
2355
		$files = $newBatch = [];
2356
2357
		foreach ( $batch as $batchItem ) {
2358
			list( $src, ) = $batchItem;
2359
			$files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
2360
		}
2361
2362
		$result = $this->file->repo->fileExistsBatch( $files );
2363 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2364
			return Status::newFatal( 'backend-fail-internal',
2365
				$this->file->repo->getBackend()->getName() );
2366
		}
2367
2368
		foreach ( $batch as $batchItem ) {
2369
			if ( $result[$batchItem[0]] ) {
2370
				$newBatch[] = $batchItem;
2371
			}
2372
		}
2373
2374
		return Status::newGood( $newBatch );
2375
	}
2376
}
2377
2378
# ------------------------------------------------------------------------------
2379
2380
/**
2381
 * Helper class for file undeletion
2382
 * @ingroup FileAbstraction
2383
 */
2384
class LocalFileRestoreBatch {
2385
	/** @var LocalFile */
2386
	private $file;
2387
2388
	/** @var array List of file IDs to restore */
2389
	private $cleanupBatch;
2390
2391
	/** @var array List of file IDs to restore */
2392
	private $ids;
2393
2394
	/** @var bool Add all revisions of the file */
2395
	private $all;
2396
2397
	/** @var bool Whether to remove all settings for suppressed fields */
2398
	private $unsuppress = false;
2399
2400
	/**
2401
	 * @param File $file
2402
	 * @param bool $unsuppress
2403
	 */
2404
	function __construct( File $file, $unsuppress = false ) {
2405
		$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...
2406
		$this->cleanupBatch = $this->ids = [];
2407
		$this->ids = [];
2408
		$this->unsuppress = $unsuppress;
2409
	}
2410
2411
	/**
2412
	 * Add a file by ID
2413
	 * @param int $fa_id
2414
	 */
2415
	public function addId( $fa_id ) {
2416
		$this->ids[] = $fa_id;
2417
	}
2418
2419
	/**
2420
	 * Add a whole lot of files by ID
2421
	 * @param int[] $ids
2422
	 */
2423
	public function addIds( $ids ) {
2424
		$this->ids = array_merge( $this->ids, $ids );
2425
	}
2426
2427
	/**
2428
	 * Add all revisions of the file
2429
	 */
2430
	public function addAll() {
2431
		$this->all = true;
2432
	}
2433
2434
	/**
2435
	 * Run the transaction, except the cleanup batch.
2436
	 * The cleanup batch should be run in a separate transaction, because it locks different
2437
	 * rows and there's no need to keep the image row locked while it's acquiring those locks
2438
	 * The caller may have its own transaction open.
2439
	 * So we save the batch and let the caller call cleanup()
2440
	 * @return FileRepoStatus
2441
	 */
2442
	public function execute() {
2443
		global $wgLang;
2444
2445
		$repo = $this->file->getRepo();
2446
		if ( !$this->all && !$this->ids ) {
2447
			// Do nothing
2448
			return $repo->newGood();
2449
		}
2450
2451
		$lockOwnsTrx = $this->file->lock();
2452
2453
		$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...
2454
		$status = $this->file->repo->newGood();
2455
2456
		$exists = (bool)$dbw->selectField( 'image', '1',
2457
			[ 'img_name' => $this->file->getName() ],
2458
			__METHOD__,
2459
			// The lock() should already prevents changes, but this still may need
2460
			// to bypass any transaction snapshot. However, if lock() started the
2461
			// trx (which it probably did) then snapshot is post-lock and up-to-date.
2462
			$lockOwnsTrx ? [] : [ 'LOCK IN SHARE MODE' ]
2463
		);
2464
2465
		// Fetch all or selected archived revisions for the file,
2466
		// sorted from the most recent to the oldest.
2467
		$conditions = [ 'fa_name' => $this->file->getName() ];
2468
2469
		if ( !$this->all ) {
2470
			$conditions['fa_id'] = $this->ids;
2471
		}
2472
2473
		$result = $dbw->select(
2474
			'filearchive',
2475
			ArchivedFile::selectFields(),
2476
			$conditions,
2477
			__METHOD__,
2478
			[ 'ORDER BY' => 'fa_timestamp DESC' ]
2479
		);
2480
2481
		$idsPresent = [];
2482
		$storeBatch = [];
2483
		$insertBatch = [];
2484
		$insertCurrent = false;
2485
		$deleteIds = [];
2486
		$first = true;
2487
		$archiveNames = [];
2488
2489
		foreach ( $result as $row ) {
2490
			$idsPresent[] = $row->fa_id;
2491
2492
			if ( $row->fa_name != $this->file->getName() ) {
2493
				$status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
2494
				$status->failCount++;
2495
				continue;
2496
			}
2497
2498
			if ( $row->fa_storage_key == '' ) {
2499
				// Revision was missing pre-deletion
2500
				$status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
2501
				$status->failCount++;
2502
				continue;
2503
			}
2504
2505
			$deletedRel = $repo->getDeletedHashPath( $row->fa_storage_key ) .
2506
				$row->fa_storage_key;
2507
			$deletedUrl = $repo->getVirtualUrl() . '/deleted/' . $deletedRel;
2508
2509
			if ( isset( $row->fa_sha1 ) ) {
2510
				$sha1 = $row->fa_sha1;
2511
			} else {
2512
				// old row, populate from key
2513
				$sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
2514
			}
2515
2516
			# Fix leading zero
2517
			if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
2518
				$sha1 = substr( $sha1, 1 );
2519
			}
2520
2521
			if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
2522
				|| is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
2523
				|| is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
2524
				|| is_null( $row->fa_metadata )
2525
			) {
2526
				// Refresh our metadata
2527
				// Required for a new current revision; nice for older ones too. :)
2528
				$props = RepoGroup::singleton()->getFileProps( $deletedUrl );
2529
			} else {
2530
				$props = [
2531
					'minor_mime' => $row->fa_minor_mime,
2532
					'major_mime' => $row->fa_major_mime,
2533
					'media_type' => $row->fa_media_type,
2534
					'metadata' => $row->fa_metadata
2535
				];
2536
			}
2537
2538
			if ( $first && !$exists ) {
2539
				// This revision will be published as the new current version
2540
				$destRel = $this->file->getRel();
2541
				$insertCurrent = [
2542
					'img_name' => $row->fa_name,
2543
					'img_size' => $row->fa_size,
2544
					'img_width' => $row->fa_width,
2545
					'img_height' => $row->fa_height,
2546
					'img_metadata' => $props['metadata'],
2547
					'img_bits' => $row->fa_bits,
2548
					'img_media_type' => $props['media_type'],
2549
					'img_major_mime' => $props['major_mime'],
2550
					'img_minor_mime' => $props['minor_mime'],
2551
					'img_description' => $row->fa_description,
2552
					'img_user' => $row->fa_user,
2553
					'img_user_text' => $row->fa_user_text,
2554
					'img_timestamp' => $row->fa_timestamp,
2555
					'img_sha1' => $sha1
2556
				];
2557
2558
				// The live (current) version cannot be hidden!
2559 View Code Duplication
				if ( !$this->unsuppress && $row->fa_deleted ) {
2560
					$storeBatch[] = [ $deletedUrl, 'public', $destRel ];
2561
					$this->cleanupBatch[] = $row->fa_storage_key;
2562
				}
2563
			} else {
2564
				$archiveName = $row->fa_archive_name;
2565
2566
				if ( $archiveName == '' ) {
2567
					// This was originally a current version; we
2568
					// have to devise a new archive name for it.
2569
					// Format is <timestamp of archiving>!<name>
2570
					$timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
2571
2572
					do {
2573
						$archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
2574
						$timestamp++;
2575
					} while ( isset( $archiveNames[$archiveName] ) );
2576
				}
2577
2578
				$archiveNames[$archiveName] = true;
2579
				$destRel = $this->file->getArchiveRel( $archiveName );
2580
				$insertBatch[] = [
2581
					'oi_name' => $row->fa_name,
2582
					'oi_archive_name' => $archiveName,
2583
					'oi_size' => $row->fa_size,
2584
					'oi_width' => $row->fa_width,
2585
					'oi_height' => $row->fa_height,
2586
					'oi_bits' => $row->fa_bits,
2587
					'oi_description' => $row->fa_description,
2588
					'oi_user' => $row->fa_user,
2589
					'oi_user_text' => $row->fa_user_text,
2590
					'oi_timestamp' => $row->fa_timestamp,
2591
					'oi_metadata' => $props['metadata'],
2592
					'oi_media_type' => $props['media_type'],
2593
					'oi_major_mime' => $props['major_mime'],
2594
					'oi_minor_mime' => $props['minor_mime'],
2595
					'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
2596
					'oi_sha1' => $sha1 ];
2597
			}
2598
2599
			$deleteIds[] = $row->fa_id;
2600
2601 View Code Duplication
			if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
2602
				// private files can stay where they are
2603
				$status->successCount++;
2604
			} else {
2605
				$storeBatch[] = [ $deletedUrl, 'public', $destRel ];
2606
				$this->cleanupBatch[] = $row->fa_storage_key;
2607
			}
2608
2609
			$first = false;
2610
		}
2611
2612
		unset( $result );
2613
2614
		// Add a warning to the status object for missing IDs
2615
		$missingIds = array_diff( $this->ids, $idsPresent );
2616
2617
		foreach ( $missingIds as $id ) {
2618
			$status->error( 'undelete-missing-filearchive', $id );
2619
		}
2620
2621
		if ( !$repo->hasSha1Storage() ) {
2622
			// Remove missing files from batch, so we don't get errors when undeleting them
2623
			$checkStatus = $this->removeNonexistentFiles( $storeBatch );
2624
			if ( !$checkStatus->isGood() ) {
2625
				$status->merge( $checkStatus );
2626
				return $status;
2627
			}
2628
			$storeBatch = $checkStatus->value;
2629
2630
			// Run the store batch
2631
			// Use the OVERWRITE_SAME flag to smooth over a common error
2632
			$storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
2633
			$status->merge( $storeStatus );
2634
2635
			if ( !$status->isGood() ) {
2636
				// Even if some files could be copied, fail entirely as that is the
2637
				// easiest thing to do without data loss
2638
				$this->cleanupFailedBatch( $storeStatus, $storeBatch );
2639
				$status->ok = false;
2640
				$this->file->unlock();
2641
2642
				return $status;
2643
			}
2644
		}
2645
2646
		// Run the DB updates
2647
		// Because we have locked the image row, key conflicts should be rare.
2648
		// If they do occur, we can roll back the transaction at this time with
2649
		// no data loss, but leaving unregistered files scattered throughout the
2650
		// public zone.
2651
		// This is not ideal, which is why it's important to lock the image row.
2652
		if ( $insertCurrent ) {
2653
			$dbw->insert( 'image', $insertCurrent, __METHOD__ );
2654
		}
2655
2656
		if ( $insertBatch ) {
2657
			$dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
2658
		}
2659
2660
		if ( $deleteIds ) {
2661
			$dbw->delete( 'filearchive',
2662
				[ 'fa_id' => $deleteIds ],
2663
				__METHOD__ );
2664
		}
2665
2666
		// If store batch is empty (all files are missing), deletion is to be considered successful
2667
		if ( $status->successCount > 0 || !$storeBatch || $repo->hasSha1Storage() ) {
2668
			if ( !$exists ) {
2669
				wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
2670
2671
				DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2672
2673
				$this->file->purgeEverything();
2674
			} else {
2675
				wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
2676
				$this->file->purgeDescription();
2677
			}
2678
		}
2679
2680
		$this->file->unlock();
2681
2682
		return $status;
2683
	}
2684
2685
	/**
2686
	 * Removes non-existent files from a store batch.
2687
	 * @param array $triplets
2688
	 * @return Status
2689
	 */
2690
	protected function removeNonexistentFiles( $triplets ) {
2691
		$files = $filteredTriplets = [];
2692
		foreach ( $triplets as $file ) {
2693
			$files[$file[0]] = $file[0];
2694
		}
2695
2696
		$result = $this->file->repo->fileExistsBatch( $files );
2697 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2698
			return Status::newFatal( 'backend-fail-internal',
2699
				$this->file->repo->getBackend()->getName() );
2700
		}
2701
2702
		foreach ( $triplets as $file ) {
2703
			if ( $result[$file[0]] ) {
2704
				$filteredTriplets[] = $file;
2705
			}
2706
		}
2707
2708
		return Status::newGood( $filteredTriplets );
2709
	}
2710
2711
	/**
2712
	 * Removes non-existent files from a cleanup batch.
2713
	 * @param array $batch
2714
	 * @return array
2715
	 */
2716
	protected function removeNonexistentFromCleanup( $batch ) {
2717
		$files = $newBatch = [];
2718
		$repo = $this->file->repo;
2719
2720
		foreach ( $batch as $file ) {
2721
			$files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
2722
				rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
2723
		}
2724
2725
		$result = $repo->fileExistsBatch( $files );
2726
2727
		foreach ( $batch as $file ) {
2728
			if ( $result[$file] ) {
2729
				$newBatch[] = $file;
2730
			}
2731
		}
2732
2733
		return $newBatch;
2734
	}
2735
2736
	/**
2737
	 * Delete unused files in the deleted zone.
2738
	 * This should be called from outside the transaction in which execute() was called.
2739
	 * @return FileRepoStatus
2740
	 */
2741
	public function cleanup() {
2742
		if ( !$this->cleanupBatch ) {
2743
			return $this->file->repo->newGood();
2744
		}
2745
2746
		$this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
2747
2748
		$status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
2749
2750
		return $status;
2751
	}
2752
2753
	/**
2754
	 * Cleanup a failed batch. The batch was only partially successful, so
2755
	 * rollback by removing all items that were succesfully copied.
2756
	 *
2757
	 * @param Status $storeStatus
2758
	 * @param array $storeBatch
2759
	 */
2760
	protected function cleanupFailedBatch( $storeStatus, $storeBatch ) {
2761
		$cleanupBatch = [];
2762
2763
		foreach ( $storeStatus->success as $i => $success ) {
2764
			// Check if this item of the batch was successfully copied
2765
			if ( $success ) {
2766
				// Item was successfully copied and needs to be removed again
2767
				// Extract ($dstZone, $dstRel) from the batch
2768
				$cleanupBatch[] = [ $storeBatch[$i][1], $storeBatch[$i][2] ];
2769
			}
2770
		}
2771
		$this->file->repo->cleanupBatch( $cleanupBatch );
2772
	}
2773
}
2774
2775
# ------------------------------------------------------------------------------
2776
2777
/**
2778
 * Helper class for file movement
2779
 * @ingroup FileAbstraction
2780
 */
2781
class LocalFileMoveBatch {
2782
	/** @var LocalFile */
2783
	protected $file;
2784
2785
	/** @var Title */
2786
	protected $target;
2787
2788
	protected $cur;
2789
2790
	protected $olds;
2791
2792
	protected $oldCount;
2793
2794
	protected $archive;
2795
2796
	/** @var DatabaseBase */
2797
	protected $db;
2798
2799
	/**
2800
	 * @param File $file
2801
	 * @param Title $target
2802
	 */
2803
	function __construct( File $file, Title $target ) {
2804
		$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...
2805
		$this->target = $target;
2806
		$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...
2807
		$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...
2808
		$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...
2809
		$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...
2810
		$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...
2811
		$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...
2812
		$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...
2813
	}
2814
2815
	/**
2816
	 * Add the current image to the batch
2817
	 */
2818
	public function addCurrent() {
2819
		$this->cur = [ $this->oldRel, $this->newRel ];
2820
	}
2821
2822
	/**
2823
	 * Add the old versions of the image to the batch
2824
	 * @return array List of archive names from old versions
2825
	 */
2826
	public function addOlds() {
2827
		$archiveBase = 'archive';
2828
		$this->olds = [];
2829
		$this->oldCount = 0;
2830
		$archiveNames = [];
2831
2832
		$result = $this->db->select( 'oldimage',
2833
			[ 'oi_archive_name', 'oi_deleted' ],
2834
			[ 'oi_name' => $this->oldName ],
2835
			__METHOD__,
2836
			[ 'LOCK IN SHARE MODE' ] // ignore snapshot
2837
		);
2838
2839
		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...
2840
			$archiveNames[] = $row->oi_archive_name;
2841
			$oldName = $row->oi_archive_name;
2842
			$bits = explode( '!', $oldName, 2 );
2843
2844
			if ( count( $bits ) != 2 ) {
2845
				wfDebug( "Old file name missing !: '$oldName' \n" );
2846
				continue;
2847
			}
2848
2849
			list( $timestamp, $filename ) = $bits;
2850
2851
			if ( $this->oldName != $filename ) {
2852
				wfDebug( "Old file name doesn't match: '$oldName' \n" );
2853
				continue;
2854
			}
2855
2856
			$this->oldCount++;
2857
2858
			// Do we want to add those to oldCount?
2859
			if ( $row->oi_deleted & File::DELETED_FILE ) {
2860
				continue;
2861
			}
2862
2863
			$this->olds[] = [
2864
				"{$archiveBase}/{$this->oldHash}{$oldName}",
2865
				"{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
2866
			];
2867
		}
2868
2869
		return $archiveNames;
2870
	}
2871
2872
	/**
2873
	 * Perform the move.
2874
	 * @return FileRepoStatus
2875
	 */
2876
	public function execute() {
2877
		$repo = $this->file->repo;
2878
		$status = $repo->newGood();
2879
		$destFile = wfLocalFile( $this->target );
2880
2881
		$this->file->lock(); // begin
2882
		$destFile->lock(); // quickly fail if destination is not available
2883
2884
		$triplets = $this->getMoveTriplets();
2885
		$checkStatus = $this->removeNonexistentFiles( $triplets );
2886
		if ( !$checkStatus->isGood() ) {
2887
			$destFile->unlock();
2888
			$this->file->unlock();
2889
			$status->merge( $checkStatus ); // couldn't talk to file backend
2890
			return $status;
2891
		}
2892
		$triplets = $checkStatus->value;
2893
2894
		// Verify the file versions metadata in the DB.
2895
		$statusDb = $this->verifyDBUpdates();
2896
		if ( !$statusDb->isGood() ) {
2897
			$destFile->unlock();
2898
			$this->file->unlock();
2899
			$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...
2900
2901
			return $statusDb;
2902
		}
2903
2904
		if ( !$repo->hasSha1Storage() ) {
2905
			// Copy the files into their new location.
2906
			// If a prior process fataled copying or cleaning up files we tolerate any
2907
			// of the existing files if they are identical to the ones being stored.
2908
			$statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
2909
			wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
2910
				"{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
2911
			if ( !$statusMove->isGood() ) {
2912
				// Delete any files copied over (while the destination is still locked)
2913
				$this->cleanupTarget( $triplets );
2914
				$destFile->unlock();
2915
				$this->file->unlock();
2916
				wfDebugLog( 'imagemove', "Error in moving files: "
2917
					. $statusMove->getWikiText( false, false, 'en' ) );
2918
				$statusMove->ok = false;
2919
2920
				return $statusMove;
2921
			}
2922
			$status->merge( $statusMove );
2923
		}
2924
2925
		// Rename the file versions metadata in the DB.
2926
		$this->doDBUpdates();
2927
2928
		wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
2929
			"{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
2930
2931
		$destFile->unlock();
2932
		$this->file->unlock(); // done
2933
2934
		// Everything went ok, remove the source files
2935
		$this->cleanupSource( $triplets );
2936
2937
		$status->merge( $statusDb );
2938
2939
		return $status;
2940
	}
2941
2942
	/**
2943
	 * Verify the database updates and return a new FileRepoStatus indicating how
2944
	 * many rows would be updated.
2945
	 *
2946
	 * @return FileRepoStatus
2947
	 */
2948
	protected function verifyDBUpdates() {
2949
		$repo = $this->file->repo;
2950
		$status = $repo->newGood();
2951
		$dbw = $this->db;
2952
2953
		$hasCurrent = $dbw->selectField(
2954
			'image',
2955
			'1',
2956
			[ 'img_name' => $this->oldName ],
2957
			__METHOD__,
2958
			[ 'FOR UPDATE' ]
2959
		);
2960
		$oldRowCount = $dbw->selectField(
2961
			'oldimage',
2962
			'COUNT(*)',
2963
			[ 'oi_name' => $this->oldName ],
2964
			__METHOD__,
2965
			[ 'FOR UPDATE' ]
2966
		);
2967
2968
		if ( $hasCurrent ) {
2969
			$status->successCount++;
2970
		} else {
2971
			$status->failCount++;
2972
		}
2973
		$status->successCount += $oldRowCount;
2974
		// Bug 34934: oldCount is based on files that actually exist.
2975
		// There may be more DB rows than such files, in which case $affected
2976
		// can be greater than $total. We use max() to avoid negatives here.
2977
		$status->failCount += max( 0, $this->oldCount - $oldRowCount );
2978
		if ( $status->failCount ) {
2979
			$status->error( 'imageinvalidfilename' );
2980
		}
2981
2982
		return $status;
2983
	}
2984
2985
	/**
2986
	 * Do the database updates and return a new FileRepoStatus indicating how
2987
	 * many rows where updated.
2988
	 */
2989
	protected function doDBUpdates() {
2990
		$dbw = $this->db;
2991
2992
		// Update current image
2993
		$dbw->update(
2994
			'image',
2995
			[ 'img_name' => $this->newName ],
2996
			[ 'img_name' => $this->oldName ],
2997
			__METHOD__
2998
		);
2999
		// Update old images
3000
		$dbw->update(
3001
			'oldimage',
3002
			[
3003
				'oi_name' => $this->newName,
3004
				'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
3005
					$dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
3006
			],
3007
			[ 'oi_name' => $this->oldName ],
3008
			__METHOD__
3009
		);
3010
	}
3011
3012
	/**
3013
	 * Generate triplets for FileRepo::storeBatch().
3014
	 * @return array
3015
	 */
3016
	protected function getMoveTriplets() {
3017
		$moves = array_merge( [ $this->cur ], $this->olds );
3018
		$triplets = []; // The format is: (srcUrl, destZone, destUrl)
3019
3020
		foreach ( $moves as $move ) {
3021
			// $move: (oldRelativePath, newRelativePath)
3022
			$srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
3023
			$triplets[] = [ $srcUrl, 'public', $move[1] ];
3024
			wfDebugLog(
3025
				'imagemove',
3026
				"Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
3027
			);
3028
		}
3029
3030
		return $triplets;
3031
	}
3032
3033
	/**
3034
	 * Removes non-existent files from move batch.
3035
	 * @param array $triplets
3036
	 * @return Status
3037
	 */
3038
	protected function removeNonexistentFiles( $triplets ) {
3039
		$files = [];
3040
3041
		foreach ( $triplets as $file ) {
3042
			$files[$file[0]] = $file[0];
3043
		}
3044
3045
		$result = $this->file->repo->fileExistsBatch( $files );
3046 View Code Duplication
		if ( in_array( null, $result, true ) ) {
3047
			return Status::newFatal( 'backend-fail-internal',
3048
				$this->file->repo->getBackend()->getName() );
3049
		}
3050
3051
		$filteredTriplets = [];
3052
		foreach ( $triplets as $file ) {
3053
			if ( $result[$file[0]] ) {
3054
				$filteredTriplets[] = $file;
3055
			} else {
3056
				wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
3057
			}
3058
		}
3059
3060
		return Status::newGood( $filteredTriplets );
3061
	}
3062
3063
	/**
3064
	 * Cleanup a partially moved array of triplets by deleting the target
3065
	 * files. Called if something went wrong half way.
3066
	 * @param array $triplets
3067
	 */
3068
	protected function cleanupTarget( $triplets ) {
3069
		// Create dest pairs from the triplets
3070
		$pairs = [];
3071
		foreach ( $triplets as $triplet ) {
3072
			// $triplet: (old source virtual URL, dst zone, dest rel)
3073
			$pairs[] = [ $triplet[1], $triplet[2] ];
3074
		}
3075
3076
		$this->file->repo->cleanupBatch( $pairs );
3077
	}
3078
3079
	/**
3080
	 * Cleanup a fully moved array of triplets by deleting the source files.
3081
	 * Called at the end of the move process if everything else went ok.
3082
	 * @param array $triplets
3083
	 */
3084
	protected function cleanupSource( $triplets ) {
3085
		// Create source file names from the triplets
3086
		$files = [];
3087
		foreach ( $triplets as $triplet ) {
3088
			$files[] = $triplet[0];
3089
		}
3090
3091
		$this->file->repo->cleanupBatch( $files );
3092
	}
3093
}
3094
3095
class LocalFileLockError extends ErrorPageError {
3096
	public function __construct( Status $status ) {
3097
		parent::__construct(
3098
			'actionfailed',
3099
			$status->getMessage()
3100
		);
3101
	}
3102
3103
	public function report() {
3104
		global $wgOut;
3105
		$wgOut->setStatusCode( 429 );
3106
		parent::report();
3107
	}
3108
}
3109