Completed
Branch master (33c24b)
by
unknown
30:03
created

LocalFile::maybeUpgradeRow()   D

Complexity

Conditions 10
Paths 13

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 32
rs 4.8196
cc 10
eloc 20
nc 13
nop 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

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

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

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

    return array();
}

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

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

Loading history...
256
257
		// Check if the key existed and belongs to this version of MediaWiki
258
		if ( is_array( $cachedValues ) && $cachedValues['version'] == MW_FILE_VERSION ) {
259
			$this->fileExists = $cachedValues['fileExists'];
260
			if ( $this->fileExists ) {
261
				$this->setProps( $cachedValues );
262
			}
263
			$this->dataLoaded = true;
264
			$this->extraDataLoaded = true;
265
			foreach ( $this->getLazyCacheFields( '' ) as $field ) {
266
				$this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
267
			}
268
		}
269
270
		return $this->dataLoaded;
271
	}
272
273
	/**
274
	 * Save the file metadata to memcached
275
	 */
276
	private function saveToCache() {
277
		$this->load();
278
279
		$key = $this->getCacheKey();
280
		if ( !$key ) {
281
			return;
282
		}
283
284
		$fields = $this->getCacheFields( '' );
285
		$cacheVal = [ 'version' => MW_FILE_VERSION ];
286
		$cacheVal['fileExists'] = $this->fileExists;
287
288
		if ( $this->fileExists ) {
289
			foreach ( $fields as $field ) {
290
				$cacheVal[$field] = $this->$field;
291
			}
292
		}
293
294
		// Strip off excessive entries from the subset of fields that can become large.
295
		// If the cache value gets to large it will not fit in memcached and nothing will
296
		// get cached at all, causing master queries for any file access.
297
		foreach ( $this->getLazyCacheFields( '' ) as $field ) {
298
			if ( isset( $cacheVal[$field] ) && strlen( $cacheVal[$field] ) > 100 * 1024 ) {
299
				unset( $cacheVal[$field] ); // don't let the value get too big
300
			}
301
		}
302
303
		// Cache presence for 1 week and negatives for 1 day
304
		$ttl = $this->fileExists ? 86400 * 7 : 86400;
305
		$opts = Database::getCacheSetOptions( $this->repo->getSlaveDB() );
0 ignored issues
show
Bug introduced by
The method getSlaveDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
306
		ObjectCache::getMainWANInstance()->set( $key, $cacheVal, $ttl, $opts );
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 279 can also be of type boolean; however, WANObjectCache::set() does only seem to accept string, maybe add an additional type check?

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

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

    return array();
}

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

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

Loading history...
307
	}
308
309
	/**
310
	 * Purge the file object/metadata cache
311
	 */
312
	public function invalidateCache() {
313
		$key = $this->getCacheKey();
314
		if ( !$key ) {
315
			return;
316
		}
317
318
		$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...
319
			ObjectCache::getMainWANInstance()->delete( $key );
0 ignored issues
show
Bug introduced by
It seems like $key defined by $this->getCacheKey() on line 313 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...
320
		} );
321
	}
322
323
	/**
324
	 * Load metadata from the file itself
325
	 */
326
	function loadFromFile() {
327
		$props = $this->repo->getFileProps( $this->getVirtualUrl() );
328
		$this->setProps( $props );
329
	}
330
331
	/**
332
	 * @param string $prefix
333
	 * @return array
334
	 */
335
	function getCacheFields( $prefix = 'img_' ) {
336
		static $fields = [ 'size', 'width', 'height', 'bits', 'media_type',
337
			'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
338
			'user_text', 'description' ];
339
		static $results = [];
340
341
		if ( $prefix == '' ) {
342
			return $fields;
343
		}
344
345 View Code Duplication
		if ( !isset( $results[$prefix] ) ) {
346
			$prefixedFields = [];
347
			foreach ( $fields as $field ) {
348
				$prefixedFields[] = $prefix . $field;
349
			}
350
			$results[$prefix] = $prefixedFields;
351
		}
352
353
		return $results[$prefix];
354
	}
355
356
	/**
357
	 * @param string $prefix
358
	 * @return array
359
	 */
360
	function getLazyCacheFields( $prefix = 'img_' ) {
361
		static $fields = [ 'metadata' ];
362
		static $results = [];
363
364
		if ( $prefix == '' ) {
365
			return $fields;
366
		}
367
368 View Code Duplication
		if ( !isset( $results[$prefix] ) ) {
369
			$prefixedFields = [];
370
			foreach ( $fields as $field ) {
371
				$prefixedFields[] = $prefix . $field;
372
			}
373
			$results[$prefix] = $prefixedFields;
374
		}
375
376
		return $results[$prefix];
377
	}
378
379
	/**
380
	 * Load file metadata from the DB
381
	 * @param int $flags
382
	 */
383
	function loadFromDB( $flags = 0 ) {
384
		$fname = get_class( $this ) . '::' . __FUNCTION__;
385
386
		# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
387
		$this->dataLoaded = true;
388
		$this->extraDataLoaded = true;
389
390
		$dbr = ( $flags & self::READ_LATEST )
391
			? $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...
392
			: $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...
393
394
		$row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
395
			[ 'img_name' => $this->getName() ], $fname );
396
397
		if ( $row ) {
398
			$this->loadFromRow( $row );
399
		} else {
400
			$this->fileExists = false;
401
		}
402
	}
403
404
	/**
405
	 * Load lazy file metadata from the DB.
406
	 * This covers fields that are sometimes not cached.
407
	 */
408
	protected function loadExtraFromDB() {
409
		$fname = get_class( $this ) . '::' . __FUNCTION__;
410
411
		# Unconditionally set loaded=true, we don't want the accessors constantly rechecking
412
		$this->extraDataLoaded = true;
413
414
		$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...
415
		if ( !$fieldMap ) {
416
			$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...
417
		}
418
419
		if ( $fieldMap ) {
420
			foreach ( $fieldMap as $name => $value ) {
421
				$this->$name = $value;
422
			}
423
		} else {
424
			throw new MWException( "Could not find data for image '{$this->getName()}'." );
425
		}
426
	}
427
428
	/**
429
	 * @param IDatabase $dbr
430
	 * @param string $fname
431
	 * @return array|bool
432
	 */
433
	private function loadFieldsWithTimestamp( $dbr, $fname ) {
434
		$fieldMap = false;
435
436
		$row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
437
			[ 'img_name' => $this->getName(), 'img_timestamp' => $this->getTimestamp() ],
438
			$fname );
439
		if ( $row ) {
440
			$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 436 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...
441
		} else {
442
			# File may have been uploaded over in the meantime; check the old versions
443
			$row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
444
				[ 'oi_name' => $this->getName(), 'oi_timestamp' => $this->getTimestamp() ],
445
				$fname );
446
			if ( $row ) {
447
				$fieldMap = $this->unprefixRow( $row, 'oi_' );
448
			}
449
		}
450
451
		return $fieldMap;
452
	}
453
454
	/**
455
	 * @param array|object $row
456
	 * @param string $prefix
457
	 * @throws MWException
458
	 * @return array
459
	 */
460
	protected function unprefixRow( $row, $prefix = 'img_' ) {
461
		$array = (array)$row;
462
		$prefixLength = strlen( $prefix );
463
464
		// Sanity check prefix once
465
		if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
466
			throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
467
		}
468
469
		$decoded = [];
470
		foreach ( $array as $name => $value ) {
471
			$decoded[substr( $name, $prefixLength )] = $value;
472
		}
473
474
		return $decoded;
475
	}
476
477
	/**
478
	 * Decode a row from the database (either object or array) to an array
479
	 * with timestamps and MIME types decoded, and the field prefix removed.
480
	 * @param object $row
481
	 * @param string $prefix
482
	 * @throws MWException
483
	 * @return array
484
	 */
485
	function decodeRow( $row, $prefix = 'img_' ) {
486
		$decoded = $this->unprefixRow( $row, $prefix );
487
488
		$decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
489
490
		$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...
491
492
		if ( empty( $decoded['major_mime'] ) ) {
493
			$decoded['mime'] = 'unknown/unknown';
494
		} else {
495
			if ( !$decoded['minor_mime'] ) {
496
				$decoded['minor_mime'] = 'unknown';
497
			}
498
			$decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
499
		}
500
501
		// Trim zero padding from char/binary field
502
		$decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
503
504
		// Normalize some fields to integer type, per their database definition.
505
		// Use unary + so that overflows will be upgraded to double instead of
506
		// being trucated as with intval(). This is important to allow >2GB
507
		// files on 32-bit systems.
508
		foreach ( [ 'size', 'width', 'height', 'bits' ] as $field ) {
509
			$decoded[$field] = +$decoded[$field];
510
		}
511
512
		return $decoded;
513
	}
514
515
	/**
516
	 * Load file metadata from a DB result row
517
	 *
518
	 * @param object $row
519
	 * @param string $prefix
520
	 */
521
	function loadFromRow( $row, $prefix = 'img_' ) {
522
		$this->dataLoaded = true;
523
		$this->extraDataLoaded = true;
524
525
		$array = $this->decodeRow( $row, $prefix );
526
527
		foreach ( $array as $name => $value ) {
528
			$this->$name = $value;
529
		}
530
531
		$this->fileExists = true;
532
		$this->maybeUpgradeRow();
533
	}
534
535
	/**
536
	 * Load file metadata from cache or DB, unless already loaded
537
	 * @param int $flags
538
	 */
539
	function load( $flags = 0 ) {
540
		if ( !$this->dataLoaded ) {
541
			if ( ( $flags & self::READ_LATEST ) || !$this->loadFromCache() ) {
542
				$this->loadFromDB( $flags );
543
				$this->saveToCache();
544
			}
545
			$this->dataLoaded = true;
546
		}
547
		if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
548
			// @note: loads on name/timestamp to reduce race condition problems
549
			$this->loadExtraFromDB();
550
		}
551
	}
552
553
	/**
554
	 * Upgrade a row if it needs it
555
	 */
556
	function maybeUpgradeRow() {
557
		global $wgUpdateCompatibleMetadata;
558
		if ( wfReadOnly() ) {
559
			return;
560
		}
561
562
		$upgrade = false;
563
		if ( is_null( $this->media_type ) ||
564
			$this->mime == 'image/svg'
565
		) {
566
			$upgrade = true;
567
		} else {
568
			$handler = $this->getHandler();
569
			if ( $handler ) {
570
				$validity = $handler->isMetadataValid( $this, $this->getMetadata() );
571
				if ( $validity === MediaHandler::METADATA_BAD
572
					|| ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
573
				) {
574
					$upgrade = true;
575
				}
576
			}
577
		}
578
579
		if ( $upgrade ) {
580
			try {
581
				$this->upgradeRow();
582
			} catch ( LocalFileLockError $e ) {
583
				// let the other process handle it (or do it next time)
584
			}
585
			$this->upgraded = true; // avoid rework/retries
586
		}
587
	}
588
589
	function getUpgraded() {
590
		return $this->upgraded;
591
	}
592
593
	/**
594
	 * Fix assorted version-related problems with the image row by reloading it from the file
595
	 */
596
	function upgradeRow() {
597
		$this->lock(); // begin
598
599
		$this->loadFromFile();
600
601
		# Don't destroy file info of missing files
602
		if ( !$this->fileExists ) {
603
			$this->unlock();
604
			wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
605
606
			return;
607
		}
608
609
		$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...
610
		list( $major, $minor ) = self::splitMime( $this->mime );
611
612
		if ( wfReadOnly() ) {
613
			$this->unlock();
614
615
			return;
616
		}
617
		wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
618
619
		$dbw->update( 'image',
620
			[
621
				'img_size' => $this->size, // sanity
622
				'img_width' => $this->width,
623
				'img_height' => $this->height,
624
				'img_bits' => $this->bits,
625
				'img_media_type' => $this->media_type,
626
				'img_major_mime' => $major,
627
				'img_minor_mime' => $minor,
628
				'img_metadata' => $dbw->encodeBlob( $this->metadata ),
629
				'img_sha1' => $this->sha1,
630
			],
631
			[ 'img_name' => $this->getName() ],
632
			__METHOD__
633
		);
634
635
		$this->invalidateCache();
636
637
		$this->unlock(); // done
638
639
	}
