Completed
Branch master (3e9d3e)
by
unknown
35:58
created

LocalFile::upload()   B

Complexity

Conditions 8
Paths 19

Size

Total Lines 53
Code Lines 27

Duplication

Lines 5
Ratio 9.43 %
Metric Value
dl 5
loc 53
rs 7.1199
cc 8
eloc 27
nc 19
nop 8

How to fix   Long Method    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1570
			$dst = $repo->getBackend()->getPathForSHA1( $sha1 );
1571
			$status = $repo->quickImport( $srcPath, $dst );
1572
			if ( $flags & File::DELETE_SOURCE ) {
1573
				unlink( $srcPath );
1574
			}
1575
1576
			if ( $this->exists() ) {
1577
				$status->value = $archiveName;
1578
			}
1579
		} else {
1580
			$flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
1581
			$status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
1582
1583
			if ( $status->value == 'new' ) {
1584
				$status->value = '';
1585
			} else {
1586
				$status->value = $archiveName;
1587
			}
1588
		}
1589
1590
		$this->unlock(); // done
1591
1592
		return $status;
1593
	}
1594
1595
	/** getLinksTo inherited */
1596
	/** getExifData inherited */
1597
	/** isLocal inherited */
1598
	/** wasDeleted inherited */
1599
1600
	/**
1601
	 * Move file to the new title
1602
	 *
1603
	 * Move current, old version and all thumbnails
1604
	 * to the new filename. Old file is deleted.
1605
	 *
1606
	 * Cache purging is done; checks for validity
1607
	 * and logging are caller's responsibility
1608
	 *
1609
	 * @param Title $target New file name
1610
	 * @return FileRepoStatus
1611
	 */
1612
	function move( $target ) {
1613
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1614
			return $this->readOnlyFatalStatus();
1615
		}
1616
1617
		wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1618
		$batch = new LocalFileMoveBatch( $this, $target );
1619
1620
		$this->lock(); // begin
1621
		$batch->addCurrent();
1622
		$archiveNames = $batch->addOlds();
1623
		$status = $batch->execute();
1624
		$this->unlock(); // done
1625
1626
		wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1627
1628
		// Purge the source and target files...
1629
		$oldTitleFile = wfLocalFile( $this->title );
1630
		$newTitleFile = wfLocalFile( $target );
1631
		// Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
1632
		// tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
1633
		$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...
1634
			function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
1635
				$oldTitleFile->purgeEverything();
1636
				foreach ( $archiveNames as $archiveName ) {
1637
					$oldTitleFile->purgeOldThumbnails( $archiveName );
1638
				}
1639
				$newTitleFile->purgeEverything();
1640
			}
1641
		);
1642
1643
		if ( $status->isOK() ) {
1644
			// Now switch the object
1645
			$this->title = $target;
1646
			// Force regeneration of the name and hashpath
1647
			unset( $this->name );
1648
			unset( $this->hashPath );
1649
		}
1650
1651
		return $status;
1652
	}
1653
1654
	/**
1655
	 * Delete all versions of the file.
1656
	 *
1657
	 * Moves the files into an archive directory (or deletes them)
1658
	 * and removes the database rows.
1659
	 *
1660
	 * Cache purging is done; logging is caller's responsibility.
1661
	 *
1662
	 * @param string $reason
1663
	 * @param bool $suppress
1664
	 * @param User|null $user
1665
	 * @return FileRepoStatus
1666
	 */
1667
	function delete( $reason, $suppress = false, $user = null ) {
1668
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1669
			return $this->readOnlyFatalStatus();
1670
		}
1671
1672
		$batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1673
1674
		$this->lock(); // begin
1675
		$batch->addCurrent();
1676
		# Get old version relative paths
1677
		$archiveNames = $batch->addOlds();
1678
		$status = $batch->execute();
1679
		$this->unlock(); // done
1680
1681
		if ( $status->isOK() ) {
1682
			DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => -1 ) ) );
1683
		}
1684
1685
		// Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
1686
		// tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
1687
		$that = $this;
1688
		$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...
1689
			function () use ( $that, $archiveNames ) {
1690
				$that->purgeEverything();
1691
				foreach ( $archiveNames as $archiveName ) {
1692
					$that->purgeOldThumbnails( $archiveName );
1693
				}
1694
			}
1695
		);
1696
1697
		// Purge the CDN
1698
		$purgeUrls = array();
1699
		foreach ( $archiveNames as $archiveName ) {
1700
			$purgeUrls[] = $this->getArchiveUrl( $archiveName );
1701
		}
1702
		DeferredUpdates::addUpdate( new CdnCacheUpdate( $purgeUrls ), DeferredUpdates::PRESEND );
1703
1704
		return $status;
1705
	}
1706
1707
	/**
1708
	 * Delete an old version of the file.
1709
	 *
1710
	 * Moves the file into an archive directory (or deletes it)
1711
	 * and removes the database row.
1712
	 *
1713
	 * Cache purging is done; logging is caller's responsibility.
1714
	 *
1715
	 * @param string $archiveName
1716
	 * @param string $reason
1717
	 * @param bool $suppress
1718
	 * @param User|null $user
1719
	 * @throws MWException Exception on database or file store failure
1720
	 * @return FileRepoStatus
1721
	 */
1722
	function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
1723
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1724
			return $this->readOnlyFatalStatus();
1725
		}
1726
1727
		$batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1728
1729
		$this->lock(); // begin
1730
		$batch->addOld( $archiveName );
1731
		$status = $batch->execute();
1732
		$this->unlock(); // done
1733
1734
		$this->purgeOldThumbnails( $archiveName );
1735
		if ( $status->isOK() ) {
1736
			$this->purgeDescription();
1737
		}
1738
1739
		DeferredUpdates::addUpdate(
1740
			new CdnCacheUpdate( array( $this->getArchiveUrl( $archiveName ) ) ),
1741
			DeferredUpdates::PRESEND
1742
		);
1743
1744
		return $status;
1745
	}
1746
1747
	/**
1748
	 * Restore all or specified deleted revisions to the given file.
1749
	 * Permissions and logging are left to the caller.
1750
	 *
1751
	 * May throw database exceptions on error.
1752
	 *
1753
	 * @param array $versions Set of record ids of deleted items to restore,
1754
	 *   or empty to restore all revisions.
1755
	 * @param bool $unsuppress
1756
	 * @return FileRepoStatus
1757
	 */
