Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
5.22% covered (danger)
5.22%
12 / 230
5.88% covered (danger)
5.88%
2 / 34
CRAP
0.00% covered (danger)
0.00%
0 / 1
ArchivedFile
5.22% covered (danger)
5.22%
12 / 230
5.88% covered (danger)
5.88%
2 / 34
6682.04
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 31
0.00% covered (danger)
0.00%
0 / 1
90
 load
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
110
 newFromRow
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 loadFromRow
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
12
 getTitle
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getName
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getID
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 exists
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getKey
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getStorageKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getWidth
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getHeight
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMetadata
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getMetadataArray
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getMetadataItems
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 getMetadataForDb
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 getJsonMetadata
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 loadMetadataFromDbFieldValue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 loadMetadataFromString
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
56
 getSize
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getBits
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getMimeType
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getHandler
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 pageCount
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 getMediaType
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTimestamp
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getSha1
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getUploader
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 getDescription
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 getVisibility
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isDeleted
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 userCan
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
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
21use MediaWiki\FileRepo\File\FileSelectQueryBuilder;
22use MediaWiki\MediaWikiServices;
23use MediaWiki\Permissions\Authority;
24use MediaWiki\Revision\RevisionRecord;
25use MediaWiki\Title\Title;
26use MediaWiki\User\UserIdentity;
27use Wikimedia\Rdbms\Blob;
28use Wikimedia\Rdbms\IDatabase;
29use Wikimedia\Rdbms\IReadableDatabase;
30use Wikimedia\Rdbms\SelectQueryBuilder;
31
32/**
33 * Deleted file in the 'filearchive' table.
34 *
35 * @stable to extend
36 * @ingroup FileAbstraction
37 */
38class ArchivedFile {
39
40    // Audience options for ::getDescription() and ::getUploader()
41    public const FOR_PUBLIC = 1;
42    public const FOR_THIS_USER = 2;
43    public const RAW = 3;
44
45    /** @var string Metadata serialization: empty string. This is a compact non-legacy format. */
46    private const MDS_EMPTY = 'empty';
47
48    /** @var string Metadata serialization: some other string */
49    private const MDS_LEGACY = 'legacy';
50
51    /** @var string Metadata serialization: PHP serialize() */
52    private const MDS_PHP = 'php';
53
54    /** @var string Metadata serialization: JSON */
55    private const MDS_JSON = 'json';
56
57    /** @var int Filearchive row ID */
58    private $id;
59
60    /** @var string|false File name */
61    private $name;
62
63    /** @var string FileStore storage group */
64    private $group;
65
66    /** @var string FileStore SHA-1 key */
67    private $key;
68
69    /** @var int File size in bytes */
70    private $size;
71
72    /** @var int Size in bytes */
73    private $bits;
74
75    /** @var int Width */
76    private $width;
77
78    /** @var int Height */
79    private $height;
80
81    /** @var array Unserialized metadata */
82    protected $metadataArray = [];
83
84    /** @var bool Whether or not lazy-loaded data has been loaded from the database */
85    protected $extraDataLoaded = false;
86
87    /**
88     * One of the MDS_* constants, giving the format of the metadata as stored
89     * in the DB, or null if the data was not loaded from the DB.
90     *
91     * @var string|null
92     */
93    protected $metadataSerializationFormat;
94
95    /** @var string[] Map of metadata item name to blob address */
96    protected $metadataBlobs = [];
97
98    /**
99     * Map of metadata item name to blob address for items that exist but
100     * have not yet been loaded into $this->metadataArray
101     *
102     * @var string[]
103     */
104    protected $unloadedMetadataBlobs = [];
105
106    /** @var string MIME type */
107    private $mime;
108
109    /** @var string Media type */
110    private $media_type;
111
112    /** @var string Upload description */
113    private $description;
114
115    /** @var UserIdentity|null Uploader */
116    private $user;
117
118    /** @var string|null Time of upload */
119    private $timestamp;
120
121    /** @var bool Whether or not all this has been loaded from the database (loadFromXxx) */
122    private $dataLoaded;
123
124    /** @var int Bitfield akin to rev_deleted */
125    private $deleted;
126
127    /** @var string SHA-1 hash of file content */
128    private $sha1;
129
130    /** @var int|false Number of pages of a multipage document, or false for
131     * documents which aren't multipage documents
132     */
133    private $pageCount;
134
135    /** @var string Original base filename */
136    private $archive_name;
137
138    /** @var MediaHandler */
139    protected $handler;
140
141    /** @var Title|null */
142    protected $title; # image title
143
144    /** @var bool */
145    protected $exists;
146
147    /** @var LocalRepo */
148    private $repo;
149
150    /** @var MetadataStorageHelper */
151    private $metadataStorageHelper;
152
153    /**
154     * @stable to call
155     * @param Title|null $title
156     * @param int $id
157     * @param string $key
158     * @param string $sha1
159     */
160    public function __construct( $title, $id = 0, $key = '', $sha1 = '' ) {
161        $this->id = -1;
162        $this->title = null;
163        $this->name = false;
164        $this->group = 'deleted'; // needed for direct use of constructor
165        $this->key = '';
166        $this->size = 0;
167        $this->bits = 0;
168        $this->width = 0;
169        $this->height = 0;
170        $this->mime = "unknown/unknown";
171        $this->media_type = '';
172        $this->description = '';
173        $this->user = null;
174        $this->timestamp = null;
175        $this->deleted = 0;
176        $this->dataLoaded = false;
177        $this->exists = false;
178        $this->sha1 = '';
179
180        if ( $title instanceof Title ) {
181            $this->title = File::normalizeTitle( $title, 'exception' );
182            $this->name = $title->getDBkey();
183        }
184
185        if ( $id ) {
186            $this->id = $id;
187        }
188
189        if ( $key ) {
190            $this->key = $key;
191        }
192
193        if ( $sha1 ) {
194            $this->sha1 = $sha1;
195        }
196
197        if ( !$id && !$key && !( $title instanceof Title ) && !$sha1 ) {
198            throw new BadMethodCallException( "No specifications provided to ArchivedFile constructor." );
199        }
200
201        $this->repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
202        $this->metadataStorageHelper = new MetadataStorageHelper( $this->repo );
203    }
204
205    /**
206     * Loads a file object from the filearchive table
207     * @stable to override
208     * @return bool|null True on success or null
209     */
210    public function load() {
211        if ( $this->dataLoaded ) {
212            return true;
213        }
214        $conds = [];
215
216        if ( $this->id > 0 ) {
217            $conds['fa_id'] = $this->id;
218        }
219        if ( $this->key ) {
220            $conds['fa_storage_group'] = $this->group;
221            $conds['fa_storage_key'] = $this->key;
222        }
223        if ( $this->title ) {
224            $conds['fa_name'] = $this->title->getDBkey();
225        }
226        if ( $this->sha1 ) {
227            $conds['fa_sha1'] = $this->sha1;
228        }
229
230        if ( $conds === [] ) {
231            throw new RuntimeException( "No specific information for retrieving archived file" );
232        }
233
234        if ( !$this->title || $this->title->getNamespace() === NS_FILE ) {
235            $this->dataLoaded = true; // set it here, to have also true on miss
236            $dbr = $this->repo->getReplicaDB();
237            $queryBuilder = FileSelectQueryBuilder::newForArchivedFile( $dbr );
238            $row = $queryBuilder->where( $conds )
239                ->orderBy( 'fa_timestamp', SelectQueryBuilder::SORT_DESC )
240                ->caller( __METHOD__ )->fetchRow();
241            if ( !$row ) {
242                // this revision does not exist?
243                return null;
244            }
245
246            // initialize fields for filestore image object
247            $this->loadFromRow( $row );
248        } else {
249            throw new UnexpectedValueException( 'This title does not correspond to an image page.' );
250        }
251
252        return true;
253    }
254
255    /**
256     * Loads a file object from the filearchive table
257     * @stable to override
258     *
259     * @param stdClass $row
260     * @return ArchivedFile
261     */
262    public static function newFromRow( $row ) {
263        $file = new ArchivedFile( Title::makeTitle( NS_FILE, $row->fa_name ) );
264        $file->loadFromRow( $row );
265
266        return $file;
267    }
268
269    /**
270     * Return the tables, fields, and join conditions to be selected to create
271     * a new archivedfile object.
272     *
273     * Since 1.34, fa_user and fa_user_text have not been present in the
274     * database, but they continue to be available in query results as an
275     * alias.
276     *
277     * @since 1.31
278     * @stable to override
279     * @deprecated since 1.41 use FileSelectQueryBuilder instead
280     * @return array[] With three keys:
281     *   - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables`
282     *   - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields`
283     *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds`
284     * @phan-return array{tables:string[],fields:string[],joins:array}
285     */
286    public static function getQueryInfo() {
287        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
288        $queryInfo = ( FileSelectQueryBuilder::newForArchivedFile( $dbr ) )->getQueryInfo();
289        return [
290            'tables' => $queryInfo['tables'],
291            'fields' => $queryInfo['fields'],
292            'joins' => $queryInfo['join_conds'],
293        ];
294    }
295
296    /**
297     * Load ArchivedFile object fields from a DB row.
298     * @stable to override
299     *
300     * @param stdClass $row Object database row
301     * @since 1.21
302     */
303    public function loadFromRow( $row ) {
304        $this->id = intval( $row->fa_id );
305        $this->name = $row->fa_name;
306        $this->archive_name = $row->fa_archive_name;
307        $this->group = $row->fa_storage_group;
308        $this->key = $row->fa_storage_key;
309        $this->size = $row->fa_size;
310        $this->bits = $row->fa_bits;
311        $this->width = $row->fa_width;
312        $this->height = $row->fa_height;
313        $this->loadMetadataFromDbFieldValue(
314            $this->repo->getReplicaDB(), $row->fa_metadata );
315        $this->mime = "$row->fa_major_mime/$row->fa_minor_mime";
316        $this->media_type = $row->fa_media_type;
317        $services = MediaWikiServices::getInstance();
318        $this->description = $services->getCommentStore()
319            // Legacy because $row may have come from self::selectFields()
320            ->getCommentLegacy( $this->repo->getReplicaDB(), 'fa_description', $row )->text;
321        $this->user = $services->getUserFactory()
322            ->newFromAnyId( $row->fa_user, $row->fa_user_text, $row->fa_actor );
323        $this->timestamp = $row->fa_timestamp;
324        $this->deleted = $row->fa_deleted;
325        if ( isset( $row->fa_sha1 ) ) {
326            $this->sha1 = $row->fa_sha1;
327        } else {
328            // old row, populate from key
329            $this->sha1 = LocalRepo::getHashFromKey( $this->key );
330        }
331        if ( !$this->title ) {
332            $this->title = Title::makeTitleSafe( NS_FILE, $row->fa_name );
333        }
334        $this->exists = $row->fa_archive_name !== '';
335    }
336
337    /**
338     * Return the associated title object
339     *
340     * @return Title
341     */
342    public function getTitle() {
343        if ( !$this->title ) {
344            $this->load();
345        }
346        return $this->title;
347    }
348
349    /**
350     * Return the file name
351     *
352     * @return string
353     */
354    public function getName() {
355        if ( $this->name === false ) {
356            $this->load();
357        }
358
359        return $this->name;
360    }
361
362    /**
363     * @return int
364     */
365    public function getID() {
366        $this->load();
367
368        return $this->id;
369    }
370
371    /**
372     * @return bool
373     */
374    public function exists() {
375        $this->load();
376
377        return $this->exists;
378    }
379
380    /**
381     * Return the FileStore key
382     * @return string
383     */
384    public function getKey() {
385        $this->load();
386
387        return $this->key;
388    }
389
390    /**
391     * Return the FileStore key (overriding base File class)
392     * @return string
393     */
394    public function getStorageKey() {
395        return $this->getKey();
396    }
397
398    /**
399     * Return the FileStore storage group
400     * @return string
401     */
402    public function getGroup() {
403        return $this->group;
404    }
405
406    /**
407     * Return the width of the image
408     * @return int
409     */
410    public function getWidth() {
411        $this->load();
412
413        return $this->width;
414    }
415
416    /**
417     * Return the height of the image
418     * @return int
419     */
420    public function getHeight() {
421        $this->load();
422
423        return $this->height;
424    }
425
426    /**
427     * Get handler-specific metadata as a serialized string
428     *
429     * @deprecated since 1.37 use getMetadataArray() or getMetadataItem()
430     * @return string
431     */
432    public function getMetadata() {
433        $data = $this->getMetadataArray();
434        if ( !$data ) {
435            return '';
436        } elseif ( array_keys( $data ) === [ '_error' ] ) {
437            // Legacy error encoding
438            return $data['_error'];
439        } else {
440            return serialize( $this->getMetadataArray() );
441        }
442    }
443
444    /**
445     * Get unserialized handler-specific metadata
446     *
447     * @since 1.39
448     * @return array
449     */
450    public function getMetadataArray(): array {
451        $this->load();
452        if ( $this->unloadedMetadataBlobs ) {
453            return $this->getMetadataItems(
454                array_unique( array_merge(
455                    array_keys( $this->metadataArray ),
456                    array_keys( $this->unloadedMetadataBlobs )
457                ) )
458            );
459        }
460        return $this->metadataArray;
461    }
462
463    public function getMetadataItems( array $itemNames ): array {
464        $this->load();
465        $result = [];
466        $addresses = [];
467        foreach ( $itemNames as $itemName ) {
468            if ( array_key_exists( $itemName, $this->metadataArray ) ) {
469                $result[$itemName] = $this->metadataArray[$itemName];
470            } elseif ( isset( $this->unloadedMetadataBlobs[$itemName] ) ) {
471                $addresses[$itemName] = $this->unloadedMetadataBlobs[$itemName];
472            }
473        }
474
475        if ( $addresses ) {
476            $resultFromBlob = $this->metadataStorageHelper->getMetadataFromBlobStore( $addresses );
477            foreach ( $addresses as $itemName => $address ) {
478                unset( $this->unloadedMetadataBlobs[$itemName] );
479                $value = $resultFromBlob[$itemName] ?? null;
480                if ( $value !== null ) {
481                    $result[$itemName] = $value;
482                    $this->metadataArray[$itemName] = $value;
483                }
484            }
485        }
486        return $result;
487    }
488
489    /**
490     * Serialize the metadata array for insertion into img_metadata, oi_metadata
491     * or fa_metadata.
492     *
493     * If metadata splitting is enabled, this may write blobs to the database,
494     * returning their addresses.
495     *
496     * @internal
497     * @param IDatabase $db
498     * @return string|Blob
499     */
500    public function getMetadataForDb( IDatabase $db ) {
501        $this->load();
502        if ( !$this->metadataArray && !$this->metadataBlobs ) {
503            $s = '';
504        } elseif ( $this->repo->isJsonMetadataEnabled() ) {
505            $s = $this->getJsonMetadata();
506        } else {
507            $s = serialize( $this->getMetadataArray() );
508        }
509        if ( !is_string( $s ) ) {
510            throw new RuntimeException( 'Could not serialize image metadata value for DB' );
511        }
512        return $db->encodeBlob( $s );
513    }
514
515    /**
516     * Get metadata in JSON format ready for DB insertion, optionally splitting
517     * items out to BlobStore.
518     *
519     * @return string
520     */
521    private function getJsonMetadata() {
522        // Directly store data that is not already in BlobStore
523        $envelope = [
524            'data' => array_diff_key( $this->metadataArray, $this->metadataBlobs )
525        ];
526
527        // Also store the blob addresses
528        if ( $this->metadataBlobs ) {
529            $envelope['blobs'] = $this->metadataBlobs;
530        }
531
532        [ $s, $blobAddresses ] = $this->metadataStorageHelper->getJsonMetadata( $this, $envelope );
533
534        // Repeated calls to this function should not keep inserting more blobs
535        $this->metadataBlobs += $blobAddresses;
536
537        return $s;
538    }
539
540    /**
541     * Unserialize a metadata blob which came from the database and store it
542     * in $this.
543     *
544     * @since 1.39
545     * @param IReadableDatabase $db
546     * @param string|Blob $metadataBlob
547     */
548    protected function loadMetadataFromDbFieldValue( IReadableDatabase $db, $metadataBlob ) {
549        $this->loadMetadataFromString( $db->decodeBlob( $metadataBlob ) );
550    }
551
552    /**
553     * Unserialize a metadata string which came from some non-DB source, or is
554     * the return value of IDatabase::decodeBlob().
555     *
556     * @since 1.37
557     * @param string $metadataString
558     */
559    protected function loadMetadataFromString( $metadataString ) {
560        $this->extraDataLoaded = true;
561        $this->metadataArray = [];
562        $this->metadataBlobs = [];
563        $this->unloadedMetadataBlobs = [];
564        $metadataString = (string)$metadataString;
565        if ( $metadataString === '' ) {
566            $this->metadataSerializationFormat = self::MDS_EMPTY;
567            return;
568        }
569        if ( $metadataString[0] === '{' ) {
570            $envelope = $this->metadataStorageHelper->jsonDecode( $metadataString );
571            if ( !$envelope ) {
572                // Legacy error encoding
573                $this->metadataArray = [ '_error' => $metadataString ];
574                $this->metadataSerializationFormat = self::MDS_LEGACY;
575            } else {
576                $this->metadataSerializationFormat = self::MDS_JSON;
577                if ( isset( $envelope['data'] ) ) {
578                    $this->metadataArray = $envelope['data'];
579                }
580                if ( isset( $envelope['blobs'] ) ) {
581                    $this->metadataBlobs = $this->unloadedMetadataBlobs = $envelope['blobs'];
582                }
583            }
584        } else {
585            // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
586            $data = @unserialize( $metadataString );
587            if ( !is_array( $data ) ) {
588                // Legacy error encoding
589                $data = [ '_error' => $metadataString ];
590                $this->metadataSerializationFormat = self::MDS_LEGACY;
591            } else {
592                $this->metadataSerializationFormat = self::MDS_PHP;
593            }
594            $this->metadataArray = $data;
595        }
596    }
597
598    /**
599     * Return the size of the image file, in bytes
600     * @return int
601     */
602    public function getSize() {
603        $this->load();
604
605        return $this->size;
606    }
607
608    /**
609     * Return the bits of the image file, in bytes
610     * @return int
611     */
612    public function getBits() {
613        $this->load();
614
615        return $this->bits;
616    }
617
618    /**
619     * Returns the MIME type of the file.
620     * @return string
621     */
622    public function getMimeType() {
623        $this->load();
624
625        return $this->mime;
626    }
627
628    /**
629     * Get a MediaHandler instance for this file
630     * @return MediaHandler
631     */
632    private function getHandler() {
633        if ( !isset( $this->handler ) ) {
634            $this->handler = MediaHandler::getHandler( $this->getMimeType() );
635        }
636
637        return $this->handler;
638    }
639
640    /**
641     * Returns the number of pages of a multipage document, or false for
642     * documents which aren't multipage documents
643     * @stable to override
644     * @return int|false
645     */
646    public function pageCount() {
647        if ( !isset( $this->pageCount ) ) {
648            // @FIXME: callers expect File objects
649            // @phan-suppress-next-line PhanTypeMismatchArgument
650            if ( $this->getHandler() && $this->handler->isMultiPage( $this ) ) {
651                // @phan-suppress-next-line PhanTypeMismatchArgument
652                $this->pageCount = $this->handler->pageCount( $this );
653            } else {
654                $this->pageCount = false;
655            }
656        }
657
658        return $this->pageCount;
659    }
660
661    /**
662     * Return the type of the media in the file.
663     * Use the value returned by this function with the MEDIATYPE_xxx constants.
664     * @return string
665     */
666    public function getMediaType() {
667        $this->load();
668
669        return $this->media_type;
670    }
671
672    /**
673     * Return upload timestamp.
674     *
675     * @return string
676     */
677    public function getTimestamp() {
678        $this->load();
679
680        return wfTimestamp( TS_MW, $this->timestamp );
681    }
682
683    /**
684     * Get the SHA-1 base 36 hash of the file
685     *
686     * @return string
687     * @since 1.21
688     */
689    public function getSha1() {
690        $this->load();
691
692        return $this->sha1;
693    }
694
695    /**
696     * @since 1.37
697     * @stable to override
698     * @param int $audience One of:
699     *   File::FOR_PUBLIC       to be displayed to all users
700     *   File::FOR_THIS_USER    to be displayed to the given user
701     *   File::RAW              get the description regardless of permissions
702     * @param Authority|null $performer to check for, only if FOR_THIS_USER is
703     *   passed to the $audience parameter
704     * @return UserIdentity|null
705     */
706    public function getUploader( int $audience = self::FOR_PUBLIC, Authority $performer = null ): ?UserIdentity {
707        $this->load();
708        if ( $audience === self::FOR_PUBLIC && $this->isDeleted( File::DELETED_USER ) ) {
709            return null;
710        } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( File::DELETED_USER, $performer ) ) {
711            return null;
712        } else {
713            return $this->user;
714        }
715    }
716
717    /**
718     * Return upload description.
719     *
720     * @since 1.37 the method takes $audience and $performer parameters.
721     * @param int $audience One of:
722     *   File::FOR_PUBLIC       to be displayed to all users
723     *   File::FOR_THIS_USER    to be displayed to the given user
724     *   File::RAW              get the description regardless of permissions
725     * @param Authority|null $performer to check for, only if FOR_THIS_USER is
726     *   passed to the $audience parameter
727     * @return string
728     */
729    public function getDescription( int $audience = self::FOR_PUBLIC, Authority $performer = null ): string {
730        $this->load();
731        if ( $audience === self::FOR_PUBLIC && $this->isDeleted( File::DELETED_COMMENT ) ) {
732            return '';
733        } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( File::DELETED_COMMENT, $performer ) ) {
734            return '';
735        } else {
736            return $this->description;
737        }
738    }
739
740    /**
741     * Returns the deletion bitfield
742     * @return int
743     */
744    public function getVisibility() {
745        $this->load();
746
747        return $this->deleted;
748    }
749
750    /**
751     * for file or revision rows
752     *
753     * @param int $field One of DELETED_* bitfield constants
754     * @return bool
755     */
756    public function isDeleted( $field ) {
757        $this->load();
758
759        return ( $this->deleted & $field ) == $field;
760    }
761
762    /**
763     * Determine if the current user is allowed to view a particular
764     * field of this FileStore image file, if it's marked as deleted.
765     * @param int $field
766     * @param Authority $performer
767     * @return bool
768     */
769    public function userCan( $field, Authority $performer ) {
770        $this->load();
771        $title = $this->getTitle();
772
773        return RevisionRecord::userCanBitfield(
774            $this->deleted,
775            $field,
776            $performer,
777            $title ?: null
778        );
779    }
780}