640
641
	/**
642
	 * Set properties in this object to be equal to those given in the
643
	 * associative array $info. Only cacheable fields can be set.
644
	 * All fields *must* be set in $info except for getLazyCacheFields().
645
	 *
646
	 * If 'mime' is given, it will be split into major_mime/minor_mime.
647
	 * If major_mime/minor_mime are given, $this->mime will also be set.
648
	 *
649
	 * @param array $info
650
	 */
651
	function setProps( $info ) {
652
		$this->dataLoaded = true;
653
		$fields = $this->getCacheFields( '' );
654
		$fields[] = 'fileExists';
655
656
		foreach ( $fields as $field ) {
657
			if ( isset( $info[$field] ) ) {
658
				$this->$field = $info[$field];
659
			}
660
		}
661
662
		// Fix up mime fields
663
		if ( isset( $info['major_mime'] ) ) {
664
			$this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
665
		} elseif ( isset( $info['mime'] ) ) {
666
			$this->mime = $info['mime'];
667
			list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
668
		}
669
	}
670
671
	/** splitMime inherited */
672
	/** getName inherited */
673
	/** getTitle inherited */
674
	/** getURL inherited */
675
	/** getViewURL inherited */
676
	/** getPath inherited */
677
	/** isVisible inherited */
678
679
	/**
680
	 * @return bool
681
	 */
682
	function isMissing() {
683
		if ( $this->missing === null ) {
684
			list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
685
			$this->missing = !$fileExists;
686
		}
687
688
		return $this->missing;
689
	}
690
691
	/**
692
	 * Return the width of the image
693
	 *
694
	 * @param int $page
695
	 * @return int
696
	 */
697 View Code Duplication
	public function getWidth( $page = 1 ) {
698
		$this->load();
699
700
		if ( $this->isMultipage() ) {
701
			$handler = $this->getHandler();
702
			if ( !$handler ) {
703
				return 0;
704
			}
705
			$dim = $handler->getPageDimensions( $this, $page );
706
			if ( $dim ) {
707
				return $dim['width'];
708
			} else {
709
				// For non-paged media, the false goes through an
710
				// intval, turning failure into 0, so do same here.
711
				return 0;
712
			}
713
		} else {
714
			return $this->width;
715
		}
716
	}
717
718
	/**
719
	 * Return the height of the image
720
	 *
721
	 * @param int $page
722
	 * @return int
723
	 */
724 View Code Duplication
	public function getHeight( $page = 1 ) {
725
		$this->load();
726
727
		if ( $this->isMultipage() ) {
728
			$handler = $this->getHandler();
729
			if ( !$handler ) {
730
				return 0;
731
			}
732
			$dim = $handler->getPageDimensions( $this, $page );
733
			if ( $dim ) {
734
				return $dim['height'];
735
			} else {
736
				// For non-paged media, the false goes through an
737
				// intval, turning failure into 0, so do same here.
738
				return 0;
739
			}
740
		} else {
741
			return $this->height;
742
		}
743
	}
744
745
	/**
746
	 * Returns ID or name of user who uploaded the file
747
	 *
748
	 * @param string $type 'text' or 'id'
749
	 * @return int|string
750
	 */
751
	function getUser( $type = 'text' ) {
752
		$this->load();
753
754
		if ( $type == 'text' ) {
755
			return $this->user_text;
756
		} elseif ( $type == 'id' ) {
757
			return (int)$this->user;
758
		}
759
	}
760
761
	/**
762
	 * Get short description URL for a file based on the page ID.
763
	 *
764
	 * @return string|null
765
	 * @throws MWException
766
	 * @since 1.27
767
	 */
768
	public function getDescriptionShortUrl() {
769
		$pageId = $this->title->getArticleID();
770
771 View Code Duplication
		if ( $pageId !== null ) {
772
			$url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
773
			if ( $url !== false ) {
774
				return $url;
775
			}
776
		}
777
		return null;
778
	}
779
780
	/**
781
	 * Get handler-specific metadata
782
	 * @return string
783
	 */
784
	function getMetadata() {
785
		$this->load( self::LOAD_ALL ); // large metadata is loaded in another step
786
		return $this->metadata;
787
	}
788
789
	/**
790
	 * @return int
791
	 */
792
	function getBitDepth() {
793
		$this->load();
794
795
		return (int)$this->bits;
796
	}
797
798
	/**
799
	 * Returns the size of the image file, in bytes
800
	 * @return int
801
	 */
802
	public function getSize() {
803
		$this->load();
804
805
		return $this->size;
806
	}
807
808
	/**
809
	 * Returns the MIME type of the file.
810
	 * @return string
811
	 */
812
	function getMimeType() {
813
		$this->load();
814
815
		return $this->mime;
816
	}
817
818
	/**
819
	 * Returns the type of the media in the file.
820
	 * Use the value returned by this function with the MEDIATYPE_xxx constants.
821
	 * @return string
822
	 */
823
	function getMediaType() {
824
		$this->load();
825
826
		return $this->media_type;
827
	}
828
829
	/** canRender inherited */
830
	/** mustRender inherited */
831
	/** allowInlineDisplay inherited */
832
	/** isSafeFile inherited */
833
	/** isTrustedFile inherited */
834
835
	/**
836
	 * Returns true if the file exists on disk.
837
	 * @return bool Whether file exist on disk.
838
	 */
839
	public function exists() {
840
		$this->load();
841
842
		return $this->fileExists;
843
	}
844
845
	/** getTransformScript inherited */
846
	/** getUnscaledThumb inherited */
847
	/** thumbName inherited */
848
	/** createThumb inherited */
849
	/** transform inherited */
850
851
	/** getHandler inherited */
852
	/** iconThumb inherited */
853
	/** getLastError inherited */
854
855
	/**
856
	 * Get all thumbnail names previously generated for this file
857
	 * @param string|bool $archiveName Name of an archive file, default false
858
	 * @return array First element is the base dir, then files in that base dir.
859
	 */
860
	function getThumbnails( $archiveName = false ) {
861
		if ( $archiveName ) {
862
			$dir = $this->getArchiveThumbPath( $archiveName );
0 ignored issues
show
Bug introduced by
It seems like $archiveName defined by parameter $archiveName on line 860 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...
863
		} else {
864
			$dir = $this->getThumbPath();
865
		}
866
867
		$backend = $this->repo->getBackend();
868
		$files = [ $dir ];
869
		try {
870
			$iterator = $backend->getFileList( [ 'dir' => $dir ] );
871
			foreach ( $iterator as $file ) {
872
				$files[] = $file;
873
			}
874
		} catch ( FileBackendError $e ) {
875
		} // suppress (bug 54674)
876
877
		return $files;
878
	}
879
880
	/**
881
	 * Refresh metadata in memcached, but don't touch thumbnails or CDN
882
	 */
883
	function purgeMetadataCache() {
884
		$this->invalidateCache();
885
	}
886
887
	/**
888
	 * Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
889
	 *
890
	 * @param array $options An array potentially with the key forThumbRefresh.
891
	 *
892
	 * @note This used to purge old thumbnails by default as well, but doesn't anymore.
893
	 */
894
	function purgeCache( $options = [] ) {
895
		// Refresh metadata cache
896
		$this->purgeMetadataCache();
897
898
		// Delete thumbnails
899
		$this->purgeThumbnails( $options );
900
901
		// Purge CDN cache for this file
902
		DeferredUpdates::addUpdate(
903
			new CdnCacheUpdate( [ $this->getUrl() ] ),
904
			DeferredUpdates::PRESEND
905
		);
906
	}
907
908
	/**
909
	 * Delete cached transformed files for an archived version only.
910
	 * @param string $archiveName Name of the archived file
911
	 */
912
	function purgeOldThumbnails( $archiveName ) {
913
		// Get a list of old thumbnails and URLs
914
		$files = $this->getThumbnails( $archiveName );
915
916
		// Purge any custom thumbnail caches
917
		Hooks::run( 'LocalFilePurgeThumbnails', [ $this, $archiveName ] );
918
919
		// Delete thumbnails
920
		$dir = array_shift( $files );
921
		$this->purgeThumbList( $dir, $files );
922
923
		// Purge the CDN
924
		$urls = [];
925
		foreach ( $files as $file ) {
926
			$urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
927
		}
928
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
929
	}
930
931
	/**
932
	 * Delete cached transformed files for the current version only.
933
	 * @param array $options
934
	 */
935
	public function purgeThumbnails( $options = [] ) {
936
		$files = $this->getThumbnails();
937
		// Always purge all files from CDN regardless of handler filters
938
		$urls = [];
939
		foreach ( $files as $file ) {
940
			$urls[] = $this->getThumbUrl( $file );
941
		}
942
		array_shift( $urls ); // don't purge directory
943
944
		// Give media handler a chance to filter the file purge list
945
		if ( !empty( $options['forThumbRefresh'] ) ) {
946
			$handler = $this->getHandler();
947
			if ( $handler ) {
948
				$handler->filterThumbnailPurgeList( $files, $options );
949
			}
950
		}
951
952
		// Purge any custom thumbnail caches
953
		Hooks::run( 'LocalFilePurgeThumbnails', [ $this, false ] );
954
955
		// Delete thumbnails
956
		$dir = array_shift( $files );
957
		$this->purgeThumbList( $dir, $files );
958
959
		// Purge the CDN
960
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
961
	}
962
963
	/**
964
	 * Delete a list of thumbnails visible at urls
965
	 * @param string $dir Base dir of the files.
966
	 * @param array $files Array of strings: relative filenames (to $dir)
967
	 */
968
	protected function purgeThumbList( $dir, $files ) {
969
		$fileListDebug = strtr(
970
			var_export( $files, true ),
971
			[ "\n" => '' ]
972
		);
973
		wfDebug( __METHOD__ . ": $fileListDebug\n" );
974
975
		$purgeList = [];
976
		foreach ( $files as $file ) {
977
			# Check that the base file name is part of the thumb name
978
			# This is a basic sanity check to avoid erasing unrelated directories
979
			if ( strpos( $file, $this->getName() ) !== false
980
				|| strpos( $file, "-thumbnail" ) !== false // "short" thumb name
981
			) {
982
				$purgeList[] = "{$dir}/{$file}";
983
			}
984
		}
985
986
		# Delete the thumbnails
987
		$this->repo->quickPurgeBatch( $purgeList );
988
		# Clear out the thumbnail directory if empty
989
		$this->repo->quickCleanDir( $dir );
990
	}
991
992
	/** purgeDescription inherited */
993
	/** purgeEverything inherited */
994
995
	/**
996
	 * @param int $limit Optional: Limit to number of results
997
	 * @param int $start Optional: Timestamp, start from
998
	 * @param int $end Optional: Timestamp, end at
999
	 * @param bool $inc
1000
	 * @return OldLocalFile[]
1001
	 */
1002
	function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1003
		$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...
1004
		$tables = [ 'oldimage' ];
1005
		$fields = OldLocalFile::selectFields();
1006
		$conds = $opts = $join_conds = [];
1007
		$eq = $inc ? '=' : '';
1008
		$conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
1009
1010
		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...
1011
			$conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
1012
		}
1013
1014
		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...
1015
			$conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