1758
	function restore( $versions = array(), $unsuppress = false ) {
1759
		if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1760
			return $this->readOnlyFatalStatus();
1761
		}
1762
1763
		$batch = new LocalFileRestoreBatch( $this, $unsuppress );
1764
1765
		$this->lock(); // begin
1766
		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...
1767
			$batch->addAll();
1768
		} else {
1769
			$batch->addIds( $versions );
1770
		}
1771
		$status = $batch->execute();
1772
		if ( $status->isGood() ) {
1773
			$cleanupStatus = $batch->cleanup();
1774
			$cleanupStatus->successCount = 0;
1775
			$cleanupStatus->failCount = 0;
1776
			$status->merge( $cleanupStatus );
1777
		}
1778
		$this->unlock(); // done
1779
1780
		return $status;
1781
	}
1782
1783
	/** isMultipage inherited */
1784
	/** pageCount inherited */
1785
	/** scaleHeight inherited */
1786
	/** getImageSize inherited */
1787
1788
	/**
1789
	 * Get the URL of the file description page.
1790
	 * @return string
1791
	 */
1792
	function getDescriptionUrl() {
1793
		return $this->title->getLocalURL();
1794
	}
1795
1796
	/**
1797
	 * Get the HTML text of the description page
1798
	 * This is not used by ImagePage for local files, since (among other things)
1799
	 * it skips the parser cache.
1800
	 *
1801
	 * @param Language $lang What language to get description in (Optional)
1802
	 * @return bool|mixed
1803
	 */
1804
	function getDescriptionText( $lang = null ) {
1805
		$revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
1806
		if ( !$revision ) {
1807
			return false;
1808
		}
1809
		$content = $revision->getContent();
1810
		if ( !$content ) {
1811
			return false;
1812
		}
1813
		$pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
1814
1815
		return $pout->getText();
1816
	}
1817
1818
	/**
1819
	 * @param int $audience
1820
	 * @param User $user
1821
	 * @return string
1822
	 */
1823 View Code Duplication
	function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
1824
		$this->load();
1825
		if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
1826
			return '';
1827
		} elseif ( $audience == self::FOR_THIS_USER
1828
			&& !$this->userCan( self::DELETED_COMMENT, $user )
1829
		) {
1830
			return '';
1831
		} else {
1832
			return $this->description;
1833
		}
1834
	}
1835
1836
	/**
1837
	 * @return bool|string
1838
	 */
1839
	function getTimestamp() {
1840
		$this->load();
1841
1842
		return $this->timestamp;
1843
	}
1844
1845
	/**
1846
	 * @return bool|string
1847
	 */
1848
	public function getDescriptionTouched() {
1849
		// The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
1850
		// itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
1851
		// need to differentiate between null (uninitialized) and false (failed to load).
1852
		if ( $this->descriptionTouched === null ) {
1853
			$cond = array(
1854
				'page_namespace' => $this->title->getNamespace(),
1855
				'page_title' => $this->title->getDBkey()
1856
			);
1857
			$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...
1858
			$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...
1859
		}
1860
1861
		return $this->descriptionTouched;
1862
	}
1863
1864
	/**
1865
	 * @return string
1866
	 */
1867
	function getSha1() {
1868
		$this->load();
1869
		// Initialise now if necessary
1870
		if ( $this->sha1 == '' && $this->fileExists ) {
1871
			$this->lock(); // begin
1872
1873
			$this->sha1 = $this->repo->getFileSha1( $this->getPath() );
1874
			if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1875
				$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...
1876
				$dbw->update( 'image',
1877
					array( 'img_sha1' => $this->sha1 ),
1878
					array( 'img_name' => $this->getName() ),
1879
					__METHOD__ );
1880
				$this->invalidateCache();
1881
			}
1882
1883
			$this->unlock(); // done
1884
		}
1885
1886
		return $this->sha1;
1887
	}
1888
1889
	/**
1890
	 * @return bool Whether to cache in RepoGroup (this avoids OOMs)
1891
	 */
1892
	function isCacheable() {
1893
		$this->load();
1894
1895
		// If extra data (metadata) was not loaded then it must have been large
1896
		return $this->extraDataLoaded
1897
		&& strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
1898
	}
1899
1900
	/**
1901
	 * Start a transaction and lock the image for update
1902
	 * Increments a reference counter if the lock is already held
1903
	 * @throws MWException Throws an error if the lock was not acquired
1904
	 * @return bool Whether the file lock owns/spawned the DB transaction
1905
	 */
1906
	function lock() {
1907
		$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...
1908
1909
		if ( !$this->locked ) {
1910
			if ( !$dbw->trxLevel() ) {
1911
				$dbw->begin( __METHOD__ );
1912
				$this->lockedOwnTrx = true;
1913
			}
1914
			$this->locked++;
1915
			// Bug 54736: use simple lock to handle when the file does not exist.
1916
			// SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
1917
			// Also, that would cause contention on INSERT of similarly named rows.
1918
			$backend = $this->getRepo()->getBackend();
1919
			$lockPaths = array( $this->getPath() ); // represents all versions of the file
1920
			$status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 5 );
1921
			if ( !$status->isGood() ) {
1922
				throw new MWException( "Could not acquire lock for '{$this->getName()}.'" );
1923
			}
1924
			$dbw->onTransactionIdle( function () use ( $backend, $lockPaths ) {
1925
				$backend->unlockFiles( $lockPaths, LockManager::LOCK_EX ); // release on commit
1926
			} );
1927
		}
1928
1929
		return $this->lockedOwnTrx;
1930
	}
1931
1932
	/**
1933
	 * Decrement the lock reference count. If the reference count is reduced to zero, commits
1934
	 * the transaction and thereby releases the image lock.
1935
	 */
1936
	function unlock() {
1937
		if ( $this->locked ) {
1938
			--$this->locked;
1939
			if ( !$this->locked && $this->lockedOwnTrx ) {
1940
				$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...
1941
				$dbw->commit( __METHOD__ );
1942
				$this->lockedOwnTrx = false;
1943
			}
1944
		}
1945
	}
1946
1947
	/**
1948
	 * Roll back the DB transaction and mark the image unlocked
1949
	 */
