Completed
Branch master (dc3656)
by
unknown
30:14
created

LocalFile::saveToCache()   C

Complexity

Conditions 8
Paths 15

Size

Total Lines 37
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 8
eloc 21
c 1
b 0
f 1
nc 15
nop 1
dl 0
loc 37
rs 5.3846

1 Method

Rating   Name   Duplication   Size   Complexity  
A LocalFile::invalidateCache() 0 10 2
1
<?php
2
/**
3
 * Local file in the wiki's own database.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup FileAbstraction
22
 */
23
24
use \MediaWiki\Logger\LoggerFactory;
25
26
/**
27
 * Class to represent a local file in the wiki's own database
28
 *
29
 * Provides methods to retrieve paths (physical, logical, URL),
30
 * to generate image thumbnails or for uploading.
31
 *
32
 * Note that only the repo object knows what its file class is called. You should
33
 * never name a file class explictly outside of the repo class. Instead use the
34
 * repo's factory functions to generate file objects, for example:
35
 *
36
 * RepoGroup::singleton()->getLocalRepo()->newFile( $title );
37
 *
38
 * The convenience functions wfLocalFile() and wfFindFile() should be sufficient
39
 * in most cases.
40
 *
41
 * @ingroup FileAbstraction
42
 */
43
class LocalFile extends File {
44
	const VERSION = 10; // cache version
45
46
	const CACHE_FIELD_MAX_LEN = 1000;
47
48
	/** @var bool Does the file exist on disk? (loadFromXxx) */
49
	protected $fileExists;
50
51
	/** @var int Image width */
52
	protected $width;
53
54
	/** @var int Image height */
55
	protected $height;
56
57
	/** @var int Returned by getimagesize (loadFromXxx) */
58
	protected $bits;
59
60
	/** @var string MEDIATYPE_xxx (bitmap, drawing, audio...) */
61
	protected $media_type;
62
63
	/** @var string MIME type, determined by MimeMagic::guessMimeType */
64
	protected $mime;
65
66
	/** @var int Size in bytes (loadFromXxx) */
67
	protected $size;
68
69
	/** @var string Handler-specific metadata */
70
	protected $metadata;
71
72
	/** @var string SHA-1 base 36 content hash */
73
	protected $sha1;
74
75
	/** @var bool Whether or not core data has been loaded from the database (loadFromXxx) */
76
	protected $dataLoaded;
77
78
	/** @var bool Whether or not lazy-loaded data has been loaded from the database */
79
	protected $extraDataLoaded;
80
81
	/** @var int Bitfield akin to rev_deleted */
82
	protected $deleted;
83
84
	/** @var string */
85
	protected $repoClass = 'LocalRepo';
86
87
	/** @var int Number of line to return by nextHistoryLine() (constructor) */
88
	private $historyLine;
89
90
	/** @var int Result of the query for the file's history (nextHistoryLine) */
91
	private $historyRes;
92
93
	/** @var string Major MIME type */
94
	private $major_mime;
95
96
	/** @var string Minor MIME type */
97
	private $minor_mime;
98
99
	/** @var string Upload timestamp */
100
	private $timestamp;
101
102
	/** @var int User ID of uploader */
103
	private $user;
104
105
	/** @var string User name of uploader */
106
	private $user_text;
107
108
	/** @var string Description of current revision of the file */
109
	private $description;
110
111
	/** @var string TS_MW timestamp of the last change of the file description */
112
	private $descriptionTouched;
113
114
	/** @var bool Whether the row was upgraded on load */
115
	private $upgraded;
116
117
	/** @var bool Whether the row was scheduled to upgrade on load */
118
	private $upgrading;
119
120
	/** @var bool True if the image row is locked */
121
	private $locked;
122
123
	/** @var bool True if the image row is locked with a lock initiated transaction */
124
	private $lockedOwnTrx;
125
126
	/** @var bool True if file is not present in file system. Not to be cached in memcached */
127
	private $missing;
128
129
	// @note: higher than IDBAccessObject constants
130
	const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
131
132
	const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
133
134
	/**
135
	 * Create a LocalFile from a title
136
	 * Do not call this except from inside a repo class.
137
	 *
138
	 * Note: $unused param is only here to avoid an E_STRICT
139
	 *
140
	 * @param Title $title
141
	 * @param FileRepo $repo
142
	 * @param null $unused
143
	 *
144
	 * @return LocalFile
145
	 */
146
	static function newFromTitle( $title, $repo, $unused = null ) {
147
		return new self( $title, $repo );
148
	}
149
150
	/**
151
	 * Create a LocalFile from a title
152
	 * Do not call this except from inside a repo class.
153
	 *
154
	 * @param stdClass $row
155
	 * @param FileRepo $repo
156
	 *
157
	 * @return LocalFile
158
	 */
159 View Code Duplication
	static function newFromRow( $row, $repo ) {
160
		$title = Title::makeTitle( NS_FILE, $row->img_name );
161
		$file = new self( $title, $repo );
162
		$file->loadFromRow( $row );
163
164
		return $file;
165
	}
166
167
	/**
168
	 * Create a LocalFile from a SHA-1 key
169
	 * Do not call this except from inside a repo class.
170
	 *
171
	 * @param string $sha1 Base-36 SHA-1
172
	 * @param LocalRepo $repo
173
	 * @param string|bool $timestamp MW_timestamp (optional)
174
	 * @return bool|LocalFile
175
	 */
176 View Code Duplication
	static function newFromKey( $sha1, $repo, $timestamp = false ) {
177
		$dbr = $repo->getSlaveDB();
178
179
		$conds = [ 'img_sha1' => $sha1 ];
180
		if ( $timestamp ) {
181
			$conds['img_timestamp'] = $dbr->timestamp( $timestamp );
182
		}
183
184
		$row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
185
		if ( $row ) {
186
			return self::newFromRow( $row, $repo );
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
		return $this->repo->getSharedCacheKey( 'file', sha1( $this->getName() ) );
241
	}
242
243
	/**
244
	 * Try to load file metadata from memcached, falling back to the database
245
	 */
246
	private function loadFromCache() {
247
		$this->dataLoaded = false;
248
		$this->extraDataLoaded = false;
249
250
		$key = $this->getCacheKey();
251
		if ( !$key ) {
252
			$this->loadFromDB( self::READ_NORMAL );
253
254
			return;
255
		}
256
257
		$cache = ObjectCache::getMainWANInstance();
258
		$cachedValues = $cache->getWithSetCallback(
259
			$key,
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 250 can also be of type boolean; however, WANObjectCache::getWithSetCallback() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
260
			$cache::TTL_WEEK,
261
			function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
262
				$setOpts += Database::getCacheSetOptions( $this->repo->getSlaveDB() );
0 ignored issues
show
Bug introduced by
The method getSlaveDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

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

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

Let’s take a look at an example:

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

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

    // do something with $myArray
}

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

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

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

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

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