1016
		}
1017
1018
		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...
1019
			$opts['LIMIT'] = $limit;
1020
		}
1021
1022
		// Search backwards for time > x queries
1023
		$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...
1024
		$opts['ORDER BY'] = "oi_timestamp $order";
1025
		$opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1026
1027
		Hooks::run( 'LocalFile::getHistory', [ &$this, &$tables, &$fields,
1028
			&$conds, &$opts, &$join_conds ] );
1029
1030
		$res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1031
		$r = [];
1032
1033
		foreach ( $res as $row ) {
1034
			$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...
1035
		}
1036
1037
		if ( $order == 'ASC' ) {
1038
			$r = array_reverse( $r ); // make sure it ends up descending
1039
		}
1040
1041
		return $r;
1042
	}
1043
1044
	/**
1045
	 * Returns the history of this file, line by line.
1046
	 * starts with current version, then old versions.
1047
	 * uses $this->historyLine to check which line to return:
1048
	 *  0      return line for current version
1049
	 *  1      query for old versions, return first one
1050
	 *  2, ... return next old version from above query
1051
	 * @return bool
1052
	 */
1053
	public function nextHistoryLine() {
1054
		# Polymorphic function name to distinguish foreign and local fetches
1055
		$fname = get_class( $this ) . '::' . __FUNCTION__;
1056
1057
		$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...
1058
1059
		if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1060
			$this->historyRes = $dbr->select( 'image',
1061
				[
1062
					'*',
1063
					"'' AS oi_archive_name",
1064
					'0 as oi_deleted',
1065
					'img_sha1'
1066
				],
1067
				[ 'img_name' => $this->title->getDBkey() ],
1068
				$fname
1069
			);
1070
1071
			if ( 0 == $dbr->numRows( $this->historyRes ) ) {
1072
				$this->historyRes = null;
1073
1074
				return false;
1075
			}
1076
		} elseif ( $this->historyLine == 1 ) {
1077
			$this->historyRes = $dbr->select( 'oldimage', '*',
1078
				[ 'oi_name' => $this->title->getDBkey() ],
1079
				$fname,
1080
				[ 'ORDER BY' => 'oi_timestamp DESC' ]
1081
			);
1082
		}
1083
		$this->historyLine++;
1084
1085
		return $dbr->fetchObject( $this->historyRes );
1086
	}
1087
1088
	/**
1089
	 * Reset the history pointer to the first element of the history
1090
	 */
1091
	public function resetHistory() {
1092
		$this->historyLine = 0;
1093
1094
		if ( !is_null( $this->historyRes ) ) {
1095
			$this->historyRes = null;
1096
		}
1097
	}
1098
1099
	/** getHashPath inherited */
1100
	/** getRel inherited */
1101
	/** getUrlRel inherited */
1102
	/** getArchiveRel inherited */
1103
	/** getArchivePath inherited */
1104
	/** getThumbPath inherited */
1105
	/** getArchiveUrl inherited */
1106
	/** getThumbUrl inherited */
1107
	/** getArchiveVirtualUrl inherited */
1108
	/** getThumbVirtualUrl inherited */
1109
	/** isHashed inherited */
1110
1111
	/**
1112
	 * Upload a file and record it in the DB
1113
	 * @param string|FSFile $src Source storage path, virtual URL, or filesystem path
1114
	 * @param string $comment Upload description
1115
	 * @param string $pageText Text to use for the new description page,
1116
	 *   if a new description page is created
1117
	 * @param int|bool $flags Flags for publish()
1118
	 * @param array|bool $props File properties, if known. This can be used to
1119
	 *   reduce the upload time when uploading virtual URLs for which the file
1120
	 *   info is already known
1121
	 * @param string|bool $timestamp Timestamp for img_timestamp, or false to use the
1122
	 *   current time
1123
	 * @param User|null $user User object or null to use $wgUser
1124
	 * @param string[] $tags Change tags to add to the log entry and page revision.
1125
	 *   (This doesn't check $user's permissions.)
1126
	 * @return FileRepoStatus On success, the value member contains the
1127
	 *     archive name, or an empty string if it was a new file.
1128
	 */
1129
	function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1130
		$timestamp = false, $user = null, $tags = []
1131
	) {
1132
		global $wgContLang;
1133
1134
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1135
			return $this->readOnlyFatalStatus();
1136
		}
1137
1138
		$srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1139
		if ( !$props ) {
1140
			if ( $this->repo->isVirtualUrl( $srcPath )
1141
				|| FileBackend::isStoragePath( $srcPath )
1142
			) {
1143
				$props = $this->repo->getFileProps( $srcPath );
1144
			} else {
1145
				$props = FSFile::getPropsFromPath( $srcPath );
1146
			}
1147
		}
1148
1149
		$options = [];
1150
		$handler = MediaHandler::getHandler( $props['mime'] );
1151 View Code Duplication
		if ( $handler ) {
1152
			$options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
1153
		} else {
1154
			$options['headers'] = [];
1155
		}
1156
1157
		// Trim spaces on user supplied text
1158
		$comment = trim( $comment );
1159
1160
		// Truncate nicely or the DB will do it for us
1161
		// non-nicely (dangling multi-byte chars, non-truncated version in cache).
1162
		$comment = $wgContLang->truncate( $comment, 255 );
1163
		$this->lock(); // begin
1164
		$status = $this->publish( $src, $flags, $options );
1165
1166
		if ( $status->successCount >= 2 ) {
1167
			// There will be a copy+(one of move,copy,store).
1168
			// The first succeeding does not commit us to updating the DB
1169
			// since it simply copied the current version to a timestamped file name.
1170
			// It is only *preferable* to avoid leaving such files orphaned.
1171
			// Once the second operation goes through, then the current version was
1172
			// updated and we must therefore update the DB too.
1173
			$oldver = $status->value;
1174
			if ( !$this->recordUpload2( $oldver, $comment, $pageText, $props, $timestamp, $user, $tags ) ) {
1175
				$status->fatal( 'filenotfound', $srcPath );
1176
			}
1177
		}
1178
1179
		$this->unlock(); // done
1180
1181
		return $status;
1182
	}
1183
1184
	/**
1185
	 * Record a file upload in the upload log and the image table
1186
	 * @param string $oldver
1187
	 * @param string $desc
1188
	 * @param string $license
1189
	 * @param string $copyStatus
1190
	 * @param string $source
1191
	 * @param bool $watch
1192
	 * @param string|bool $timestamp
1193
	 * @param User|null $user User object or null to use $wgUser
1194
	 * @return bool
1195
	 */
1196
	function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
1197
		$watch = false, $timestamp = false, User $user = null ) {
1198
		if ( !$user ) {
1199
			global $wgUser;
1200
			$user = $wgUser;
1201
		}
1202
1203
		$pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
1204
1205
		if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
1206
			return false;
1207
		}
1208
1209
		if ( $watch ) {
1210
			$user->addWatch( $this->getTitle() );
1211
		}
1212
1213
		return true;
1214
	}
1215
1216
	/**
1217
	 * Record a file upload in the upload log and the image table
1218
	 * @param string $oldver
1219
	 * @param string $comment
1220
	 * @param string $pageText
1221
	 * @param bool|array $props
1222
	 * @param string|bool $timestamp
1223
	 * @param null|User $user
1224
	 * @param string[] $tags
1225
	 * @return bool
1226
	 */
1227
	function recordUpload2(
1228
		$oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
1229
	) {
1230
		if ( is_null( $user ) ) {
1231
			global $wgUser;
1232
			$user = $wgUser;
1233
		}
1234
1235
		$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...
1236
1237
		# Imports or such might force a certain timestamp; otherwise we generate
1238
		# it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1239
		if ( $timestamp === false ) {
1240
			$timestamp = $dbw->timestamp();
1241
			$allowTimeKludge = true;
1242
		} else {
1243
			$allowTimeKludge = false;
1244
		}
1245
1246
		$props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1247
		$props['description'] = $comment;
1248
		$props['user'] = $user->getId();
1249
		$props['user_text'] = $user->getName();
1250
		$props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1251
		$this->setProps( $props );
1252
1253
		# Fail now if the file isn't there
1254
		if ( !$this->fileExists ) {
1255
			wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
1256
1257
			return false;
1258
		}
1259
1260
		$dbw->startAtomic( __METHOD__ );
1261
1262
		# Test to see if the row exists using INSERT IGNORE
1263
		# This avoids race conditions by locking the row until the commit, and also
1264
		# doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1265
		$dbw->insert( 'image',
1266
			[
1267
				'img_name' => $this->getName(),
1268
				'img_size' => $this->size,
1269
				'img_width' => intval( $this->width ),
1270
				'img_height' => intval( $this->height ),
1271
				'img_bits' => $this->bits,
1272
				'img_media_type' => $this->media_type,
1273
				'img_major_mime' => $this->major_mime,
1274
				'img_minor_mime' => $this->minor_mime,
1275
				'img_timestamp' => $timestamp,
1276
				'img_description' => $comment,
1277
				'img_user' => $user->getId(),
1278
				'img_user_text' => $user->getName(),
1279
				'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1280
				'img_sha1' => $this->sha1
1281
			],
1282
			__METHOD__,
1283
			'IGNORE'
1284
		);
1285
1286
		$reupload = ( $dbw->affectedRows() == 0 );
1287
		if ( $reupload ) {
1288
			if ( $allowTimeKludge ) {
1289
				# Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1290
				$ltimestamp = $dbw->selectField(
1291
					'image',
1292
					'img_timestamp',
1293
					[ 'img_name' => $this->getName() ],
1294
					__METHOD__,
1295
					[ 'LOCK IN SHARE MODE' ]
1296
				);
1297
				$lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
1298
				# Avoid a timestamp that is not newer than the last version
1299
				# TODO: the image/oldimage tables should be like page/revision with an ID field
1300
				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...
1301
					sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1302
					$timestamp = $dbw->timestamp( $lUnixtime + 1 );
1303
					$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...
1304
				}
1305
			}
1306
1307
			# (bug 34993) Note: $oldver can be empty here, if the previous
1308
			# version of the file was broken. Allow registration of the new
1309
			# version to continue anyway, because that's better than having
1310
			# an image that's not fixable by user operations.
1311
			# Collision, this is an update of a file
1312
			# Insert previous contents into oldimage
1313
			$dbw->insertSelect( 'oldimage', 'image',
1314
				[
1315
					'oi_name' => 'img_name',
1316
					'oi_archive_name' => $dbw->addQuotes( $oldver ),
1317
					'oi_size' => 'img_size',
1318
					'oi_width' => 'img_width',
1319
					'oi_height' => 'img_height',
1320
					'oi_bits' => 'img_bits',
1321
					'oi_timestamp' => 'img_timestamp',
1322
					'oi_description' => 'img_description',
1323
					'oi_user' => 'img_user',
1324
					'oi_user_text' => 'img_user_text',
1325
					'oi_metadata' => 'img_metadata',
1326
					'oi_media_type' => 'img_media_type',
1327
					'oi_major_mime' => 'img_major_mime',
1328
					'oi_minor_mime' => 'img_minor_mime',
1329
					'oi_sha1' => 'img_sha1'
1330
				],
1331
				[ 'img_name' => $this->getName() ],
1332
				__METHOD__
1333
			);
1334
1335
			# Update the current image row
1336
			$dbw->update( 'image',
1337
				[
1338
					'img_size' => $this->size,
1339
					'img_width' => intval( $this->width ),
1340
					'img_height' => intval( $this->height ),
1341
					'img_bits' => $this->bits,
1342
					'img_media_type' => $this->media_type,
1343
					'img_major_mime' => $this->major_mime,
1344
					'img_minor_mime' => $this->minor_mime,
1345
					'img_timestamp' => $timestamp,
1346
					'img_description' => $comment,
1347
					'img_user' => $user->getId(),
1348
					'img_user_text' => $user->getName(),
1349
					'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1350
					'img_sha1' => $this->sha1
1351
				],
1352
				[ 'img_name' => $this->getName() ],
1353
				__METHOD__
1354
			);
1355
		}