1950
	function unlockAndRollback() {
1951
		$this->locked = false;
1952
		$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...
1953
		$dbw->rollback( __METHOD__ );
1954
		$this->lockedOwnTrx = false;
1955
	}
1956
1957
	/**
1958
	 * @return Status
1959
	 */
1960
	protected function readOnlyFatalStatus() {
1961
		return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
1962
			$this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
1963
	}
1964
1965
	/**
1966
	 * Clean up any dangling locks
1967
	 */
1968
	function __destruct() {
1969
		$this->unlock();
1970
	}
1971
} // LocalFile class
1972
1973
# ------------------------------------------------------------------------------
1974
1975
/**
1976
 * Helper class for file deletion
1977
 * @ingroup FileAbstraction
1978
 */
1979
class LocalFileDeleteBatch {
1980
	/** @var LocalFile */
1981
	private $file;
1982
1983
	/** @var string */
1984
	private $reason;
1985
1986
	/** @var array */
1987
	private $srcRels = array();
1988
1989
	/** @var array */
1990
	private $archiveUrls = array();
1991
1992
	/** @var array Items to be processed in the deletion batch */
1993
	private $deletionBatch;
1994
1995
	/** @var bool Whether to suppress all suppressable fields when deleting */
1996
	private $suppress;
1997
1998
	/** @var FileRepoStatus */
1999
	private $status;
2000
2001
	/** @var User */
2002
	private $user;
2003
2004
	/**
2005
	 * @param File $file
2006
	 * @param string $reason
2007
	 * @param bool $suppress
2008
	 * @param User|null $user
2009
	 */
2010
	function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
2011
		$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...
2012
		$this->reason = $reason;
2013
		$this->suppress = $suppress;
2014
		if ( $user ) {
2015
			$this->user = $user;
2016
		} else {
2017
			global $wgUser;
2018
			$this->user = $wgUser;
2019
		}
2020
		$this->status = $file->repo->newGood();
2021
	}
2022
2023
	public function addCurrent() {
2024
		$this->srcRels['.'] = $this->file->getRel();
2025
	}
2026
2027
	/**
2028
	 * @param string $oldName
2029
	 */
2030
	public function addOld( $oldName ) {
2031
		$this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
2032
		$this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
2033
	}
2034
2035
	/**
2036
	 * Add the old versions of the image to the batch
2037
	 * @return array List of archive names from old versions
2038
	 */
2039
	public function addOlds() {
2040
		$archiveNames = array();
2041
2042
		$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...
2043
		$result = $dbw->select( 'oldimage',
2044
			array( 'oi_archive_name' ),
2045
			array( 'oi_name' => $this->file->getName() ),
2046
			__METHOD__
2047
		);
2048
2049
		foreach ( $result as $row ) {
2050
			$this->addOld( $row->oi_archive_name );
2051
			$archiveNames[] = $row->oi_archive_name;
2052
		}
2053
2054
		return $archiveNames;
2055
	}
2056
2057
	/**
2058
	 * @return array
2059
	 */
2060
	protected function getOldRels() {
2061
		if ( !isset( $this->srcRels['.'] ) ) {
2062
			$oldRels =& $this->srcRels;
2063
			$deleteCurrent = false;
2064
		} else {
2065
			$oldRels = $this->srcRels;
2066
			unset( $oldRels['.'] );
2067
			$deleteCurrent = true;
2068
		}
2069
2070
		return array( $oldRels, $deleteCurrent );
2071
	}
2072
2073
	/**
2074
	 * @return array
2075
	 */
2076
	protected function getHashes() {
2077
		$hashes = array();
2078
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2079
2080
		if ( $deleteCurrent ) {
2081
			$hashes['.'] = $this->file->getSha1();
2082
		}
2083
2084
		if ( count( $oldRels ) ) {
2085
			$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...
2086
			$res = $dbw->select(
2087
				'oldimage',
2088
				array( 'oi_archive_name', 'oi_sha1' ),
2089
				array( 'oi_archive_name' => array_keys( $oldRels ),
2090
					'oi_name' => $this->file->getName() ), // performance
2091
				__METHOD__
2092
			);
2093
2094
			foreach ( $res as $row ) {
2095
				if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
2096
					// Get the hash from the file
2097
					$oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
2098
					$props = $this->file->repo->getFileProps( $oldUrl );
2099
2100
					if ( $props['fileExists'] ) {
2101
						// Upgrade the oldimage row
2102
						$dbw->update( 'oldimage',
2103
							array( 'oi_sha1' => $props['sha1'] ),
2104
							array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
2105
							__METHOD__ );
2106
						$hashes[$row->oi_archive_name] = $props['sha1'];
2107
					} else {
2108
						$hashes[$row->oi_archive_name] = false;
2109
					}
2110
				} else {
2111
					$hashes[$row->oi_archive_name] = $row->oi_sha1;
2112
				}
2113
			}
2114
		}
2115
2116
		$missing = array_diff_key( $this->srcRels, $hashes );
2117
2118
		foreach ( $missing as $name => $rel ) {
2119
			$this->status->error( 'filedelete-old-unregistered', $name );
2120
		}
2121
2122
		foreach ( $hashes as $name => $hash ) {
2123
			if ( !$hash ) {
2124
				$this->status->error( 'filedelete-missing', $this->srcRels[$name] );
2125
				unset( $hashes[$name] );
2126
			}
2127
		}
2128
2129
		return $hashes;
2130
	}
