Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 196 |
|
0.00% |
0 / 25 |
CRAP | |
0.00% |
0 / 1 |
OldLocalFile | |
0.00% |
0 / 196 |
|
0.00% |
0 / 25 |
2162 | |
0.00% |
0 / 1 |
newFromTitle | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
newFromArchiveName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
newFromRow | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
newFromKey | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
getQueryInfo | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
loadFromRow | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
getCacheKey | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getArchiveName | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isOld | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isVisible | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
loadFromDB | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 | |||
loadExtraFromDB | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
buildQueryBuilderForLoad | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getCacheFields | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
getRel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUrlRel | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
upgradeRow | |
0.00% |
0 / 45 |
|
0.00% |
0 / 1 |
12 | |||
reserializeMetadata | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isDeleted | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getVisibility | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
userCan | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
uploadOld | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
recordOldUpload | |
0.00% |
0 / 54 |
|
0.00% |
0 / 1 |
12 | |||
exists | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | use MediaWiki\FileRepo\File\FileSelectQueryBuilder; |
22 | use MediaWiki\MainConfigNames; |
23 | use MediaWiki\MediaWikiServices; |
24 | use MediaWiki\Permissions\Authority; |
25 | use MediaWiki\Revision\RevisionRecord; |
26 | use MediaWiki\Status\Status; |
27 | use MediaWiki\Title\Title; |
28 | use MediaWiki\User\UserIdentity; |
29 | use Wikimedia\Rdbms\IDBAccessObject; |
30 | use Wikimedia\Rdbms\IReadableDatabase; |
31 | use Wikimedia\Rdbms\SelectQueryBuilder; |
32 | |
33 | /** |
34 | * Old file in the oldimage table. |
35 | * |
36 | * @stable to extend |
37 | * @ingroup FileAbstraction |
38 | */ |
39 | class OldLocalFile extends LocalFile { |
40 | /** @var string|int Timestamp */ |
41 | protected $requestedTime; |
42 | |
43 | /** @var string|null Archive name */ |
44 | protected $archive_name; |
45 | |
46 | public const CACHE_VERSION = 1; |
47 | |
48 | /** |
49 | * @stable to override |
50 | * @param Title $title |
51 | * @param LocalRepo $repo |
52 | * @param string|int|null $time |
53 | * @return static |
54 | */ |
55 | public static function newFromTitle( $title, $repo, $time = null ) { |
56 | # The null default value is only here to avoid an E_STRICT |
57 | if ( $time === null ) { |
58 | throw new InvalidArgumentException( __METHOD__ . ' got null for $time parameter' ); |
59 | } |
60 | |
61 | return new static( $title, $repo, $time, null ); |
62 | } |
63 | |
64 | /** |
65 | * @stable to override |
66 | * |
67 | * @param Title $title |
68 | * @param LocalRepo $repo |
69 | * @param string $archiveName |
70 | * @return static |
71 | */ |
72 | public static function newFromArchiveName( $title, $repo, $archiveName ) { |
73 | return new static( $title, $repo, null, $archiveName ); |
74 | } |
75 | |
76 | /** |
77 | * @stable to override |
78 | * |
79 | * @param stdClass $row |
80 | * @param LocalRepo $repo |
81 | * @return static |
82 | */ |
83 | public static function newFromRow( $row, $repo ) { |
84 | $title = Title::makeTitle( NS_FILE, $row->oi_name ); |
85 | $file = new static( $title, $repo, null, $row->oi_archive_name ); |
86 | $file->loadFromRow( $row, 'oi_' ); |
87 | |
88 | return $file; |
89 | } |
90 | |
91 | /** |
92 | * Create a OldLocalFile from a SHA-1 key |
93 | * Do not call this except from inside a repo class. |
94 | * |
95 | * @stable to override |
96 | * |
97 | * @param string $sha1 Base-36 SHA-1 |
98 | * @param LocalRepo $repo |
99 | * @param string|false $timestamp MW_timestamp (optional) |
100 | * |
101 | * @return static|false |
102 | */ |
103 | public static function newFromKey( $sha1, $repo, $timestamp = false ) { |
104 | $dbr = $repo->getReplicaDB(); |
105 | $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr ); |
106 | |
107 | $queryBuilder->where( [ 'oi_sha1' => $sha1 ] ); |
108 | if ( $timestamp ) { |
109 | $queryBuilder->andWhere( [ 'oi_timestamp' => $dbr->timestamp( $timestamp ) ] ); |
110 | } |
111 | |
112 | $row = $queryBuilder->caller( __METHOD__ )->fetchRow(); |
113 | if ( $row ) { |
114 | return static::newFromRow( $row, $repo ); |
115 | } else { |
116 | return false; |
117 | } |
118 | } |
119 | |
120 | /** |
121 | * Return the tables, fields, and join conditions to be selected to create |
122 | * a new oldlocalfile object. |
123 | * |
124 | * Since 1.34, oi_user and oi_user_text have not been present in the |
125 | * database, but they continue to be available in query results as |
126 | * aliases. |
127 | * |
128 | * @since 1.31 |
129 | * @stable to override |
130 | * |
131 | * @deprecated since 1.41 use FileSelectQueryBuilder instead |
132 | * @param string[] $options |
133 | * - omit-lazy: Omit fields that are lazily cached. |
134 | * @return array[] With three keys: |
135 | * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` |
136 | * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields` |
137 | * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` |
138 | * @phan-return array{tables:string[],fields:string[],joins:array} |
139 | */ |
140 | public static function getQueryInfo( array $options = [] ) { |
141 | $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase(); |
142 | $queryInfo = FileSelectQueryBuilder::newForOldFile( $dbr, $options )->getQueryInfo(); |
143 | return [ |
144 | 'tables' => $queryInfo['tables'], |
145 | 'fields' => $queryInfo['fields'], |
146 | 'joins' => $queryInfo['join_conds'], |
147 | ]; |
148 | } |
149 | |
150 | /** |
151 | * @stable to call |
152 | * |
153 | * @param Title $title |
154 | * @param LocalRepo $repo |
155 | * @param string|int|null $time Timestamp or null to load by archive name |
156 | * @param string|null $archiveName Archive name or null to load by timestamp |
157 | */ |
158 | public function __construct( $title, $repo, $time, $archiveName ) { |
159 | parent::__construct( $title, $repo ); |
160 | $this->requestedTime = $time; |
161 | $this->archive_name = $archiveName; |
162 | if ( $time === null && $archiveName === null ) { |
163 | throw new LogicException( __METHOD__ . ': must specify at least one of $time or $archiveName' ); |
164 | } |
165 | } |
166 | |
167 | public function loadFromRow( $row, $prefix = 'img_' ) { |
168 | $this->archive_name = $row->{"{$prefix}archive_name"}; |
169 | $this->deleted = $row->{"{$prefix}deleted"}; |
170 | $row = clone $row; |
171 | unset( $row->{"{$prefix}archive_name"} ); |
172 | unset( $row->{"{$prefix}deleted"} ); |
173 | parent::loadFromRow( $row, $prefix ); |
174 | } |
175 | |
176 | /** |
177 | * @stable to override |
178 | * @return bool |
179 | */ |
180 | protected function getCacheKey() { |
181 | return false; |
182 | } |
183 | |
184 | /** |
185 | * @stable to override |
186 | * @return string |
187 | */ |
188 | public function getArchiveName() { |
189 | if ( $this->archive_name === null ) { |
190 | $this->load(); |
191 | } |
192 | |
193 | return $this->archive_name; |
194 | } |
195 | |
196 | /** |
197 | * @return bool |
198 | */ |
199 | public function isOld() { |
200 | return true; |
201 | } |
202 | |
203 | /** |
204 | * @return bool |
205 | */ |
206 | public function isVisible() { |
207 | return $this->exists() && !$this->isDeleted( File::DELETED_FILE ); |
208 | } |
209 | |
210 | /** |
211 | * @stable to override |
212 | * @param int $flags |
213 | */ |
214 | protected function loadFromDB( $flags = 0 ) { |
215 | $this->dataLoaded = true; |
216 | |
217 | $dbr = ( $flags & IDBAccessObject::READ_LATEST ) |
218 | ? $this->repo->getPrimaryDB() |
219 | : $this->repo->getReplicaDB(); |
220 | $queryBuilder = $this->buildQueryBuilderForLoad( $dbr, [] ); |
221 | $row = $queryBuilder->caller( __METHOD__ )->fetchRow(); |
222 | if ( $row ) { |
223 | $this->loadFromRow( $row, 'oi_' ); |
224 | } else { |
225 | $this->fileExists = false; |
226 | } |
227 | } |
228 | |
229 | /** |
230 | * Load lazy file metadata from the DB |
231 | * @stable to override |
232 | */ |
233 | protected function loadExtraFromDB() { |
234 | $this->extraDataLoaded = true; |
235 | $dbr = $this->repo->getReplicaDB(); |
236 | $queryBuilder = $this->buildQueryBuilderForLoad( $dbr ); |
237 | |
238 | // In theory the file could have just been renamed/deleted...oh well |
239 | $row = $queryBuilder->caller( __METHOD__ )->fetchRow(); |
240 | |
241 | if ( !$row ) { // fallback to primary DB |
242 | $dbr = $this->repo->getPrimaryDB(); |
243 | $queryBuilder = $this->buildQueryBuilderForLoad( $dbr ); |
244 | $row = $queryBuilder->caller( __METHOD__ )->fetchRow(); |
245 | } |
246 | |
247 | if ( $row ) { |
248 | foreach ( $this->unprefixRow( $row, 'oi_' ) as $name => $value ) { |
249 | $this->$name = $value; |
250 | } |
251 | } else { |
252 | throw new RuntimeException( "Could not find data for image '{$this->archive_name}'." ); |
253 | } |
254 | } |
255 | |
256 | private function buildQueryBuilderForLoad( IReadableDatabase $dbr, $options = [ 'omit-nonlazy' ] ) { |
257 | $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr, $options ); |
258 | $queryBuilder->where( [ 'oi_name' => $this->getName() ] ) |
259 | ->orderBy( 'oi_timestamp', SelectQueryBuilder::SORT_DESC ); |
260 | if ( $this->requestedTime === null ) { |
261 | $queryBuilder->andWhere( [ 'oi_archive_name' => $this->archive_name ] ); |
262 | } else { |
263 | $queryBuilder->andWhere( [ 'oi_timestamp' => $dbr->timestamp( $this->requestedTime ) ] ); |
264 | } |
265 | return $queryBuilder; |
266 | } |
267 | |
268 | /** |
269 | * @inheritDoc |
270 | * @stable to override |
271 | */ |
272 | protected function getCacheFields( $prefix = 'img_' ) { |
273 | $fields = parent::getCacheFields( $prefix ); |
274 | $fields[] = $prefix . 'archive_name'; |
275 | $fields[] = $prefix . 'deleted'; |
276 | |
277 | return $fields; |
278 | } |
279 | |
280 | /** |
281 | * @return string |
282 | * @stable to override |
283 | */ |
284 | public function getRel() { |
285 | return $this->getArchiveRel( $this->getArchiveName() ); |
286 | } |
287 | |
288 | /** |
289 | * @return string |
290 | * @stable to override |
291 | */ |
292 | public function getUrlRel() { |
293 | return $this->getArchiveRel( rawurlencode( $this->getArchiveName() ) ); |
294 | } |
295 | |
296 | /** |
297 | * @stable to override |
298 | */ |
299 | public function upgradeRow() { |
300 | $this->loadFromFile(); |
301 | |
302 | # Don't destroy file info of missing files |
303 | if ( !$this->fileExists ) { |
304 | wfDebug( __METHOD__ . ": file does not exist, aborting" ); |
305 | |
306 | return; |
307 | } |
308 | |
309 | $dbw = $this->repo->getPrimaryDB(); |
310 | [ $major, $minor ] = self::splitMime( $this->mime ); |
311 | $metadata = $this->getMetadataForDb( $dbw ); |
312 | |
313 | wfDebug( __METHOD__ . ': upgrading ' . $this->archive_name . " to the current schema" ); |
314 | $dbw->newUpdateQueryBuilder() |
315 | ->update( 'oldimage' ) |
316 | ->set( [ |
317 | 'oi_size' => $this->size, |
318 | 'oi_width' => $this->width, |
319 | 'oi_height' => $this->height, |
320 | 'oi_bits' => $this->bits, |
321 | 'oi_media_type' => $this->media_type, |
322 | 'oi_major_mime' => $major, |
323 | 'oi_minor_mime' => $minor, |
324 | 'oi_metadata' => $metadata, |
325 | 'oi_sha1' => $this->sha1, |
326 | ] ) |
327 | ->where( [ |
328 | 'oi_name' => $this->getName(), |
329 | 'oi_archive_name' => $this->archive_name, |
330 | ] ) |
331 | ->caller( __METHOD__ )->execute(); |
332 | |
333 | $migrationStage = MediaWikiServices::getInstance()->getMainConfig()->get( |
334 | MainConfigNames::FileSchemaMigrationStage |
335 | ); |
336 | if ( $migrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
337 | $dbw->newUpdateQueryBuilder() |
338 | ->update( 'filerevision' ) |
339 | ->set( [ |
340 | 'fr_size' => $this->size, |
341 | 'fr_width' => $this->width, |
342 | 'fr_height' => $this->height, |
343 | 'fr_bits' => $this->bits, |
344 | 'fr_metadata' => $metadata, |
345 | 'fr_sha1' => $this->sha1, |
346 | ] ) |
347 | ->where( [ |
348 | 'fr_file' => $this->acquireFileIdFromName(), |
349 | 'fr_archive_name' => $this->archive_name, |
350 | ] ) |
351 | ->caller( __METHOD__ )->execute(); |
352 | } |
353 | } |
354 | |
355 | protected function reserializeMetadata() { |
356 | // TODO: implement this and make it possible to hit it from refreshImageMetadata.php |
357 | // It can be hit from action=purge but that's not very useful if the |
358 | // goal is to reserialize the whole oldimage table. |
359 | } |
360 | |
361 | /** |
362 | * @param int $field One of DELETED_* bitfield constants for file or |
363 | * revision rows |
364 | * @return bool |
365 | */ |
366 | public function isDeleted( $field ) { |
367 | $this->load(); |
368 | |
369 | return ( $this->deleted & $field ) == $field; |
370 | } |
371 | |
372 | /** |
373 | * Returns bitfield value |
374 | * @return int |
375 | */ |
376 | public function getVisibility() { |
377 | $this->load(); |
378 | |
379 | return (int)$this->deleted; |
380 | } |
381 | |
382 | /** |
383 | * Determine if the current user is allowed to view a particular |
384 | * field of this image file, if it's marked as deleted. |
385 | * |
386 | * @param int $field |
387 | * @param Authority $performer User object to check |
388 | * @return bool |
389 | */ |
390 | public function userCan( $field, Authority $performer ) { |
391 | $this->load(); |
392 | |
393 | return RevisionRecord::userCanBitfield( |
394 | $this->deleted, |
395 | $field, |
396 | $performer |
397 | ); |
398 | } |
399 | |
400 | /** |
401 | * Upload a file directly into archive. Generally for Special:Import. |
402 | * |
403 | * @param string $srcPath File system path of the source file |
404 | * @param string $timestamp |
405 | * @param string $comment |
406 | * @param UserIdentity $user |
407 | * @return Status |
408 | */ |
409 | public function uploadOld( $srcPath, $timestamp, $comment, UserIdentity $user ) { |
410 | $archiveName = $this->getArchiveName(); |
411 | $dstRel = $this->getArchiveRel( $archiveName ); |
412 | $status = $this->publishTo( $srcPath, $dstRel ); |
413 | |
414 | if ( $status->isGood() && |
415 | !$this->recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) |
416 | ) { |
417 | $status->fatal( 'filenotfound', $srcPath ); |
418 | } |
419 | |
420 | return $status; |
421 | } |
422 | |
423 | /** |
424 | * Record a file upload in the oldimage table, without adding log entries. |
425 | * @stable to override |
426 | * |
427 | * @param string $srcPath File system path to the source file |
428 | * @param string $archiveName The archive name of the file |
429 | * @param string $timestamp |
430 | * @param string $comment Upload comment |
431 | * @param UserIdentity $user User who did this upload |
432 | * @return bool |
433 | */ |
434 | protected function recordOldUpload( $srcPath, $archiveName, $timestamp, $comment, $user ) { |
435 | $dbw = $this->repo->getPrimaryDB(); |
436 | |
437 | $services = MediaWikiServices::getInstance(); |
438 | $mwProps = new MWFileProps( $services->getMimeAnalyzer() ); |
439 | $props = $mwProps->getPropsFromPath( $srcPath, true ); |
440 | if ( !$props['fileExists'] ) { |
441 | return false; |
442 | } |
443 | $this->setProps( $props ); |
444 | |
445 | $dbw->startAtomic( __METHOD__ ); |
446 | |
447 | $commentFields = $services->getCommentStore() |
448 | ->insert( $dbw, 'oi_description', $comment ); |
449 | $actorId = $services->getActorNormalization() |
450 | ->acquireActorId( $user, $dbw ); |
451 | $dbw->newInsertQueryBuilder() |
452 | ->insertInto( 'oldimage' ) |
453 | ->row( [ |
454 | 'oi_name' => $this->getName(), |
455 | 'oi_archive_name' => $archiveName, |
456 | 'oi_size' => $props['size'], |
457 | 'oi_width' => intval( $props['width'] ), |
458 | 'oi_height' => intval( $props['height'] ), |
459 | 'oi_bits' => $props['bits'], |
460 | 'oi_actor' => $actorId, |
461 | 'oi_timestamp' => $dbw->timestamp( $timestamp ), |
462 | 'oi_metadata' => $this->getMetadataForDb( $dbw ), |
463 | 'oi_media_type' => $props['media_type'], |
464 | 'oi_major_mime' => $props['major_mime'], |
465 | 'oi_minor_mime' => $props['minor_mime'], |
466 | 'oi_sha1' => $props['sha1'], |
467 | ] + $commentFields ) |
468 | ->caller( __METHOD__ )->execute(); |
469 | |
470 | $migrationStage = MediaWikiServices::getInstance()->getMainConfig()->get( |
471 | MainConfigNames::FileSchemaMigrationStage |
472 | ); |
473 | if ( $migrationStage & SCHEMA_COMPAT_WRITE_NEW ) { |
474 | $commentFields = $services->getCommentStore() |
475 | ->insert( $dbw, 'fr_description', $comment ); |
476 | $dbw->newInsertQueryBuilder() |
477 | ->insertInto( 'filerevision' ) |
478 | ->ignore() |
479 | ->row( [ |
480 | 'fr_file' => $this->acquireFileIdFromName(), |
481 | 'fr_size' => $this->size, |
482 | 'fr_width' => intval( $this->width ), |
483 | 'fr_height' => intval( $this->height ), |
484 | 'fr_bits' => $this->bits, |
485 | 'fr_actor' => $actorId, |
486 | 'fr_deleted' => 0, |
487 | 'fr_timestamp' => $dbw->timestamp( $timestamp ), |
488 | 'fr_metadata' => $this->getMetadataForDb( $dbw ), |
489 | 'fr_sha1' => $this->sha1 |
490 | ] + $commentFields ) |
491 | ->caller( __METHOD__ )->execute(); |
492 | } |
493 | |
494 | $dbw->endAtomic( __METHOD__ ); |
495 | |
496 | return true; |
497 | } |
498 | |
499 | /** |
500 | * If archive name is an empty string, then file does not "exist" |
501 | * |
502 | * This is the case for a couple files on Wikimedia servers where |
503 | * the old version is "lost". |
504 | * @return bool |
505 | */ |
506 | public function exists() { |
507 | $archiveName = $this->getArchiveName(); |
508 | if ( $archiveName === '' || !is_string( $archiveName ) ) { |
509 | return false; |
510 | } |
511 | return parent::exists(); |
512 | } |
513 | } |