1356
1357
		$descTitle = $this->getTitle();
1358
		$descId = $descTitle->getArticleID();
1359
		$wikiPage = new WikiFilePage( $descTitle );
1360
		$wikiPage->setFile( $this );
1361
1362
		// Add the log entry...
1363
		$logEntry = new ManualLogEntry( 'upload', $reupload ? 'overwrite' : 'upload' );
1364
		$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...
1365
		$logEntry->setPerformer( $user );
1366
		$logEntry->setComment( $comment );
1367
		$logEntry->setTarget( $descTitle );
1368
		// Allow people using the api to associate log entries with the upload.
1369
		// Log has a timestamp, but sometimes different from upload timestamp.
1370
		$logEntry->setParameters(
1371
			[
1372
				'img_sha1' => $this->sha1,
1373
				'img_timestamp' => $timestamp,
1374
			]
1375
		);
1376
		// Note we keep $logId around since during new image
1377
		// creation, page doesn't exist yet, so log_page = 0
1378
		// but we want it to point to the page we're making,
1379
		// so we later modify the log entry.
1380
		// For a similar reason, we avoid making an RC entry
1381
		// now and wait until the page exists.
1382
		$logId = $logEntry->insert();
1383
1384
		if ( $descTitle->exists() ) {
1385
			// Use own context to get the action text in content language
1386
			$formatter = LogFormatter::newFromEntry( $logEntry );
1387
			$formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1388
			$editSummary = $formatter->getPlainActionText();
1389
1390
			$nullRevision = Revision::newNullRevision(
1391
				$dbw,
1392
				$descId,
1393
				$editSummary,
1394
				false,
1395
				$user
1396
			);
1397
			if ( $nullRevision ) {
1398
				$nullRevision->insertOn( $dbw );
1399
				Hooks::run(
1400
					'NewRevisionFromEditComplete',
1401
					[ $wikiPage, $nullRevision, $nullRevision->getParentId(), $user ]
1402
				);
1403
				$wikiPage->updateRevisionOn( $dbw, $nullRevision );
1404
				// Associate null revision id
1405
				$logEntry->setAssociatedRevId( $nullRevision->getId() );
1406
			}
1407
1408
			$newPageContent = null;
1409
		} else {
1410
			// Make the description page and RC log entry post-commit
1411
			$newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1412
		}
1413
1414
		# Defer purges, page creation, and link updates in case they error out.
1415
		# The most important thing is that files and the DB registry stay synced.
1416
		$dbw->endAtomic( __METHOD__ );
1417
1418
		# Do some cache purges after final commit so that:
1419
		# a) Changes are more likely to be seen post-purge
1420
		# b) They won't cause rollback of the log publish/update above
1421
		$that = $this;
1422
		$dbw->onTransactionIdle( function () use (
1423
			$that, $reupload, $wikiPage, $newPageContent, $comment, $user, $logEntry, $logId, $descId, $tags
1424
		) {
1425
			# Update memcache after the commit
1426
			$that->invalidateCache();
1427
1428
			$updateLogPage = false;
1429
			if ( $newPageContent ) {
1430
				# New file page; create the description page.
1431
				# There's already a log entry, so don't make a second RC entry
1432
				# CDN and file cache for the description page are purged by doEditContent.
1433
				$status = $wikiPage->doEditContent(
1434
					$newPageContent,
1435
					$comment,
1436
					EDIT_NEW | EDIT_SUPPRESS_RC,
1437
					false,
1438
					$user
1439
				);
1440
1441
				if ( isset( $status->value['revision'] ) ) {
1442
					// Associate new page revision id
1443
					$logEntry->setAssociatedRevId( $status->value['revision']->getId() );
1444
				}
1445
				// This relies on the resetArticleID() call in WikiPage::insertOn(),
1446
				// which is triggered on $descTitle by doEditContent() above.
1447
				if ( isset( $status->value['revision'] ) ) {
1448
					/** @var $rev Revision */
1449
					$rev = $status->value['revision'];
1450
					$updateLogPage = $rev->getPage();
1451
				}
1452
			} else {
1453
				# Existing file page: invalidate description page cache
1454
				$wikiPage->getTitle()->invalidateCache();
1455
				$wikiPage->getTitle()->purgeSquid();
1456
				# Allow the new file version to be patrolled from the page footer
1457
				Article::purgePatrolFooterCache( $descId );
1458
			}
1459
1460
			# Update associated rev id. This should be done by $logEntry->insert() earlier,
1461
			# but setAssociatedRevId() wasn't called at that point yet...
1462
			$logParams = $logEntry->getParameters();
1463
			$logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
1464
			$update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
1465
			if ( $updateLogPage ) {
1466
				# Also log page, in case where we just created it above
1467
				$update['log_page'] = $updateLogPage;
1468
			}
1469
			$that->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...
1470
				'logging',
1471
				$update,
1472
				[ 'log_id' => $logId ],
1473
				__METHOD__
1474
			);
1475
			$that->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...
1476
				'log_search',
1477
				[
1478
					'ls_field' => 'associated_rev_id',
1479
					'ls_value' => $logEntry->getAssociatedRevId(),
1480
					'ls_log_id' => $logId,
1481
				],
1482
				__METHOD__
1483
			);
1484
1485
			# Add change tags, if any
1486
			if ( $tags ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tags of type string[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1487
				$logEntry->setTags( $tags );
1488
			}
1489
1490
			# Uploads can be patrolled
1491
			$logEntry->setIsPatrollable( true );
1492
1493
			# Now that the log entry is up-to-date, make an RC entry.
1494
			$logEntry->publish( $logId );
1495
1496
			# Run hook for other updates (typically more cache purging)
1497
			Hooks::run( 'FileUpload', [ $that, $reupload, !$newPageContent ] );
1498
1499
			if ( $reupload ) {
1500
				# Delete old thumbnails
1501
				$that->purgeThumbnails();
1502
				# Remove the old file from the CDN cache
1503
				DeferredUpdates::addUpdate(
1504
					new CdnCacheUpdate( [ $that->getUrl() ] ),
1505
					DeferredUpdates::PRESEND
1506
				);
1507
			} else {
1508
				# Update backlink pages pointing to this title if created
1509
				LinksUpdate::queueRecursiveJobsForTable( $that->getTitle(), 'imagelinks' );
1510
			}
1511
		} );
1512
1513
		if ( !$reupload ) {
1514
			# This is a new file, so update the image count
1515
			DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
1516
		}
1517
1518
		# Invalidate cache for all pages using this file
1519
		DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ) );
1520
1521
		return true;
1522
	}
1523
1524
	/**
1525
	 * Move or copy a file to its public location. If a file exists at the
1526
	 * destination, move it to an archive. Returns a FileRepoStatus object with
1527
	 * the archive name in the "value" member on success.
1528
	 *
1529
	 * The archive name should be passed through to recordUpload for database
1530
	 * registration.
1531
	 *
1532
	 * @param string|FSFile $src Local filesystem path or virtual URL to the source image
1533
	 * @param int $flags A bitwise combination of:
1534
	 *     File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
1535
	 * @param array $options Optional additional parameters
1536
	 * @return FileRepoStatus On success, the value member contains the
1537
	 *     archive name, or an empty string if it was a new file.
1538
	 */
1539
	function publish( $src, $flags = 0, array $options = [] ) {
1540
		return $this->publishTo( $src, $this->getRel(), $flags, $options );
1541
	}
1542
1543
	/**
1544
	 * Move or copy a file to a specified location. Returns a FileRepoStatus
1545
	 * object with the archive name in the "value" member on success.
1546
	 *
1547
	 * The archive name should be passed through to recordUpload for database
1548
	 * registration.
1549
	 *
1550
	 * @param string|FSFile $src Local filesystem path or virtual URL to the source image
1551
	 * @param string $dstRel Target relative path
1552
	 * @param int $flags A bitwise combination of:
1553
	 *     File::DELETE_SOURCE    Delete the source file, i.e. move rather than copy
1554
	 * @param array $options Optional additional parameters
1555
	 * @return FileRepoStatus On success, the value member contains the
1556
	 *     archive name, or an empty string if it was a new file.
1557
	 */
1558
	function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
1559
		$srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1560
1561
		$repo = $this->getRepo();
1562
		if ( $repo->getReadOnlyReason() !== false ) {
1563
			return $this->readOnlyFatalStatus();
1564
		}
1565
1566
		$this->lock(); // begin
1567
1568
		$archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
1569
		$archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
1570
1571
		if ( $repo->hasSha1Storage() ) {
1572
			$sha1 = $repo->isVirtualUrl( $srcPath )
1573
				? $repo->getFileSha1( $srcPath )
1574
				: FSFile::getSha1Base36FromPath( $srcPath );
1575
			$dst = $repo->getBackend()->getPathForSHA1( $sha1 );
1576
			$status = $repo->quickImport( $src, $dst );
1577
			if ( $flags & File::DELETE_SOURCE ) {
1578
				unlink( $srcPath );
1579
			}
1580
1581
			if ( $this->exists() ) {
1582
				$status->value = $archiveName;
1583
			}
1584
		} else {
1585
			$flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
1586
			$status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
1587
1588
			if ( $status->value == 'new' ) {
1589
				$status->value = '';
1590
			} else {
1591
				$status->value = $archiveName;
1592
			}
1593
		}
1594
1595
		$this->unlock(); // done
1596
1597
		return $status;
1598
	}
1599
1600
	/** getLinksTo inherited */
1601
	/** getExifData inherited */
1602
	/** isLocal inherited */
1603
	/** wasDeleted inherited */
1604
1605
	/**
1606
	 * Move file to the new title
1607
	 *
1608
	 * Move current, old version and all thumbnails
1609
	 * to the new filename. Old file is deleted.
1610
	 *
1611
	 * Cache purging is done; checks for validity
1612
	 * and logging are caller's responsibility
1613
	 *
1614
	 * @param Title $target New file name
1615
	 * @return FileRepoStatus
1616
	 */
1617
	function move( $target ) {
1618
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1619
			return $this->readOnlyFatalStatus();
1620
		}
1621
1622
		wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1623
		$batch = new LocalFileMoveBatch( $this, $target );
1624
1625
		$this->lock(); // begin
1626
		$batch->addCurrent();
1627
		$archiveNames = $batch->addOlds();
1628
		$status = $batch->execute();
1629
		$this->unlock(); // done
1630
1631
		wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1632
1633
		// Purge the source and target files...
1634
		$oldTitleFile = wfLocalFile( $this->title );
1635
		$newTitleFile = wfLocalFile( $target );
1636
		// Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
1637
		// tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
1638
		$this->getRepo()->getMasterDB()->onTransactionIdle(
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...
1639
			function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
1640
				$oldTitleFile->purgeEverything();
1641
				foreach ( $archiveNames as $archiveName ) {
1642
					$oldTitleFile->purgeOldThumbnails( $archiveName );
1643
				}
1644
				$newTitleFile->purgeEverything();
1645
			}
1646
		);
1647
1648
		if ( $status->isOK() ) {
1649
			// Now switch the object
1650
			$this->title = $target;
1651
			// Force regeneration of the name and hashpath
1652
			unset( $this->name );
1653
			unset( $this->hashPath );
1654
		}
1655
1656
		return $status;
1657
	}
