MediaWiki master
LocalFile.php
Go to the documentation of this file.
1<?php
22
23use HTMLCacheUpdateJob;
24use InvalidArgumentException;
25use LockManager;
26use 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 $dbw->startAtomic( __METHOD__ );
1935
1936 $actorId = $actorNormalizaton->acquireActorId( $performer->getUser(), $dbw );
1937 $this->user = $performer->getUser();
1938
1939 # Test to see if the row exists using INSERT IGNORE
1940 # This avoids race conditions by locking the row until the commit, and also
1941 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1942 $commentStore = MediaWikiServices::getInstance()->getCommentStore();
1943 $commentFields = $commentStore->insert( $dbw, 'img_description', $comment );
1944 $actorFields = [ 'img_actor' => $actorId ];
1945 $dbw->newInsertQueryBuilder()
1946 ->insertInto( 'image' )
1947 ->ignore()
1948 ->row( [
1949 'img_name' => $this->getName(),
1950 'img_size' => $this->size,
1951 'img_width' => intval( $this->width ),
1952 'img_height' => intval( $this->height ),
1953 'img_bits' => $this->bits,
1954 'img_media_type' => $this->media_type,
1955 'img_major_mime' => $this->major_mime,
1956 'img_minor_mime' => $this->minor_mime,
1957 'img_timestamp' => $dbw->timestamp( $timestamp ),
1958 'img_metadata' => $this->getMetadataForDb( $dbw ),
1959 'img_sha1' => $this->sha1
1960 ] + $commentFields + $actorFields )
1961 ->caller( __METHOD__ )->execute();
1962 $reupload = ( $dbw->affectedRows() == 0 );
1963
1964 $latestFileRevId = null;
1965 if ( $this->migrationStage & SCHEMA_COMPAT_WRITE_NEW ) {
1966 if ( $reupload ) {
1967 $latestFileRevId = $dbw->newSelectQueryBuilder()
1968 ->select( 'fr_id' )
1969 ->from( 'filerevision' )
1970 ->where( [ 'fr_file' => $this->acquireFileIdFromName() ] )
1971 ->orderBy( 'fr_timestamp', 'DESC' )
1972 ->caller( __METHOD__ )
1973 ->fetchField();
1974 }
1975 $commentFieldsNew = $commentStore->insert( $dbw, 'fr_description', $comment );
1976 $dbw->newInsertQueryBuilder()
1977 ->insertInto( 'filerevision' )
1978 ->row( [
1979 'fr_file' => $this->acquireFileIdFromName(),
1980 'fr_size' => $this->size,
1981 'fr_width' => intval( $this->width ),
1982 'fr_height' => intval( $this->height ),
1983 'fr_bits' => $this->bits,
1984 'fr_actor' => $actorId,
1985 'fr_deleted' => 0,
1986 'fr_timestamp' => $dbw->timestamp( $timestamp ),
1987 'fr_metadata' => $this->getMetadataForDb( $dbw ),
1988 'fr_sha1' => $this->sha1
1989 ] + $commentFieldsNew )
1990 ->caller( __METHOD__ )->execute();
1991 $dbw->newUpdateQueryBuilder()
1992 ->update( 'file' )
1993 ->set( [ 'file_latest' => $dbw->insertId() ] )
1994 ->where( [ 'file_id' => $this->getFileIdFromName() ] )
1995 ->caller( __METHOD__ )->execute();
1996 }
1997
1998 if ( $reupload ) {
1999 $row = $dbw->newSelectQueryBuilder()
2000 ->select( [ 'img_timestamp', 'img_sha1' ] )
2001 ->from( 'image' )
2002 ->where( [ 'img_name' => $this->getName() ] )
2003 ->caller( __METHOD__ )->fetchRow();
2004
2005 if ( $row && $row->img_sha1 === $this->sha1 ) {
2006 $dbw->endAtomic( __METHOD__ );
2007 wfDebug( __METHOD__ . ": File " . $this->getRel() . " already exists!" );
2008 $title = Title::newFromText( $this->getName(), NS_FILE );
2009 return Status::newFatal( 'fileexists-no-change', $title->getPrefixedText() );
2010 }
2011
2012 if ( $allowTimeKludge ) {
2013 # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
2014 $lUnixtime = $row ? (int)wfTimestamp( TS_UNIX, $row->img_timestamp ) : false;
2015 # Avoid a timestamp that is not newer than the last version
2016 # TODO: the image/oldimage tables should be like page/revision with an ID field
2017 if ( $lUnixtime && (int)wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
2018 sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
2019 $timestamp = $dbw->timestamp( $lUnixtime + 1 );
2020 $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
2021 }
2022 }
2023
2024 $tables = [ 'image' ];
2025 $fields = [
2026 'oi_name' => 'img_name',
2027 'oi_archive_name' => $dbw->addQuotes( $oldver ),
2028 'oi_size' => 'img_size',
2029 'oi_width' => 'img_width',
2030 'oi_height' => 'img_height',
2031 'oi_bits' => 'img_bits',
2032 'oi_description_id' => 'img_description_id',
2033 'oi_timestamp' => 'img_timestamp',
2034 'oi_metadata' => 'img_metadata',
2035 'oi_media_type' => 'img_media_type',
2036 'oi_major_mime' => 'img_major_mime',
2037 'oi_minor_mime' => 'img_minor_mime',
2038 'oi_sha1' => 'img_sha1',
2039 'oi_actor' => 'img_actor',
2040 ];
2041
2042 if ( ( $this->migrationStage & SCHEMA_COMPAT_WRITE_NEW ) && $latestFileRevId && $oldver ) {
2043 $dbw->newUpdateQueryBuilder()
2044 ->update( 'filerevision' )
2045 ->set( [ 'fr_archive_name' => $oldver ] )
2046 ->where( [ 'fr_id' => $latestFileRevId ] )
2047 ->caller( __METHOD__ )->execute();
2048 }
2049
2050 $joins = [];
2051 # (T36993) Note: $oldver can be empty here, if the previous
2052 # version of the file was broken. Allow registration of the new
2053 # version to continue anyway, because that's better than having
2054 # an image that's not fixable by user operations.
2055 # Collision, this is an update of a file
2056 # Insert previous contents into oldimage
2057 $dbw->insertSelect( 'oldimage', $tables, $fields,
2058 [ 'img_name' => $this->getName() ], __METHOD__, [], [], $joins );
2059
2060 # Update the current image row
2061 $dbw->newUpdateQueryBuilder()
2062 ->update( 'image' )
2063 ->set( [
2064 'img_size' => $this->size,
2065 'img_width' => intval( $this->width ),
2066 'img_height' => intval( $this->height ),
2067 'img_bits' => $this->bits,
2068 'img_media_type' => $this->media_type,
2069 'img_major_mime' => $this->major_mime,
2070 'img_minor_mime' => $this->minor_mime,
2071 'img_timestamp' => $dbw->timestamp( $timestamp ),
2072 'img_metadata' => $this->getMetadataForDb( $dbw ),
2073 'img_sha1' => $this->sha1
2074 ] + $commentFields + $actorFields )
2075 ->where( [ 'img_name' => $this->getName() ] )
2076 ->caller( __METHOD__ )->execute();
2077 }
2078
2079 $descTitle = $this->getTitle();
2080 $descId = $descTitle->getArticleID();
2081 $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $descTitle );
2082 if ( !$wikiPage instanceof WikiFilePage ) {
2083 throw new UnexpectedValueException( 'Cannot obtain instance of WikiFilePage for ' . $this->getName()
2084 . ', got instance of ' . get_class( $wikiPage ) );
2085 }
2086 $wikiPage->setFile( $this );
2087
2088 // Determine log action. If reupload is done by reverting, use a special log_action.
2089 if ( $revert ) {
2090 $logAction = 'revert';
2091 } elseif ( $reupload ) {
2092 $logAction = 'overwrite';
2093 } else {
2094 $logAction = 'upload';
2095 }
2096 // Add the log entry...
2097 $logEntry = new ManualLogEntry( 'upload', $logAction );
2098 $logEntry->setTimestamp( $this->timestamp );
2099 $logEntry->setPerformer( $performer->getUser() );
2100 $logEntry->setComment( $comment );
2101 $logEntry->setTarget( $descTitle );
2102 // Allow people using the api to associate log entries with the upload.
2103 // Log has a timestamp, but sometimes different from upload timestamp.
2104 $logEntry->setParameters(
2105 [
2106 'img_sha1' => $this->sha1,
2107 'img_timestamp' => $timestamp,
2108 ]
2109 );
2110 // Note we keep $logId around since during new image
2111 // creation, page doesn't exist yet, so log_page = 0
2112 // but we want it to point to the page we're making,
2113 // so we later modify the log entry.
2114 // For a similar reason, we avoid making an RC entry
2115 // now and wait until the page exists.
2116 $logId = $logEntry->insert();
2117
2118 if ( $descTitle->exists() ) {
2119 if ( $createNullRevision ) {
2120 $services = MediaWikiServices::getInstance();
2121 // Use own context to get the action text in content language
2122 $formatter = $services->getLogFormatterFactory()->newFromEntry( $logEntry );
2123 $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
2124 $editSummary = $formatter->getPlainActionText();
2125
2126 $nullRevRecord = $wikiPage->newPageUpdater( $performer->getUser() )
2127 ->setCause( PageUpdater::CAUSE_UPLOAD )
2128 ->saveDummyRevision( $editSummary, EDIT_SILENT );
2129
2130 // Associate null revision id
2131 $logEntry->setAssociatedRevId( $nullRevRecord->getId() );
2132 }
2133
2134 $newPageContent = null;
2135 } else {
2136 // Make the description page and RC log entry post-commit
2137 $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
2138 }
2139
2140 // NOTE: Even after ending this atomic section, we are probably still in the implicit
2141 // transaction started by any prior master query in the request. We cannot yet safely
2142 // schedule jobs, see T263301.
2143 $dbw->endAtomic( __METHOD__ );
2144 $fname = __METHOD__;
2145
2146 # Do some cache purges after final commit so that:
2147 # a) Changes are more likely to be seen post-purge
2148 # b) They won't cause rollback of the log publish/update above
2149 $purgeUpdate = new AutoCommitUpdate(
2150 $dbw,
2151 __METHOD__,
2152 function () use (
2153 $reupload, $wikiPage, $newPageContent, $comment, $performer,
2154 $logEntry, $logId, $descId, $tags, $fname
2155 ) {
2156 # Update memcache after the commit
2157 $this->invalidateCache();
2158
2159 $updateLogPage = false;
2160 if ( $newPageContent ) {
2161 # New file page; create the description page.
2162 # There's already a log entry, so don't make a second RC entry
2163 # CDN and file cache for the description page are purged by doUserEditContent.
2164 $revRecord = $wikiPage->newPageUpdater( $performer )
2165 ->setCause( PageUpdater::CAUSE_UPLOAD )
2166 ->setContent( SlotRecord::MAIN, $newPageContent )
2167 ->saveRevision( $comment, EDIT_NEW | EDIT_SUPPRESS_RC );
2168
2169 if ( $revRecord ) {
2170 // Associate new page revision id
2171 $logEntry->setAssociatedRevId( $revRecord->getId() );
2172
2173 // This relies on the resetArticleID() call in WikiPage::insertOn(),
2174 // which is triggered on $descTitle by doUserEditContent() above.
2175 $updateLogPage = $revRecord->getPageId();
2176 }
2177 } else {
2178 # Existing file page: invalidate description page cache
2179 $title = $wikiPage->getTitle();
2180 $title->invalidateCache();
2181 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2182 $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2183 # Allow the new file version to be patrolled from the page footer
2184 Article::purgePatrolFooterCache( $descId );
2185 }
2186
2187 # Update associated rev id. This should be done by $logEntry->insert() earlier,
2188 # but setAssociatedRevId() wasn't called at that point yet...
2189 $logParams = $logEntry->getParameters();
2190 $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
2191 $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
2192 if ( $updateLogPage ) {
2193 # Also log page, in case where we just created it above
2194 $update['log_page'] = $updateLogPage;
2195 }
2196 $this->getRepo()->getPrimaryDB()->newUpdateQueryBuilder()
2197 ->update( 'logging' )
2198 ->set( $update )
2199 ->where( [ 'log_id' => $logId ] )
2200 ->caller( $fname )->execute();
2201
2202 $this->getRepo()->getPrimaryDB()->newInsertQueryBuilder()
2203 ->insertInto( 'log_search' )
2204 ->row( [
2205 'ls_field' => 'associated_rev_id',
2206 'ls_value' => (string)$logEntry->getAssociatedRevId(),
2207 'ls_log_id' => $logId,
2208 ] )
2209 ->caller( $fname )->execute();
2210
2211 # Add change tags, if any
2212 if ( $tags ) {
2213 $logEntry->addTags( $tags );
2214 }
2215
2216 # Uploads can be patrolled
2217 $logEntry->setIsPatrollable( true );
2218
2219 # Now that the log entry is up-to-date, make an RC entry.
2220 $logEntry->publish( $logId );
2221
2222 # Run hook for other updates (typically more cache purging)
2223 $this->getHookRunner()->onFileUpload( $this, $reupload, !$newPageContent );
2224
2225 if ( $reupload ) {
2226 # Delete old thumbnails
2227 $this->purgeThumbnails();
2228 # Remove the old file from the CDN cache
2229 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2230 $hcu->purgeUrls( $this->getUrl(), $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2231 } else {
2232 # Update backlink pages pointing to this title if created
2233 $blcFactory = MediaWikiServices::getInstance()->getBacklinkCacheFactory();
2234 LinksUpdate::queueRecursiveJobsForTable(
2235 $this->getTitle(),
2236 'imagelinks',
2237 'upload-image',
2238 $performer->getUser()->getName(),
2239 $blcFactory->getBacklinkCache( $this->getTitle() )
2240 );
2241 }
2242
2243 $this->prerenderThumbnails();
2244 }
2245 );
2246
2247 # Invalidate cache for all pages using this file
2248 $cacheUpdateJob = HTMLCacheUpdateJob::newForBacklinks(
2249 $this->getTitle(),
2250 'imagelinks',
2251 [ 'causeAction' => 'file-upload', 'causeAgent' => $performer->getUser()->getName() ]
2252 );
2253
2254 // NOTE: We are probably still in the implicit transaction started by DBO_TRX. We should
2255 // only schedule jobs after that transaction was committed, so a job queue failure
2256 // doesn't cause the upload to fail (T263301). Also, we should generally not schedule any
2257 // Jobs or the DeferredUpdates that assume the update is complete until after the
2258 // transaction has been committed and we are sure that the upload was indeed successful.
2259 $dbw->onTransactionCommitOrIdle( static function () use ( $reupload, $purgeUpdate, $cacheUpdateJob ) {
2260 DeferredUpdates::addUpdate( $purgeUpdate, DeferredUpdates::PRESEND );
2261
2262 if ( !$reupload ) {
2263 // This is a new file, so update the image count
2264 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2265 }
2266
2267 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $cacheUpdateJob );
2268 }, __METHOD__ );
2269
2270 return Status::newGood();
2271 }
2272
2289 public function publish( $src, $flags = 0, array $options = [] ) {
2290 return $this->publishTo( $src, $this->getRel(), $flags, $options );
2291 }
2292
2309 protected function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
2310 $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
2311
2312 $repo = $this->getRepo();
2313 if ( $repo->getReadOnlyReason() !== false ) {
2314 return $this->readOnlyFatalStatus();
2315 }
2316
2317 $status = $this->acquireFileLock();
2318 if ( !$status->isOK() ) {
2319 return $status;
2320 }
2321
2322 if ( $this->isOld() ) {
2323 $archiveRel = $dstRel;
2324 $archiveName = basename( $archiveRel );
2325 } else {
2326 $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
2327 $archiveRel = $this->getArchiveRel( $archiveName );
2328 }
2329
2330 if ( $repo->hasSha1Storage() ) {
2331 $sha1 = FileRepo::isVirtualUrl( $srcPath )
2332 ? $repo->getFileSha1( $srcPath )
2333 : FSFile::getSha1Base36FromPath( $srcPath );
2335 $wrapperBackend = $repo->getBackend();
2336 '@phan-var FileBackendDBRepoWrapper $wrapperBackend';
2337 $dst = $wrapperBackend->getPathForSHA1( $sha1 );
2338 $status = $repo->quickImport( $src, $dst );
2339 if ( $flags & File::DELETE_SOURCE ) {
2340 unlink( $srcPath );
2341 }
2342
2343 if ( $this->exists() ) {
2344 $status->value = $archiveName;
2345 }
2346 } else {
2347 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
2348 $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
2349
2350 if ( $status->value == 'new' ) {
2351 $status->value = '';
2352 } else {
2353 $status->value = $archiveName;
2354 }
2355 }
2356
2357 $this->releaseFileLock();
2358 return $status;
2359 }
2360
2379 public function move( $target ) {
2380 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2381 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2382 return $this->readOnlyFatalStatus();
2383 }
2384
2385 wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
2386 $batch = new LocalFileMoveBatch( $this, $target );
2387
2388 $status = $batch->addCurrent();
2389 if ( !$status->isOK() ) {
2390 return $status;
2391 }
2392 $archiveNames = $batch->addOlds();
2393 $status = $batch->execute();
2394
2395 wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
2396
2397 // Purge the source and target files outside the transaction...
2398 $oldTitleFile = $localRepo->newFile( $this->title );
2399 $newTitleFile = $localRepo->newFile( $target );
2400 DeferredUpdates::addUpdate(
2401 new AutoCommitUpdate(
2402 $this->getRepo()->getPrimaryDB(),
2403 __METHOD__,
2404 static function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
2405 $oldTitleFile->purgeEverything();
2406 foreach ( $archiveNames as $archiveName ) {
2408 '@phan-var OldLocalFile $oldTitleFile';
2409 $oldTitleFile->purgeOldThumbnails( $archiveName );
2410 }
2411 $newTitleFile->purgeEverything();
2412 }
2413 ),
2414 DeferredUpdates::PRESEND
2415 );
2416
2417 if ( $status->isOK() ) {
2418 // Now switch the object
2419 $this->title = $target;
2420 // Force regeneration of the name and hashpath
2421 $this->name = null;
2422 $this->hashPath = null;
2423 }
2424
2425 return $status;
2426 }
2427
2444 public function deleteFile( $reason, UserIdentity $user, $suppress = false ) {
2445 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2446 return $this->readOnlyFatalStatus();
2447 }
2448
2449 $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2450
2451 $batch->addCurrent();
2452 // Get old version relative paths
2453 $archiveNames = $batch->addOlds();
2454 $status = $batch->execute();
2455
2456 if ( $status->isOK() ) {
2457 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
2458 }
2459
2460 // To avoid slow purges in the transaction, move them outside...
2461 DeferredUpdates::addUpdate(
2462 new AutoCommitUpdate(
2463 $this->getRepo()->getPrimaryDB(),
2464 __METHOD__,
2465 function () use ( $archiveNames ) {
2466 $this->purgeEverything();
2467 foreach ( $archiveNames as $archiveName ) {
2468 $this->purgeOldThumbnails( $archiveName );
2469 }
2470 }
2471 ),
2472 DeferredUpdates::PRESEND
2473 );
2474
2475 // Purge the CDN
2476 $purgeUrls = [];
2477 foreach ( $archiveNames as $archiveName ) {
2478 $purgeUrls[] = $this->getArchiveUrl( $archiveName );
2479 }
2480
2481 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2482 $hcu->purgeUrls( $purgeUrls, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2483
2484 return $status;
2485 }
2486
2504 public function deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress = false ) {
2505 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2506 return $this->readOnlyFatalStatus();
2507 }
2508
2509 $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2510
2511 $batch->addOld( $archiveName );
2512 $status = $batch->execute();
2513
2514 $this->purgeOldThumbnails( $archiveName );
2515 if ( $status->isOK() ) {
2516 $this->purgeDescription();
2517 }
2518
2519 $url = $this->getArchiveUrl( $archiveName );
2520 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2521 $hcu->purgeUrls( $url, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2522
2523 return $status;
2524 }
2525
2538 public function restore( $versions = [], $unsuppress = false ) {
2539 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2540 return $this->readOnlyFatalStatus();
2541 }
2542
2543 $batch = new LocalFileRestoreBatch( $this, $unsuppress );
2544
2545 if ( !$versions ) {
2546 $batch->addAll();
2547 } else {
2548 $batch->addIds( $versions );
2549 }
2550 $status = $batch->execute();
2551 if ( $status->isGood() ) {
2552 $cleanupStatus = $batch->cleanup();
2553 $cleanupStatus->successCount = 0;
2554 $cleanupStatus->failCount = 0;
2555 $status->merge( $cleanupStatus );
2556 }
2557
2558 return $status;
2559 }
2560
2571 public function getDescriptionUrl() {
2572 // Avoid hard failure when the file does not exist. T221812
2573 return $this->title ? $this->title->getLocalURL() : false;
2574 }
2575
2585 public function getDescriptionText( ?Language $lang = null ) {
2586 if ( !$this->title ) {
2587 return false; // Avoid hard failure when the file does not exist. T221812
2588 }
2589
2590 $services = MediaWikiServices::getInstance();
2591 $page = $services->getPageStore()->getPageByReference( $this->getTitle() );
2592 if ( !$page ) {
2593 return false;
2594 }
2595
2596 if ( $lang ) {
2597 $parserOptions = ParserOptions::newFromUserAndLang(
2598 RequestContext::getMain()->getUser(),
2599 $lang
2600 );
2601 } else {
2602 $parserOptions = ParserOptions::newFromContext( RequestContext::getMain() );
2603 }
2604
2605 $parseStatus = $services->getParserOutputAccess()
2606 ->getParserOutput( $page, $parserOptions );
2607
2608 if ( !$parseStatus->isGood() ) {
2609 // Rendering failed.
2610 return false;
2611 }
2612 return $parseStatus->getValue()->getText();
2613 }
2614
2622 public function getUploader( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ): ?UserIdentity {
2623 $this->load();
2624 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
2625 return null;
2626 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $performer ) ) {
2627 return null;
2628 } else {
2629 return $this->user;
2630 }
2631 }
2632
2639 public function getDescription( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) {
2640 $this->load();
2641 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
2642 return '';
2643 } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $performer ) ) {
2644 return '';
2645 } else {
2646 return $this->description;
2647 }
2648 }
2649
2654 public function getTimestamp() {
2655 $this->load();
2656
2657 return $this->timestamp;
2658 }
2659
2664 public function getDescriptionTouched() {
2665 if ( !$this->exists() ) {
2666 return false; // Avoid hard failure when the file does not exist. T221812
2667 }
2668
2669 // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
2670 // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
2671 // need to differentiate between null (uninitialized) and false (failed to load).
2672 if ( $this->descriptionTouched === null ) {
2673 $touched = $this->repo->getReplicaDB()->newSelectQueryBuilder()
2674 ->select( 'page_touched' )
2675 ->from( 'page' )
2676 ->where( [ 'page_namespace' => $this->title->getNamespace() ] )
2677 ->andWhere( [ 'page_title' => $this->title->getDBkey() ] )
2678 ->caller( __METHOD__ )->fetchField();
2679 $this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
2680 }
2681
2682 return $this->descriptionTouched;
2683 }
2684
2689 public function getSha1() {
2690 $this->load();
2691 return $this->sha1;
2692 }
2693
2697 public function isCacheable() {
2698 $this->load();
2699
2700 // If extra data (metadata) was not loaded then it must have been large
2701 return $this->extraDataLoaded
2702 && strlen( serialize( $this->metadataArray ) ) <= self::CACHE_FIELD_MAX_LEN;
2703 }
2704
2713 public function acquireFileLock( $timeout = 0 ) {
2714 return Status::wrap( $this->getRepo()->getBackend()->lockFiles(
2715 [ $this->getPath() ], LockManager::LOCK_EX, $timeout
2716 ) );
2717 }
2718
2725 public function releaseFileLock() {
2726 return Status::wrap( $this->getRepo()->getBackend()->unlockFiles(
2727 [ $this->getPath() ], LockManager::LOCK_EX
2728 ) );
2729 }
2730
2741 public function lock() {
2742 if ( !$this->locked ) {
2743 $logger = LoggerFactory::getInstance( 'LocalFile' );
2744
2745 $dbw = $this->repo->getPrimaryDB();
2746 $makesTransaction = !$dbw->trxLevel();
2747 $dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
2748 // T56736: use simple lock to handle when the file does not exist.
2749 // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
2750 // Also, that would cause contention on INSERT of similarly named rows.
2751 $status = $this->acquireFileLock( 10 ); // represents all versions of the file
2752 if ( !$status->isGood() ) {
2753 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2754 $logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
2755
2756 throw new LocalFileLockError( $status );
2757 }
2758 // Release the lock *after* commit to avoid row-level contention.
2759 // Make sure it triggers on rollback() as well as commit() (T132921).
2760 $dbw->onTransactionResolution(
2761 function () use ( $logger ) {
2762 $status = $this->releaseFileLock();
2763 if ( !$status->isGood() ) {
2764 $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
2765 }
2766 },
2767 __METHOD__
2768 );
2769 // Callers might care if the SELECT snapshot is safely fresh
2770 $this->lockedOwnTrx = $makesTransaction;
2771 }
2772
2773 $this->locked++;
2774
2775 return $this->lockedOwnTrx;
2776 }
2777
2788 public function unlock() {
2789 if ( $this->locked ) {
2790 --$this->locked;
2791 if ( !$this->locked ) {
2792 $dbw = $this->repo->getPrimaryDB();
2793 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2794 $this->lockedOwnTrx = false;
2795 }
2796 }
2797 }
2798
2802 protected function readOnlyFatalStatus() {
2803 return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
2804 $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2805 }
2806
2810 public function __destruct() {
2811 $this->unlock();
2812 }
2813}
2814
2816class_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 for asynchronous rendering of thumbnails, e.g.
Base class for language-specific code.
Definition Language.php:82
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:77
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...