2131
2132
	protected function doDBInserts() {
2133
		$dbw = $this->file->repo->getMasterDB();
0 ignored issues
show
Bug introduced by
The method getMasterDB does only exist in LocalRepo, but not in FileRepo and ForeignAPIRepo.

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

Let’s take a look at an example:

class A
{
    public function foo() { }
}

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

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

Available Fixes

  1. Add an additional type-check:

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

    function someFunction(B $x) { /** ... */ }
    
Loading history...
2134
		$encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
2135
		$encUserId = $dbw->addQuotes( $this->user->getId() );
2136
		$encReason = $dbw->addQuotes( $this->reason );
2137
		$encGroup = $dbw->addQuotes( 'deleted' );
2138
		$ext = $this->file->getExtension();
2139
		$dotExt = $ext === '' ? '' : ".$ext";
2140
		$encExt = $dbw->addQuotes( $dotExt );
2141
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2142
2143
		// Bitfields to further suppress the content
2144 View Code Duplication
		if ( $this->suppress ) {
2145
			$bitfield = 0;
2146
			// This should be 15...
2147
			$bitfield |= Revision::DELETED_TEXT;
2148
			$bitfield |= Revision::DELETED_COMMENT;
2149
			$bitfield |= Revision::DELETED_USER;
2150
			$bitfield |= Revision::DELETED_RESTRICTED;
2151
		} else {
2152
			$bitfield = 'oi_deleted';
2153
		}
2154
2155
		if ( $deleteCurrent ) {
2156
			$concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
2157
			$where = array( 'img_name' => $this->file->getName() );
2158
			$dbw->insertSelect( 'filearchive', 'image',
2159
				array(
2160
					'fa_storage_group' => $encGroup,
2161
					'fa_storage_key' => $dbw->conditional(
2162
						array( 'img_sha1' => '' ),
2163
						$dbw->addQuotes( '' ),
2164
						$concat
2165
					),
2166
					'fa_deleted_user' => $encUserId,
2167
					'fa_deleted_timestamp' => $encTimestamp,
2168
					'fa_deleted_reason' => $encReason,
2169
					'fa_deleted' => $this->suppress ? $bitfield : 0,
2170
2171
					'fa_name' => 'img_name',
2172
					'fa_archive_name' => 'NULL',
2173
					'fa_size' => 'img_size',
2174
					'fa_width' => 'img_width',
2175
					'fa_height' => 'img_height',
2176
					'fa_metadata' => 'img_metadata',
2177
					'fa_bits' => 'img_bits',
2178
					'fa_media_type' => 'img_media_type',
2179
					'fa_major_mime' => 'img_major_mime',
2180
					'fa_minor_mime' => 'img_minor_mime',
2181
					'fa_description' => 'img_description',
2182
					'fa_user' => 'img_user',
2183
					'fa_user_text' => 'img_user_text',
2184
					'fa_timestamp' => 'img_timestamp',
2185
					'fa_sha1' => 'img_sha1',
2186
				), $where, __METHOD__ );
2187
		}
2188
2189
		if ( count( $oldRels ) ) {
2190
			$concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
2191
			$where = array(
2192
				'oi_name' => $this->file->getName(),
2193
				'oi_archive_name' => array_keys( $oldRels ) );
2194
			$dbw->insertSelect( 'filearchive', 'oldimage',
2195
				array(
2196
					'fa_storage_group' => $encGroup,
2197
					'fa_storage_key' => $dbw->conditional(
2198
						array( 'oi_sha1' => '' ),
2199
						$dbw->addQuotes( '' ),
2200
						$concat
2201
					),
2202
					'fa_deleted_user' => $encUserId,
2203
					'fa_deleted_timestamp' => $encTimestamp,
2204
					'fa_deleted_reason' => $encReason,
2205
					'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
2206
2207
					'fa_name' => 'oi_name',
2208
					'fa_archive_name' => 'oi_archive_name',
2209
					'fa_size' => 'oi_size',
2210
					'fa_width' => 'oi_width',
2211
					'fa_height' => 'oi_height',
2212
					'fa_metadata' => 'oi_metadata',
2213
					'fa_bits' => 'oi_bits',
2214
					'fa_media_type' => 'oi_media_type',
2215
					'fa_major_mime' => 'oi_major_mime',
2216
					'fa_minor_mime' => 'oi_minor_mime',
2217
					'fa_description' => 'oi_description',
2218
					'fa_user' => 'oi_user',
2219
					'fa_user_text' => 'oi_user_text',
2220
					'fa_timestamp' => 'oi_timestamp',
2221
					'fa_sha1' => 'oi_sha1',
2222
				), $where, __METHOD__ );
2223
		}
2224
	}
2225
2226
	function doDBDeletes() {
2227
		$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...
2228
		list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2229
2230
		if ( count( $oldRels ) ) {
2231
			$dbw->delete( 'oldimage',
2232
				array(
2233
					'oi_name' => $this->file->getName(),
2234
					'oi_archive_name' => array_keys( $oldRels )
2235
				), __METHOD__ );
2236
		}
2237
2238
		if ( $deleteCurrent ) {
2239
			$dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
2240
		}
2241
	}
2242
2243
	/**
2244
	 * Run the transaction
2245
	 * @return FileRepoStatus
2246
	 */
2247
	public function execute() {
2248
		$repo = $this->file->getRepo();
2249
		$this->file->lock();
2250
2251
		// Prepare deletion batch
2252
		$hashes = $this->getHashes();
2253
		$this->deletionBatch = array();
2254
		$ext = $this->file->getExtension();
2255
		$dotExt = $ext === '' ? '' : ".$ext";
2256
2257
		foreach ( $this->srcRels as $name => $srcRel ) {
2258
			// Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
2259
			if ( isset( $hashes[$name] ) ) {
2260
				$hash = $hashes[$name];
2261
				$key = $hash . $dotExt;
2262
				$dstRel = $repo->getDeletedHashPath( $key ) . $key;
2263
				$this->deletionBatch[$name] = array( $srcRel, $dstRel );
2264
			}
2265
		}
2266
2267
		// Lock the filearchive rows so that the files don't get deleted by a cleanup operation
2268
		// We acquire this lock by running the inserts now, before the file operations.
2269
		// This potentially has poor lock contention characteristics -- an alternative
2270
		// scheme would be to insert stub filearchive entries with no fa_name and commit
2271
		// them in a separate transaction, then run the file ops, then update the fa_name fields.
2272
		$this->doDBInserts();
2273
2274
		if ( !$repo->hasSha1Storage() ) {
2275
			// Removes non-existent file from the batch, so we don't get errors.
2276
			// This also handles files in the 'deleted' zone deleted via revision deletion.
2277
			$checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
2278
			if ( !$checkStatus->isGood() ) {
2279
				$this->status->merge( $checkStatus );
2280
				return $this->status;
2281
			}
2282
			$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...
2283
2284
			// Execute the file deletion batch
2285
			$status = $this->file->repo->deleteBatch( $this->deletionBatch );
2286
2287
			if ( !$status->isGood() ) {
2288
				$this->status->merge( $status );
2289
			}
2290
		}
2291
2292
		if ( !$this->status->isOK() ) {
2293
			// Critical file deletion error
2294
			// Roll back inserts, release lock and abort
2295
			// TODO: delete the defunct filearchive rows if we are using a non-transactional DB
2296
			$this->file->unlockAndRollback();
2297
2298
			return $this->status;
2299
		}
2300
2301
		// Delete image/oldimage rows
2302
		$this->doDBDeletes();
2303
2304
		// Commit and return
2305
		$this->file->unlock();
2306
2307
		return $this->status;
2308
	}
