MediaWiki 1.44.0
LocalFile.php
Go to the documentation of this file.
1<?php
22
23use InvalidArgumentException;
24use LockManager;
25use MediaHandler;
53use MWFileProps;
54use RuntimeException;
55use stdClass;
56use UnexpectedValueException;
66
93class LocalFile extends File {
94 private const VERSION = 13; // cache version
95
96 private const CACHE_FIELD_MAX_LEN = 1000;
97
99 private const MDS_EMPTY = 'empty';
100
102 private const MDS_LEGACY = 'legacy';
103
105 private const MDS_PHP = 'php';
106
108 private const MDS_JSON = 'json';
109
111 private const MAX_PAGE_RENDER_JOBS = 50;
112
114 protected $fileExists;
115
117 private $fileId;
118
120 private $fileTypeId;
121
123 protected $width;
124
126 protected $height;
127
129 protected $bits;
130
132 protected $media_type;
133
135 protected $mime;
136
138 protected $size;
139
141 protected $metadataArray = [];
142
150
152 protected $metadataBlobs = [];
153
161
163 protected $sha1;
164
166 protected $dataLoaded = false;
167
169 protected $extraDataLoaded = false;
170
172 protected $deleted;
173
175 protected $file_id;
176
179
181 protected $repoClass = LocalRepo::class;
182
184 private $historyLine = 0;
185
187 private $historyRes = null;
188
190 private $major_mime;
191
193 private $minor_mime;
194
196 private $timestamp;
197
199 private $user;
200
202 private $description;
203
205 private $descriptionTouched;
206
208 private $upgraded;
209
211 private $upgrading;
212
214 private $locked;
215
217 private $lockedOwnTrx;
218
220 private $missing;
221
223 private $metadataStorageHelper;
224
226 private $migrationStage = SCHEMA_COMPAT_OLD;
227
228 // @note: higher than IDBAccessObject constants
229 private const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
230
231 private const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
232
247 public static function newFromTitle( $title, $repo, $unused = null ) {
248 return new static( $title, $repo );
249 }
250
262 public static function newFromRow( $row, $repo ) {
263 $title = Title::makeTitle( NS_FILE, $row->img_name );
264 $file = new static( $title, $repo );
265 $file->loadFromRow( $row );
266
267 return $file;
268 }
269
281 public static function newFromKey( $sha1, $repo, $timestamp = false ) {
282 $dbr = $repo->getReplicaDB();
283 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr );
284
285 $queryBuilder->where( [ 'img_sha1' => $sha1 ] );
286
287 if ( $timestamp ) {
288 $queryBuilder->andWhere( [ 'img_timestamp' => $dbr->timestamp( $timestamp ) ] );
289 }
290
291 $row = $queryBuilder->caller( __METHOD__ )->fetchRow();
292 if ( $row ) {
293 return static::newFromRow( $row, $repo );
294 } else {
295 return false;
296 }
297 }
298
319 public static function getQueryInfo( array $options = [] ) {
320 wfDeprecated( __METHOD__, '1.41' );
321 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
322 $queryInfo = FileSelectQueryBuilder::newForFile( $dbr, $options )->getQueryInfo();
323 // needs remapping...
324 return [
325 'tables' => $queryInfo['tables'],
326 'fields' => $queryInfo['fields'],
327 'joins' => $queryInfo['join_conds'],
328 ];
329 }
330
338 public function __construct( $title, $repo ) {
339 parent::__construct( $title, $repo );
340 $this->metadataStorageHelper = new MetadataStorageHelper( $repo );
341 $this->migrationStage = MediaWikiServices::getInstance()->getMainConfig()->get(
343 );
344
345 $this->assertRepoDefined();
346 $this->assertTitleDefined();
347 }
348
352 public function getRepo() {
353 return $this->repo;
354 }
355
362 protected function getCacheKey() {
363 return $this->repo->getSharedCacheKey( 'file', sha1( $this->getName() ) );
364 }
365
369 private function loadFromCache() {
370 $this->dataLoaded = false;
371 $this->extraDataLoaded = false;
372
373 $key = $this->getCacheKey();
374 if ( !$key ) {
375 $this->loadFromDB( IDBAccessObject::READ_NORMAL );
376
377 return;
378 }
379
380 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
381 $cachedValues = $cache->getWithSetCallback(
382 $key,
383 $cache::TTL_WEEK,
384 function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
385 $setOpts += Database::getCacheSetOptions( $this->repo->getReplicaDB() );
386
387 $this->loadFromDB( IDBAccessObject::READ_NORMAL );
388
389 $fields = $this->getCacheFields( '' );
390 $cacheVal = [];
391 $cacheVal['fileExists'] = $this->fileExists;
392 if ( $this->fileExists ) {
393 foreach ( $fields as $field ) {
394 $cacheVal[$field] = $this->$field;
395 }
396 }
397 if ( $this->user ) {
398 $cacheVal['user'] = $this->user->getId();
399 $cacheVal['user_text'] = $this->user->getName();
400 }
401
402 // Don't cache metadata items stored as blobs, since they tend to be large
403 if ( $this->metadataBlobs ) {
404 $cacheVal['metadata'] = array_diff_key(
405 $this->metadataArray, $this->metadataBlobs );
406 // Save the blob addresses
407 $cacheVal['metadataBlobs'] = $this->metadataBlobs;
408 } else {
409 $cacheVal['metadata'] = $this->metadataArray;
410 }
411
412 // Strip off excessive entries from the subset of fields that can become large.
413 // If the cache value gets too large and might not fit in the cache,
414 // causing repeat database queries for each access to the file.
415 foreach ( $this->getLazyCacheFields( '' ) as $field ) {
416 if ( isset( $cacheVal[$field] )
417 && strlen( serialize( $cacheVal[$field] ) ) > 100 * 1024
418 ) {
419 unset( $cacheVal[$field] ); // don't let the value get too big
420 if ( $field === 'metadata' ) {
421 unset( $cacheVal['metadataBlobs'] );
422 }
423 }
424 }
425
426 if ( $this->fileExists ) {
427 $ttl = $cache->adaptiveTTL( (int)wfTimestamp( TS_UNIX, $this->timestamp ), $ttl );
428 } else {
429 $ttl = $cache::TTL_DAY;
430 }
431
432 return $cacheVal;
433 },
434 [ 'version' => self::VERSION ]
435 );
436
437 $this->fileExists = $cachedValues['fileExists'];
438 if ( $this->fileExists ) {
439 $this->setProps( $cachedValues );
440 }
441
442 $this->dataLoaded = true;
443 $this->extraDataLoaded = true;
444 foreach ( $this->getLazyCacheFields( '' ) as $field ) {
445 $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
446 }
447 }
448
452 public function invalidateCache() {
453 $key = $this->getCacheKey();
454 if ( !$key ) {
455 return;
456 }
457
458 $this->repo->getPrimaryDB()->onTransactionPreCommitOrIdle(
459 static function () use ( $key ) {
460 MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key );
461 },
462 __METHOD__
463 );
464 }
465
473 public function loadFromFile( $path = null ) {
474 $props = $this->repo->getFileProps( $path ?? $this->getVirtualUrl() );
475 $this->setProps( $props );
476 }
477
485 protected function getCacheFields( $prefix = 'img_' ) {
486 if ( $prefix !== '' ) {
487 throw new InvalidArgumentException(
488 __METHOD__ . ' with a non-empty prefix is no longer supported.'
489 );
490 }
491
492 // See self::getQueryInfo() for the fetching of the data from the DB,
493 // self::loadFromRow() for the loading of the object from the DB row,
494 // and self::loadFromCache() for the caching, and self::setProps() for
495 // populating the object from an array of data.
496 return [ 'size', 'width', 'height', 'bits', 'media_type',
497 'major_mime', 'minor_mime', 'timestamp', 'sha1', 'description' ];
498 }
499
507 protected function getLazyCacheFields( $prefix = 'img_' ) {
508 if ( $prefix !== '' ) {
509 throw new InvalidArgumentException(
510 __METHOD__ . ' with a non-empty prefix is no longer supported.'
511 );
512 }
513
514 // Keep this in sync with the omit-lazy option in self::getQueryInfo().
515 return [ 'metadata' ];
516 }
517
523 protected function loadFromDB( $flags = 0 ) {
524 $fname = static::class . '::' . __FUNCTION__;
525
526 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
527 $this->dataLoaded = true;
528 $this->extraDataLoaded = true;
529
530 $dbr = ( $flags & IDBAccessObject::READ_LATEST )
531 ? $this->repo->getPrimaryDB()
532 : $this->repo->getReplicaDB();
533 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr );
534
535 $queryBuilder->where( [ 'img_name' => $this->getName() ] );
536 $row = $queryBuilder->caller( $fname )->fetchRow();
537
538 if ( $row ) {
539 $this->loadFromRow( $row );
540 } else {
541 $this->fileExists = false;
542 }
543 }
544
550 protected function loadExtraFromDB() {
551 if ( !$this->title ) {
552 return; // Avoid hard failure when the file does not exist. T221812
553 }
554
555 $fname = static::class . '::' . __FUNCTION__;
556
557 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
558 $this->extraDataLoaded = true;
559
560 $db = $this->repo->getReplicaDB();
561 $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
562 if ( !$fieldMap ) {
563 $db = $this->repo->getPrimaryDB();
564 $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
565 }
566
567 if ( $fieldMap ) {
568 if ( isset( $fieldMap['metadata'] ) ) {
569 $this->loadMetadataFromDbFieldValue( $db, $fieldMap['metadata'] );
570 }
571 } else {
572 throw new RuntimeException( "Could not find data for image '{$this->getName()}'." );
573 }
574 }
575
581 private function loadExtraFieldsWithTimestamp( IReadableDatabase $dbr, $fname ) {
582 $fieldMap = false;
583
584 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr, [ 'omit-nonlazy' ] );
585 $queryBuilder->where( [ 'img_name' => $this->getName() ] )
586 ->andWhere( [ 'img_timestamp' => $dbr->timestamp( $this->getTimestamp() ) ] );
587 $row = $queryBuilder->caller( $fname )->fetchRow();
588 if ( $row ) {
589 $fieldMap = $this->unprefixRow( $row, 'img_' );
590 } else {
591 # File may have been uploaded over in the meantime; check the old versions
592 $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr, [ 'omit-nonlazy' ] );
593 $row = $queryBuilder->where( [ 'oi_name' => $this->getName() ] )
594 ->andWhere( [ 'oi_timestamp' => $dbr->timestamp( $this->getTimestamp() ) ] )
595 ->caller( __METHOD__ )->fetchRow();
596 if ( $row ) {
597 $fieldMap = $this->unprefixRow( $row, 'oi_' );
598 }
599 }
600
601 return $fieldMap;
602 }
603
609 protected function unprefixRow( $row, $prefix = 'img_' ) {
610 $array = (array)$row;
611 $prefixLength = strlen( $prefix );
612
613 // Double check prefix once
614 if ( substr( array_key_first( $array ), 0, $prefixLength ) !== $prefix ) {
615 throw new InvalidArgumentException( __METHOD__ . ': incorrect $prefix parameter' );
616 }
617
618 $decoded = [];
619 foreach ( $array as $name => $value ) {
620 $decoded[substr( $name, $prefixLength )] = $value;
621 }
622
623 return $decoded;
624 }
625
641 public function loadFromRow( $row, $prefix = 'img_' ) {
642 $this->dataLoaded = true;
643
644 $unprefixed = $this->unprefixRow( $row, $prefix );
645
646 $this->name = $unprefixed['name'];
647 $this->media_type = $unprefixed['media_type'];
648
649 $services = MediaWikiServices::getInstance();
650 $this->description = $services->getCommentStore()
651 ->getComment( "{$prefix}description", $row )->text;
652
653 $this->user = $services->getUserFactory()->newFromAnyId(
654 $unprefixed['user'] ?? null,
655 $unprefixed['user_text'] ?? null,
656 $unprefixed['actor'] ?? null
657 );
658
659 $this->timestamp = wfTimestamp( TS_MW, $unprefixed['timestamp'] );
660
662 $this->repo->getReplicaDB(), $unprefixed['metadata'] );
663
664 if ( empty( $unprefixed['major_mime'] ) ) {
665 $this->major_mime = 'unknown';
666 $this->minor_mime = 'unknown';
667 $this->mime = 'unknown/unknown';
668 } else {
669 if ( !$unprefixed['minor_mime'] ) {
670 $unprefixed['minor_mime'] = 'unknown';
671 }
672 $this->major_mime = $unprefixed['major_mime'];
673 $this->minor_mime = $unprefixed['minor_mime'];
674 $this->mime = $unprefixed['major_mime'] . '/' . $unprefixed['minor_mime'];
675 }
676
677 // Trim zero padding from char/binary field
678 $this->sha1 = rtrim( $unprefixed['sha1'], "\0" );
679
680 // Normalize some fields to integer type, per their database definition.
681 // Use unary + so that overflows will be upgraded to double instead of
682 // being truncated as with intval(). This is important to allow > 2 GiB
683 // files on 32-bit systems.
684 $this->size = +$unprefixed['size'];
685 $this->width = +$unprefixed['width'];
686 $this->height = +$unprefixed['height'];
687 $this->bits = +$unprefixed['bits'];
688
689 // Check for extra fields (deprecated since MW 1.37)
690 $extraFields = array_diff(
691 array_keys( $unprefixed ),
692 [
693 'name', 'media_type', 'description_text', 'description_data',
694 'description_cid', 'user', 'user_text', 'actor', 'timestamp',
695 'metadata', 'major_mime', 'minor_mime', 'sha1', 'size', 'width',
696 'height', 'bits', 'file_id', 'filerevision_id'
697 ]
698 );
699 if ( $extraFields ) {
701 'Passing extra fields (' .
702 implode( ', ', $extraFields )
703 . ') to ' . __METHOD__ . ' was deprecated in MediaWiki 1.37. ' .
704 'Property assignment will be removed in a later version.',
705 '1.37' );
706 foreach ( $extraFields as $field ) {
707 $this->$field = $unprefixed[$field];
708 }
709 }
710
711 $this->fileExists = true;
712 }
713
719 public function load( $flags = 0 ) {
720 if ( !$this->dataLoaded ) {
721 if ( $flags & IDBAccessObject::READ_LATEST ) {
722 $this->loadFromDB( $flags );
723 } else {
724 $this->loadFromCache();
725 }
726 }
727
728 if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
729 // @note: loads on name/timestamp to reduce race condition problems
730 $this->loadExtraFromDB();
731 }
732 }
733
738 public function maybeUpgradeRow() {
739 if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() || $this->upgrading ) {
740 return;
741 }
742
743 $upgrade = false;
744 $reserialize = false;
745 if ( $this->media_type === null || $this->mime == 'image/svg' ) {
746 $upgrade = true;
747 } else {
748 $handler = $this->getHandler();
749 if ( $handler ) {
750 $validity = $handler->isFileMetadataValid( $this );
751 if ( $validity === MediaHandler::METADATA_BAD ) {
752 $upgrade = true;
753 } elseif ( $validity === MediaHandler::METADATA_COMPATIBLE
754 && $this->repo->isMetadataUpdateEnabled()
755 ) {
756 $upgrade = true;
757 } elseif ( $this->repo->isJsonMetadataEnabled()
758 && $this->repo->isMetadataReserializeEnabled()
759 ) {
760 if ( $this->repo->isSplitMetadataEnabled() && $this->isMetadataOversize() ) {
761 $reserialize = true;
762 } elseif ( $this->metadataSerializationFormat !== self::MDS_EMPTY &&
763 $this->metadataSerializationFormat !== self::MDS_JSON ) {
764 $reserialize = true;
765 }
766 }
767 }
768 }
769
770 if ( $upgrade || $reserialize ) {
771 $this->upgrading = true;
772 // Defer updates unless in auto-commit CLI mode
773 DeferredUpdates::addCallableUpdate( function () use ( $upgrade ) {
774 $this->upgrading = false; // avoid duplicate updates
775 try {
776 if ( $upgrade ) {
777 $this->upgradeRow();
778 } else {
779 $this->reserializeMetadata();
780 }
781 } catch ( LocalFileLockError $e ) {
782 // let the other process handle it (or do it next time)
783 }
784 } );
785 }
786 }
787
791 public function getUpgraded() {
792 return $this->upgraded;
793 }
794
801 public function getFileIdFromName() {
802 if ( !$this->fileId ) {
803 $dbw = $this->repo->getPrimaryDB();
804 $id = $dbw->newSelectQueryBuilder()
805 ->select( 'file_id' )
806 ->from( 'file' )
807 ->where( [
808 'file_name' => $this->getName(),
809 'file_deleted' => 0
810 ] )
811 ->caller( __METHOD__ )
812 ->fetchField();
813 $this->fileId = $id;
814 }
815
816 return $this->fileId;
817 }
818
825 public function acquireFileIdFromName() {
826 $dbw = $this->repo->getPrimaryDB();
827 $id = $this->getFileIdFromName();
828 if ( $id ) {
829 return $id;
830 }
831 $id = $dbw->newSelectQueryBuilder()
832 ->select( 'file_id' )
833 ->from( 'file' )
834 ->where( [
835 'file_name' => $this->getName(),
836 ] )
837 ->caller( __METHOD__ )
838 ->fetchField();
839 if ( !$id ) {
840 $dbw->newInsertQueryBuilder()
841 ->insertInto( 'file' )
842 ->row( [
843 'file_name' => $this->getName(),
844 // The value will be updated later
845 'file_latest' => 0,
846 'file_deleted' => 0,
847 'file_type' => $this->getFileTypeId(),
848 ] )
849 ->caller( __METHOD__ )->execute();
850 $insertId = $dbw->insertId();
851 if ( !$insertId ) {
852 throw new RuntimeException( 'File entry could not be inserted' );
853 }
854 return $insertId;
855 } else {
856 // Undelete
857 $dbw->newUpdateQueryBuilder()
858 ->update( 'file' )
859 ->set( [ 'file_deleted' => 0 ] )
860 ->where( [ 'file_id' => $id ] )
861 ->caller( __METHOD__ )->execute();
862 return $id;
863 }
864 }
865
866 protected function getFileTypeId() {
867 if ( $this->fileTypeId ) {
868 return $this->fileTypeId;
869 }
870 [ $major, $minor ] = self::splitMime( $this->mime );
871 $dbw = $this->repo->getPrimaryDB();
872 $id = $dbw->newSelectQueryBuilder()
873 ->select( 'ft_id' )
874 ->from( 'filetypes' )
875 ->where( [
876 'ft_media_type' => $this->getMediaType(),
877 'ft_major_mime' => $major,
878 'ft_minor_mime' => $minor,
879 ] )
880 ->caller( __METHOD__ )
881 ->fetchField();
882 if ( $id ) {
883 $this->fileTypeId = $id;
884 return $id;
885 }
886 $dbw->newInsertQueryBuilder()
887 ->insertInto( 'filetypes' )
888 ->row( [
889 'ft_media_type' => $this->getMediaType(),
890 'ft_major_mime' => $major,
891 'ft_minor_mime' => $minor,
892 ] )
893 ->caller( __METHOD__ )->execute();
894
895 $id = $dbw->insertId();
896 if ( !$id ) {
897 throw new RuntimeException( 'File entry could not be inserted' );
898 }
899
900 $this->fileTypeId = $id;
901 return $id;
902 }
903
908 public function upgradeRow() {
909 $dbw = $this->repo->getPrimaryDB();
910
911 // Make a DB query condition that will fail to match the image row if the
912 // image was reuploaded while the upgrade was in process.
913 $freshnessCondition = [ 'img_timestamp' => $dbw->timestamp( $this->getTimestamp() ) ];
914
915 $this->loadFromFile();
916
917 # Don't destroy file info of missing files
918 if ( !$this->fileExists ) {
919 wfDebug( __METHOD__ . ": file does not exist, aborting" );
920
921 return;
922 }
923
924 [ $major, $minor ] = self::splitMime( $this->mime );
925
926 wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema" );
927
928 $metadata = $this->getMetadataForDb( $dbw );
929 $dbw->newUpdateQueryBuilder()
930 ->update( 'image' )
931 ->set( [
932 'img_size' => $this->size,
933 'img_width' => $this->width,
934 'img_height' => $this->height,
935 'img_bits' => $this->bits,
936 'img_media_type' => $this->media_type,
937 'img_major_mime' => $major,
938 'img_minor_mime' => $minor,
939 'img_metadata' => $metadata,
940 'img_sha1' => $this->sha1,
941 ] )
942 ->where( [ 'img_name' => $this->getName() ] )
943 ->andWhere( $freshnessCondition )
944 ->caller( __METHOD__ )->execute();
945
946 if ( $this->migrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
947 $dbw->newUpdateQueryBuilder()
948 ->update( 'filerevision' )
949 ->set( [
950 'fr_size' => $this->size,
951 'fr_width' => $this->width,
952 'fr_height' => $this->height,
953 'fr_bits' => $this->bits,
954 'fr_metadata' => $metadata,
955 'fr_sha1' => $this->sha1,
956 ] )
957 ->where( [ 'fr_file' => $this->acquireFileIdFromName() ] )
958 ->andWhere( [ 'fr_timestamp' => $dbw->timestamp( $this->getTimestamp() ) ] )
959 ->caller( __METHOD__ )->execute();
960 }
961
962 $this->invalidateCache();
963
964 $this->upgraded = true; // avoid rework/retries
965 }
966
971 protected function reserializeMetadata() {
972 if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
973 return;
974 }
975 $dbw = $this->repo->getPrimaryDB();
976 $metadata = $this->getMetadataForDb( $dbw );
977 $dbw->newUpdateQueryBuilder()
978 ->update( 'image' )
979 ->set( [ 'img_metadata' => $metadata ] )
980 ->where( [
981 'img_name' => $this->name,
982 'img_timestamp' => $dbw->timestamp( $this->timestamp ),
983 ] )
984 ->caller( __METHOD__ )->execute();
985 if ( $this->migrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
986 $dbw->newUpdateQueryBuilder()
987 ->update( 'filerevision' )
988 ->set( [ 'fr_metadata' => $metadata ] )
989 ->where( [ 'fr_file' => $this->acquireFileIdFromName() ] )
990 ->andWhere( [ 'fr_timestamp' => $dbw->timestamp( $this->getTimestamp() ) ] )
991 ->caller( __METHOD__ )->execute();
992 }
993 $this->upgraded = true;
994 }
995
1008 public function setProps( $info ) {
1009 $this->dataLoaded = true;
1010 $fields = $this->getCacheFields( '' );
1011 $fields[] = 'fileExists';
1012
1013 foreach ( $fields as $field ) {
1014 if ( isset( $info[$field] ) ) {
1015 $this->$field = $info[$field];
1016 }
1017 }
1018
1019 // Only our own cache sets these properties, so they both should be present.
1020 if ( isset( $info['user'] ) &&
1021 isset( $info['user_text'] ) &&
1022 $info['user_text'] !== ''
1023 ) {
1024 $this->user = new UserIdentityValue( $info['user'], $info['user_text'] );
1025 }
1026
1027 // Fix up mime fields
1028 if ( isset( $info['major_mime'] ) ) {
1029 $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
1030 } elseif ( isset( $info['mime'] ) ) {
1031 $this->mime = $info['mime'];
1032 [ $this->major_mime, $this->minor_mime ] = self::splitMime( $this->mime );
1033 }
1034
1035 if ( isset( $info['metadata'] ) ) {
1036 if ( is_string( $info['metadata'] ) ) {
1037 $this->loadMetadataFromString( $info['metadata'] );
1038 } elseif ( is_array( $info['metadata'] ) ) {
1039 $this->metadataArray = $info['metadata'];
1040 if ( isset( $info['metadataBlobs'] ) ) {
1041 $this->metadataBlobs = $info['metadataBlobs'];
1042 $this->unloadedMetadataBlobs = array_diff_key(
1043 $this->metadataBlobs,
1044 $this->metadataArray
1045 );
1046 } else {
1047 $this->metadataBlobs = [];
1048 $this->unloadedMetadataBlobs = [];
1049 }
1050 } else {
1051 $logger = LoggerFactory::getInstance( 'LocalFile' );
1052 $logger->warning( __METHOD__ . ' given invalid metadata of type ' .
1053 get_debug_type( $info['metadata'] ) );
1054 $this->metadataArray = [];
1055 }
1056 $this->extraDataLoaded = true;
1057 }
1058 }
1059
1075 public function isMissing() {
1076 if ( $this->missing === null ) {
1077 $fileExists = $this->repo->fileExists( $this->getVirtualUrl() );
1078 $this->missing = !$fileExists;
1079 }
1080
1081 return $this->missing;
1082 }
1083
1091 public function getWidth( $page = 1 ) {
1092 $page = (int)$page;
1093 if ( $page < 1 ) {
1094 $page = 1;
1095 }
1096
1097 $this->load();
1098
1099 if ( $this->isMultipage() ) {
1100 $handler = $this->getHandler();
1101 if ( !$handler ) {
1102 return 0;
1103 }
1104 $dim = $handler->getPageDimensions( $this, $page );
1105 if ( $dim ) {
1106 return $dim['width'];
1107 } else {
1108 // For non-paged media, the false goes through an
1109 // intval, turning failure into 0, so do same here.
1110 return 0;
1111 }
1112 } else {
1113 return $this->width;
1114 }
1115 }
1116
1124 public function getHeight( $page = 1 ) {
1125 $page = (int)$page;
1126 if ( $page < 1 ) {
1127 $page = 1;
1128 }
1129
1130 $this->load();
1131
1132 if ( $this->isMultipage() ) {
1133 $handler = $this->getHandler();
1134 if ( !$handler ) {
1135 return 0;
1136 }
1137 $dim = $handler->getPageDimensions( $this, $page );
1138 if ( $dim ) {
1139 return $dim['height'];
1140 } else {
1141 // For non-paged media, the false goes through an
1142 // intval, turning failure into 0, so do same here.
1143 return 0;
1144 }
1145 } else {
1146 return $this->height;
1147 }
1148 }
1149
1157 public function getDescriptionShortUrl() {
1158 if ( !$this->title ) {
1159 return null; // Avoid hard failure when the file does not exist. T221812
1160 }
1161
1162 $pageId = $this->title->getArticleID();
1163
1164 if ( $pageId ) {
1165 $url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
1166 if ( $url !== false ) {
1167 return $url;
1168 }
1169 }
1170 return null;
1171 }
1172
1179 public function getMetadata() {
1180 $data = $this->getMetadataArray();
1181 if ( !$data ) {
1182 return '';
1183 } elseif ( array_keys( $data ) === [ '_error' ] ) {
1184 // Legacy error encoding
1185 return $data['_error'];
1186 } else {
1187 return serialize( $this->getMetadataArray() );
1188 }
1189 }
1190
1197 public function getMetadataArray(): array {
1198 $this->load( self::LOAD_ALL );
1199 if ( $this->unloadedMetadataBlobs ) {
1200 return $this->getMetadataItems(
1201 array_unique( array_merge(
1202 array_keys( $this->metadataArray ),
1203 array_keys( $this->unloadedMetadataBlobs )
1204 ) )
1205 );
1206 }
1207 return $this->metadataArray;
1208 }
1209
1210 public function getMetadataItems( array $itemNames ): array {
1211 $this->load( self::LOAD_ALL );
1212 $result = [];
1213 $addresses = [];
1214 foreach ( $itemNames as $itemName ) {
1215 if ( array_key_exists( $itemName, $this->metadataArray ) ) {
1216 $result[$itemName] = $this->metadataArray[$itemName];
1217 } elseif ( isset( $this->unloadedMetadataBlobs[$itemName] ) ) {
1218 $addresses[$itemName] = $this->unloadedMetadataBlobs[$itemName];
1219 }
1220 }
1221
1222 if ( $addresses ) {
1223 $resultFromBlob = $this->metadataStorageHelper->getMetadataFromBlobStore( $addresses );
1224 foreach ( $addresses as $itemName => $address ) {
1225 unset( $this->unloadedMetadataBlobs[$itemName] );
1226 $value = $resultFromBlob[$itemName] ?? null;
1227 if ( $value !== null ) {
1228 $result[$itemName] = $value;
1229 $this->metadataArray[$itemName] = $value;
1230 }
1231 }
1232 }
1233 return $result;
1234 }
1235
1247 public function getMetadataForDb( IReadableDatabase $db ) {
1248 $this->load( self::LOAD_ALL );
1249 if ( !$this->metadataArray && !$this->metadataBlobs ) {
1250 $s = '';
1251 } elseif ( $this->repo->isJsonMetadataEnabled() ) {
1252 $s = $this->getJsonMetadata();
1253 } else {
1254 $s = serialize( $this->getMetadataArray() );
1255 }
1256 if ( !is_string( $s ) ) {
1257 throw new RuntimeException( 'Could not serialize image metadata value for DB' );
1258 }
1259 return $db->encodeBlob( $s );
1260 }
1261
1268 private function getJsonMetadata() {
1269 // Directly store data that is not already in BlobStore
1270 $envelope = [
1271 'data' => array_diff_key( $this->metadataArray, $this->metadataBlobs )
1272 ];
1273
1274 // Also store the blob addresses
1275 if ( $this->metadataBlobs ) {
1276 $envelope['blobs'] = $this->metadataBlobs;
1277 }
1278
1279 [ $s, $blobAddresses ] = $this->metadataStorageHelper->getJsonMetadata( $this, $envelope );
1280
1281 // Repeated calls to this function should not keep inserting more blobs
1282 $this->metadataBlobs += $blobAddresses;
1283
1284 return $s;
1285 }
1286
1293 private function isMetadataOversize() {
1294 if ( !$this->repo->isSplitMetadataEnabled() ) {
1295 return false;
1296 }
1297 $threshold = $this->repo->getSplitMetadataThreshold();
1298 $directItems = array_diff_key( $this->metadataArray, $this->metadataBlobs );
1299 foreach ( $directItems as $value ) {
1300 if ( strlen( $this->metadataStorageHelper->jsonEncode( $value ) ) > $threshold ) {
1301 return true;
1302 }
1303 }
1304 return false;
1305 }
1306
1315 protected function loadMetadataFromDbFieldValue( IReadableDatabase $db, $metadataBlob ) {
1316 $this->loadMetadataFromString( $db->decodeBlob( $metadataBlob ) );
1317 }
1318
1326 protected function loadMetadataFromString( $metadataString ) {
1327 $this->extraDataLoaded = true;
1328 $this->metadataArray = [];
1329 $this->metadataBlobs = [];
1330 $this->unloadedMetadataBlobs = [];
1331 $metadataString = (string)$metadataString;
1332 if ( $metadataString === '' ) {
1333 $this->metadataSerializationFormat = self::MDS_EMPTY;
1334 return;
1335 }
1336 if ( $metadataString[0] === '{' ) {
1337 $envelope = $this->metadataStorageHelper->jsonDecode( $metadataString );
1338 if ( !$envelope ) {
1339 // Legacy error encoding
1340 $this->metadataArray = [ '_error' => $metadataString ];
1341 $this->metadataSerializationFormat = self::MDS_LEGACY;
1342 } else {
1343 $this->metadataSerializationFormat = self::MDS_JSON;
1344 if ( isset( $envelope['data'] ) ) {
1345 $this->metadataArray = $envelope['data'];
1346 }
1347 if ( isset( $envelope['blobs'] ) ) {
1348 $this->metadataBlobs = $this->unloadedMetadataBlobs = $envelope['blobs'];
1349 }
1350 }
1351 } else {
1352 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1353 $data = @unserialize( $metadataString );
1354 if ( !is_array( $data ) ) {
1355 // Legacy error encoding
1356 $data = [ '_error' => $metadataString ];
1357 $this->metadataSerializationFormat = self::MDS_LEGACY;
1358 } else {
1359 $this->metadataSerializationFormat = self::MDS_PHP;
1360 }
1361 $this->metadataArray = $data;
1362 }
1363 }
1364
1369 public function getBitDepth() {
1370 $this->load();
1371
1372 return (int)$this->bits;
1373 }
1374
1380 public function getSize() {
1381 $this->load();
1382
1383 return $this->size;
1384 }
1385
1391 public function getMimeType() {
1392 $this->load();
1393
1394 return $this->mime;
1395 }
1396
1403 public function getMediaType() {
1404 $this->load();
1405
1406 return $this->media_type;
1407 }
1408
1420 public function exists() {
1421 $this->load();
1422
1423 return $this->fileExists;
1424 }
1425
1447 protected function getThumbnails( $archiveName = false ) {
1448 if ( $archiveName ) {
1449 $dir = $this->getArchiveThumbPath( $archiveName );
1450 } else {
1451 $dir = $this->getThumbPath();
1452 }
1453
1454 $backend = $this->repo->getBackend();
1455 $files = [ $dir ];
1456 try {
1457 $iterator = $backend->getFileList( [ 'dir' => $dir, 'forWrite' => true ] );
1458 if ( $iterator !== null ) {
1459 foreach ( $iterator as $file ) {
1460 $files[] = $file;
1461 }
1462 }
1463 } catch ( FileBackendError $e ) {
1464 } // suppress (T56674)
1465
1466 return $files;
1467 }
1468
1477 public function purgeCache( $options = [] ) {
1478 // Refresh metadata in memcached, but don't touch thumbnails or CDN
1479 $this->maybeUpgradeRow();
1480 $this->invalidateCache();
1481
1482 // Delete thumbnails
1483 $this->purgeThumbnails( $options );
1484
1485 // Purge CDN cache for this file
1486 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1487 $hcu->purgeUrls(
1488 $this->getUrl(),
1489 !empty( $options['forThumbRefresh'] )
1490 ? $hcu::PURGE_PRESEND // just a manual purge
1491 : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1492 );
1493 }
1494
1500 public function purgeOldThumbnails( $archiveName ) {
1501 // Get a list of old thumbnails
1502 $thumbs = $this->getThumbnails( $archiveName );
1503
1504 // Delete thumbnails from storage, and prevent the directory itself from being purged
1505 $dir = array_shift( $thumbs );
1506 $this->purgeThumbList( $dir, $thumbs );
1507
1508 $urls = [];
1509 foreach ( $thumbs as $thumb ) {
1510 $urls[] = $this->getArchiveThumbUrl( $archiveName, $thumb );
1511 }
1512
1513 // Purge any custom thumbnail caches
1514 $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, $archiveName, $urls );
1515
1516 // Purge the CDN
1517 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1518 $hcu->purgeUrls( $urls, $hcu::PURGE_PRESEND );
1519 }
1520
1527 public function purgeThumbnails( $options = [] ) {
1528 $thumbs = $this->getThumbnails();
1529
1530 // Delete thumbnails from storage, and prevent the directory itself from being purged
1531 $dir = array_shift( $thumbs );
1532 $this->purgeThumbList( $dir, $thumbs );
1533
1534 // Always purge all files from CDN regardless of handler filters
1535 $urls = [];
1536 foreach ( $thumbs as $thumb ) {
1537 $urls[] = $this->getThumbUrl( $thumb );
1538 }
1539
1540 // Give the media handler a chance to filter the file purge list
1541 if ( !empty( $options['forThumbRefresh'] ) ) {
1542 $handler = $this->getHandler();
1543 if ( $handler ) {
1544 $handler->filterThumbnailPurgeList( $thumbs, $options );
1545 }
1546 }
1547
1548 // Purge any custom thumbnail caches
1549 $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, false, $urls );
1550
1551 // Purge the CDN
1552 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1553 $hcu->purgeUrls(
1554 $urls,
1555 !empty( $options['forThumbRefresh'] )
1556 ? $hcu::PURGE_PRESEND // just a manual purge
1557 : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1558 );
1559 }
1560
1567 public function prerenderThumbnails() {
1568 $uploadThumbnailRenderMap = MediaWikiServices::getInstance()
1569 ->getMainConfig()->get( MainConfigNames::UploadThumbnailRenderMap );
1570
1571 $jobs = [];
1572
1573 $sizes = $uploadThumbnailRenderMap;
1574 rsort( $sizes );
1575
1576 foreach ( $sizes as $size ) {
1577 if ( $this->isMultipage() ) {
1578 // (T309114) Only trigger render jobs up to MAX_PAGE_RENDER_JOBS to avoid
1579 // a flood of jobs for huge files.
1580 $pageLimit = min( $this->pageCount(), self::MAX_PAGE_RENDER_JOBS );
1581
1582 $jobs[] = new ThumbnailRenderJob(
1583 $this->getTitle(),
1584 [
1585 'transformParams' => [ 'width' => $size, 'page' => 1 ],
1586 'enqueueNextPage' => true,
1587 'pageLimit' => $pageLimit
1588 ]
1589 );
1590 } elseif ( $this->isVectorized() || $this->getWidth() > $size ) {
1591 $jobs[] = new ThumbnailRenderJob(
1592 $this->getTitle(),
1593 [ 'transformParams' => [ 'width' => $size ] ]
1594 );
1595 }
1596 }
1597
1598 if ( $jobs ) {
1599 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs );
1600 }
1601 }
1602
1609 protected function purgeThumbList( $dir, $files ) {
1610 $fileListDebug = strtr(
1611 var_export( $files, true ),
1612 [ "\n" => '' ]
1613 );
1614 wfDebug( __METHOD__ . ": $fileListDebug" );
1615
1616 if ( $this->repo->supportsSha1URLs() ) {
1617 $reference = $this->getSha1();
1618 } else {
1619 $reference = $this->getName();
1620 }
1621
1622 $purgeList = [];
1623 foreach ( $files as $file ) {
1624 # Check that the reference (filename or sha1) is part of the thumb name
1625 # This is a basic check to avoid erasing unrelated directories
1626 if ( str_contains( $file, $reference )
1627 || str_contains( $file, "-thumbnail" ) // "short" thumb name
1628 ) {
1629 $purgeList[] = "{$dir}/{$file}";
1630 }
1631 }
1632
1633 # Delete the thumbnails
1634 $this->repo->quickPurgeBatch( $purgeList );
1635 # Clear out the thumbnail directory if empty
1636 $this->repo->quickCleanDir( $dir );
1637 }
1638
1650 public function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1651 if ( !$this->exists() ) {
1652 return []; // Avoid hard failure when the file does not exist. T221812
1653 }
1654
1655 $dbr = $this->repo->getReplicaDB();
1656 $oldFileQuery = FileSelectQueryBuilder::newForOldFile( $dbr )->getQueryInfo();
1657
1658 $tables = $oldFileQuery['tables'];
1659 $fields = $oldFileQuery['fields'];
1660 $join_conds = $oldFileQuery['join_conds'];
1661 $conds = $opts = [];
1662 $eq = $inc ? '=' : '';
1663 $conds[] = $dbr->expr( 'oi_name', '=', $this->title->getDBkey() );
1664
1665 if ( $start ) {
1666 $conds[] = $dbr->expr( 'oi_timestamp', "<$eq", $dbr->timestamp( $start ) );
1667 }
1668
1669 if ( $end ) {
1670 $conds[] = $dbr->expr( 'oi_timestamp', ">$eq", $dbr->timestamp( $end ) );
1671 }
1672
1673 if ( $limit ) {
1674 $opts['LIMIT'] = $limit;
1675 }
1676
1677 // Search backwards for time > x queries
1678 $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
1679 $opts['ORDER BY'] = "oi_timestamp $order";
1680 $opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1681
1682 $this->getHookRunner()->onLocalFile__getHistory( $this, $tables, $fields,
1683 $conds, $opts, $join_conds );
1684
1685 $res = $dbr->newSelectQueryBuilder()
1686 ->tables( $tables )
1687 ->fields( $fields )
1688 ->conds( $conds )
1689 ->caller( __METHOD__ )
1690 ->options( $opts )
1691 ->joinConds( $join_conds )
1692 ->fetchResultSet();
1693 $r = [];
1694
1695 foreach ( $res as $row ) {
1696 $r[] = $this->repo->newFileFromRow( $row );
1697 }
1698
1699 if ( $order == 'ASC' ) {
1700 $r = array_reverse( $r ); // make sure it ends up descending
1701 }
1702
1703 return $r;
1704 }
1705
1716 public function nextHistoryLine() {
1717 if ( !$this->exists() ) {
1718 return false; // Avoid hard failure when the file does not exist. T221812
1719 }
1720
1721 # Polymorphic function name to distinguish foreign and local fetches
1722 $fname = static::class . '::' . __FUNCTION__;
1723
1724 $dbr = $this->repo->getReplicaDB();
1725
1726 if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1727 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr );
1728
1729 $queryBuilder->fields( [ 'oi_archive_name' => $dbr->addQuotes( '' ), 'oi_deleted' => '0' ] )
1730 ->where( [ 'img_name' => $this->title->getDBkey() ] );
1731 $this->historyRes = $queryBuilder->caller( $fname )->fetchResultSet();
1732
1733 if ( $this->historyRes->numRows() == 0 ) {
1734 $this->historyRes = null;
1735
1736 return false;
1737 }
1738 } elseif ( $this->historyLine == 1 ) {
1739 $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr );
1740
1741 $this->historyRes = $queryBuilder->where( [ 'oi_name' => $this->title->getDBkey() ] )
1742 ->orderBy( 'oi_timestamp', SelectQueryBuilder::SORT_DESC )
1743 ->caller( $fname )->fetchResultSet();
1744 }
1745 $this->historyLine++;
1746
1747 return $this->historyRes->fetchObject();
1748 }
1749
1754 public function resetHistory() {
1755 $this->historyLine = 0;
1756
1757 if ( $this->historyRes !== null ) {
1758 $this->historyRes = null;
1759 }
1760 }
1761
1795 public function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1796 $timestamp = false, ?Authority $uploader = null, $tags = [],
1797 $createNullRevision = true, $revert = false
1798 ) {
1799 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1800 return $this->readOnlyFatalStatus();
1801 } elseif ( MediaWikiServices::getInstance()->getRevisionStore()->isReadOnly() ) {
1802 // Check this in advance to avoid writing to FileBackend and the file tables,
1803 // only to fail on insert the revision due to the text store being unavailable.
1804 return $this->readOnlyFatalStatus();
1805 }
1806
1807 $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1808 if ( !$props ) {
1809 if ( FileRepo::isVirtualUrl( $srcPath )
1810 || FileBackend::isStoragePath( $srcPath )
1811 ) {
1812 $props = $this->repo->getFileProps( $srcPath );
1813 } else {
1814 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
1815 $props = $mwProps->getPropsFromPath( $srcPath, true );
1816 }
1817 }
1818
1819 $options = [];
1820 $handler = MediaHandler::getHandler( $props['mime'] );
1821 if ( $handler ) {
1822 if ( is_string( $props['metadata'] ) ) {
1823 // This supports callers directly fabricating a metadata
1824 // property using serialize(). Normally the metadata property
1825 // comes from MWFileProps, in which case it won't be a string.
1826 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1827 $metadata = @unserialize( $props['metadata'] );
1828 } else {
1829 $metadata = $props['metadata'];
1830 }
1831
1832 if ( is_array( $metadata ) ) {
1833 $options['headers'] = $handler->getContentHeaders( $metadata );
1834 }
1835 } else {
1836 $options['headers'] = [];
1837 }
1838
1839 // Trim spaces on user supplied text
1840 $comment = trim( $comment );
1841
1842 $status = $this->publish( $src, $flags, $options );
1843
1844 if ( $status->successCount >= 2 ) {
1845 // There will be a copy+(one of move,copy,store).
1846 // The first succeeding does not commit us to updating the DB
1847 // since it simply copied the current version to a timestamped file name.
1848 // It is only *preferable* to avoid leaving such files orphaned.
1849 // Once the second operation goes through, then the current version was
1850 // updated and we must therefore update the DB too.
1851 $oldver = $status->value;
1852
1853 $uploadStatus = $this->recordUpload3(
1854 $oldver,
1855 $comment,
1856 $pageText,
1857 $uploader ?? RequestContext::getMain()->getAuthority(),
1858 $props,
1859 $timestamp,
1860 $tags,
1861 $createNullRevision,
1862 $revert
1863 );
1864 if ( !$uploadStatus->isOK() ) {
1865 if ( $uploadStatus->hasMessage( 'filenotfound' ) ) {
1866 // update filenotfound error with more specific path
1867 $status->fatal( 'filenotfound', $srcPath );
1868 } else {
1869 $status->merge( $uploadStatus );
1870 }
1871 }
1872 }
1873
1874 return $status;
1875 }
1876
1893 public function recordUpload3(
1894 string $oldver,
1895 string $comment,
1896 string $pageText,
1897 Authority $performer,
1898 $props = false,
1899 $timestamp = false,
1900 $tags = [],
1901 bool $createNullRevision = true,
1902 bool $revert = false
1903 ): Status {
1904 $dbw = $this->repo->getPrimaryDB();
1905
1906 # Imports or such might force a certain timestamp; otherwise we generate
1907 # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1908 if ( $timestamp === false ) {
1909 $timestamp = $dbw->timestamp();
1910 $allowTimeKludge = true;
1911 } else {
1912 $allowTimeKludge = false;
1913 }
1914
1915 $props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1916 $props['description'] = $comment;
1917 $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1918 $this->setProps( $props );
1919
1920 # Fail now if the file isn't there
1921 if ( !$this->fileExists ) {
1922 wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!" );
1923
1924 return Status::newFatal( 'filenotfound', $this->getRel() );
1925 }
1926
1927 $mimeAnalyzer = MediaWikiServices::getInstance()->getMimeAnalyzer();
1928 if ( !$mimeAnalyzer->isValidMajorMimeType( $this->major_mime ) ) {
1929 $this->major_mime = 'unknown';
1930 }
1931
1932 $actorNormalizaton = MediaWikiServices::getInstance()->getActorNormalization();
1933
1934 // T391473: File uploads can involve moving a lot of bytes around. Sometimes in
1935 // that time the DB connection can timeout. Normally this is automatically
1936 // reconnected, but reconnection does not work inside atomic sections.
1937 // Ping the DB to ensure it is still there prior to entering the atomic
1938 // section. TODO: Refactor upload jobs to be smarter about implicit transactions.
1939 $dbw->ping();
1940 $dbw->startAtomic( __METHOD__ );
1941
1942 $actorId = $actorNormalizaton->acquireActorId( $performer->getUser(), $dbw );
1943 $this->user = $performer->getUser();
1944
1945 # Test to see if the row exists using INSERT IGNORE
1946 # This avoids race conditions by locking the row until the commit, and also
1947 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1948 $commentStore = MediaWikiServices::getInstance()->getCommentStore();
1949 $commentFields = $commentStore->insert( $dbw, 'img_description', $comment );
1950 $actorFields = [ 'img_actor' => $actorId ];
1951 $dbw->newInsertQueryBuilder()
1952 ->insertInto( 'image' )
1953 ->ignore()
1954 ->row( [
1955 'img_name' => $this->getName(),
1956 'img_size' => $this->size,
1957 'img_width' => intval( $this->width ),
1958 'img_height' => intval( $this->height ),
1959 'img_bits' => $this->bits,
1960 'img_media_type' => $this->media_type,
1961 'img_major_mime' => $this->major_mime,
1962 'img_minor_mime' => $this->minor_mime,
1963 'img_timestamp' => $dbw->timestamp( $timestamp ),
1964 'img_metadata' => $this->getMetadataForDb( $dbw ),
1965 'img_sha1' => $this->sha1
1966 ] + $commentFields + $actorFields )
1967 ->caller( __METHOD__ )->execute();
1968 $reupload = ( $dbw->affectedRows() == 0 );
1969
1970 $latestFileRevId = null;
1971 if ( $this->migrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
1972 if ( $reupload ) {
1973 $latestFileRevId = $dbw->newSelectQueryBuilder()
1974 ->select( 'fr_id' )
1975 ->from( 'filerevision' )
1976 ->where( [ 'fr_file' => $this->acquireFileIdFromName() ] )
1977 ->orderBy( 'fr_timestamp', 'DESC' )
1978 ->caller( __METHOD__ )
1979 ->fetchField();
1980 }
1981 $commentFieldsNew = $commentStore->insert( $dbw, 'fr_description', $comment );
1982 $dbw->newInsertQueryBuilder()
1983 ->insertInto( 'filerevision' )
1984 ->row( [
1985 'fr_file' => $this->acquireFileIdFromName(),
1986 'fr_size' => $this->size,
1987 'fr_width' => intval( $this->width ),
1988 'fr_height' => intval( $this->height ),
1989 'fr_bits' => $this->bits,
1990 'fr_actor' => $actorId,
1991 'fr_deleted' => 0,
1992 'fr_timestamp' => $dbw->timestamp( $timestamp ),
1993 'fr_metadata' => $this->getMetadataForDb( $dbw ),
1994 'fr_sha1' => $this->sha1
1995 ] + $commentFieldsNew )
1996 ->caller( __METHOD__ )->execute();
1997 $dbw->newUpdateQueryBuilder()
1998 ->update( 'file' )
1999 ->set( [ 'file_latest' => $dbw->insertId() ] )
2000 ->where( [ 'file_id' => $this->getFileIdFromName() ] )
2001 ->caller( __METHOD__ )->execute();
2002 }
2003
2004 if ( $reupload ) {
2005 $row = $dbw->newSelectQueryBuilder()
2006 ->select( [ 'img_timestamp', 'img_sha1' ] )
2007 ->from( 'image' )
2008 ->where( [ 'img_name' => $this->getName() ] )
2009 ->caller( __METHOD__ )->fetchRow();
2010
2011 if ( $row && $row->img_sha1 === $this->sha1 ) {
2012 $dbw->endAtomic( __METHOD__ );
2013 wfDebug( __METHOD__ . ": File " . $this->getRel() . " already exists!" );
2014 $title = Title::newFromText( $this->getName(), NS_FILE );
2015 return Status::newFatal( 'fileexists-no-change', $title->getPrefixedText() );
2016 }
2017
2018 if ( $allowTimeKludge ) {
2019 # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
2020 $lUnixtime = $row ? (int)wfTimestamp( TS_UNIX, $row->img_timestamp ) : false;
2021 # Avoid a timestamp that is not newer than the last version
2022 # TODO: the image/oldimage tables should be like page/revision with an ID field
2023 if ( $lUnixtime && (int)wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
2024 sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
2025 $timestamp = $dbw->timestamp( $lUnixtime + 1 );
2026 $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
2027 }
2028 }
2029
2030 $tables = [ 'image' ];
2031 $fields = [
2032 'oi_name' => 'img_name',
2033 'oi_archive_name' => $dbw->addQuotes( $oldver ),
2034 'oi_size' => 'img_size',
2035 'oi_width' => 'img_width',
2036 'oi_height' => 'img_height',
2037 'oi_bits' => 'img_bits',
2038 'oi_description_id' => 'img_description_id',
2039 'oi_timestamp' => 'img_timestamp',
2040 'oi_metadata' => 'img_metadata',
2041 'oi_media_type' => 'img_media_type',
2042 'oi_major_mime' => 'img_major_mime',
2043 'oi_minor_mime' => 'img_minor_mime',
2044 'oi_sha1' => 'img_sha1',
2045 'oi_actor' => 'img_actor',
2046 ];
2047
2048 if ( ( $this->migrationStage & SCHEMA_COMPAT_WRITE_NEW ) && $latestFileRevId && $oldver ) {
2049 $dbw->newUpdateQueryBuilder()
2050 ->update( 'filerevision' )
2051 ->set( [ 'fr_archive_name' => $oldver ] )
2052 ->where( [ 'fr_id' => $latestFileRevId ] )
2053 ->caller( __METHOD__ )->execute();
2054 }
2055
2056 $joins = [];
2057 # (T36993) Note: $oldver can be empty here, if the previous
2058 # version of the file was broken. Allow registration of the new
2059 # version to continue anyway, because that's better than having
2060 # an image that's not fixable by user operations.
2061 # Collision, this is an update of a file
2062 # Insert previous contents into oldimage
2063 $dbw->insertSelect( 'oldimage', $tables, $fields,
2064 [ 'img_name' => $this->getName() ], __METHOD__, [], [], $joins );
2065
2066 # Update the current image row
2067 $dbw->newUpdateQueryBuilder()
2068 ->update( 'image' )
2069 ->set( [
2070 'img_size' => $this->size,
2071 'img_width' => intval( $this->width ),
2072 'img_height' => intval( $this->height ),
2073 'img_bits' => $this->bits,
2074 'img_media_type' => $this->media_type,
2075 'img_major_mime' => $this->major_mime,
2076 'img_minor_mime' => $this->minor_mime,
2077 'img_timestamp' => $dbw->timestamp( $timestamp ),
2078 'img_metadata' => $this->getMetadataForDb( $dbw ),
2079 'img_sha1' => $this->sha1
2080 ] + $commentFields + $actorFields )
2081 ->where( [ 'img_name' => $this->getName() ] )
2082 ->caller( __METHOD__ )->execute();
2083 }
2084
2085 $descTitle = $this->getTitle();
2086 $descId = $descTitle->getArticleID();
2087 $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $descTitle );
2088 if ( !$wikiPage instanceof WikiFilePage ) {
2089 throw new UnexpectedValueException( 'Cannot obtain instance of WikiFilePage for ' . $this->getName()
2090 . ', got instance of ' . get_class( $wikiPage ) );
2091 }
2092 $wikiPage->setFile( $this );
2093
2094 // Determine log action. If reupload is done by reverting, use a special log_action.
2095 if ( $revert ) {
2096 $logAction = 'revert';
2097 } elseif ( $reupload ) {
2098 $logAction = 'overwrite';
2099 } else {
2100 $logAction = 'upload';
2101 }
2102 // Add the log entry...
2103 $logEntry = new ManualLogEntry( 'upload', $logAction );
2104 $logEntry->setTimestamp( $this->timestamp );
2105 $logEntry->setPerformer( $performer->getUser() );
2106 $logEntry->setComment( $comment );
2107 $logEntry->setTarget( $descTitle );
2108 // Allow people using the api to associate log entries with the upload.
2109 // Log has a timestamp, but sometimes different from upload timestamp.
2110 $logEntry->setParameters(
2111 [
2112 'img_sha1' => $this->sha1,
2113 'img_timestamp' => $timestamp,
2114 ]
2115 );
2116 // Note we keep $logId around since during new image
2117 // creation, page doesn't exist yet, so log_page = 0
2118 // but we want it to point to the page we're making,
2119 // so we later modify the log entry.
2120 // For a similar reason, we avoid making an RC entry
2121 // now and wait until the page exists.
2122 $logId = $logEntry->insert();
2123
2124 if ( $descTitle->exists() ) {
2125 if ( $createNullRevision ) {
2126 $services = MediaWikiServices::getInstance();
2127 // Use own context to get the action text in content language
2128 $formatter = $services->getLogFormatterFactory()->newFromEntry( $logEntry );
2129 $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
2130 $editSummary = $formatter->getPlainActionText();
2131
2132 $nullRevRecord = $wikiPage->newPageUpdater( $performer->getUser() )
2133 ->setCause( PageUpdater::CAUSE_UPLOAD )
2134 ->saveDummyRevision( $editSummary, EDIT_SILENT );
2135
2136 // Associate null revision id
2137 $logEntry->setAssociatedRevId( $nullRevRecord->getId() );
2138 }
2139
2140 $newPageContent = null;
2141 } else {
2142 // Make the description page and RC log entry post-commit
2143 $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
2144 }
2145
2146 // NOTE: Even after ending this atomic section, we are probably still in the implicit
2147 // transaction started by any prior master query in the request. We cannot yet safely
2148 // schedule jobs, see T263301.
2149 $dbw->endAtomic( __METHOD__ );
2150 $fname = __METHOD__;
2151
2152 # Do some cache purges after final commit so that:
2153 # a) Changes are more likely to be seen post-purge
2154 # b) They won't cause rollback of the log publish/update above
2155 $purgeUpdate = new AutoCommitUpdate(
2156 $dbw,
2157 __METHOD__,
2158 function () use (
2159 $reupload, $wikiPage, $newPageContent, $comment, $performer,
2160 $logEntry, $logId, $descId, $tags, $fname
2161 ) {
2162 # Update memcache after the commit
2163 $this->invalidateCache();
2164
2165 $updateLogPage = false;
2166 if ( $newPageContent ) {
2167 # New file page; create the description page.
2168 # There's already a log entry, so don't make a second RC entry
2169 # CDN and file cache for the description page are purged by doUserEditContent.
2170 $revRecord = $wikiPage->newPageUpdater( $performer )
2171 ->setCause( PageUpdater::CAUSE_UPLOAD )
2172 ->setContent( SlotRecord::MAIN, $newPageContent )
2173 ->saveRevision( $comment, EDIT_NEW | EDIT_SUPPRESS_RC );
2174
2175 if ( $revRecord ) {
2176 // Associate new page revision id
2177 $logEntry->setAssociatedRevId( $revRecord->getId() );
2178
2179 // This relies on the resetArticleID() call in WikiPage::insertOn(),
2180 // which is triggered on $descTitle by doUserEditContent() above.
2181 $updateLogPage = $revRecord->getPageId();
2182 }
2183 } else {
2184 # Existing file page: invalidate description page cache
2185 $title = $wikiPage->getTitle();
2186 $title->invalidateCache();
2187 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2188 $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2189 # Allow the new file version to be patrolled from the page footer
2190 Article::purgePatrolFooterCache( $descId );
2191 }
2192
2193 # Update associated rev id. This should be done by $logEntry->insert() earlier,
2194 # but setAssociatedRevId() wasn't called at that point yet...
2195 $logParams = $logEntry->getParameters();
2196 $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
2197 $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
2198 if ( $updateLogPage ) {
2199 # Also log page, in case where we just created it above
2200 $update['log_page'] = $updateLogPage;
2201 }
2202 $this->getRepo()->getPrimaryDB()->newUpdateQueryBuilder()
2203 ->update( 'logging' )
2204 ->set( $update )
2205 ->where( [ 'log_id' => $logId ] )
2206 ->caller( $fname )->execute();
2207
2208 $this->getRepo()->getPrimaryDB()->newInsertQueryBuilder()
2209 ->insertInto( 'log_search' )
2210 ->row( [
2211 'ls_field' => 'associated_rev_id',
2212 'ls_value' => (string)$logEntry->getAssociatedRevId(),
2213 'ls_log_id' => $logId,
2214 ] )
2215 ->caller( $fname )->execute();
2216
2217 # Add change tags, if any
2218 if ( $tags ) {
2219 $logEntry->addTags( $tags );
2220 }
2221
2222 # Uploads can be patrolled
2223 $logEntry->setIsPatrollable( true );
2224
2225 # Now that the log entry is up-to-date, make an RC entry.
2226 $logEntry->publish( $logId );
2227
2228 # Run hook for other updates (typically more cache purging)
2229 $this->getHookRunner()->onFileUpload( $this, $reupload, !$newPageContent );
2230
2231 if ( $reupload ) {
2232 # Delete old thumbnails
2233 $this->purgeThumbnails();
2234 # Remove the old file from the CDN cache
2235 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2236 $hcu->purgeUrls( $this->getUrl(), $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2237 } else {
2238 # Update backlink pages pointing to this title if created
2239 $blcFactory = MediaWikiServices::getInstance()->getBacklinkCacheFactory();
2240 LinksUpdate::queueRecursiveJobsForTable(
2241 $this->getTitle(),
2242 'imagelinks',
2243 'upload-image',
2244 $performer->getUser()->getName(),
2245 $blcFactory->getBacklinkCache( $this->getTitle() )
2246 );
2247 }
2248
2249 $this->prerenderThumbnails();
2250 }
2251 );
2252
2253 # Invalidate cache for all pages using this file
2254 $cacheUpdateJob = HTMLCacheUpdateJob::newForBacklinks(
2255 $this->getTitle(),
2256 'imagelinks',
2257 [ 'causeAction' => 'file-upload', 'causeAgent' => $performer->getUser()->getName() ]
2258 );
2259
2260 // NOTE: We are probably still in the implicit transaction started by DBO_TRX. We should
2261 // only schedule jobs after that transaction was committed, so a job queue failure
2262 // doesn't cause the upload to fail (T263301). Also, we should generally not schedule any
2263 // Jobs or the DeferredUpdates that assume the update is complete until after the
2264 // transaction has been committed and we are sure that the upload was indeed successful.
2265 $dbw->onTransactionCommitOrIdle( static function () use ( $reupload, $purgeUpdate, $cacheUpdateJob ) {
2266 DeferredUpdates::addUpdate( $purgeUpdate, DeferredUpdates::PRESEND );
2267
2268 if ( !$reupload ) {
2269 // This is a new file, so update the image count
2270 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2271 }
2272
2273 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $cacheUpdateJob );
2274 }, __METHOD__ );
2275
2276 return Status::newGood();
2277 }
2278
2295 public function publish( $src, $flags = 0, array $options = [] ) {
2296 return $this->publishTo( $src, $this->getRel(), $flags, $options );
2297 }
2298
2315 protected function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
2316 $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
2317
2318 $repo = $this->getRepo();
2319 if ( $repo->getReadOnlyReason() !== false ) {
2320 return $this->readOnlyFatalStatus();
2321 }
2322
2323 $status = $this->acquireFileLock();
2324 if ( !$status->isOK() ) {
2325 return $status;
2326 }
2327
2328 if ( $this->isOld() ) {
2329 $archiveRel = $dstRel;
2330 $archiveName = basename( $archiveRel );
2331 } else {
2332 $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
2333 $archiveRel = $this->getArchiveRel( $archiveName );
2334 }
2335
2336 if ( $repo->hasSha1Storage() ) {
2337 $sha1 = FileRepo::isVirtualUrl( $srcPath )
2338 ? $repo->getFileSha1( $srcPath )
2339 : FSFile::getSha1Base36FromPath( $srcPath );
2341 $wrapperBackend = $repo->getBackend();
2342 '@phan-var FileBackendDBRepoWrapper $wrapperBackend';
2343 $dst = $wrapperBackend->getPathForSHA1( $sha1 );
2344 $status = $repo->quickImport( $src, $dst );
2345 if ( $flags & File::DELETE_SOURCE ) {
2346 unlink( $srcPath );
2347 }
2348
2349 if ( $this->exists() ) {
2350 $status->value = $archiveName;
2351 }
2352 } else {
2353 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
2354 $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
2355
2356 if ( $status->value == 'new' ) {
2357 $status->value = '';
2358 } else {
2359 $status->value = $archiveName;
2360 }
2361 }
2362
2363 $this->releaseFileLock();
2364 return $status;
2365 }
2366
2385 public function move( $target ) {
2386 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2387 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2388 return $this->readOnlyFatalStatus();
2389 }
2390
2391 wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
2392 $batch = new LocalFileMoveBatch( $this, $target );
2393
2394 $status = $batch->addCurrent();
2395 if ( !$status->isOK() ) {
2396 return $status;
2397 }
2398 $archiveNames = $batch->addOlds();
2399 $status = $batch->execute();
2400
2401 wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
2402
2403 // Purge the source and target files outside the transaction...
2404 $oldTitleFile = $localRepo->newFile( $this->title );
2405 $newTitleFile = $localRepo->newFile( $target );
2406 DeferredUpdates::addUpdate(
2407 new AutoCommitUpdate(
2408 $this->getRepo()->getPrimaryDB(),
2409 __METHOD__,
2410 static function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
2411 $oldTitleFile->purgeEverything();
2412 foreach ( $archiveNames as $archiveName ) {
2414 '@phan-var OldLocalFile $oldTitleFile';
2415 $oldTitleFile->purgeOldThumbnails( $archiveName );
2416 }
2417 $newTitleFile->purgeEverything();
2418 }
2419 ),
2420 DeferredUpdates::PRESEND
2421 );
2422
2423 if ( $status->isOK() ) {
2424 // Now switch the object
2425 $this->title = $target;
2426 // Force regeneration of the name and hashpath
2427 $this->name = null;
2428 $this->hashPath = null;
2429 }
2430
2431 return $status;
2432 }
2433
2450 public function deleteFile( $reason, UserIdentity $user, $suppress = false ) {
2451 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2452 return $this->readOnlyFatalStatus();
2453 }
2454
2455 $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2456
2457 $batch->addCurrent();
2458 // Get old version relative paths
2459 $archiveNames = $batch->addOlds();
2460 $status = $batch->execute();
2461
2462 if ( $status->isOK() ) {
2463 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
2464 }
2465
2466 // To avoid slow purges in the transaction, move them outside...
2467 DeferredUpdates::addUpdate(
2468 new AutoCommitUpdate(
2469 $this->getRepo()->getPrimaryDB(),
2470 __METHOD__,
2471 function () use ( $archiveNames ) {
2472 $this->purgeEverything();
2473 foreach ( $archiveNames as $archiveName ) {
2474 $this->purgeOldThumbnails( $archiveName );
2475 }
2476 }
2477 ),
2478 DeferredUpdates::PRESEND
2479 );
2480
2481 // Purge the CDN
2482 $purgeUrls = [];
2483 foreach ( $archiveNames as $archiveName ) {
2484 $purgeUrls[] = $this->getArchiveUrl( $archiveName );
2485 }
2486
2487 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2488 $hcu->purgeUrls( $purgeUrls, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2489
2490 return $status;
2491 }
2492
2510 public function deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress = false ) {
2511 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2512 return $this->readOnlyFatalStatus();
2513 }
2514
2515 $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2516
2517 $batch->addOld( $archiveName );
2518 $status = $batch->execute();
2519
2520 $this->purgeOldThumbnails( $archiveName );
2521 if ( $status->isOK() ) {
2522 $this->purgeDescription();
2523 }
2524
2525 $url = $this->getArchiveUrl( $archiveName );
2526 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2527 $hcu->purgeUrls( $url, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2528
2529 return $status;
2530 }
2531
2544 public function restore( $versions = [], $unsuppress = false ) {
2545 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2546 return $this->readOnlyFatalStatus();
2547 }
2548
2549 $batch = new LocalFileRestoreBatch( $this, $unsuppress );
2550
2551 if ( !$versions ) {
2552 $batch->addAll();
2553 } else {
2554 $batch->addIds( $versions );
2555 }
2556 $status = $batch->execute();
2557 if ( $status->isGood() ) {
2558 $cleanupStatus = $batch->cleanup();
2559 $cleanupStatus->successCount = 0;
2560 $cleanupStatus->failCount = 0;
2561 $status->merge( $cleanupStatus );
2562 }
2563
2564 return $status;
2565 }
2566
2577 public function getDescriptionUrl() {
2578 // Avoid hard failure when the file does not exist. T221812
2579 return $this->title ? $this->title->getLocalURL() : false;
2580 }
2581
2591 public function getDescriptionText( ?Language $lang = null ) {
2592 if ( !$this->title ) {
2593 return false; // Avoid hard failure when the file does not exist. T221812
2594 }
2595
2596 $services = MediaWikiServices::getInstance();
2597 $page = $services->getPageStore()->getPageByReference( $this->getTitle() );
2598 if ( !$page ) {
2599 return false;
2600 }
2601
2602 if ( $lang ) {
2603 $parserOptions = ParserOptions::newFromUserAndLang(
2604 RequestContext::getMain()->getUser(),
2605 $lang
2606 );
2607 } else {
2608 $parserOptions = ParserOptions::newFromContext( RequestContext::getMain() );
2609 }
2610
2611 $parseStatus = $services->getParserOutputAccess()
2612 ->getParserOutput( $page, $parserOptions );
2613
2614 if ( !$parseStatus->isGood() ) {
2615 // Rendering failed.
2616 return false;
2617 }
2618 // TODO T371004 move runOutputPipeline out of $parserOutput
2619 return $parseStatus->getValue()->runOutputPipeline( $parserOptions, [] )->getContentHolderText();
2620 }
2621
2629 public function getUploader( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ): ?UserIdentity {
2630 $this->load();
2631 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
2632 return null;
2633 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $performer ) ) {
2634 return null;
2635 } else {
2636 return $this->user;
2637 }
2638 }
2639
2646 public function getDescription( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) {
2647 $this->load();
2648 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
2649 return '';
2650 } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $performer ) ) {
2651 return '';
2652 } else {
2653 return $this->description;
2654 }
2655 }
2656
2661 public function getTimestamp() {
2662 $this->load();
2663
2664 return $this->timestamp;
2665 }
2666
2671 public function getDescriptionTouched() {
2672 if ( !$this->exists() ) {
2673 return false; // Avoid hard failure when the file does not exist. T221812
2674 }
2675
2676 // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
2677 // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
2678 // need to differentiate between null (uninitialized) and false (failed to load).
2679 if ( $this->descriptionTouched === null ) {
2680 $touched = $this->repo->getReplicaDB()->newSelectQueryBuilder()
2681 ->select( 'page_touched' )
2682 ->from( 'page' )
2683 ->where( [ 'page_namespace' => $this->title->getNamespace() ] )
2684 ->andWhere( [ 'page_title' => $this->title->getDBkey() ] )
2685 ->caller( __METHOD__ )->fetchField();
2686 $this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
2687 }
2688
2689 return $this->descriptionTouched;
2690 }
2691
2696 public function getSha1() {
2697 $this->load();
2698 return $this->sha1;
2699 }
2700
2704 public function isCacheable() {
2705 $this->load();
2706
2707 // If extra data (metadata) was not loaded then it must have been large
2708 return $this->extraDataLoaded
2709 && strlen( serialize( $this->metadataArray ) ) <= self::CACHE_FIELD_MAX_LEN;
2710 }
2711
2720 public function acquireFileLock( $timeout = 0 ) {
2721 return Status::wrap( $this->getRepo()->getBackend()->lockFiles(
2722 [ $this->getPath() ], LockManager::LOCK_EX, $timeout
2723 ) );
2724 }
2725
2732 public function releaseFileLock() {
2733 return Status::wrap( $this->getRepo()->getBackend()->unlockFiles(
2734 [ $this->getPath() ], LockManager::LOCK_EX
2735 ) );
2736 }
2737
2748 public function lock() {
2749 if ( !$this->locked ) {
2750 $logger = LoggerFactory::getInstance( 'LocalFile' );
2751
2752 $dbw = $this->repo->getPrimaryDB();
2753 $makesTransaction = !$dbw->trxLevel();
2754 $dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
2755 // T56736: use simple lock to handle when the file does not exist.
2756 // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
2757 // Also, that would cause contention on INSERT of similarly named rows.
2758 $status = $this->acquireFileLock( 10 ); // represents all versions of the file
2759 if ( !$status->isGood() ) {
2760 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2761 $logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
2762
2763 throw new LocalFileLockError( $status );
2764 }
2765 // Release the lock *after* commit to avoid row-level contention.
2766 // Make sure it triggers on rollback() as well as commit() (T132921).
2767 $dbw->onTransactionResolution(
2768 function () use ( $logger ) {
2769 $status = $this->releaseFileLock();
2770 if ( !$status->isGood() ) {
2771 $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
2772 }
2773 },
2774 __METHOD__
2775 );
2776 // Callers might care if the SELECT snapshot is safely fresh
2777 $this->lockedOwnTrx = $makesTransaction;
2778 }
2779
2780 $this->locked++;
2781
2782 return $this->lockedOwnTrx;
2783 }
2784
2795 public function unlock() {
2796 if ( $this->locked ) {
2797 --$this->locked;
2798 if ( !$this->locked ) {
2799 $dbw = $this->repo->getPrimaryDB();
2800 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2801 $this->lockedOwnTrx = false;
2802 }
2803 }
2804 }
2805
2809 protected function readOnlyFatalStatus() {
2810 return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
2811 $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2812 }
2813
2817 public function __destruct() {
2818 $this->unlock();
2819 }
2820}
2821
2823class_alias( LocalFile::class, 'LocalFile' );
const SCHEMA_COMPAT_OLD
Definition Defines.php:315
const NS_FILE
Definition Defines.php:71
const EDIT_SUPPRESS_RC
Definition Defines.php:140
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:307
const EDIT_SILENT
Do not notify other users (e.g.
Definition Defines.php:137
const EDIT_NEW
Article is assumed to be non-existent, fail if it exists.
Definition Defines.php:128
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfDeprecatedMsg( $msg, $version=false, $component=false, $callerOffset=2)
Log a deprecation warning with arbitrary message text.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Resource locking handling.
MimeMagic helper wrapper.
Base media handler class.
isFileMetadataValid( $image)
Check if the metadata is valid for this handler.
getPageDimensions(File $image, $page)
Get an associative array of page dimensions Currently "width" and "height" are understood,...
Base class for content handling.
Group all the pieces relevant to the context of a request into one instance.
Deferrable Update for closure/callback updates that should use auto-commit mode.
Defer callable updates to run later in the PHP process.
Class the manages updates of *_link tables as well as similar extension-managed tables.
Class for handling updates to the site_stats table.
Proxy backend that manages file layout rewriting for FileRepo.
Base class for file repositories.
Definition FileRepo.php:68
static newForOldFile(IReadableDatabase $db, array $options=[])
static newForFile(IReadableDatabase $db, array $options=[])
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:93
assertTitleDefined()
Assert that $this->title is set to a Title.
Definition File.php:2563
FileRepo LocalRepo ForeignAPIRepo false $repo
Some member variables can be lazy-initialised using __get().
Definition File.php:140
string null $path
The storage path corresponding to one of the zones.
Definition File.php:173
string null $url
The URL corresponding to one of the four basic zones.
Definition File.php:164
MediaHandler null $handler
Definition File.php:161
isMultipage()
Returns 'true' if this file is a type which supports multiple pages, e.g.
Definition File.php:2251
string null $name
The name of a file from its title object.
Definition File.php:170
assertRepoDefined()
Assert that $this->repo is set to a valid FileRepo instance.
Definition File.php:2554
Title string false $title
Definition File.php:143
getVirtualUrl( $suffix=false)
Get the public zone virtual URL for a current version source file.
Definition File.php:2017
getHandler()
Get a MediaHandler instance for this file.
Definition File.php:1636
getName()
Return the name of this file.
Definition File.php:361
static splitMime(?string $mime)
Split an internet media type into its two components; if not a two-part name, set the minor type to '...
Definition File.php:334
Local file in the wiki's own database.
Definition LocalFile.php:93
static getQueryInfo(array $options=[])
Return the tables, fields, and join conditions to be selected to create a new localfile object.
isMissing()
splitMime inherited
invalidateCache()
Purge the file object/metadata cache.
loadFromRow( $row, $prefix='img_')
Load file metadata from a DB result row.
unprefixRow( $row, $prefix='img_')
getDescriptionShortUrl()
Get short description URL for a file based on the page ID.
getMetadataForDb(IReadableDatabase $db)
Serialize the metadata array for insertion into img_metadata, oi_metadata or fa_metadata.
getWidth( $page=1)
Return the width of the image.
publishTo( $src, $dstRel, $flags=0, array $options=[])
Move or copy a file to a specified location.
static newFromTitle( $title, $repo, $unused=null)
Create a LocalFile from a title Do not call this except from inside a repo class.
getMetadataItems(array $itemNames)
Get multiple elements of the unserialized handler-specific metadata.
int $filerevision_id
id in filerevision table, null on read old
int $file_id
id in file table, null on read old
string $sha1
SHA-1 base 36 content hash.
bool $fileExists
Does the file exist on disk? (loadFromXxx)
getMimeType()
Returns the MIME type of the file.
string $mime
MIME type, determined by MimeAnalyzer::guessMimeType.
int $bits
Returned by getimagesize (loadFromXxx)
getCacheFields( $prefix='img_')
Returns the list of object properties that are included as-is in the cache.
string[] $unloadedMetadataBlobs
Map of metadata item name to blob address for items that exist but have not yet been loaded into $thi...
deleteFile( $reason, UserIdentity $user, $suppress=false)
Delete all versions of the file.
getLazyCacheFields( $prefix='img_')
Returns the list of object properties that are included as-is in the cache, only when they're not too...
getHeight( $page=1)
Return the height of the image.
setProps( $info)
Set properties in this object to be equal to those given in the associative array $info.
array $metadataArray
Unserialized metadata.
upload( $src, $comment, $pageText, $flags=0, $props=false, $timestamp=false, ?Authority $uploader=null, $tags=[], $createNullRevision=true, $revert=false)
getHashPath inherited
loadMetadataFromString( $metadataString)
Unserialize a metadata string which came from some non-DB source, or is the return value of IReadable...
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.
load( $flags=0)
Load file metadata from cache or DB, unless already loaded.
getDescriptionText(?Language $lang=null)
Get the HTML text of the description page This is not used by ImagePage for local files,...
getMetadata()
Get handler-specific metadata as a serialized string.
reserializeMetadata()
Write the metadata back to the database with the current serialization format.
static newFromRow( $row, $repo)
Create a LocalFile from a title Do not call this except from inside a repo class.
deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress=false)
Delete an old version of the file.
unlock()
Decrement the lock reference count and end the atomic section if it reaches zero.
acquireFileIdFromName()
This is mostly for the migration period.
int $size
Size in bytes (loadFromXxx)
loadFromFile( $path=null)
Load metadata from the file itself.
string $media_type
MEDIATYPE_xxx (bitmap, drawing, audio...)
getDescriptionUrl()
isMultipage inherited
getSize()
Returns the size of the image file, in bytes.
publish( $src, $flags=0, array $options=[])
Move or copy a file to its public location.
nextHistoryLine()
Returns the history of this file, line by line.
string null $metadataSerializationFormat
One of the MDS_* constants, giving the format of the metadata as stored in the DB,...
bool $dataLoaded
Whether or not core data has been loaded from the database (loadFromXxx)
lock()
Start an atomic DB section and lock the image for update or increments a reference counter if the loc...
acquireFileLock( $timeout=0)
Acquire an exclusive lock on the file, indicating an intention to write to the file backend.
bool $extraDataLoaded
Whether or not lazy-loaded data has been loaded from the database.
getThumbnails( $archiveName=false)
getTransformScript inherited
prerenderThumbnails()
Prerenders a configurable set of thumbnails.
getUploader(int $audience=self::FOR_PUBLIC, ?Authority $performer=null)
getFileIdFromName()
This is mostly for the migration period.
__construct( $title, $repo)
Do not call this except from inside a repo class.
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
getDescription( $audience=self::FOR_PUBLIC, ?Authority $performer=null)
recordUpload3(string $oldver, string $comment, string $pageText, Authority $performer, $props=false, $timestamp=false, $tags=[], bool $createNullRevision=true, bool $revert=false)
Record a file upload in the upload log and the image table (version 3)
__destruct()
Clean up any dangling locks.
move( $target)
getLinksTo inherited
loadFromDB( $flags=0)
Load file metadata from the DB.
resetHistory()
Reset the history pointer to the first element of the history.
int $deleted
Bitfield akin to rev_deleted.
getMediaType()
Returns the type of the media in the file.
loadExtraFromDB()
Load lazy file metadata from the DB.
static newFromKey( $sha1, $repo, $timestamp=false)
Create a LocalFile from a SHA-1 key Do not call this except from inside a repo class.
purgeThumbnails( $options=[])
Delete cached transformed files for the current version only.
restore( $versions=[], $unsuppress=false)
Restore all or specified deleted revisions to the given file.
maybeUpgradeRow()
Upgrade a row if it needs it.
getCacheKey()
Get the memcached key for the main data for this file, or false if there is no access to the shared c...
releaseFileLock()
Release a lock acquired with acquireFileLock().
purgeCache( $options=[])
Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
purgeOldThumbnails( $archiveName)
Delete cached transformed files for an archived version only.
purgeThumbList( $dir, $files)
Delete a list of thumbnails visible at urls.
upgradeRow()
Fix assorted version-related problems with the image row by reloading it from the file.
getMetadataArray()
Get unserialized handler-specific metadata.
Local repository that stores files in the local filesystem and registers them in the wiki's own datab...
Definition LocalRepo.php:57
Job to purge the HTML/file cache for all pages that link to or use another page or file.
Job for asynchronous rendering of thumbnails, e.g.
Base class for language-specific code.
Definition Language.php:81
Create PSR-3 logger objects.
Extends the LogEntry Interface with some basic functionality.
Class for creating new log entries and inserting them into the database.
A class containing constants representing the names of configuration variables.
const FileSchemaMigrationStage
Name constant for the FileSchemaMigrationStage setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Legacy class representing an editable page and handling UI for some page actions.
Definition Article.php:76
Special handling for representing file pages.
Set options of the Parser.
Value object representing a content slot associated with a page revision.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Controller-like object for creating and updating pages by creating new revisions.
Represents a title within MediaWiki.
Definition Title.php:78
Value object representing a user's identity.
Class representing a non-directory file on the file system.
Definition FSFile.php:34
File backend exception for checked exceptions (e.g.
Base class for all file backend classes (including multi-write backends).
static getCacheSetOptions(?IReadableDatabase ... $dbs)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Build SELECT queries with a fluent interface.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
getUser()
Returns the performer of the actions associated with this authority.
Interface for objects representing user identity.
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
Interface for database access objects.
A database connection without write operations.
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection.
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.
expr(string $field, string $op, $value)
See Expression::__construct()
Result wrapper for grabbing data queried from an IDatabase object.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...