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