2309
2310
	/**
2311
	 * Removes non-existent files from a deletion batch.
2312
	 * @param array $batch
2313
	 * @return Status
2314
	 */
2315
	protected function removeNonexistentFiles( $batch ) {
2316
		$files = $newBatch = array();
2317
2318
		foreach ( $batch as $batchItem ) {
2319
			list( $src, ) = $batchItem;
2320
			$files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
2321
		}
2322
2323
		$result = $this->file->repo->fileExistsBatch( $files );
2324 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2325
			return Status::newFatal( 'backend-fail-internal',
2326
				$this->file->repo->getBackend()->getName() );
2327
		}
2328
2329
		foreach ( $batch as $batchItem ) {
2330
			if ( $result[$batchItem[0]] ) {
2331
				$newBatch[] = $batchItem;
2332
			}
2333
		}
2334
2335
		return Status::newGood( $newBatch );
2336
	}
2337
}
2338
2339
# ------------------------------------------------------------------------------
2340
2341
/**
2342
 * Helper class for file undeletion
2343
 * @ingroup FileAbstraction
2344
 */
2345
class LocalFileRestoreBatch {
2346
	/** @var LocalFile */
2347
	private $file;
2348
2349
	/** @var array List of file IDs to restore */
2350
	private $cleanupBatch;
2351
2352
	/** @var array List of file IDs to restore */
2353
	private $ids;
2354
2355
	/** @var bool Add all revisions of the file */
2356
	private $all;
2357
2358
	/** @var bool Whether to remove all settings for suppressed fields */
2359
	private $unsuppress = false;
2360
2361
	/**
2362
	 * @param File $file
2363
	 * @param bool $unsuppress
2364
	 */
2365
	function __construct( File $file, $unsuppress = false ) {
2366
		$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...
2367
		$this->cleanupBatch = $this->ids = array();
2368
		$this->ids = array();
2369
		$this->unsuppress = $unsuppress;
2370
	}
2371
2372
	/**
2373
	 * Add a file by ID
2374
	 * @param int $fa_id
2375
	 */
2376
	public function addId( $fa_id ) {
2377
		$this->ids[] = $fa_id;
2378
	}
2379
2380
	/**
2381
	 * Add a whole lot of files by ID
2382
	 * @param int[] $ids
2383
	 */
2384
	public function addIds( $ids ) {
2385
		$this->ids = array_merge( $this->ids, $ids );
2386
	}
2387
2388
	/**
2389
	 * Add all revisions of the file
2390
	 */
2391
	public function addAll() {
2392
		$this->all = true;
2393
	}
2394
2395
	/**
2396
	 * Run the transaction, except the cleanup batch.
2397
	 * The cleanup batch should be run in a separate transaction, because it locks different
2398
	 * rows and there's no need to keep the image row locked while it's acquiring those locks
2399
	 * The caller may have its own transaction open.
2400
	 * So we save the batch and let the caller call cleanup()
2401
	 * @return FileRepoStatus
2402
	 */
2403
	public function execute() {
2404
		global $wgLang;
2405
2406
		$repo = $this->file->getRepo();
2407
		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...
2408
			// Do nothing
2409
			return $repo->newGood();
2410
		}
2411
2412
		$lockOwnsTrx = $this->file->lock();
2413
2414
		$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...
2415
		$status = $this->file->repo->newGood();
2416
2417
		$exists = (bool)$dbw->selectField( 'image', '1',
2418
			array( 'img_name' => $this->file->getName() ),
2419
			__METHOD__,
2420
			// The lock() should already prevents changes, but this still may need
2421
			// to bypass any transaction snapshot. However, if lock() started the
2422
			// trx (which it probably did) then snapshot is post-lock and up-to-date.
2423
			$lockOwnsTrx ? array() : array( 'LOCK IN SHARE MODE' )
2424
		);
2425
2426
		// Fetch all or selected archived revisions for the file,
2427
		// sorted from the most recent to the oldest.
2428
		$conditions = array( 'fa_name' => $this->file->getName() );
2429
2430
		if ( !$this->all ) {
2431
			$conditions['fa_id'] = $this->ids;
2432
		}
2433
2434
		$result = $dbw->select(
2435
			'filearchive',
2436
			ArchivedFile::selectFields(),
2437
			$conditions,
2438
			__METHOD__,
2439
			array( 'ORDER BY' => 'fa_timestamp DESC' )
2440
		);
2441
2442
		$idsPresent = array();
2443
		$storeBatch = array();
2444
		$insertBatch = array();
2445
		$insertCurrent = false;
2446
		$deleteIds = array();
2447
		$first = true;
2448
		$archiveNames = array();
2449
2450
		foreach ( $result as $row ) {
2451
			$idsPresent[] = $row->fa_id;
2452
2453
			if ( $row->fa_name != $this->file->getName() ) {
2454
				$status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
2455
				$status->failCount++;
2456
				continue;
2457
			}
2458
2459
			if ( $row->fa_storage_key == '' ) {
2460
				// Revision was missing pre-deletion
2461
				$status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
2462
				$status->failCount++;
2463
				continue;
2464
			}
2465
2466
			$deletedRel = $repo->getDeletedHashPath( $row->fa_storage_key ) .
2467
				$row->fa_storage_key;
2468
			$deletedUrl = $repo->getVirtualUrl() . '/deleted/' . $deletedRel;
2469
2470
			if ( isset( $row->fa_sha1 ) ) {
2471
				$sha1 = $row->fa_sha1;
2472
			} else {
2473
				// old row, populate from key
2474
				$sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
2475
			}
2476
2477
			# Fix leading zero
2478
			if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
2479
				$sha1 = substr( $sha1, 1 );
2480
			}
2481
2482
			if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
2483
				|| is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
2484
				|| is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
2485
				|| is_null( $row->fa_metadata )
2486
			) {
2487
				// Refresh our metadata
2488
				// Required for a new current revision; nice for older ones too. :)
2489
				$props = RepoGroup::singleton()->getFileProps( $deletedUrl );
2490
			} else {
2491
				$props = array(
2492
					'minor_mime' => $row->fa_minor_mime,
2493
					'major_mime' => $row->fa_major_mime,
2494
					'media_type' => $row->fa_media_type,
2495
					'metadata' => $row->fa_metadata
2496
				);
2497
			}