1658
1659
	/**
1660
	 * Delete all versions of the file.
1661
	 *
1662
	 * Moves the files into an archive directory (or deletes them)
1663
	 * and removes the database rows.
1664
	 *
1665
	 * Cache purging is done; logging is caller's responsibility.
1666
	 *
1667
	 * @param string $reason
1668
	 * @param bool $suppress
1669
	 * @param User|null $user
1670
	 * @return FileRepoStatus
1671
	 */
1672
	function delete( $reason, $suppress = false, $user = null ) {
1673
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1674
			return $this->readOnlyFatalStatus();
1675
		}
1676
1677
		$batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1678
1679
		$this->lock(); // begin
1680
		$batch->addCurrent();
1681
		# Get old version relative paths
1682
		$archiveNames = $batch->addOlds();
1683
		$status = $batch->execute();
1684
		$this->unlock(); // done
1685
1686
		if ( $status->isOK() ) {
1687
			DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
1688
		}
1689
1690
		// Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
1691
		// tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
1692
		$that = $this;
1693
		$this->getRepo()->getMasterDB()->onTransactionIdle(
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...
1694
			function () use ( $that, $archiveNames ) {
1695
				$that->purgeEverything();
1696
				foreach ( $archiveNames as $archiveName ) {
1697
					$that->purgeOldThumbnails( $archiveName );
1698
				}
1699
			}
1700
		);
1701
1702
		// Purge the CDN
1703
		$purgeUrls = [];
1704
		foreach ( $archiveNames as $archiveName ) {
1705
			$purgeUrls[] = $this->getArchiveUrl( $archiveName );
1706
		}
1707
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $purgeUrls ), DeferredUpdates::PRESEND );
1708
1709
		return $status;
1710
	}
1711
1712
	/**
1713
	 * Delete an old version of the file.
1714
	 *
1715
	 * Moves the file into an archive directory (or deletes it)
1716
	 * and removes the database row.
1717
	 *
1718
	 * Cache purging is done; logging is caller's responsibility.
1719
	 *
1720
	 * @param string $archiveName
1721
	 * @param string $reason
1722
	 * @param bool $suppress
1723
	 * @param User|null $user
1724
	 * @throws MWException Exception on database or file store failure
1725
	 * @return FileRepoStatus
1726
	 */
1727
	function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
1728
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1729
			return $this->readOnlyFatalStatus();
1730
		}
1731
1732
		$batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1733
1734
		$this->lock(); // begin
1735
		$batch->addOld( $archiveName );
1736
		$status = $batch->execute();
1737
		$this->unlock(); // done
1738
1739
		$this->purgeOldThumbnails( $archiveName );
1740
		if ( $status->isOK() ) {
1741
			$this->purgeDescription();
1742
		}
1743
1744
		DeferredUpdates::addUpdate(
1745
			new CdnCacheUpdate( [ $this->getArchiveUrl( $archiveName ) ] ),
1746
			DeferredUpdates::PRESEND
1747
		);
1748
1749
		return $status;
1750
	}
1751
1752
	/**
1753
	 * Restore all or specified deleted revisions to the given file.
1754
	 * Permissions and logging are left to the caller.
1755
	 *
1756
	 * May throw database exceptions on error.
1757
	 *
1758
	 * @param array $versions Set of record ids of deleted items to restore,
1759
	 *   or empty to restore all revisions.
1760
	 * @param bool $unsuppress
1761
	 * @return FileRepoStatus
1762
	 */
1763
	function restore( $versions = [], $unsuppress = false ) {
1764
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1765
			return $this->readOnlyFatalStatus();
1766
		}
1767
1768
		$batch = new LocalFileRestoreBatch( $this, $unsuppress );
1769
1770
		$this->lock(); // begin
1771
		if ( !$versions ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $versions of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1772
			$batch->addAll();
1773
		} else {
1774
			$batch->addIds( $versions );
1775
		}
1776
		$status = $batch->execute();
1777
		if ( $status->isGood() ) {
1778
			$cleanupStatus = $batch->cleanup();
1779
			$cleanupStatus->successCount = 0;
1780
			$cleanupStatus->failCount = 0;
1781
			$status->merge( $cleanupStatus );
1782
		}
1783
		$this->unlock(); // done
1784
1785
		return $status;
1786
	}
1787
1788
	/** isMultipage inherited */
1789
	/** pageCount inherited */
1790
	/** scaleHeight inherited */
1791
	/** getImageSize inherited */
1792
1793
	/**
1794
	 * Get the URL of the file description page.
1795
	 * @return string
1796
	 */
1797
	function getDescriptionUrl() {
1798
		return $this->title->getLocalURL();
1799
	}
1800
1801
	/**
1802
	 * Get the HTML text of the description page
1803
	 * This is not used by ImagePage for local files, since (among other things)
1804
	 * it skips the parser cache.
1805
	 *
1806
	 * @param Language $lang What language to get description in (Optional)
1807
	 * @return bool|mixed
1808
	 */
1809
	function getDescriptionText( $lang = null ) {
1810
		$revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
1811
		if ( !$revision ) {
1812
			return false;
1813
		}
1814
		$content = $revision->getContent();
1815
		if ( !$content ) {
1816
			return false;
1817
		}
1818
		$pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
1819
1820
		return $pout->getText();
1821
	}
1822
1823
	/**
1824
	 * @param int $audience
1825
	 * @param User $user
1826
	 * @return string
1827
	 */
1828 View Code Duplication
	function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
1829
		$this->load();
1830
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
1831
			return '';
1832
		} elseif ( $audience == self::FOR_THIS_USER
1833
			&& !$this->userCan( self::DELETED_COMMENT, $user )
1834
		) {
1835
			return '';
1836
		} else {
1837
			return $this->description;
1838
		}
1839
	}
1840
1841
	/**
1842
	 * @return bool|string
1843
	 */
1844
	function getTimestamp() {
1845
		$this->load();
1846
1847
		return $this->timestamp;
1848
	}
1849
1850
	/**
1851
	 * @return bool|string
1852
	 */
1853
	public function getDescriptionTouched() {
1854
		// The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
1855
		// itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
1856
		// need to differentiate between null (uninitialized) and false (failed to load).
1857
		if ( $this->descriptionTouched === null ) {
1858
			$cond = [
1859
				'page_namespace' => $this->title->getNamespace(),
1860
				'page_title' => $this->title->getDBkey()
1861
			];
1862
			$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...
1863
			$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...
1864
		}
1865
1866
		return $this->descriptionTouched;
1867
	}
1868
1869
	/**
1870
	 * @return string
1871
	 */
1872
	function getSha1() {
1873
		$this->load();
1874
		// Initialise now if necessary
1875
		if ( $this->sha1 == '' && $this->fileExists ) {
1876
			$this->lock(); // begin
1877
1878
			$this->sha1 = $this->repo->getFileSha1( $this->getPath() );
1879
			if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1880
				$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...
1881
				$dbw->update( 'image',
1882
					[ 'img_sha1' => $this->sha1 ],
1883
					[ 'img_name' => $this->getName() ],
1884
					__METHOD__ );
1885
				$this->invalidateCache();
1886
			}
1887
1888
			$this->unlock(); // done
1889
		}
1890
1891
		return $this->sha1;
1892
	}
1893
1894
	/**
1895
	 * @return bool Whether to cache in RepoGroup (this avoids OOMs)
1896
	 */
1897
	function isCacheable() {
1898
		$this->load();
1899
1900
		// If extra data (metadata) was not loaded then it must have been large
1901
		return $this->extraDataLoaded
1902
		&& strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
1903
	}
1904
1905
	/**
1906
	 * Start a transaction and lock the image for update
1907
	 * Increments a reference counter if the lock is already held
1908
	 * @throws LocalFileLockError Throws an error if the lock was not acquired
1909
	 * @return bool Whether the file lock owns/spawned the DB transaction
1910
	 */
1911
	function lock() {
1912
		if ( !$this->locked ) {
1913
			$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...
1914
			if ( !$dbw->trxLevel() ) {
1915
				$dbw->begin( __METHOD__ );
1916
				$this->lockedOwnTrx = true;
1917
			}
1918
			// Bug 54736: use simple lock to handle when the file does not exist.
1919
			// SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
1920
			// Also, that would cause contention on INSERT of similarly named rows.
1921
			$backend = $this->getRepo()->getBackend();
1922
			$lockPaths = [ $this->getPath() ]; // represents all versions of the file
1923
			$status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 5 );
1924
			if ( !$status->isGood() ) {
1925
				if ( $this->lockedOwnTrx ) {
1926
					$dbw->rollback( __METHOD__ );
1927
				}
1928
				throw new LocalFileLockError( "Could not acquire lock for '{$this->getName()}.'" );
1929
			}
1930
			// Release the lock *after* commit to avoid row-level contention
1931
			$this->locked++;
1932
			$dbw->onTransactionIdle( function () use ( $backend, $lockPaths ) {
1933
				$backend->unlockFiles( $lockPaths, LockManager::LOCK_EX );
1934
			} );
1935
		}
1936
1937
		return $this->lockedOwnTrx;
1938
	}
1939
1940
	/**
1941
	 * Decrement the lock reference count. If the reference count is reduced to zero, commits
1942
	 * the transaction and thereby releases the image lock.
1943
	 */
1944
	function unlock() {
1945
		if ( $this->locked ) {
1946
			--$this->locked;
1947
			if ( !$this->locked && $this->lockedOwnTrx ) {
1948
				$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...
1949
				$dbw->commit( __METHOD__ );
1950
				$this->lockedOwnTrx = false;
1951
			}
1952
		}
1953
	}
1954
1955
	/**
1956
	 * Roll back the DB transaction and mark the image unlocked
1957
	 */
1958
	function unlockAndRollback() {
1959
		$this->locked = false;
1960
		$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...
1961
		$dbw->rollback( __METHOD__ );
1962
		$this->lockedOwnTrx = false;
1963
	}
1964
1965
	/**
1966
	 * @return Status
1967
	 */
1968
	protected function readOnlyFatalStatus() {
1969
		return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
1970
			$this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
1971
	}
1972
1973
	/**
1974
	 * Clean up any dangling locks
1975
	 */
1976
	function __destruct() {
1977
		$this->unlock();
1978
	}
1979
} // LocalFile class
1980
1981
# ------------------------------------------------------------------------------
1982
1983
/**
1984
 * Helper class for file deletion
1985
 * @ingroup FileAbstraction
1986
 */
1987
class LocalFileDeleteBatch {
1988
	/** @var LocalFile */
1989
	private $file;
1990
1991
	/** @var string */
1992
	private $reason;
1993
1994
	/** @var array */
1995
	private $srcRels = [];
1996
1997
	/** @var array */
1998
	private $archiveUrls = [];
1999
2000
	/** @var array Items to be processed in the deletion batch */
2001
	private $deletionBatch;
2002
2003
	/** @var bool Whether to suppress all suppressable fields when deleting */
2004
	private $suppress;
2005
2006
	/** @var FileRepoStatus */
2007
	private $status;
2008
2009
	/** @var User */
2010
	private $user;
2011
2012
	/**
2013
	 * @param File $file
2014
	 * @param string $reason
2015
	 * @param bool $suppress
2016
	 * @param User|null $user
2017
	 */
2018
	function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
2019
		$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...
2020
		$this->reason = $reason;
2021
		$this->suppress = $suppress;
2022
		if ( $user ) {
2023
			$this->user = $user;
2024
		} else {
2025
			global $wgUser;
2026
			$this->user = $wgUser;
2027
		}
2028
		$this->status = $file->repo->newGood();
2029
	}
2030
2031
	public function addCurrent() {
2032
		$this->srcRels['.'] = $this->file->getRel();
2033
	}
2034
2035
	/**
2036
	 * @param string $oldName
2037
	 */
2038
	public function addOld( $oldName ) {
2039
		$this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
2040
		$this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
2041
	}
