MediaWiki master
ArchivedFile.php
Go to the documentation of this file.
1<?php
22
23use BadMethodCallException;
24use MediaHandler;
31use RuntimeException;
32use stdClass;
33use UnexpectedValueException;
37
45
46 // Audience options for ::getDescription() and ::getUploader()
47 public const FOR_PUBLIC = 1;
48 public const FOR_THIS_USER = 2;
49 public const RAW = 3;
50
52 private const MDS_EMPTY = 'empty';
53
55 private const MDS_LEGACY = 'legacy';
56
58 private const MDS_PHP = 'php';
59
61 private const MDS_JSON = 'json';
62
64 private $id;
65
67 private $name;
68
70 private $group;
71
73 private $key;
74
76 private $size;
77
79 private $bits;
80
82 private $width;
83
85 private $height;
86
88 protected $metadataArray = [];
89
91 protected $extraDataLoaded = false;
92
100
102 protected $metadataBlobs = [];
103
111
113 private $mime;
114
116 private $media_type;
117
119 private $description;
120
122 private $user;
123
125 private $timestamp;
126
128 private $dataLoaded;
129
131 private $deleted;
132
134 private $sha1;
135
139 private $pageCount;
140
142 private $archive_name;
143
145 protected $handler;
146
148 protected $title; # image title
149
151 protected $exists;
152
154 private $repo;
155
157 private $metadataStorageHelper;
158
166 public function __construct( $title, $id = 0, $key = '', $sha1 = '' ) {
167 $this->id = -1;
168 $this->title = null;
169 $this->name = false;
170 $this->group = 'deleted'; // needed for direct use of constructor
171 $this->key = '';
172 $this->size = 0;
173 $this->bits = 0;
174 $this->width = 0;
175 $this->height = 0;
176 $this->mime = "unknown/unknown";
177 $this->media_type = '';
178 $this->description = '';
179 $this->user = null;
180 $this->timestamp = null;
181 $this->deleted = 0;
182 $this->dataLoaded = false;
183 $this->exists = false;
184 $this->sha1 = '';
185
186 if ( $title instanceof Title ) {
187 $this->title = File::normalizeTitle( $title, 'exception' );
188 $this->name = $title->getDBkey();
189 }
190
191 if ( $id ) {
192 $this->id = $id;
193 }
194
195 if ( $key ) {
196 $this->key = $key;
197 }
198
199 if ( $sha1 ) {
200 $this->sha1 = $sha1;
201 }
202
203 if ( !$id && !$key && !( $title instanceof Title ) && !$sha1 ) {
204 throw new BadMethodCallException( "No specifications provided to ArchivedFile constructor." );
205 }
206
207 $this->repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
208 $this->metadataStorageHelper = new MetadataStorageHelper( $this->repo );
209 }
210
216 public function load() {
217 if ( $this->dataLoaded ) {
218 return true;
219 }
220 $conds = [];
221
222 if ( $this->id > 0 ) {
223 $conds['fa_id'] = $this->id;
224 }
225 if ( $this->key ) {
226 $conds['fa_storage_group'] = $this->group;
227 $conds['fa_storage_key'] = $this->key;
228 }
229 if ( $this->title ) {
230 $conds['fa_name'] = $this->title->getDBkey();
231 }
232 if ( $this->sha1 ) {
233 $conds['fa_sha1'] = $this->sha1;
234 }
235
236 if ( $conds === [] ) {
237 throw new RuntimeException( "No specific information for retrieving archived file" );
238 }
239
240 if ( !$this->title || $this->title->getNamespace() === NS_FILE ) {
241 $this->dataLoaded = true; // set it here, to have also true on miss
242 $dbr = $this->repo->getReplicaDB();
243 $queryBuilder = FileSelectQueryBuilder::newForArchivedFile( $dbr );
244 $row = $queryBuilder->where( $conds )
245 ->orderBy( 'fa_timestamp', SelectQueryBuilder::SORT_DESC )
246 ->caller( __METHOD__ )->fetchRow();
247 if ( !$row ) {
248 // this revision does not exist?
249 return null;
250 }
251
252 // initialize fields for filestore image object
253 $this->loadFromRow( $row );
254 } else {
255 throw new UnexpectedValueException( 'This title does not correspond to an image page.' );
256 }
257
258 return true;
259 }
260
268 public static function newFromRow( $row ) {
269 $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
270 $file->loadFromRow( $row );
271
272 return $file;
273 }
274
292 public static function getQueryInfo() {
293 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
295 return [
296 'tables' => $queryInfo['tables'],
297 'fields' => $queryInfo['fields'],
298 'joins' => $queryInfo['join_conds'],
299 ];
300 }
301
309 public function loadFromRow( $row ) {
310 $this->id = intval( $row->fa_id );
311 $this->name = $row->fa_name;
312 $this->archive_name = $row->fa_archive_name;
313 $this->group = $row->fa_storage_group;
314 $this->key = $row->fa_storage_key;
315 $this->size = $row->fa_size;
316 $this->bits = $row->fa_bits;
317 $this->width = $row->fa_width;
318 $this->height = $row->fa_height;
320 $this->repo->getReplicaDB(), $row->fa_metadata );
321 $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
322 $this->media_type = $row->fa_media_type;
323 $services = MediaWikiServices::getInstance();
324 $this->description = $services->getCommentStore()
325 // Legacy because $row may have come from self::selectFields()
326 ->getCommentLegacy( $this->repo->getReplicaDB(), 'fa_description', $row )->text;
327 $this->user = $services->getUserFactory()
328 ->newFromAnyId( $row->fa_user, $row->fa_user_text, $row->fa_actor );
329 $this->timestamp = $row->fa_timestamp;
330 $this->deleted = $row->fa_deleted;
331 if ( isset( $row->fa_sha1 ) ) {
332 $this->sha1 = $row->fa_sha1;
333 } else {
334 // old row, populate from key
335 $this->sha1 = LocalRepo::getHashFromKey( $this->key );
336 }
337 if ( !$this->title ) {
338 $this->title = Title::makeTitleSafe( NS_FILE, $row->fa_name );
339 }
340 $this->exists = $row->fa_archive_name !== '';
341 }
342
348 public function getTitle() {
349 if ( !$this->title ) {
350 $this->load();
351 }
352 return $this->title;
353 }
354
360 public function getName() {
361 if ( $this->name === false ) {
362 $this->load();
363 }
364
365 return $this->name;
366 }
367
371 public function getID() {
372 $this->load();
373
374 return $this->id;
375 }
376
380 public function exists() {
381 $this->load();
382
383 return $this->exists;
384 }
385
390 public function getKey() {
391 $this->load();
392
393 return $this->key;
394 }
395
400 public function getStorageKey() {
401 return $this->getKey();
402 }
403
408 public function getGroup() {
409 return $this->group;
410 }
411
416 public function getWidth() {
417 $this->load();
418
419 return $this->width;
420 }
421
426 public function getHeight() {
427 $this->load();
428
429 return $this->height;
430 }
431
438 public function getMetadata() {
439 $data = $this->getMetadataArray();
440 if ( !$data ) {
441 return '';
442 } elseif ( array_keys( $data ) === [ '_error' ] ) {
443 // Legacy error encoding
444 return $data['_error'];
445 } else {
446 return serialize( $this->getMetadataArray() );
447 }
448 }
449
456 public function getMetadataArray(): array {
457 $this->load();
458 if ( $this->unloadedMetadataBlobs ) {
459 return $this->getMetadataItems(
460 array_unique( array_merge(
461 array_keys( $this->metadataArray ),
462 array_keys( $this->unloadedMetadataBlobs )
463 ) )
464 );
465 }
467 }
468
469 public function getMetadataItems( array $itemNames ): array {
470 $this->load();
471 $result = [];
472 $addresses = [];
473 foreach ( $itemNames as $itemName ) {
474 if ( array_key_exists( $itemName, $this->metadataArray ) ) {
475 $result[$itemName] = $this->metadataArray[$itemName];
476 } elseif ( isset( $this->unloadedMetadataBlobs[$itemName] ) ) {
477 $addresses[$itemName] = $this->unloadedMetadataBlobs[$itemName];
478 }
479 }
480
481 if ( $addresses ) {
482 $resultFromBlob = $this->metadataStorageHelper->getMetadataFromBlobStore( $addresses );
483 foreach ( $addresses as $itemName => $address ) {
484 unset( $this->unloadedMetadataBlobs[$itemName] );
485 $value = $resultFromBlob[$itemName] ?? null;
486 if ( $value !== null ) {
487 $result[$itemName] = $value;
488 $this->metadataArray[$itemName] = $value;
489 }
490 }
491 }
492 return $result;
493 }
494
506 public function getMetadataForDb( IReadableDatabase $db ) {
507 $this->load();
508 if ( !$this->metadataArray && !$this->metadataBlobs ) {
509 $s = '';
510 } elseif ( $this->repo->isJsonMetadataEnabled() ) {
511 $s = $this->getJsonMetadata();
512 } else {
513 $s = serialize( $this->getMetadataArray() );
514 }
515 if ( !is_string( $s ) ) {
516 throw new RuntimeException( 'Could not serialize image metadata value for DB' );
517 }
518 return $db->encodeBlob( $s );
519 }
520
527 private function getJsonMetadata() {
528 // Directly store data that is not already in BlobStore
529 $envelope = [
530 'data' => array_diff_key( $this->metadataArray, $this->metadataBlobs )
531 ];
532
533 // Also store the blob addresses
534 if ( $this->metadataBlobs ) {
535 $envelope['blobs'] = $this->metadataBlobs;
536 }
537
538 [ $s, $blobAddresses ] = $this->metadataStorageHelper->getJsonMetadata( $this, $envelope );
539
540 // Repeated calls to this function should not keep inserting more blobs
541 $this->metadataBlobs += $blobAddresses;
542
543 return $s;
544 }
545
554 protected function loadMetadataFromDbFieldValue( IReadableDatabase $db, $metadataBlob ) {
555 $this->loadMetadataFromString( $db->decodeBlob( $metadataBlob ) );
556 }
557
565 protected function loadMetadataFromString( $metadataString ) {
566 $this->extraDataLoaded = true;
567 $this->metadataArray = [];
568 $this->metadataBlobs = [];
569 $this->unloadedMetadataBlobs = [];
570 $metadataString = (string)$metadataString;
571 if ( $metadataString === '' ) {
572 $this->metadataSerializationFormat = self::MDS_EMPTY;
573 return;
574 }
575 if ( $metadataString[0] === '{' ) {
576 $envelope = $this->metadataStorageHelper->jsonDecode( $metadataString );
577 if ( !$envelope ) {
578 // Legacy error encoding
579 $this->metadataArray = [ '_error' => $metadataString ];
580 $this->metadataSerializationFormat = self::MDS_LEGACY;
581 } else {
582 $this->metadataSerializationFormat = self::MDS_JSON;
583 if ( isset( $envelope['data'] ) ) {
584 $this->metadataArray = $envelope['data'];
585 }
586 if ( isset( $envelope['blobs'] ) ) {
587 $this->metadataBlobs = $this->unloadedMetadataBlobs = $envelope['blobs'];
588 }
589 }
590 } else {
591 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
592 $data = @unserialize( $metadataString );
593 if ( !is_array( $data ) ) {
594 // Legacy error encoding
595 $data = [ '_error' => $metadataString ];
596 $this->metadataSerializationFormat = self::MDS_LEGACY;
597 } else {
598 $this->metadataSerializationFormat = self::MDS_PHP;
599 }
600 $this->metadataArray = $data;
601 }
602 }
603
608 public function getSize() {
609 $this->load();
610
611 return $this->size;
612 }
613
617 public function getBits() {
618 $this->load();
619
620 return $this->bits;
621 }
622
627 public function getMimeType() {
628 $this->load();
629
630 return $this->mime;
631 }
632
637 private function getHandler() {
638 if ( !$this->handler ) {
639 $this->handler = MediaHandler::getHandler( $this->getMimeType() );
640 }
641
642 return $this->handler;
643 }
644
651 public function pageCount() {
652 if ( $this->pageCount === null ) {
653 // @FIXME: callers expect File objects
654 // @phan-suppress-next-line PhanTypeMismatchArgument
655 if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
656 // @phan-suppress-next-line PhanTypeMismatchArgument
657 $this->pageCount = $this->handler->pageCount( $this );
658 } else {
659 $this->pageCount = false;
660 }
661 }
662
663 return $this->pageCount;
664 }
665
671 public function getMediaType() {
672 $this->load();
673
674 return $this->media_type;
675 }
676
682 public function getTimestamp() {
683 $this->load();
684
685 return wfTimestamp( TS_MW, $this->timestamp );
686 }
687
694 public function getSha1() {
695 $this->load();
696
697 return $this->sha1;
698 }
699
711 public function getUploader( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ): ?UserIdentity {
712 $this->load();
713 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( File::DELETED_USER ) ) {
714 return null;
715 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( File::DELETED_USER, $performer ) ) {
716 return null;
717 } else {
718 return $this->user;
719 }
720 }
721
734 public function getDescription( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ): string {
735 $this->load();
736 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( File::DELETED_COMMENT ) ) {
737 return '';
738 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( File::DELETED_COMMENT, $performer ) ) {
739 return '';
740 } else {
741 return $this->description;
742 }
743 }
744
749 public function getVisibility() {
750 $this->load();
751
752 return $this->deleted;
753 }
754
761 public function isDeleted( $field ) {
762 $this->load();
763
764 return ( $this->deleted & $field ) == $field;
765 }
766
774 public function userCan( $field, Authority $performer ) {
775 $this->load();
776 $title = $this->getTitle();
777
778 return RevisionRecord::userCanBitfield(
779 $this->deleted,
780 $field,
781 $performer,
782 $title ?: null
783 );
784 }
785}
786
788class_alias( ArchivedFile::class, 'ArchivedFile' );
const NS_FILE
Definition Defines.php:71
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
Base media handler class.
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:234
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:57
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:78
getDBkey()
Get the main part with underscores.
Definition Title.php:1031
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.