2498
2499
			if ( $first && !$exists ) {
2500
				// This revision will be published as the new current version
2501
				$destRel = $this->file->getRel();
2502
				$insertCurrent = array(
2503
					'img_name' => $row->fa_name,
2504
					'img_size' => $row->fa_size,
2505
					'img_width' => $row->fa_width,
2506
					'img_height' => $row->fa_height,
2507
					'img_metadata' => $props['metadata'],
2508
					'img_bits' => $row->fa_bits,
2509
					'img_media_type' => $props['media_type'],
2510
					'img_major_mime' => $props['major_mime'],
2511
					'img_minor_mime' => $props['minor_mime'],
2512
					'img_description' => $row->fa_description,
2513
					'img_user' => $row->fa_user,
2514
					'img_user_text' => $row->fa_user_text,
2515
					'img_timestamp' => $row->fa_timestamp,
2516
					'img_sha1' => $sha1
2517
				);
2518
2519
				// The live (current) version cannot be hidden!
2520 View Code Duplication
				if ( !$this->unsuppress && $row->fa_deleted ) {
2521
					$storeBatch[] = array( $deletedUrl, 'public', $destRel );
2522
					$this->cleanupBatch[] = $row->fa_storage_key;
2523
				}
2524
			} else {
2525
				$archiveName = $row->fa_archive_name;
2526
2527
				if ( $archiveName == '' ) {
2528
					// This was originally a current version; we
2529
					// have to devise a new archive name for it.
2530
					// Format is <timestamp of archiving>!<name>
2531
					$timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
2532
2533
					do {
2534
						$archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
2535
						$timestamp++;
2536
					} while ( isset( $archiveNames[$archiveName] ) );
2537
				}
2538
2539
				$archiveNames[$archiveName] = true;
2540
				$destRel = $this->file->getArchiveRel( $archiveName );
2541
				$insertBatch[] = array(
2542
					'oi_name' => $row->fa_name,
2543
					'oi_archive_name' => $archiveName,
2544
					'oi_size' => $row->fa_size,
2545
					'oi_width' => $row->fa_width,
2546
					'oi_height' => $row->fa_height,
2547
					'oi_bits' => $row->fa_bits,
2548
					'oi_description' => $row->fa_description,
2549
					'oi_user' => $row->fa_user,
2550
					'oi_user_text' => $row->fa_user_text,
2551
					'oi_timestamp' => $row->fa_timestamp,
2552
					'oi_metadata' => $props['metadata'],
2553
					'oi_media_type' => $props['media_type'],
2554
					'oi_major_mime' => $props['major_mime'],
2555
					'oi_minor_mime' => $props['minor_mime'],
2556
					'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
2557
					'oi_sha1' => $sha1 );
2558
			}
2559
2560
			$deleteIds[] = $row->fa_id;
2561
2562 View Code Duplication
			if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
2563
				// private files can stay where they are
2564
				$status->successCount++;
2565
			} else {
2566
				$storeBatch[] = array( $deletedUrl, 'public', $destRel );
2567
				$this->cleanupBatch[] = $row->fa_storage_key;
2568
			}
2569
2570
			$first = false;
2571
		}
2572
2573
		unset( $result );
2574
2575
		// Add a warning to the status object for missing IDs
2576
		$missingIds = array_diff( $this->ids, $idsPresent );
2577
2578
		foreach ( $missingIds as $id ) {
2579
			$status->error( 'undelete-missing-filearchive', $id );
2580
		}
2581
2582
		if ( !$repo->hasSha1Storage() ) {
2583
			// Remove missing files from batch, so we don't get errors when undeleting them
2584
			$checkStatus = $this->removeNonexistentFiles( $storeBatch );
2585
			if ( !$checkStatus->isGood() ) {
2586
				$status->merge( $checkStatus );
2587
				return $status;
2588
			}
2589
			$storeBatch = $checkStatus->value;
2590
2591
			// Run the store batch
2592
			// Use the OVERWRITE_SAME flag to smooth over a common error
2593
			$storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
2594
			$status->merge( $storeStatus );
2595
2596
			if ( !$status->isGood() ) {
2597
				// Even if some files could be copied, fail entirely as that is the
2598
				// easiest thing to do without data loss
2599
				$this->cleanupFailedBatch( $storeStatus, $storeBatch );
2600
				$status->ok = false;
2601
				$this->file->unlock();
2602
2603
				return $status;
2604
			}
2605
		}
2606
2607
		// Run the DB updates
2608
		// Because we have locked the image row, key conflicts should be rare.
2609
		// If they do occur, we can roll back the transaction at this time with
2610
		// no data loss, but leaving unregistered files scattered throughout the
2611
		// public zone.
2612
		// This is not ideal, which is why it's important to lock the image row.
2613
		if ( $insertCurrent ) {
2614
			$dbw->insert( 'image', $insertCurrent, __METHOD__ );
2615
		}
2616
2617
		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...
2618
			$dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
2619
		}
2620
2621
		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...
2622
			$dbw->delete( 'filearchive',
2623
				array( 'fa_id' => $deleteIds ),
2624
				__METHOD__ );
2625
		}
2626
2627
		// If store batch is empty (all files are missing), deletion is to be considered successful
2628
		if ( $status->successCount > 0 || !$storeBatch || $repo->hasSha1Storage() ) {
2629
			if ( !$exists ) {
2630
				wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
2631
2632
				DeferredUpdates::addUpdate( SiteStatsUpdate::factory( array( 'images' => 1 ) ) );
2633
2634
				$this->file->purgeEverything();
2635
			} else {
2636
				wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
2637
				$this->file->purgeDescription();
2638
			}
2639
		}
2640
2641
		$this->file->unlock();
2642
2643
		return $status;
2644
	}
