MediaWiki master
ArchivedFile.php
Go to the documentation of this file.
1<?php
30
38
39 // Audience options for ::getDescription() and ::getUploader()
40 public const FOR_PUBLIC = 1;
41 public const FOR_THIS_USER = 2;
42 public const RAW = 3;
43
45 private const MDS_EMPTY = 'empty';
46
48 private const MDS_LEGACY = 'legacy';
49
51 private const MDS_PHP = 'php';
52
54 private const MDS_JSON = 'json';
55
57 private $id;
58
60 private $name;
61
63 private $group;
64
66 private $key;
67
69 private $size;
70
72 private $bits;
73
75 private $width;
76
78 private $height;
79
81 protected $metadataArray = [];
82
84 protected $extraDataLoaded = false;
85
93
95 protected $metadataBlobs = [];
96
103 protected $unloadedMetadataBlobs = [];
104
106 private $mime;
107
109 private $media_type;
110
112 private $description;
113
115 private $user;
116
118 private $timestamp;
119
121 private $dataLoaded;
122
124 private $deleted;
125
127 private $sha1;
128
132 private $pageCount;
133
135 private $archive_name;
136
138 protected $handler;
139
141 protected $title; # image title
142
144 protected $exists;
145
147 private $repo;
148
150 private $metadataStorageHelper;
151
159 public function __construct( $title, $id = 0, $key = '', $sha1 = '' ) {
160 $this->id = -1;
161 $this->title = null;
162 $this->name = false;
163 $this->group = 'deleted'; // needed for direct use of constructor
164 $this->key = '';
165 $this->size = 0;
166 $this->bits = 0;
167 $this->width = 0;
168 $this->height = 0;
169 $this->mime = "unknown/unknown";
170 $this->media_type = '';
171 $this->description = '';
172 $this->user = null;
173 $this->timestamp = null;
174 $this->deleted = 0;
175 $this->dataLoaded = false;
176 $this->exists = false;
177 $this->sha1 = '';
178
179 if ( $title instanceof Title ) {
180 $this->title = File::normalizeTitle( $title, 'exception' );
181 $this->name = $title->getDBkey();
182 }
183
184 if ( $id ) {
185 $this->id = $id;
186 }
187
188 if ( $key ) {
189 $this->key = $key;
190 }
191
192 if ( $sha1 ) {
193 $this->sha1 = $sha1;
194 }
195
196 if ( !$id && !$key && !( $title instanceof Title ) && !$sha1 ) {
197 throw new BadMethodCallException( "No specifications provided to ArchivedFile constructor." );
198 }
199
200 $this->repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
201 $this->metadataStorageHelper = new MetadataStorageHelper( $this->repo );
202 }
203
209 public function load() {
210 if ( $this->dataLoaded ) {
211 return true;
212 }
213 $conds = [];
214
215 if ( $this->id > 0 ) {
216 $conds['fa_id'] = $this->id;
217 }
218 if ( $this->key ) {
219 $conds['fa_storage_group'] = $this->group;
220 $conds['fa_storage_key'] = $this->key;
221 }
222 if ( $this->title ) {
223 $conds['fa_name'] = $this->title->getDBkey();
224 }
225 if ( $this->sha1 ) {
226 $conds['fa_sha1'] = $this->sha1;
227 }
228
229 if ( $conds === [] ) {
230 throw new RuntimeException( "No specific information for retrieving archived file" );
231 }
232
233 if ( !$this->title || $this->title->getNamespace() === NS_FILE ) {
234 $this->dataLoaded = true; // set it here, to have also true on miss
235 $dbr = $this->repo->getReplicaDB();
236 $queryBuilder = FileSelectQueryBuilder::newForArchivedFile( $dbr );
237 $row = $queryBuilder->where( $conds )
238 ->orderBy( 'fa_timestamp', SelectQueryBuilder::SORT_DESC )
239 ->caller( __METHOD__ )->fetchRow();
240 if ( !$row ) {
241 // this revision does not exist?
242 return null;
243 }
244
245 // initialize fields for filestore image object
246 $this->loadFromRow( $row );
247 } else {
248 throw new UnexpectedValueException( 'This title does not correspond to an image page.' );
249 }
250
251 return true;
252 }
253
261 public static function newFromRow( $row ) {
262 $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
263 $file->loadFromRow( $row );
264
265 return $file;
266 }
267
285 public static function getQueryInfo() {
286 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
287 $queryInfo = ( FileSelectQueryBuilder::newForArchivedFile( $dbr ) )->getQueryInfo();
288 return [
289 'tables' => $queryInfo['tables'],
290 'fields' => $queryInfo['fields'],
291 'joins' => $queryInfo['join_conds'],
292 ];
293 }
294
302 public function loadFromRow( $row ) {
303 $this->id = intval( $row->fa_id );
304 $this->name = $row->fa_name;
305 $this->archive_name = $row->fa_archive_name;
306 $this->group = $row->fa_storage_group;
307 $this->key = $row->fa_storage_key;
308 $this->size = $row->fa_size;
309 $this->bits = $row->fa_bits;
310 $this->width = $row->fa_width;
311 $this->height = $row->fa_height;
313 $this->repo->getReplicaDB(), $row->fa_metadata );
314 $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
315 $this->media_type = $row->fa_media_type;
316 $services = MediaWikiServices::getInstance();
317 $this->description = $services->getCommentStore()
318 // Legacy because $row may have come from self::selectFields()
319 ->getCommentLegacy( $this->repo->getReplicaDB(), 'fa_description', $row )->text;
320 $this->user = $services->getUserFactory()
321 ->newFromAnyId( $row->fa_user, $row->fa_user_text, $row->fa_actor );
322 $this->timestamp = $row->fa_timestamp;
323 $this->deleted = $row->fa_deleted;
324 if ( isset( $row->fa_sha1 ) ) {
325 $this->sha1 = $row->fa_sha1;
326 } else {
327 // old row, populate from key
328 $this->sha1 = LocalRepo::getHashFromKey( $this->key );
329 }
330 if ( !$this->title ) {
331 $this->title = Title::makeTitleSafe( NS_FILE, $row->fa_name );
332 }
333 $this->exists = $row->fa_archive_name !== '';
334 }
335
341 public function getTitle() {
342 if ( !$this->title ) {
343 $this->load();
344 }
345 return $this->title;
346 }
347
353 public function getName() {
354 if ( $this->name === false ) {
355 $this->load();
356 }
357
358 return $this->name;
359 }
360
364 public function getID() {
365 $this->load();
366
367 return $this->id;
368 }
369
373 public function exists() {
374 $this->load();
375
376 return $this->exists;
377 }
378
383 public function getKey() {
384 $this->load();
385
386 return $this->key;
387 }
388
393 public function getStorageKey() {
394 return $this->getKey();
395 }
396
401 public function getGroup() {
402 return $this->group;
403 }
404
409 public function getWidth() {
410 $this->load();
411
412 return $this->width;
413 }
414
419 public function getHeight() {
420 $this->load();
421
422 return $this->height;
423 }
424
431 public function getMetadata() {
432 $data = $this->getMetadataArray();
433 if ( !$data ) {
434 return '';
435 } elseif ( array_keys( $data ) === [ '_error' ] ) {
436 // Legacy error encoding
437 return $data['_error'];
438 } else {
439 return serialize( $this->getMetadataArray() );
440 }
441 }
442
449 public function getMetadataArray(): array {
450 $this->load();
451 if ( $this->unloadedMetadataBlobs ) {
452 return $this->getMetadataItems(
453 array_unique( array_merge(
454 array_keys( $this->metadataArray ),
455 array_keys( $this->unloadedMetadataBlobs )
456 ) )
457 );
458 }
459 return $this->metadataArray;
460 }
461
462 public function getMetadataItems( array $itemNames ): array {
463 $this->load();
464 $result = [];
465 $addresses = [];
466 foreach ( $itemNames as $itemName ) {
467 if ( array_key_exists( $itemName, $this->metadataArray ) ) {
468 $result[$itemName] = $this->metadataArray[$itemName];
469 } elseif ( isset( $this->unloadedMetadataBlobs[$itemName] ) ) {
470 $addresses[$itemName] = $this->unloadedMetadataBlobs[$itemName];
471 }
472 }
473
474 if ( $addresses ) {
475 $resultFromBlob = $this->metadataStorageHelper->getMetadataFromBlobStore( $addresses );
476 foreach ( $addresses as $itemName => $address ) {
477 unset( $this->unloadedMetadataBlobs[$itemName] );
478 $value = $resultFromBlob[$itemName] ?? null;
479 if ( $value !== null ) {
480 $result[$itemName] = $value;
481 $this->metadataArray[$itemName] = $value;
482 }
483 }
484 }
485 return $result;
486 }
487
499 public function getMetadataForDb( IReadableDatabase $db ) {
500 $this->load();
501 if ( !$this->metadataArray && !$this->metadataBlobs ) {
502 $s = '';
503 } elseif ( $this->repo->isJsonMetadataEnabled() ) {
504 $s = $this->getJsonMetadata();
505 } else {
506 $s = serialize( $this->getMetadataArray() );
507 }
508 if ( !is_string( $s ) ) {
509 throw new RuntimeException( 'Could not serialize image metadata value for DB' );
510 }
511 return $db->encodeBlob( $s );
512 }
513
520 private function getJsonMetadata() {
521 // Directly store data that is not already in BlobStore
522 $envelope = [
523 'data' => array_diff_key( $this->metadataArray, $this->metadataBlobs )
524 ];
525
526 // Also store the blob addresses
527 if ( $this->metadataBlobs ) {
528 $envelope['blobs'] = $this->metadataBlobs;
529 }
530
531 [ $s, $blobAddresses ] = $this->metadataStorageHelper->getJsonMetadata( $this, $envelope );
532
533 // Repeated calls to this function should not keep inserting more blobs
534 $this->metadataBlobs += $blobAddresses;
535
536 return $s;
537 }
538
547 protected function loadMetadataFromDbFieldValue( IReadableDatabase $db, $metadataBlob ) {
548 $this->loadMetadataFromString( $db->decodeBlob( $metadataBlob ) );
549 }
550
558 protected function loadMetadataFromString( $metadataString ) {
559 $this->extraDataLoaded = true;
560 $this->metadataArray = [];
561 $this->metadataBlobs = [];
562 $this->unloadedMetadataBlobs = [];
563 $metadataString = (string)$metadataString;
564 if ( $metadataString === '' ) {
565 $this->metadataSerializationFormat = self::MDS_EMPTY;
566 return;
567 }
568 if ( $metadataString[0] === '{' ) {
569 $envelope = $this->metadataStorageHelper->jsonDecode( $metadataString );
570 if ( !$envelope ) {
571 // Legacy error encoding
572 $this->metadataArray = [ '_error' => $metadataString ];
573 $this->metadataSerializationFormat = self::MDS_LEGACY;
574 } else {
575 $this->metadataSerializationFormat = self::MDS_JSON;
576 if ( isset( $envelope['data'] ) ) {
577 $this->metadataArray = $envelope['data'];
578 }
579 if ( isset( $envelope['blobs'] ) ) {
580 $this->metadataBlobs = $this->unloadedMetadataBlobs = $envelope['blobs'];
581 }
582 }
583 } else {
584 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
585 $data = @unserialize( $metadataString );
586 if ( !is_array( $data ) ) {
587 // Legacy error encoding
588 $data = [ '_error' => $metadataString ];
589 $this->metadataSerializationFormat = self::MDS_LEGACY;
590 } else {
591 $this->metadataSerializationFormat = self::MDS_PHP;
592 }
593 $this->metadataArray = $data;
594 }
595 }
596
601 public function getSize() {
602 $this->load();
603
604 return $this->size;
605 }
606
610 public function getBits() {
611 $this->load();
612
613 return $this->bits;
614 }
615
620 public function getMimeType() {
621 $this->load();
622
623 return $this->mime;
624 }
625
630 private function getHandler() {
631 if ( !$this->handler ) {
632 $this->handler = MediaHandler::getHandler( $this->getMimeType() );
633 }
634
635 return $this->handler;
636 }
637
644 public function pageCount() {
645 if ( $this->pageCount === null ) {
646 // @FIXME: callers expect File objects
647 // @phan-suppress-next-line PhanTypeMismatchArgument
648 if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
649 // @phan-suppress-next-line PhanTypeMismatchArgument
650 $this->pageCount = $this->handler->pageCount( $this );
651 } else {
652 $this->pageCount = false;
653 }
654 }
655
656 return $this->pageCount;
657 }
658
664 public function getMediaType() {
665 $this->load();
666
667 return $this->media_type;
668 }
669
675 public function getTimestamp() {
676 $this->load();
677
678 return wfTimestamp( TS_MW, $this->timestamp );
679 }
680
687 public function getSha1() {
688 $this->load();
689
690 return $this->sha1;
691 }
692
704 public function getUploader( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ): ?UserIdentity {
705 $this->load();
706 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( File::DELETED_USER ) ) {
707 return null;
708 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( File::DELETED_USER, $performer ) ) {
709 return null;
710 } else {
711 return $this->user;
712 }
713 }
714
727 public function getDescription( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ): string {
728 $this->load();
729 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( File::DELETED_COMMENT ) ) {
730 return '';
731 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( File::DELETED_COMMENT, $performer ) ) {
732 return '';
733 } else {
734 return $this->description;
735 }
736 }
737
742 public function getVisibility() {
743 $this->load();
744
745 return $this->deleted;
746 }
747
754 public function isDeleted( $field ) {
755 $this->load();
756
757 return ( $this->deleted & $field ) == $field;
758 }
759
767 public function userCan( $field, Authority $performer ) {
768 $this->load();
769 $title = $this->getTitle();
770
771 return RevisionRecord::userCanBitfield(
772 $this->deleted,
773 $field,
774 $performer,
775 $title ?: null
776 );
777 }
778}
const NS_FILE
Definition Defines.php:71
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Deleted file in the 'filearchive' table.
getTimestamp()
Return upload timestamp.
getSize()
Return the size of the image file, in bytes.
MediaHandler $handler
isDeleted( $field)
for file or revision rows
getHeight()
Return the height of the image.
string[] $metadataBlobs
Map of metadata item name to blob address.
loadMetadataFromDbFieldValue(IReadableDatabase $db, $metadataBlob)
Unserialize a metadata blob which came from the database and store it in $this.
array $metadataArray
Unserialized metadata.
const FOR_THIS_USER
string[] $unloadedMetadataBlobs
Map of metadata item name to blob address for items that exist but have not yet been loaded into $thi...
getSha1()
Get the SHA-1 base 36 hash of the file.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archivedfile object.
loadFromRow( $row)
Load ArchivedFile object fields from a DB row.
string null $metadataSerializationFormat
One of the MDS_* constants, giving the format of the metadata as stored in the DB,...
getDescription(int $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Return upload description.
getMetadataItems(array $itemNames)
__construct( $title, $id=0, $key='', $sha1='')
load()
Loads a file object from the filearchive table.
pageCount()
Returns the number of pages of a multipage document, or false for documents which aren't multipage do...
getGroup()
Return the FileStore storage group.
static newFromRow( $row)
Loads a file object from the filearchive table.
getTitle()
Return the associated title object.
Title null $title
getWidth()
Return the width of the image.
getStorageKey()
Return the FileStore key (overriding base File class)
getMimeType()
Returns the MIME type of the file.
getKey()
Return the FileStore key.
userCan( $field, Authority $performer)
Determine if the current user is allowed to view a particular field of this FileStore image file,...
getMetadataArray()
Get unserialized handler-specific metadata.
loadMetadataFromString( $metadataString)
Unserialize a metadata string which came from some non-DB source, or is the return value of IReadable...
getName()
Return the file name.
getUploader(int $audience=self::FOR_PUBLIC, ?Authority $performer=null)
getMediaType()
Return the type of the media in the file.
getMetadataForDb(IReadableDatabase $db)
Serialize the metadata array for insertion into img_metadata, oi_metadata or fa_metadata.
getVisibility()
Returns the deletion bitfield.
getMetadata()
Get handler-specific metadata as a serialized string.
bool $extraDataLoaded
Whether or not lazy-loaded data has been loaded from the database.
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:49
Base media handler class.
Service locator for MediaWiki core services.
Page revision base class.
Represents a title within MediaWiki.
Definition Title.php:78
getDBkey()
Get the main part with underscores.
Definition Title.php:1032
Helper for storage of metadata.
Build SELECT queries with a fluent interface.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
Interface for objects representing user identity.
A database connection without write operations.
encodeBlob( $b)
Some DBMSs have a special format for inserting into blob fields, they don't allow simple quoted strin...
decodeBlob( $b)
Some DBMSs return a special placeholder object representing blob fields in result objects.