2042
2043
	/**
2044
	 * Add the old versions of the image to the batch
2045
	 * @return array List of archive names from old versions
2046
	 */
2047
	public function addOlds() {
2048
		$archiveNames = [];
2049
2050
		$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...
2051
		$result = $dbw->select( 'oldimage',
2052
			[ 'oi_archive_name' ],
2053
			[ 'oi_name' => $this->file->getName() ],
2054
			__METHOD__
2055
		);
2056
2057
		foreach ( $result as $row ) {
2058
			$this->addOld( $row->oi_archive_name );
2059
			$archiveNames[] = $row->oi_archive_name;
2060
		}
2061
2062
		return $archiveNames;
2063
	}
2064
2065
	/**
2066
	 * @return array
2067
	 */
2068
	protected function getOldRels() {
2069
		if ( !isset( $this->srcRels['.'] ) ) {
2070
			$oldRels =& $this->srcRels;
2071
			$deleteCurrent = false;
2072
		} else {
2073
			$oldRels = $this->srcRels;
2074
			unset( $oldRels['.'] );
2075
			$deleteCurrent = true;
2076
		}
2077
2078
		return [ $oldRels, $deleteCurrent ];
2079
	}
2080
2081
	/**
2082
	 * @return array
2083
	 */
2084
	protected function getHashes() {
2085
		$hashes = [];
2086
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2087
2088
		if ( $deleteCurrent ) {
2089
			$hashes['.'] = $this->file->getSha1();
2090
		}
2091
2092
		if ( count( $oldRels ) ) {
2093
			$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...
2094
			$res = $dbw->select(
2095
				'oldimage',
2096
				[ 'oi_archive_name', 'oi_sha1' ],
2097
				[ 'oi_archive_name' => array_keys( $oldRels ),
2098
					'oi_name' => $this->file->getName() ], // performance
2099
				__METHOD__
2100
			);
2101
2102
			foreach ( $res as $row ) {
2103
				if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
2104
					// Get the hash from the file
2105
					$oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
2106
					$props = $this->file->repo->getFileProps( $oldUrl );
2107
2108
					if ( $props['fileExists'] ) {
2109
						// Upgrade the oldimage row
2110
						$dbw->update( 'oldimage',
2111
							[ 'oi_sha1' => $props['sha1'] ],
2112
							[ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
2113
							__METHOD__ );
2114
						$hashes[$row->oi_archive_name] = $props['sha1'];
2115
					} else {
2116
						$hashes[$row->oi_archive_name] = false;
2117
					}
2118
				} else {
2119
					$hashes[$row->oi_archive_name] = $row->oi_sha1;
2120
				}
2121
			}
2122
		}
2123
2124
		$missing = array_diff_key( $this->srcRels, $hashes );
2125
2126
		foreach ( $missing as $name => $rel ) {
2127
			$this->status->error( 'filedelete-old-unregistered', $name );
2128
		}
2129
2130
		foreach ( $hashes as $name => $hash ) {
2131
			if ( !$hash ) {
2132
				$this->status->error( 'filedelete-missing', $this->srcRels[$name] );
2133
				unset( $hashes[$name] );
2134
			}
2135
		}
2136
2137
		return $hashes;
2138
	}
2139
2140
	protected function doDBInserts() {
2141
		$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...
2142
		$encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
2143
		$encUserId = $dbw->addQuotes( $this->user->getId() );
2144
		$encReason = $dbw->addQuotes( $this->reason );
2145
		$encGroup = $dbw->addQuotes( 'deleted' );
2146
		$ext = $this->file->getExtension();
2147
		$dotExt = $ext === '' ? '' : ".$ext";
2148
		$encExt = $dbw->addQuotes( $dotExt );
2149
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2150
2151
		// Bitfields to further suppress the content
2152 View Code Duplication
		if ( $this->suppress ) {
2153
			$bitfield = 0;
2154
			// This should be 15...
2155
			$bitfield |= Revision::DELETED_TEXT;
2156
			$bitfield |= Revision::DELETED_COMMENT;
2157
			$bitfield |= Revision::DELETED_USER;
2158
			$bitfield |= Revision::DELETED_RESTRICTED;
2159
		} else {
2160
			$bitfield = 'oi_deleted';
2161
		}
2162
2163
		if ( $deleteCurrent ) {
2164
			$concat = $dbw->buildConcat( [ "img_sha1", $encExt ] );
2165
			$where = [ 'img_name' => $this->file->getName() ];
2166
			$dbw->insertSelect( 'filearchive', 'image',
2167
				[
2168
					'fa_storage_group' => $encGroup,
2169
					'fa_storage_key' => $dbw->conditional(
2170
						[ 'img_sha1' => '' ],
2171
						$dbw->addQuotes( '' ),
2172
						$concat
2173
					),
2174
					'fa_deleted_user' => $encUserId,
2175
					'fa_deleted_timestamp' => $encTimestamp,
2176
					'fa_deleted_reason' => $encReason,
2177
					'fa_deleted' => $this->suppress ? $bitfield : 0,
2178
2179
					'fa_name' => 'img_name',
2180
					'fa_archive_name' => 'NULL',
2181
					'fa_size' => 'img_size',
2182
					'fa_width' => 'img_width',
2183
					'fa_height' => 'img_height',
2184
					'fa_metadata' => 'img_metadata',
2185
					'fa_bits' => 'img_bits',
2186
					'fa_media_type' => 'img_media_type',
2187
					'fa_major_mime' => 'img_major_mime',
2188
					'fa_minor_mime' => 'img_minor_mime',
2189
					'fa_description' => 'img_description',
2190
					'fa_user' => 'img_user',
2191
					'fa_user_text' => 'img_user_text',
2192
					'fa_timestamp' => 'img_timestamp',
2193
					'fa_sha1' => 'img_sha1',
2194
				], $where, __METHOD__ );
2195
		}
2196
2197
		if ( count( $oldRels ) ) {
2198
			$concat = $dbw->buildConcat( [ "oi_sha1", $encExt ] );
2199
			$where = [
2200
				'oi_name' => $this->file->getName(),
2201
				'oi_archive_name' => array_keys( $oldRels ) ];
2202
			$dbw->insertSelect( 'filearchive', 'oldimage',
2203
				[
2204
					'fa_storage_group' => $encGroup,
2205
					'fa_storage_key' => $dbw->conditional(
2206
						[ 'oi_sha1' => '' ],
2207
						$dbw->addQuotes( '' ),
2208
						$concat
2209
					),
2210
					'fa_deleted_user' => $encUserId,
2211
					'fa_deleted_timestamp' => $encTimestamp,
2212
					'fa_deleted_reason' => $encReason,
2213
					'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
2214
2215
					'fa_name' => 'oi_name',
2216
					'fa_archive_name' => 'oi_archive_name',
2217
					'fa_size' => 'oi_size',
2218
					'fa_width' => 'oi_width',
2219
					'fa_height' => 'oi_height',
2220
					'fa_metadata' => 'oi_metadata',
2221
					'fa_bits' => 'oi_bits',
2222
					'fa_media_type' => 'oi_media_type',
2223
					'fa_major_mime' => 'oi_major_mime',
2224
					'fa_minor_mime' => 'oi_minor_mime',
2225
					'fa_description' => 'oi_description',
2226
					'fa_user' => 'oi_user',
2227
					'fa_user_text' => 'oi_user_text',
2228
					'fa_timestamp' => 'oi_timestamp',
2229
					'fa_sha1' => 'oi_sha1',
2230
				], $where, __METHOD__ );
2231
		}
2232
	}
2233
2234
	function doDBDeletes() {
2235
		$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...
2236
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2237
2238
		if ( count( $oldRels ) ) {
2239
			$dbw->delete( 'oldimage',
2240
				[
2241
					'oi_name' => $this->file->getName(),
2242
					'oi_archive_name' => array_keys( $oldRels )
2243
				], __METHOD__ );
2244
		}
2245
2246
		if ( $deleteCurrent ) {
2247
			$dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
2248
		}
2249
	}
2250
2251
	/**
2252
	 * Run the transaction
2253
	 * @return FileRepoStatus
2254
	 */
2255
	public function execute() {
2256
		$repo = $this->file->getRepo();
2257
		$this->file->lock();
2258
2259
		// Prepare deletion batch
2260
		$hashes = $this->getHashes();
2261
		$this->deletionBatch = [];
2262
		$ext = $this->file->getExtension();
2263
		$dotExt = $ext === '' ? '' : ".$ext";
2264
2265
		foreach ( $this->srcRels as $name => $srcRel ) {
2266
			// Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
2267
			if ( isset( $hashes[$name] ) ) {
2268
				$hash = $hashes[$name];
2269
				$key = $hash . $dotExt;
2270
				$dstRel = $repo->getDeletedHashPath( $key ) . $key;
2271
				$this->deletionBatch[$name] = [ $srcRel, $dstRel ];
2272
			}
2273
		}
2274
2275
		// Lock the filearchive rows so that the files don't get deleted by a cleanup operation
2276
		// We acquire this lock by running the inserts now, before the file operations.
2277
		// This potentially has poor lock contention characteristics -- an alternative
2278
		// scheme would be to insert stub filearchive entries with no fa_name and commit
2279
		// them in a separate transaction, then run the file ops, then update the fa_name fields.
2280
		$this->doDBInserts();
2281
2282
		if ( !$repo->hasSha1Storage() ) {
2283
			// Removes non-existent file from the batch, so we don't get errors.
2284
			// This also handles files in the 'deleted' zone deleted via revision deletion.
2285
			$checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
2286
			if ( !$checkStatus->isGood() ) {
2287
				$this->status->merge( $checkStatus );
2288
				return $this->status;
2289
			}
2290
			$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...
2291
2292
			// Execute the file deletion batch
2293
			$status = $this->file->repo->deleteBatch( $this->deletionBatch );
2294
2295
			if ( !$status->isGood() ) {
2296
				$this->status->merge( $status );
2297
			}
2298
		}
2299
2300
		if ( !$this->status->isOK() ) {
2301
			// Critical file deletion error
2302
			// Roll back inserts, release lock and abort
2303
			// TODO: delete the defunct filearchive rows if we are using a non-transactional DB
2304
			$this->file->unlockAndRollback();
2305
2306
			return $this->status;
2307
		}
2308
2309
		// Delete image/oldimage rows
2310
		$this->doDBDeletes();
2311
2312
		// Commit and return
2313
		$this->file->unlock();
2314
2315
		return $this->status;
2316
	}
2317
2318
	/**
2319
	 * Removes non-existent files from a deletion batch.
2320
	 * @param array $batch
2321
	 * @return Status
2322
	 */
2323
	protected function removeNonexistentFiles( $batch ) {
2324
		$files = $newBatch = [];
2325
2326
		foreach ( $batch as $batchItem ) {
2327
			list( $src, ) = $batchItem;
2328
			$files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
2329
		}
2330
2331
		$result = $this->file->repo->fileExistsBatch( $files );
2332 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2333
			return Status::newFatal( 'backend-fail-internal',
2334
				$this->file->repo->getBackend()->getName() );
2335
		}
2336
2337
		foreach ( $batch as $batchItem ) {
2338
			if ( $result[$batchItem[0]] ) {
2339
				$newBatch[] = $batchItem;
2340
			}
2341
		}
2342
2343
		return Status::newGood( $newBatch );
2344
	}
2345
}
2346
2347
# ------------------------------------------------------------------------------
2348
2349
/**
2350
 * Helper class for file undeletion
2351
 * @ingroup FileAbstraction
2352
 */