2645
2646
	/**
2647
	 * Removes non-existent files from a store batch.
2648
	 * @param array $triplets
2649
	 * @return Status
2650
	 */
2651
	protected function removeNonexistentFiles( $triplets ) {
2652
		$files = $filteredTriplets = array();
2653
		foreach ( $triplets as $file ) {
2654
			$files[$file[0]] = $file[0];
2655
		}
2656
2657
		$result = $this->file->repo->fileExistsBatch( $files );
2658 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2659
			return Status::newFatal( 'backend-fail-internal',
2660
				$this->file->repo->getBackend()->getName() );
2661
		}
2662
2663
		foreach ( $triplets as $file ) {
2664
			if ( $result[$file[0]] ) {
2665
				$filteredTriplets[] = $file;
2666
			}
2667
		}
2668
2669
		return Status::newGood( $filteredTriplets );
2670
	}
2671
2672
	/**
2673
	 * Removes non-existent files from a cleanup batch.
2674
	 * @param array $batch
2675
	 * @return array
2676
	 */
2677
	protected function removeNonexistentFromCleanup( $batch ) {
2678
		$files = $newBatch = array();
2679
		$repo = $this->file->repo;
2680
2681
		foreach ( $batch as $file ) {
2682
			$files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
2683
				rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
2684
		}
2685
2686
		$result = $repo->fileExistsBatch( $files );
2687
2688
		foreach ( $batch as $file ) {
2689
			if ( $result[$file] ) {
2690
				$newBatch[] = $file;
2691
			}
2692
		}
2693
2694
		return $newBatch;
2695
	}
2696
2697
	/**
2698
	 * Delete unused files in the deleted zone.
2699
	 * This should be called from outside the transaction in which execute() was called.
2700
	 * @return FileRepoStatus
2701
	 */
2702
	public function cleanup() {
2703
		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...
2704
			return $this->file->repo->newGood();
2705
		}
2706
2707
		$this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
2708
2709
		$status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
2710
2711
		return $status;
2712
	}
2713
2714
	/**
2715
	 * Cleanup a failed batch. The batch was only partially successful, so
2716
	 * rollback by removing all items that were succesfully copied.
2717
	 *
2718
	 * @param Status $storeStatus
2719
	 * @param array $storeBatch
2720
	 */
2721
	protected function cleanupFailedBatch( $storeStatus, $storeBatch ) {
2722
		$cleanupBatch = array();
2723
2724
		foreach ( $storeStatus->success as $i => $success ) {
2725
			// Check if this item of the batch was successfully copied
2726
			if ( $success ) {
2727
				// Item was successfully copied and needs to be removed again
2728
				// Extract ($dstZone, $dstRel) from the batch
2729
				$cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][2] );
2730
			}
2731
		}
2732
		$this->file->repo->cleanupBatch( $cleanupBatch );
2733
	}
2734
}
2735
2736
# ------------------------------------------------------------------------------
2737
2738
/**
2739
 * Helper class for file movement
2740
 * @ingroup FileAbstraction
2741
 */
2742
class LocalFileMoveBatch {
2743
	/** @var LocalFile */
2744
	protected $file;
2745
2746
	/** @var Title */
2747
	protected $target;
2748
2749
	protected $cur;
2750
2751
	protected $olds;
2752
2753
	protected $oldCount;
2754
2755
	protected $archive;
2756
2757
	/** @var DatabaseBase */
2758
	protected $db;
2759
2760
	/**
2761
	 * @param File $file
2762
	 * @param Title $target
2763
	 */
2764
	function __construct( File $file, Title $target ) {
2765
		$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...
2766
		$this->target = $target;
2767
		$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...
2768
		$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...
2769
		$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...
2770
		$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...
2771
		$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...
2772
		$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...
2773
		$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...
2774
	}
2775
2776
	/**
2777
	 * Add the current image to the batch
2778
	 */
2779
	public function addCurrent() {
2780
		$this->cur = array( $this->oldRel, $this->newRel );
2781
	}
2782
2783
	/**
2784
	 * Add the old versions of the image to the batch
2785
	 * @return array List of archive names from old versions
2786
	 */
2787
	public function addOlds() {
2788
		$archiveBase = 'archive';
2789
		$this->olds = array();
2790
		$this->oldCount = 0;
2791
		$archiveNames = array();
2792
2793
		$result = $this->db->select( 'oldimage',
2794
			array( 'oi_archive_name', 'oi_deleted' ),
2795
			array( 'oi_name' => $this->oldName ),
2796
			__METHOD__,
2797
			array( 'LOCK IN SHARE MODE' ) // ignore snapshot
2798
		);
2799
2800
		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...
2801
			$archiveNames[] = $row->oi_archive_name;
2802
			$oldName = $row->oi_archive_name;
2803
			$bits = explode( '!', $oldName, 2 );
2804
2805
			if ( count( $bits ) != 2 ) {
2806
				wfDebug( "Old file name missing !: '$oldName' \n" );
2807
				continue;
2808
			}
2809
2810
			list( $timestamp, $filename ) = $bits;
2811
2812
			if ( $this->oldName != $filename ) {
2813
				wfDebug( "Old file name doesn't match: '$oldName' \n" );
2814
				continue;
2815
			}
2816
2817
			$this->oldCount++;
2818
2819
			// Do we want to add those to oldCount?
2820
			if ( $row->oi_deleted & File::DELETED_FILE ) {
2821
				continue;
2822
			}
2823
2824
			$this->olds[] = array(
2825
				"{$archiveBase}/{$this->oldHash}{$oldName}",
2826
				"{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
2827
			);
2828
		}
2829
2830
		return $archiveNames;
2831
	}
2832
2833
	/**
2834
	 * Perform the move.
2835
	 * @return FileRepoStatus
2836
	 */
