Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
5.19% covered (danger)
5.19%
12 / 231
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
21namespace MediaWiki\FileRepo\File;
22
23use BadMethodCallException;
24use MediaHandler;
25use MediaWiki\FileRepo\LocalRepo;
26use MediaWiki\MediaWikiServices;
27use MediaWiki\Permissions\Authority;
28use MediaWiki\Revision\RevisionRecord;
29use MediaWiki\Title\Title;
30use MediaWiki\User\UserIdentity;
31use RuntimeException;
32use stdClass;
33use UnexpectedValueException;
34use Wikimedia\Rdbms\Blob;
35use Wikimedia\Rdbms\IReadableDatabase;
36use Wikimedia\Rdbms\SelectQueryBuilder;
37
38/**
39 * Deleted file in the 'filearchive' table.
40 *
41 * @stable to extend
42 * @ingroup FileAbstraction
43 */
44class ArchivedFile {
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
51    /** @var string Metadata serialization: empty string. This is a compact non-legacy format. */
52    private const MDS_EMPTY = 'empty';
53
54    /** @var string Metadata serialization: some other string */
55    private const MDS_LEGACY = 'legacy';
56
57    /** @var string Metadata serialization: PHP serialize() */
58    private const MDS_PHP = 'php';
59
60    /** @var string Metadata serialization: JSON */
61    private const MDS_JSON = 'json';
62
63    /** @var int Filearchive row ID */
64    private $id;
65
66    /** @var string|false File name */
67    private $name;
68
69    /** @var string FileStore storage group */
70    private $group;
71
72    /** @var string FileStore SHA-1 key */
73    private $key;
74
75    /** @var int File size in bytes */
76    private $size;
77
78    /** @var int Bitdepth */
79    private $bits;
80
81    /** @var int */
82    private $width;
83
84    /** @var int */
85    private $height;
86
87    /** @var array Unserialized metadata */
88    protected $metadataArray = [];
89
90    /** @var bool Whether or not lazy-loaded data has been loaded from the database */
91    protected $extraDataLoaded = false;
92
93    /**
94     * One of the MDS_* constants, giving the format of the metadata as stored
95     * in the DB, or null if the data was not loaded from the DB.
96     *
97     * @var string|null
98     */
99    protected $metadataSerializationFormat;
100
101    /** @var string[] Map of metadata item name to blob address */
102    protected $metadataBlobs = [];
103
104    /**
105     * Map of metadata item name to blob address for items that exist but
106     * have not yet been loaded into $this->metadataArray
107     *
108     * @var string[]
109     */
110    protected $unloadedMetadataBlobs = [];
111
112    /** @var string MIME type */
113    private $mime;
114
115    /** @var string Media type */
116    private $media_type;
117
118    /** @var string Upload description */
119    private $description;
120
121    /** @var UserIdentity|null Uploader */
122    private $user;
123
124    /** @var string|null Time of upload */
125    private $timestamp;
126
127    /** @var bool Whether or not all this has been loaded from the database (loadFromXxx) */
128    private $dataLoaded;
129
130    /** @var int Bitfield akin to rev_deleted */
131    private $deleted;
132
133    /** @var string SHA-1 hash of file content */
134    private $sha1;
135
136    /** @var int|false Number of pages of a multipage document, or false for
137     * documents which aren't multipage documents
138     */
139    private $pageCount;
140
141    /** @var string Original base filename */
142    private $archive_name;
143
144    /** @var MediaHandler */
145    protected $handler;
146
147    /** @var Title|null */
148    protected $title; # image title
149
150    /** @var bool */
151    protected $exists;
152
153    /** @var LocalRepo */
154    private $repo;
155
156    /** @var MetadataStorageHelper */
157    private $metadataStorageHelper;
158
159    /**
160     * @stable to call
161     * @param Title|null $title
162     * @param int $id
163     * @param string $key
164     * @param string $sha1
165     */
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
211    /**
212     * Loads a file object from the filearchive table
213     * @stable to override
214     * @return bool|null True on success or null
215     */
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
261    /**
262     * Loads a file object from the filearchive table
263     * @stable to override
264     *
265     * @param stdClass $row
266     * @return ArchivedFile
267     */
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
275    /**
276     * Return the tables, fields, and join conditions to be selected to create
277     * a new archivedfile object.
278     *
279     * Since 1.34, fa_user and fa_user_text have not been present in the
280     * database, but they continue to be available in query results as an
281     * alias.
282     *
283     * @since 1.31
284     * @stable to override
285     * @deprecated since 1.41 use FileSelectQueryBuilder instead
286     * @return array[] With three keys:
287     *   - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables`
288     *   - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields`
289     *   - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds`
290     * @phan-return array{tables:string[],fields:string[],joins:array}
291     */
292    public static function getQueryInfo() {
293        $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
294        $queryInfo = ( FileSelectQueryBuilder::newForArchivedFile( $dbr ) )->getQueryInfo();
295        return [
296            'tables' => $queryInfo['tables'],
297            'fields' => $queryInfo['fields'],
298            'joins' => $queryInfo['join_conds'],
299        ];
300    }
301
302    /**
303     * Load ArchivedFile object fields from a DB row.
304     * @stable to override
305     *
306     * @param stdClass $row Object database row
307     * @since 1.21
308     */
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;
319        $this->loadMetadataFromDbFieldValue(
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
343    /**
344     * Return the associated title object
345     *
346     * @return Title
347     */
348    public function getTitle() {
349        if ( !$this->title ) {
350            $this->load();
351        }
352        return $this->title;
353    }
354
355    /**
356     * Return the file name
357     *
358     * @return string
359     */
360    public function getName() {
361        if ( $this->name === false ) {
362            $this->load();
363        }
364
365        return $this->name;
366    }
367
368    /**
369     * @return int
370     */
371    public function getID() {
372        $this->load();
373
374        return $this->id;
375    }
376
377    /**
378     * @return bool
379     */
380    public function exists() {
381        $this->load();
382
383        return $this->exists;
384    }
385
386    /**
387     * Return the FileStore key
388     * @return string
389     */
390    public function getKey() {
391        $this->load();
392
393        return $this->key;
394    }
395
396    /**
397     * Return the FileStore key (overriding base File class)
398     * @return string
399     */
400    public function getStorageKey() {
401        return $this->getKey();
402    }
403
404    /**
405     * Return the FileStore storage group
406     * @return string
407     */
408    public function getGroup() {
409        return $this->group;
410    }
411
412    /**
413     * Return the width of the image
414     * @return int
415     */
416    public function getWidth() {
417        $this->load();
418
419        return $this->width;
420    }
421
422    /**
423     * Return the height of the image
424     * @return int
425     */
426    public function getHeight() {
427        $this->load();
428
429        return $this->height;
430    }
431
432    /**
433     * Get handler-specific metadata as a serialized string
434     *
435     * @deprecated since 1.37 use getMetadataArray() or getMetadataItem()
436     * @return string
437     */
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
450    /**
451     * Get unserialized handler-specific metadata
452     *
453     * @since 1.39
454     * @return array
455     */
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        }
466        return $this->metadataArray;
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
495    /**
496     * Serialize the metadata array for insertion into img_metadata, oi_metadata
497     * or fa_metadata.
498     *
499     * If metadata splitting is enabled, this may write blobs to the database,
500     * returning their addresses.
501     *
502     * @internal
503     * @param IReadableDatabase $db
504     * @return string|Blob
505     */
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
521    /**
522     * Get metadata in JSON format ready for DB insertion, optionally splitting
523     * items out to BlobStore.
524     *
525     * @return string
526     */
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
546    /**
547     * Unserialize a metadata blob which came from the database and store it
548     * in $this.
549     *
550     * @since 1.39
551     * @param IReadableDatabase $db
552     * @param string|Blob $metadataBlob
553     */
554    protected function loadMetadataFromDbFieldValue( IReadableDatabase $db, $metadataBlob ) {
555        $this->loadMetadataFromString( $db->decodeBlob( $metadataBlob ) );
556    }
557
558    /**
559     * Unserialize a metadata string which came from some non-DB source, or is
560     * the return value of IReadableDatabase::decodeBlob().
561     *
562     * @since 1.37
563     * @param string $metadataString
564     */
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
604    /**
605     * Return the size of the image file, in bytes
606     * @return int
607     */
608    public function getSize() {
609        $this->load();
610
611        return $this->size;
612    }
613
614    /**
615     * @return int
616     */
617    public function getBits() {
618        $this->load();
619
620        return $this->bits;
621    }
622
623    /**
624     * Returns the MIME type of the file.
625     * @return string
626     */
627    public function getMimeType() {
628        $this->load();
629
630        return $this->mime;
631    }
632
633    /**
634     * Get a MediaHandler instance for this file
635     * @return MediaHandler
636     */
637    private function getHandler() {
638        if ( !$this->handler ) {
639            $this->handler = MediaHandler::getHandler( $this->getMimeType() );
640        }
641
642        return $this->handler;
643    }
644
645    /**
646     * Returns the number of pages of a multipage document, or false for
647     * documents which aren't multipage documents
648     * @stable to override
649     * @return int|false
650     */
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
666    /**
667     * Return the type of the media in the file.
668     * Use the value returned by this function with the MEDIATYPE_xxx constants.
669     * @return string
670     */
671    public function getMediaType() {
672        $this->load();
673
674        return $this->media_type;
675    }
676
677    /**
678     * Return upload timestamp.
679     *
680     * @return string
681     */
682    public function getTimestamp() {
683        $this->load();
684
685        return wfTimestamp( TS_MW, $this->timestamp );
686    }
687
688    /**
689     * Get the SHA-1 base 36 hash of the file
690     *
691     * @return string
692     * @since 1.21
693     */
694    public function getSha1() {
695        $this->load();
696
697        return $this->sha1;
698    }
699
700    /**
701     * @since 1.37
702     * @stable to override
703     * @param int $audience One of:
704     *   File::FOR_PUBLIC       to be displayed to all users
705     *   File::FOR_THIS_USER    to be displayed to the given user
706     *   File::RAW              get the description regardless of permissions
707     * @param Authority|null $performer to check for, only if FOR_THIS_USER is
708     *   passed to the $audience parameter
709     * @return UserIdentity|null
710     */
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
722    /**
723     * Return upload description.
724     *
725     * @since 1.37 the method takes $audience and $performer parameters.
726     * @param int $audience One of:
727     *   File::FOR_PUBLIC       to be displayed to all users
728     *   File::FOR_THIS_USER    to be displayed to the given user
729     *   File::RAW              get the description regardless of permissions
730     * @param Authority|null $performer to check for, only if FOR_THIS_USER is
731     *   passed to the $audience parameter
732     * @return string
733     */
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
745    /**
746     * Returns the deletion bitfield
747     * @return int
748     */
749    public function getVisibility() {
750        $this->load();
751
752        return $this->deleted;
753    }
754
755    /**
756     * for file or revision rows
757     *
758     * @param int $field One of DELETED_* bitfield constants
759     * @return bool
760     */
761    public function isDeleted( $field ) {
762        $this->load();
763
764        return ( $this->deleted & $field ) == $field;
765    }
766
767    /**
768     * Determine if the current user is allowed to view a particular
769     * field of this FileStore image file, if it's marked as deleted.
770     * @param int $field
771     * @param Authority $performer
772     * @return bool
773     */
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
787/** @deprecated class alias since 1.44 */
788class_alias( ArchivedFile::class, 'ArchivedFile' );