2353
class LocalFileRestoreBatch {
2354
	/** @var LocalFile */
2355
	private $file;
2356
2357
	/** @var array List of file IDs to restore */
2358
	private $cleanupBatch;
2359
2360
	/** @var array List of file IDs to restore */
2361
	private $ids;
2362
2363
	/** @var bool Add all revisions of the file */
2364
	private $all;
2365
2366
	/** @var bool Whether to remove all settings for suppressed fields */
2367
	private $unsuppress = false;
2368
2369
	/**
2370
	 * @param File $file
2371
	 * @param bool $unsuppress
2372
	 */
2373
	function __construct( File $file, $unsuppress = false ) {
2374
		$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...
2375
		$this->cleanupBatch = $this->ids = [];
2376
		$this->ids = [];
2377
		$this->unsuppress = $unsuppress;
2378
	}
2379
2380
	/**
2381
	 * Add a file by ID
2382
	 * @param int $fa_id
2383
	 */
2384
	public function addId( $fa_id ) {
2385
		$this->ids[] = $fa_id;
2386
	}
2387
2388
	/**
2389
	 * Add a whole lot of files by ID
2390
	 * @param int[] $ids
2391
	 */
2392
	public function addIds( $ids ) {
2393
		$this->ids = array_merge( $this->ids, $ids );
2394
	}
2395
2396
	/**
2397
	 * Add all revisions of the file
2398
	 */
2399
	public function addAll() {
2400
		$this->all = true;
2401
	}
2402
2403
	/**
2404
	 * Run the transaction, except the cleanup batch.
2405
	 * The cleanup batch should be run in a separate transaction, because it locks different
2406
	 * rows and there's no need to keep the image row locked while it's acquiring those locks
2407
	 * The caller may have its own transaction open.
2408
	 * So we save the batch and let the caller call cleanup()
2409
	 * @return FileRepoStatus
2410
	 */
2411
	public function execute() {
2412
		global $wgLang;
2413
2414
		$repo = $this->file->getRepo();
2415
		if ( !$this->all && !$this->ids ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->ids of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2416
			// Do nothing
2417
			return $repo->newGood();
2418
		}
2419
2420
		$lockOwnsTrx = $this->file->lock();
2421
2422
		$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...
2423
		$status = $this->file->repo->newGood();
2424
2425
		$exists = (bool)$dbw->selectField( 'image', '1',
2426
			[ 'img_name' => $this->file->getName() ],
2427
			__METHOD__,
2428
			// The lock() should already prevents changes, but this still may need
2429
			// to bypass any transaction snapshot. However, if lock() started the
2430
			// trx (which it probably did) then snapshot is post-lock and up-to-date.
2431
			$lockOwnsTrx ? [] : [ 'LOCK IN SHARE MODE' ]
2432
		);
2433
2434
		// Fetch all or selected archived revisions for the file,
2435
		// sorted from the most recent to the oldest.
2436
		$conditions = [ 'fa_name' => $this->file->getName() ];
2437
2438
		if ( !$this->all ) {
2439
			$conditions['fa_id'] = $this->ids;
2440
		}
2441
2442
		$result = $dbw->select(
2443
			'filearchive',
2444
			ArchivedFile::selectFields(),
2445
			$conditions,
2446
			__METHOD__,
2447
			[ 'ORDER BY' => 'fa_timestamp DESC' ]
2448
		);
2449
2450
		$idsPresent = [];
2451
		$storeBatch = [];
2452
		$insertBatch = [];
2453
		$insertCurrent = false;
2454
		$deleteIds = [];
2455
		$first = true;
2456
		$archiveNames = [];
2457
2458
		foreach ( $result as $row ) {
2459
			$idsPresent[] = $row->fa_id;
2460
2461
			if ( $row->fa_name != $this->file->getName() ) {
2462
				$status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
2463
				$status->failCount++;
2464
				continue;
2465
			}
2466
2467
			if ( $row->fa_storage_key == '' ) {
2468
				// Revision was missing pre-deletion
2469
				$status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
2470
				$status->failCount++;
2471
				continue;
2472
			}
2473
2474
			$deletedRel = $repo->getDeletedHashPath( $row->fa_storage_key ) .
2475
				$row->fa_storage_key;
2476
			$deletedUrl = $repo->getVirtualUrl() . '/deleted/' . $deletedRel;
2477
2478
			if ( isset( $row->fa_sha1 ) ) {
2479
				$sha1 = $row->fa_sha1;
2480
			} else {
2481
				// old row, populate from key
2482
				$sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
2483
			}
2484
2485
			# Fix leading zero
2486
			if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
2487
				$sha1 = substr( $sha1, 1 );
2488
			}
2489
2490
			if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
2491
				|| is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
2492
				|| is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
2493
				|| is_null( $row->fa_metadata )
2494
			) {
2495
				// Refresh our metadata
2496
				// Required for a new current revision; nice for older ones too. :)
2497
				$props = RepoGroup::singleton()->getFileProps( $deletedUrl );
2498
			} else {
2499
				$props = [
2500
					'minor_mime' => $row->fa_minor_mime,
2501
					'major_mime' => $row->fa_major_mime,
2502
					'media_type' => $row->fa_media_type,
2503
					'metadata' => $row->fa_metadata
2504
				];
2505
			}
2506
2507
			if ( $first && !$exists ) {
2508
				// This revision will be published as the new current version
2509
				$destRel = $this->file->getRel();
2510
				$insertCurrent = [
2511
					'img_name' => $row->fa_name,
2512
					'img_size' => $row->fa_size,
2513
					'img_width' => $row->fa_width,
2514
					'img_height' => $row->fa_height,
2515
					'img_metadata' => $props['metadata'],
2516
					'img_bits' => $row->fa_bits,
2517
					'img_media_type' => $props['media_type'],
2518
					'img_major_mime' => $props['major_mime'],
2519
					'img_minor_mime' => $props['minor_mime'],
2520
					'img_description' => $row->fa_description,
2521
					'img_user' => $row->fa_user,
2522
					'img_user_text' => $row->fa_user_text,
2523
					'img_timestamp' => $row->fa_timestamp,
2524
					'img_sha1' => $sha1
2525
				];
2526
2527
				// The live (current) version cannot be hidden!
2528 View Code Duplication
				if ( !$this->unsuppress && $row->fa_deleted ) {
2529
					$storeBatch[] = [ $deletedUrl, 'public', $destRel ];
2530
					$this->cleanupBatch[] = $row->fa_storage_key;
2531
				}
2532
			} else {
2533
				$archiveName = $row->fa_archive_name;
2534
2535
				if ( $archiveName == '' ) {
2536
					// This was originally a current version; we
2537
					// have to devise a new archive name for it.
2538
					// Format is <timestamp of archiving>!<name>
2539
					$timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
2540
2541
					do {
2542
						$archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
2543
						$timestamp++;
2544
					} while ( isset( $archiveNames[$archiveName] ) );
2545
				}
2546
2547
				$archiveNames[$archiveName] = true;
2548
				$destRel = $this->file->getArchiveRel( $archiveName );
2549
				$insertBatch[] = [
2550
					'oi_name' => $row->fa_name,
2551
					'oi_archive_name' => $archiveName,
2552
					'oi_size' => $row->fa_size,
2553
					'oi_width' => $row->fa_width,
2554
					'oi_height' => $row->fa_height,
2555
					'oi_bits' => $row->fa_bits,
2556
					'oi_description' => $row->fa_description,
2557
					'oi_user' => $row->fa_user,
2558
					'oi_user_text' => $row->fa_user_text,
2559
					'oi_timestamp' => $row->fa_timestamp,
2560
					'oi_metadata' => $props['metadata'],
2561
					'oi_media_type' => $props['media_type'],
2562
					'oi_major_mime' => $props['major_mime'],
2563
					'oi_minor_mime' => $props['minor_mime'],
2564
					'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
2565
					'oi_sha1' => $sha1 ];
2566
			}
2567
2568
			$deleteIds[] = $row->fa_id;
2569
2570 View Code Duplication
			if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
2571
				// private files can stay where they are
2572
				$status->successCount++;
2573
			} else {
2574
				$storeBatch[] = [ $deletedUrl, 'public', $destRel ];
2575
				$this->cleanupBatch[] = $row->fa_storage_key;
2576
			}
2577
2578
			$first = false;
2579
		}
2580
2581
		unset( $result );
2582
2583
		// Add a warning to the status object for missing IDs
2584
		$missingIds = array_diff( $this->ids, $idsPresent );
2585
2586
		foreach ( $missingIds as $id ) {
2587
			$status->error( 'undelete-missing-filearchive', $id );
2588
		}
2589
2590
		if ( !$repo->hasSha1Storage() ) {
2591
			// Remove missing files from batch, so we don't get errors when undeleting them
2592
			$checkStatus = $this->removeNonexistentFiles( $storeBatch );
2593
			if ( !$checkStatus->isGood() ) {
2594
				$status->merge( $checkStatus );
2595
				return $status;
2596
			}
2597
			$storeBatch = $checkStatus->value;
2598
2599
			// Run the store batch
2600
			// Use the OVERWRITE_SAME flag to smooth over a common error
2601
			$storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
2602
			$status->merge( $storeStatus );
2603
2604
			if ( !$status->isGood() ) {
2605
				// Even if some files could be copied, fail entirely as that is the
2606
				// easiest thing to do without data loss
2607
				$this->cleanupFailedBatch( $storeStatus, $storeBatch );
2608
				$status->ok = false;
2609
				$this->file->unlock();
2610
2611
				return $status;
2612
			}
2613
		}
2614
2615
		// Run the DB updates
2616
		// Because we have locked the image row, key conflicts should be rare.
2617
		// If they do occur, we can roll back the transaction at this time with
2618
		// no data loss, but leaving unregistered files scattered throughout the
2619
		// public zone.
2620
		// This is not ideal, which is why it's important to lock the image row.
2621
		if ( $insertCurrent ) {
2622
			$dbw->insert( 'image', $insertCurrent, __METHOD__ );
2623
		}
2624
2625
		if ( $insertBatch ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $insertBatch of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2626
			$dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
2627
		}
2628
2629
		if ( $deleteIds ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $deleteIds of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2630
			$dbw->delete( 'filearchive',
2631
				[ 'fa_id' => $deleteIds ],
2632
				__METHOD__ );
2633
		}
2634
2635
		// If store batch is empty (all files are missing), deletion is to be considered successful
2636
		if ( $status->successCount > 0 || !$storeBatch || $repo->hasSha1Storage() ) {
2637
			if ( !$exists ) {
2638
				wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
2639
2640
				DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2641
2642
				$this->file->purgeEverything();
2643
			} else {
2644
				wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
2645
				$this->file->purgeDescription();
2646
			}
2647
		}
2648
2649
		$this->file->unlock();
2650
2651
		return $status;
2652
	}
2653
2654
	/**
2655
	 * Removes non-existent files from a store batch.
2656
	 * @param array $triplets
2657
	 * @return Status
2658
	 */
2659
	protected function removeNonexistentFiles( $triplets ) {
2660
		$files = $filteredTriplets = [];
2661
		foreach ( $triplets as $file ) {
2662
			$files[$file[0]] = $file[0];
2663
		}
2664
2665
		$result = $this->file->repo->fileExistsBatch( $files );
2666 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2667
			return Status::newFatal( 'backend-fail-internal',
2668
				$this->file->repo->getBackend()->getName() );
2669
		}
2670
2671
		foreach ( $triplets as $file ) {
2672
			if ( $result[$file[0]] ) {
2673
				$filteredTriplets[] = $file;
2674
			}
2675
		}
2676
2677
		return Status::newGood( $filteredTriplets );
2678
	}
2679
2680
	/**
2681
	 * Removes non-existent files from a cleanup batch.
2682
	 * @param array $batch
2683
	 * @return array
2684
	 */