2837
	public function execute() {
2838
		$repo = $this->file->repo;
2839
		$status = $repo->newGood();
2840
2841
		$triplets = $this->getMoveTriplets();
2842
		$checkStatus = $this->removeNonexistentFiles( $triplets );
2843
		if ( !$checkStatus->isGood() ) {
2844
			$status->merge( $checkStatus );
2845
			return $status;
2846
		}
2847
		$triplets = $checkStatus->value;
2848
		$destFile = wfLocalFile( $this->target );
2849
2850
		$this->file->lock(); // begin
2851
		$destFile->lock(); // quickly fail if destination is not available
2852
		// Rename the file versions metadata in the DB.
2853
		// This implicitly locks the destination file, which avoids race conditions.
2854
		// If we moved the files from A -> C before DB updates, another process could
2855
		// move files from B -> C at this point, causing storeBatch() to fail and thus
2856
		// cleanupTarget() to trigger. It would delete the C files and cause data loss.
2857
		$statusDb = $this->doDBUpdates();
2858
		if ( !$statusDb->isGood() ) {
2859
			$destFile->unlock();
2860
			$this->file->unlockAndRollback();
2861
			$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...
2862
2863
			return $statusDb;
2864
		}
2865
		wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
2866
			"{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
2867
2868
		if ( !$repo->hasSha1Storage() ) {
2869
			// Copy the files into their new location.
2870
			// If a prior process fataled copying or cleaning up files we tolerate any
2871
			// of the existing files if they are identical to the ones being stored.
2872
			$statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
2873
			wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
2874
				"{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
2875
			if ( !$statusMove->isGood() ) {
2876
				// Delete any files copied over (while the destination is still locked)
2877
				$this->cleanupTarget( $triplets );
2878
				$destFile->unlock();
2879
				$this->file->unlockAndRollback(); // unlocks the destination
2880
				wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
2881
				$statusMove->ok = false;
2882
2883
				return $statusMove;
2884
			}
2885
			$status->merge( $statusMove );
2886
		}
2887
2888
		$destFile->unlock();
2889
		$this->file->unlock(); // done
2890
2891
		// Everything went ok, remove the source files
2892
		$this->cleanupSource( $triplets );
2893
2894
		$status->merge( $statusDb );
2895
2896
		return $status;
2897
	}
2898
2899
	/**
2900
	 * Do the database updates and return a new FileRepoStatus indicating how
2901
	 * many rows where updated.
2902
	 *
2903
	 * @return FileRepoStatus
2904
	 */
2905
	protected function doDBUpdates() {
2906
		$repo = $this->file->repo;
2907
		$status = $repo->newGood();
2908
		$dbw = $this->db;
2909
2910
		// Update current image
2911
		$dbw->update(
2912
			'image',
2913
			array( 'img_name' => $this->newName ),
2914
			array( 'img_name' => $this->oldName ),
2915
			__METHOD__
2916
		);
2917
2918
		if ( $dbw->affectedRows() ) {
2919
			$status->successCount++;
2920
		} else {
2921
			$status->failCount++;
2922
			$status->fatal( 'imageinvalidfilename' );
2923
2924
			return $status;
2925
		}
2926
2927
		// Update old images
2928
		$dbw->update(
2929
			'oldimage',
2930
			array(
2931
				'oi_name' => $this->newName,
2932
				'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
2933
					$dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
2934
			),
2935
			array( 'oi_name' => $this->oldName ),
2936
			__METHOD__
2937
		);
2938
2939
		$affected = $dbw->affectedRows();
2940
		$total = $this->oldCount;
2941
		$status->successCount += $affected;
2942
		// Bug 34934: $total is based on files that actually exist.
2943
		// There may be more DB rows than such files, in which case $affected
2944
		// can be greater than $total. We use max() to avoid negatives here.
2945
		$status->failCount += max( 0, $total - $affected );
2946
		if ( $status->failCount ) {
2947
			$status->error( 'imageinvalidfilename' );
2948
		}
2949
2950
		return $status;
2951
	}
2952
2953
	/**
2954
	 * Generate triplets for FileRepo::storeBatch().
2955
	 * @return array
2956
	 */
2957
	protected function getMoveTriplets() {
2958
		$moves = array_merge( array( $this->cur ), $this->olds );
2959
		$triplets = array(); // The format is: (srcUrl, destZone, destUrl)
2960
2961
		foreach ( $moves as $move ) {
2962
			// $move: (oldRelativePath, newRelativePath)
2963
			$srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
2964
			$triplets[] = array( $srcUrl, 'public', $move[1] );
2965
			wfDebugLog(
2966
				'imagemove',
2967
				"Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
2968
			);
2969
		}
2970
2971
		return $triplets;
2972
	}
2973
2974
	/**
2975
	 * Removes non-existent files from move batch.
2976
	 * @param array $triplets
2977
	 * @return Status
2978
	 */
2979
	protected function removeNonexistentFiles( $triplets ) {
2980
		$files = array();
2981
2982
		foreach ( $triplets as $file ) {
2983
			$files[$file[0]] = $file[0];
2984
		}
2985
2986
		$result = $this->file->repo->fileExistsBatch( $files );
2987 View Code Duplication
		if ( in_array( null, $result, true ) ) {
2988
			return Status::newFatal( 'backend-fail-internal',
2989
				$this->file->repo->getBackend()->getName() );
2990
		}
2991
2992
		$filteredTriplets = array();
2993
		foreach ( $triplets as $file ) {
2994
			if ( $result[$file[0]] ) {
2995
				$filteredTriplets[] = $file;
2996
			} else {
2997
				wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
2998
			}
2999
		}
3000
3001
		return Status::newGood( $filteredTriplets );
3002
	}
3003
3004
	/**
3005
	 * Cleanup a partially moved array of triplets by deleting the target
3006
	 * files. Called if something went wrong half way.
3007
	 * @param array $triplets
3008
	 */
3009
	protected function cleanupTarget( $triplets ) {
3010
		// Create dest pairs from the triplets
3011
		$pairs = array();
3012
		foreach ( $triplets as $triplet ) {
3013
			// $triplet: (old source virtual URL, dst zone, dest rel)
3014
			$pairs[] = array( $triplet[1], $triplet[2] );
3015
		}
3016
3017
		$this->file->repo->cleanupBatch( $pairs );
3018
	}
3019
3020
	/**
3021
	 * Cleanup a fully moved array of triplets by deleting the source files.
3022
	 * Called at the end of the move process if everything else went ok.
3023
	 * @param array $triplets
3024
	 */
3025
	protected function cleanupSource( $triplets ) {
3026
		// Create source file names from the triplets
3027
		$files = array();
3028
		foreach ( $triplets as $triplet ) {
3029
			$files[] = $triplet[0];
3030
		}
3031
3032
		$this->file->repo->cleanupBatch( $files );
3033
	}
3034
}
3035