MediaWiki master
ArchivedFile.php
Go to the documentation of this file.
1<?php
8
9use BadMethodCallException;
10use MediaHandler;
17use RuntimeException;
18use stdClass;
19use UnexpectedValueException;
23use Wikimedia\Timestamp\TimestampFormat as TS;
24
32
33 // Audience options for ::getDescription() and ::getUploader()
34 public const FOR_PUBLIC = 1;
35 public const FOR_THIS_USER = 2;
36 public const RAW = 3;
37
39 private const MDS_EMPTY = 'empty';
40
42 private const MDS_LEGACY = 'legacy';
43
45 private const MDS_PHP = 'php';
46
48 private const MDS_JSON = 'json';
49
51 private $id;
52
54 private $name;
55
57 private $group;
58
60 private $key;
61
63 private $size;
64
66 private $bits;
67
69 private $width;
70
72 private $height;
73
75 protected $metadataArray = [];
76
78 protected $extraDataLoaded = false;
79
87
89 protected $metadataBlobs = [];
90
97 protected $unloadedMetadataBlobs = [];
98
100 private $mime;
101
103 private $media_type;
104
106 private $description;
107
109 private $user;
110
112 private $timestamp;
113
115 private $dataLoaded;
116
118 private $deleted;
119
121 private $sha1;
122
126 private $pageCount;
127
129 private $archive_name;
130
132 protected $handler;
133
135 protected $title; # image title
136
138 protected $exists;
139
141 private $repo;
142
144 private $metadataStorageHelper;
145
153 public function __construct( $title, $id = 0, $key = '', $sha1 = '' ) {
154 $this->id = -1;
155 $this->title = null;
156 $this->name = false;
157 $this->group = 'deleted'; // needed for direct use of constructor
158 $this->key = '';
159 $this->size = 0;
160 $this->bits = 0;
161 $this->width = 0;
162 $this->height = 0;
163 $this->mime = "unknown/unknown";
164 $this->media_type = '';
165 $this->description = '';
166 $this->user = null;
167 $this->timestamp = null;
168 $this->deleted = 0;
169 $this->dataLoaded = false;
170 $this->exists = false;
171 $this->sha1 = '';
172
173 if ( $title instanceof Title ) {
174 $this->title = File::normalizeTitle( $title, 'exception' );
175 $this->name = $title->getDBkey();
176 }
177
178 if ( $id ) {
179 $this->id = $id;
180 }
181
182 if ( $key ) {
183 $this->key = $key;
184 }
185
186 if ( $sha1 ) {
187 $this->sha1 = $sha1;
188 }
189
190 if ( !$id && !$key && !( $title instanceof Title ) && !$sha1 ) {
191 throw new BadMethodCallException( "No specifications provided to ArchivedFile constructor." );
192 }
193
194 $this->repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
195 $this->metadataStorageHelper = new MetadataStorageHelper( $this->repo );
196 }
197
203 public function load() {
204 if ( $this->dataLoaded ) {
205 return true;
206 }
207 $conds = [];
208
209 if ( $this->id > 0 ) {
210 $conds['fa_id'] = $this->id;
211 }
212 if ( $this->key ) {
213 $conds['fa_storage_group'] = $this->group;
214 $conds['fa_storage_key'] = $this->key;
215 }
216 if ( $this->title ) {
217 $conds['fa_name'] = $this->title->getDBkey();
218 }
219 if ( $this->sha1 ) {
220 $conds['fa_sha1'] = $this->sha1;
221 }
222
223 if ( $conds === [] ) {
224 throw new RuntimeException( "No specific information for retrieving archived file" );
225 }
226
227 if ( !$this->title || $this->title->getNamespace() === NS_FILE ) {
228 $this->dataLoaded = true; // set it here, to have also true on miss
229 $dbr = $this->repo->getReplicaDB();
230 $queryBuilder = FileSelectQueryBuilder::newForArchivedFile( $dbr );
231 $row = $queryBuilder->where( $conds )
232 ->orderBy( 'fa_timestamp', SelectQueryBuilder::SORT_DESC )
233 ->caller( __METHOD__ )->fetchRow();
234 if ( !$row ) {
235 // this revision does not exist?
236 return null;
237 }
238
239 // initialize fields for filestore image object
240 $this->loadFromRow( $row );
241 } else {
242 throw new UnexpectedValueException( 'This title does not correspond to an image page.' );
243 }
244
245 return true;
246 }
247
255 public static function newFromRow( $row ) {
256 $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
257 $file->loadFromRow( $row );
258
259 return $file;
260 }
261
279 public static function getQueryInfo() {
280 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
282 return [
283 'tables' => $queryInfo['tables'],
284 'fields' => $queryInfo['fields'],
285 'joins' => $queryInfo['join_conds'],
286 ];
287 }
288
296 public function loadFromRow( $row ) {
297 $this->id = intval( $row->fa_id );
298 $this->name = $row->fa_name;
299 $this->archive_name = $row->fa_archive_name;
300 $this->group = $row->fa_storage_group;
301 $this->key = $row->fa_storage_key;
302 $this->size = $row->fa_size;
303 $this->bits = $row->fa_bits;
304 $this->width = $row->fa_width;
305 $this->height = $row->fa_height;
307 $this->repo->getReplicaDB(), $row->fa_metadata );
308 $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
309 $this->media_type = $row->fa_media_type;
310 $services = MediaWikiServices::getInstance();
311 $this->description = $services->getCommentStore()
312 // Legacy because $row may have come from self::selectFields()
313 ->getCommentLegacy( $this->repo->getReplicaDB(), 'fa_description', $row )->text;
314 $this->user = $services->getUserFactory()
315 ->newFromAnyId( $row->fa_user, $row->fa_user_text, $row->fa_actor );
316 $this->timestamp = $row->fa_timestamp;
317 $this->deleted = $row->fa_deleted;
318 if ( isset( $row->fa_sha1 ) ) {
319 $this->sha1 = $row->fa_sha1;
320 } else {
321 // old row, populate from key
322 $this->sha1 = LocalRepo::getHashFromKey( $this->key );
323 }
324 if ( !$this->title ) {
325 $this->title = Title::makeTitleSafe( NS_FILE, $row->fa_name );
326 }
327 $this->exists = $row->fa_archive_name !== '';
328 }
329
335 public function getTitle() {
336 if ( !$this->title ) {
337 $this->load();
338 }
339 return $this->title;
340 }
341
347 public function getName() {
348 if ( $this->name === false ) {
349 $this->load();
350 }
351
352 return $this->name;
353 }
354
358 public function getID() {
359 $this->load();
360
361 return $this->id;
362 }
363
367 public function exists() {
368 $this->load();
369
370 return $this->exists;
371 }
372
377 public function getKey() {
378 $this->load();
379
380 return $this->key;
381 }
382
387 public function getStorageKey() {
388 return $this->getKey();
389 }
390
395 public function getGroup() {
396 return $this->group;
397 }
398
403 public function getWidth() {
404 $this->load();
405
406 return $this->width;
407 }
408
413 public function getHeight() {
414 $this->load();
415
416 return $this->height;
417 }
418
425 public function getMetadata() {
426 $data = $this->getMetadataArray();
427 if ( !$data ) {
428 return '';
429 } elseif ( array_keys( $data ) === [ '_error' ] ) {
430 // Legacy error encoding
431 return $data['_error'];
432 } else {
433 return serialize( $this->getMetadataArray() );
434 }
435 }
436
443 public function getMetadataArray(): array {
444 $this->load();
445 if ( $this->unloadedMetadataBlobs ) {
446 return $this->getMetadataItems(
447 array_unique( array_merge(
448 array_keys( $this->metadataArray ),
449 array_keys( $this->unloadedMetadataBlobs )
450 ) )
451 );
452 }
454 }
455
456 public function getMetadataItems( array $itemNames ): array {
457 $this->load();
458 $result = [];
459 $addresses = [];
460 foreach ( $itemNames as $itemName ) {
461 if ( array_key_exists( $itemName, $this->metadataArray ) ) {
462 $result[$itemName] = $this->metadataArray[$itemName];
463 } elseif ( isset( $this->unloadedMetadataBlobs[$itemName] ) ) {
464 $addresses[$itemName] = $this->unloadedMetadataBlobs[$itemName];
465 }
466 }
467
468 if ( $addresses ) {
469 $resultFromBlob = $this->metadataStorageHelper->getMetadataFromBlobStore( $addresses );
470 foreach ( $addresses as $itemName => $address ) {
471 unset( $this->unloadedMetadataBlobs[$itemName] );
472 $value = $resultFromBlob[$itemName] ?? null;
473 if ( $value !== null ) {
474 $result[$itemName] = $value;
475 $this->metadataArray[$itemName] = $value;
476 }
477 }
478 }
479 return $result;
480 }
481
493 public function getMetadataForDb( IReadableDatabase $db ) {
494 $this->load();
495 if ( !$this->metadataArray && !$this->metadataBlobs ) {
496 $s = '';
497 } elseif ( $this->repo->isJsonMetadataEnabled() ) {
498 $s = $this->getJsonMetadata();
499 } else {
500 $s = serialize( $this->getMetadataArray() );
501 }
502 if ( !is_string( $s ) ) {
503 throw new RuntimeException( 'Could not serialize image metadata value for DB' );
504 }
505 return $db->encodeBlob( $s );
506 }
507
514 private function getJsonMetadata() {
515 // Directly store data that is not already in BlobStore
516 $envelope = [
517 'data' => array_diff_key( $this->metadataArray, $this->metadataBlobs )
518 ];
519
520 // Also store the blob addresses
521 if ( $this->metadataBlobs ) {
522 $envelope['blobs'] = $this->metadataBlobs;
523 }
524
525 [ $s, $blobAddresses ] = $this->metadataStorageHelper->getJsonMetadata( $this, $envelope );
526
527 // Repeated calls to this function should not keep inserting more blobs
528 $this->metadataBlobs += $blobAddresses;
529
530 return $s;
531 }
532
541 protected function loadMetadataFromDbFieldValue( IReadableDatabase $db, $metadataBlob ) {
542 $this->loadMetadataFromString( $db->decodeBlob( $metadataBlob ) );
543 }
544
552 protected function loadMetadataFromString( $metadataString ) {
553 $this->extraDataLoaded = true;
554 $this->metadataArray = [];
555 $this->metadataBlobs = [];
556 $this->unloadedMetadataBlobs = [];
557 $metadataString = (string)$metadataString;
558 if ( $metadataString === '' ) {
559 $this->metadataSerializationFormat = self::MDS_EMPTY;
560 return;
561 }
562 if ( $metadataString[0] === '{' ) {
563 $envelope = $this->metadataStorageHelper->jsonDecode( $metadataString );
564 if ( !$envelope ) {
565 // Legacy error encoding
566 $this->metadataArray = [ '_error' => $metadataString ];
567 $this->metadataSerializationFormat = self::MDS_LEGACY;
568 } else {
569 $this->metadataSerializationFormat = self::MDS_JSON;
570 if ( isset( $envelope['data'] ) ) {
571 $this->metadataArray = $envelope['data'];
572 }
573 if ( isset( $envelope['blobs'] ) ) {
574 $this->metadataBlobs = $this->unloadedMetadataBlobs = $envelope['blobs'];
575 }
576 }
577 } else {
578 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
579 $data = @unserialize( $metadataString );
580 if ( !is_array( $data ) ) {
581 // Legacy error encoding
582 $data = [ '_error' => $metadataString ];
583 $this->metadataSerializationFormat = self::MDS_LEGACY;
584 } else {
585 $this->metadataSerializationFormat = self::MDS_PHP;
586 }
587 $this->metadataArray = $data;
588 }
589 }
590
595 public function getSize() {
596 $this->load();
597
598 return $this->size;
599 }
600
604 public function getBits() {
605 $this->load();
606
607 return $this->bits;
608 }
609
614 public function getMimeType() {
615 $this->load();
616
617 return $this->mime;
618 }
619
624 private function getHandler() {
625 if ( !$this->handler ) {
626 $this->handler = MediaHandler::getHandler( $this->getMimeType() );
627 }
628
629 return $this->handler;
630 }
631
638 public function pageCount() {
639 if ( $this->pageCount === null ) {
640 // @FIXME: callers expect File objects
641 // @phan-suppress-next-line PhanTypeMismatchArgument
642 if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
643 // @phan-suppress-next-line PhanTypeMismatchArgument
644 $this->pageCount = $this->handler->pageCount( $this );
645 } else {
646 $this->pageCount = false;
647 }
648 }
649
650 return $this->pageCount;
651 }
652
658 public function getMediaType() {
659 $this->load();
660
661 return $this->media_type;
662 }
663
669 public function getTimestamp() {
670 $this->load();
671
672 return wfTimestamp( TS::MW, $this->timestamp );
673 }
674
681 public function getSha1() {
682 $this->load();
683
684 return $this->sha1;
685 }
686
698 public function getUploader( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ): ?UserIdentity {
699 $this->load();
700 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( File::DELETED_USER ) ) {
701 return null;
702 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( File::DELETED_USER, $performer ) ) {
703 return null;
704 } else {
705 return $this->user;
706 }
707 }
708
721 public function getDescription( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ): string {
722 $this->load();
723 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( File::DELETED_COMMENT ) ) {
724 return '';
725 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( File::DELETED_COMMENT, $performer ) ) {
726 return '';
727 } else {
728 return $this->description;
729 }
730 }
731
736 public function getVisibility() {
737 $this->load();
738
739 return $this->deleted;
740 }
741
748 public function isDeleted( $field ) {
749 $this->load();
750
751 return ( $this->deleted & $field ) == $field;
752 }
753
761 public function userCan( $field, Authority $performer ) {
762 $this->load();
763 $title = $this->getTitle();
764
765 return RevisionRecord::userCanBitfield(
766 $this->deleted,
767 $field,
768 $performer,
769 $title ?: null
770 );
771 }
772}
773
775class_alias( ArchivedFile::class, 'ArchivedFile' );
const NS_FILE
Definition Defines.php:57
wfTimestamp( $outputtype=TS::UNIX, $ts=0)
Get a timestamp string in one of various formats.
Base media handler class.
makeTitle( $linkId)
Convert a link ID to a Title.to override Title
Deleted file in the 'filearchive' table.
string[] $metadataBlobs
Map of metadata item name to blob address.
getSize()
Return the size of the image file, in bytes.
getMimeType()
Returns the MIME type of the file.
getTimestamp()
Return upload timestamp.
getWidth()
Return the width of the image.
getVisibility()
Returns the deletion bitfield.
load()
Loads a file object from the filearchive table.
static newFromRow( $row)
Loads a file object from the filearchive table.
getHeight()
Return the height of the image.
getDescription(int $audience=self::FOR_PUBLIC, ?Authority $performer=null)
Return upload description.
array $metadataArray
Unserialized metadata.
__construct( $title, $id=0, $key='', $sha1='')
getGroup()
Return the FileStore storage group.
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new archivedfile object.
loadMetadataFromString( $metadataString)
Unserialize a metadata string which came from some non-DB source, or is the return value of IReadable...
getSha1()
Get the SHA-1 base 36 hash of the file.
getStorageKey()
Return the FileStore key (overriding base File class)
userCan( $field, Authority $performer)
Determine if the current user is allowed to view a particular field of this FileStore image file,...
string null $metadataSerializationFormat
One of the MDS_* constants, giving the format of the metadata as stored in the DB,...
loadMetadataFromDbFieldValue(IReadableDatabase $db, $metadataBlob)
Unserialize a metadata blob which came from the database and store it in $this.
getUploader(int $audience=self::FOR_PUBLIC, ?Authority $performer=null)
getMetadataArray()
Get unserialized handler-specific metadata.
pageCount()
Returns the number of pages of a multipage document, or false for documents which aren't multipage do...
string[] $unloadedMetadataBlobs
Map of metadata item name to blob address for items that exist but have not yet been loaded into $thi...
isDeleted( $field)
for file or revision rows
bool $extraDataLoaded
Whether or not lazy-loaded data has been loaded from the database.
getMetadata()
Get handler-specific metadata as a serialized string.
getKey()
Return the FileStore key.
getMetadataForDb(IReadableDatabase $db)
Serialize the metadata array for insertion into img_metadata, oi_metadata or fa_metadata.
getTitle()
Return the associated title object.
getMediaType()
Return the type of the media in the file.
loadFromRow( $row)
Load ArchivedFile object fields from a DB row.
static newForArchivedFile(IReadableDatabase $db, array $options=[])
static normalizeTitle( $title, $exception=false)
Given a string or Title object return either a valid Title object with namespace NS_FILE or null.
Definition File.php:220
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:45
static getHashFromKey( $key)
Gets the SHA1 hash from a storage key.
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Page revision base class.
Represents a title within MediaWiki.
Definition Title.php:70
getDBkey()
Get the main part with underscores.
Definition Title.php:1029
Build SELECT queries with a fluent interface.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
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.