2685
	protected function removeNonexistentFromCleanup( $batch ) {
2686
		$files = $newBatch = [];
2687
		$repo = $this->file->repo;
2688
2689
		foreach ( $batch as $file ) {
2690
			$files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
2691
				rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
2692
		}
2693
2694
		$result = $repo->fileExistsBatch( $files );
2695
2696
		foreach ( $batch as $file ) {
2697
			if ( $result[$file] ) {
2698
				$newBatch[] = $file;
2699
			}
2700
		}
2701
2702
		return $newBatch;
2703
	}
2704
2705
	/**
2706
	 * Delete unused files in the deleted zone.
2707
	 * This should be called from outside the transaction in which execute() was called.
2708
	 * @return FileRepoStatus
2709
	 */
2710
	public function cleanup() {
2711
		if ( !$this->cleanupBatch ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->cleanupBatch of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
2712
			return $this->file->repo->newGood();
2713
		}
2714
2715
		$this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
2716
2717
		$status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
2718
2719
		return $status;
2720
	}
2721
2722
	/**
2723
	 * Cleanup a failed batch. The batch was only partially successful, so
2724
	 * rollback by removing all items that were succesfully copied.
2725
	 *
2726
	 * @param Status $storeStatus
2727
	 * @param array $storeBatch
2728
	 */
2729
	protected function cleanupFailedBatch( $storeStatus, $storeBatch ) {
2730
		$cleanupBatch = [];
2731
2732
		foreach ( $storeStatus->success as $i => $success ) {
2733
			// Check if this item of the batch was successfully copied
2734
			if ( $success ) {
2735
				// Item was successfully copied and needs to be removed again
2736
				// Extract ($dstZone, $dstRel) from the batch
2737
				$cleanupBatch[] = [ $storeBatch[$i][1], $storeBatch[$i][2] ];
2738
			}
2739
		}
2740
		$this->file->repo->cleanupBatch( $cleanupBatch );
2741
	}
2742
}
2743
2744
# ------------------------------------------------------------------------------
2745
2746
/**
2747
 * Helper class for file movement
2748
 * @ingroup FileAbstraction
2749
 */
2750
class LocalFileMoveBatch {
2751
	/** @var LocalFile */
2752
	protected $file;
2753
2754
	/** @var Title */
2755
	protected $target;
2756
2757
	protected $cur;
2758
2759
	protected $olds;
2760
2761
	protected $oldCount;
2762
2763
	protected $archive;
2764
2765
	/** @var DatabaseBase */
2766
	protected $db;
2767
2768
	/**
2769
	 * @param File $file
2770
	 * @param Title $target
2771
	 */
2772
	function __construct( File $file, Title $target ) {
2773
		$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...
2774
		$this->target = $target;
2775
		$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...
2776
		$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...
2777
		$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...
2778
		$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...
2779
		$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...
2780
		$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...
2781
		$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...
2782
	}
2783
2784
	/**
2785
	 * Add the current image to the batch
2786
	 */
2787
	public function addCurrent() {
2788
		$this->cur = [ $this->oldRel, $this->newRel ];
2789
	}
2790
2791
	/**
2792
	 * Add the old versions of the image to the batch
2793
	 * @return array List of archive names from old versions
2794
	 */
2795
	public function addOlds() {
2796
		$archiveBase = 'archive';
2797
		$this->olds = [];
2798
		$this->oldCount = 0;
2799
		$archiveNames = [];
2800
2801
		$result = $this->db->select( 'oldimage',
2802
			[ 'oi_archive_name', 'oi_deleted' ],
2803
			[ 'oi_name' => $this->oldName ],
2804
			__METHOD__,
2805
			[ 'LOCK IN SHARE MODE' ] // ignore snapshot
2806
		);
2807
2808
		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...
2809
			$archiveNames[] = $row->oi_archive_name;
2810
			$oldName = $row->oi_archive_name;
2811
			$bits = explode( '!', $oldName, 2 );
2812
2813
			if ( count( $bits ) != 2 ) {
2814
				wfDebug( "Old file name missing !: '$oldName' \n" );
2815
				continue;
2816
			}
2817
2818
			list( $timestamp, $filename ) = $bits;
2819
2820
			if ( $this->oldName != $filename ) {
2821
				wfDebug( "Old file name doesn't match: '$oldName' \n" );
2822
				continue;
2823
			}
2824
2825
			$this->oldCount++;
2826
2827
			// Do we want to add those to oldCount?
2828
			if ( $row->oi_deleted & File::DELETED_FILE ) {
2829
				continue;
2830
			}
2831
2832
			$this->olds[] = [
2833
				"{$archiveBase}/{$this->oldHash}{$oldName}",
2834
				"{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
2835
			];
2836
		}
2837
2838
		return $archiveNames;
2839
	}
2840
2841
	/**
2842
	 * Perform the move.
2843
	 * @return FileRepoStatus
2844
	 */
2845
	public function execute() {
2846
		$repo = $this->file->repo;
2847
		$status = $repo->newGood();
2848
2849
		$triplets = $this->getMoveTriplets();
2850
		$checkStatus = $this->removeNonexistentFiles( $triplets );
2851
		if ( !$checkStatus->isGood() ) {
2852
			$status->merge( $checkStatus );
2853
			return $status;
2854
		}
2855
		$triplets = $checkStatus->value;
2856
		$destFile = wfLocalFile( $this->target );
2857
2858
		$this->file->lock(); // begin
2859
		$destFile->lock(); // quickly fail if destination is not available
2860
		// Rename the file versions metadata in the DB.
2861
		// This implicitly locks the destination file, which avoids race conditions.
2862
		// If we moved the files from A -> C before DB updates, another process could
2863
		// move files from B -> C at this point, causing storeBatch() to fail and thus
2864
		// cleanupTarget() to trigger. It would delete the C files and cause data loss.
2865
		$statusDb = $this->doDBUpdates();
2866
		if ( !$statusDb->isGood() ) {
2867
			$destFile->unlock();
2868
			$this->file->unlockAndRollback();
2869
			$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...
2870
2871
			return $statusDb;
2872
		}
2873
		wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
2874
			"{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
2875
2876
		if ( !$repo->hasSha1Storage() ) {
2877
			// Copy the files into their new location.
2878
			// If a prior process fataled copying or cleaning up files we tolerate any
2879
			// of the existing files if they are identical to the ones being stored.
2880
			$statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
2881
			wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
2882
				"{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
2883
			if ( !$statusMove->isGood() ) {
2884
				// Delete any files copied over (while the destination is still locked)
2885
				$this->cleanupTarget( $triplets );
2886
				$destFile->unlock();
2887
				$this->file->unlockAndRollback(); // unlocks the destination
2888
				wfDebugLog( 'imagemove', "Error in moving files: "
2889
					. $statusMove->getWikiText( false, false, 'en' ) );
2890
				$statusMove->ok = false;
2891
2892
				return $statusMove;
2893
			}
2894
			$status->merge( $statusMove );
2895
		}
2896
2897
		$destFile->unlock();
2898
		$this->file->unlock(); // done
2899
2900
		// Everything went ok, remove the source files
2901
		$this->cleanupSource( $triplets );
2902
2903
		$status->merge( $statusDb );
2904
2905
		return $status;
2906
	}
2907
2908
	/**
2909
	 * Do the database updates and return a new FileRepoStatus indicating how
2910
	 * many rows where updated.
2911
	 *
2912
	 * @return FileRepoStatus
2913
	 */
2914
	protected function doDBUpdates() {
2915
		$repo = $this->file->repo;
2916
		$status = $repo->newGood();
2917
		$dbw = $this->db;
2918
2919
		// Update current image
2920
		$dbw->update(
2921
			'image',
2922
			[ 'img_name' => $this->newName ],
2923
			[ 'img_name' => $this->oldName ],
2924
			__METHOD__
2925
		);
2926
2927
		if ( $dbw->affectedRows() ) {
2928
			$status->successCount++;
2929
		} else {
2930
			$status->failCount++;
2931
			$status->fatal( 'imageinvalidfilename' );
2932
2933
			return $status;
2934
		}
2935
2936
		// Update old images
2937
		$dbw->update(
2938
			'oldimage',
2939
			[
2940
				'oi_name' => $this->newName,
2941
				'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
2942
					$dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
2943
			],
2944
			[ 'oi_name' => $this->oldName ],
2945
			__METHOD__
2946
		);
2947
2948
		$affected = $dbw->affectedRows();
2949
		$total = $this->oldCount;
2950
		$status->successCount += $affected;
2951
		// Bug 34934: $total is based on files that actually exist.
2952
		// There may be more DB rows than such files, in which case $affected
2953
		// can be greater than $total. We use max() to avoid negatives here.
2954
		$status->failCount += max( 0, $total - $affected );
2955
		if ( $status->failCount ) {
2956
			$status->error( 'imageinvalidfilename' );
2957
		}
2958
2959
		return $status;
2960
	}
2961
2962
	/**
2963
	 * Generate triplets for FileRepo::storeBatch().
2964
	 * @return array
2965
	 */
2966
	protected function getMoveTriplets() {
2967
		$moves = array_merge( [ $this->cur ], $this->olds );
2968
		$triplets = []; // The format is: (srcUrl, destZone, destUrl)
2969
2970
		foreach ( $moves as $move ) {
2971
			// $move: (oldRelativePath, newRelativePath)
2972
			$srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
2973
			$triplets[] = [ $srcUrl, 'public', $move[1] ];
2974
			wfDebugLog(
2975
				'imagemove',
2976
				"Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
2977
			);
2978
		}
2979
2980
		return $triplets;
2981
	}
2982
2983
	/**
2984
	 * Removes non-existent files from move batch.
2985
	 * @param array $triplets
2986
	 * @return Status
2987
	 */
2988
	protected function removeNonexistentFiles( $triplets ) {
2989
		$files = [];
2990
2991
		foreach ( $triplets as $file ) {
2992
			$files[$file[0]] = $file[0];
2993
		}
2994
2995
		$result = $this->file->repo->fileExistsBatch( $files );
2996 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2997
			return Status::newFatal( 'backend-fail-internal',
2998
				$this->file->repo->getBackend()->getName() );
2999
		}
3000
3001
		$filteredTriplets = [];
3002
		foreach ( $triplets as $file ) {
3003
			if ( $result[$file[0]] ) {
3004
				$filteredTriplets[] = $file;
3005
			} else {
3006
				wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
3007
			}
3008
		}
3009
3010
		return Status::newGood( $filteredTriplets );
3011
	}
3012
3013
	/**
3014
	 * Cleanup a partially moved array of triplets by deleting the target
3015
	 * files. Called if something went wrong half way.
3016
	 * @param array $triplets
3017
	 */
3018
	protected function cleanupTarget( $triplets ) {
3019
		// Create dest pairs from the triplets
3020
		$pairs = [];
3021
		foreach ( $triplets as $triplet ) {
3022
			// $triplet: (old source virtual URL, dst zone, dest rel)
3023
			$pairs[] = [ $triplet[1], $triplet[2] ];
3024
		}
3025
3026
		$this->file->repo->cleanupBatch( $pairs );
3027
	}
3028
3029
	/**
3030
	 * Cleanup a fully moved array of triplets by deleting the source files.
3031
	 * Called at the end of the move process if everything else went ok.
3032
	 * @param array $triplets
3033
	 */
3034
	protected function cleanupSource( $triplets ) {
3035
		// Create source file names from the triplets
3036
		$files = [];
3037
		foreach ( $triplets as $triplet ) {
3038
			$files[] = $triplet[0];
3039
		}
3040
3041
		$this->file->repo->cleanupBatch( $files );
3042
	}
3043
}
3044
3045
class LocalFileLockError extends Exception {
3046
3047
}
3048