MediaWiki master
LocalFile.php
Go to the documentation of this file.
1<?php
42
69class LocalFile extends File {
70 private const VERSION = 13; // cache version
71
72 private const CACHE_FIELD_MAX_LEN = 1000;
73
75 private const MDS_EMPTY = 'empty';
76
78 private const MDS_LEGACY = 'legacy';
79
81 private const MDS_PHP = 'php';
82
84 private const MDS_JSON = 'json';
85
87 private const MAX_PAGE_RENDER_JOBS = 50;
88
90 protected $fileExists;
91
93 protected $width;
94
96 protected $height;
97
99 protected $bits;
100
102 protected $media_type;
103
105 protected $mime;
106
108 protected $size;
109
111 protected $metadataArray = [];
112
120
122 protected $metadataBlobs = [];
123
130 protected $unloadedMetadataBlobs = [];
131
133 protected $sha1;
134
136 protected $dataLoaded = false;
137
139 protected $extraDataLoaded = false;
140
142 protected $deleted;
143
145 protected $repoClass = LocalRepo::class;
146
148 private $historyLine = 0;
149
151 private $historyRes = null;
152
154 private $major_mime;
155
157 private $minor_mime;
158
160 private $timestamp;
161
163 private $user;
164
166 private $description;
167
169 private $descriptionTouched;
170
172 private $upgraded;
173
175 private $upgrading;
176
178 private $locked;
179
181 private $lockedOwnTrx;
182
184 private $missing;
185
187 private $metadataStorageHelper;
188
189 // @note: higher than IDBAccessObject constants
190 private const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
191
192 private const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
193
208 public static function newFromTitle( $title, $repo, $unused = null ) {
209 return new static( $title, $repo );
210 }
211
223 public static function newFromRow( $row, $repo ) {
224 $title = Title::makeTitle( NS_FILE, $row->img_name );
225 $file = new static( $title, $repo );
226 $file->loadFromRow( $row );
227
228 return $file;
229 }
230
242 public static function newFromKey( $sha1, $repo, $timestamp = false ) {
243 $dbr = $repo->getReplicaDB();
244 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr );
245
246 $queryBuilder->where( [ 'img_sha1' => $sha1 ] );
247
248 if ( $timestamp ) {
249 $queryBuilder->andWhere( [ 'img_timestamp' => $dbr->timestamp( $timestamp ) ] );
250 }
251
252 $row = $queryBuilder->caller( __METHOD__ )->fetchRow();
253 if ( $row ) {
254 return static::newFromRow( $row, $repo );
255 } else {
256 return false;
257 }
258 }
259
280 public static function getQueryInfo( array $options = [] ) {
281 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
282 $queryInfo = FileSelectQueryBuilder::newForFile( $dbr, $options )->getQueryInfo();
283 // needs remapping...
284 return [
285 'tables' => $queryInfo['tables'],
286 'fields' => $queryInfo['fields'],
287 'joins' => $queryInfo['join_conds'],
288 ];
289 }
290
298 public function __construct( $title, $repo ) {
299 parent::__construct( $title, $repo );
300 $this->metadataStorageHelper = new MetadataStorageHelper( $repo );
301
302 $this->assertRepoDefined();
303 $this->assertTitleDefined();
304 }
305
309 public function getRepo() {
310 return $this->repo;
311 }
312
319 protected function getCacheKey() {
320 return $this->repo->getSharedCacheKey( 'file', sha1( $this->getName() ) );
321 }
322
326 private function loadFromCache() {
327 $this->dataLoaded = false;
328 $this->extraDataLoaded = false;
329
330 $key = $this->getCacheKey();
331 if ( !$key ) {
332 $this->loadFromDB( IDBAccessObject::READ_NORMAL );
333
334 return;
335 }
336
337 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
338 $cachedValues = $cache->getWithSetCallback(
339 $key,
340 $cache::TTL_WEEK,
341 function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
342 $setOpts += Database::getCacheSetOptions( $this->repo->getReplicaDB() );
343
344 $this->loadFromDB( IDBAccessObject::READ_NORMAL );
345
346 $fields = $this->getCacheFields( '' );
347 $cacheVal = [];
348 $cacheVal['fileExists'] = $this->fileExists;
349 if ( $this->fileExists ) {
350 foreach ( $fields as $field ) {
351 $cacheVal[$field] = $this->$field;
352 }
353 }
354 if ( $this->user ) {
355 $cacheVal['user'] = $this->user->getId();
356 $cacheVal['user_text'] = $this->user->getName();
357 }
358
359 // Don't cache metadata items stored as blobs, since they tend to be large
360 if ( $this->metadataBlobs ) {
361 $cacheVal['metadata'] = array_diff_key(
362 $this->metadataArray, $this->metadataBlobs );
363 // Save the blob addresses
364 $cacheVal['metadataBlobs'] = $this->metadataBlobs;
365 } else {
366 $cacheVal['metadata'] = $this->metadataArray;
367 }
368
369 // Strip off excessive entries from the subset of fields that can become large.
370 // If the cache value gets too large and might not fit in the cache,
371 // causing repeat database queries for each access to the file.
372 foreach ( $this->getLazyCacheFields( '' ) as $field ) {
373 if ( isset( $cacheVal[$field] )
374 && strlen( serialize( $cacheVal[$field] ) ) > 100 * 1024
375 ) {
376 unset( $cacheVal[$field] ); // don't let the value get too big
377 if ( $field === 'metadata' ) {
378 unset( $cacheVal['metadataBlobs'] );
379 }
380 }
381 }
382
383 if ( $this->fileExists ) {
384 $ttl = $cache->adaptiveTTL( (int)wfTimestamp( TS_UNIX, $this->timestamp ), $ttl );
385 } else {
386 $ttl = $cache::TTL_DAY;
387 }
388
389 return $cacheVal;
390 },
391 [ 'version' => self::VERSION ]
392 );
393
394 $this->fileExists = $cachedValues['fileExists'];
395 if ( $this->fileExists ) {
396 $this->setProps( $cachedValues );
397 }
398
399 $this->dataLoaded = true;
400 $this->extraDataLoaded = true;
401 foreach ( $this->getLazyCacheFields( '' ) as $field ) {
402 $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
403 }
404 }
405
409 public function invalidateCache() {
410 $key = $this->getCacheKey();
411 if ( !$key ) {
412 return;
413 }
414
415 $this->repo->getPrimaryDB()->onTransactionPreCommitOrIdle(
416 static function () use ( $key ) {
417 MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key );
418 },
419 __METHOD__
420 );
421 }
422
430 public function loadFromFile( $path = null ) {
431 $props = $this->repo->getFileProps( $path ?? $this->getVirtualUrl() );
432 $this->setProps( $props );
433 }
434
442 protected function getCacheFields( $prefix = 'img_' ) {
443 if ( $prefix !== '' ) {
444 throw new InvalidArgumentException(
445 __METHOD__ . ' with a non-empty prefix is no longer supported.'
446 );
447 }
448
449 // See self::getQueryInfo() for the fetching of the data from the DB,
450 // self::loadFromRow() for the loading of the object from the DB row,
451 // and self::loadFromCache() for the caching, and self::setProps() for
452 // populating the object from an array of data.
453 return [ 'size', 'width', 'height', 'bits', 'media_type',
454 'major_mime', 'minor_mime', 'timestamp', 'sha1', 'description' ];
455 }
456
464 protected function getLazyCacheFields( $prefix = 'img_' ) {
465 if ( $prefix !== '' ) {
466 throw new InvalidArgumentException(
467 __METHOD__ . ' with a non-empty prefix is no longer supported.'
468 );
469 }
470
471 // Keep this in sync with the omit-lazy option in self::getQueryInfo().
472 return [ 'metadata' ];
473 }
474
480 protected function loadFromDB( $flags = 0 ) {
481 $fname = static::class . '::' . __FUNCTION__;
482
483 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
484 $this->dataLoaded = true;
485 $this->extraDataLoaded = true;
486
487 $dbr = ( $flags & IDBAccessObject::READ_LATEST )
488 ? $this->repo->getPrimaryDB()
489 : $this->repo->getReplicaDB();
490 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr );
491
492 $queryBuilder->where( [ 'img_name' => $this->getName() ] );
493 $row = $queryBuilder->caller( $fname )->fetchRow();
494
495 if ( $row ) {
496 $this->loadFromRow( $row );
497 } else {
498 $this->fileExists = false;
499 }
500 }
501
507 protected function loadExtraFromDB() {
508 if ( !$this->title ) {
509 return; // Avoid hard failure when the file does not exist. T221812
510 }
511
512 $fname = static::class . '::' . __FUNCTION__;
513
514 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
515 $this->extraDataLoaded = true;
516
517 $db = $this->repo->getReplicaDB();
518 $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
519 if ( !$fieldMap ) {
520 $db = $this->repo->getPrimaryDB();
521 $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
522 }
523
524 if ( $fieldMap ) {
525 if ( isset( $fieldMap['metadata'] ) ) {
526 $this->loadMetadataFromDbFieldValue( $db, $fieldMap['metadata'] );
527 }
528 } else {
529 throw new RuntimeException( "Could not find data for image '{$this->getName()}'." );
530 }
531 }
532
538 private function loadExtraFieldsWithTimestamp( IReadableDatabase $dbr, $fname ) {
539 $fieldMap = false;
540
541 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr, [ 'omit-nonlazy' ] );
542 $queryBuilder->where( [ 'img_name' => $this->getName() ] )
543 ->andWhere( [ 'img_timestamp' => $dbr->timestamp( $this->getTimestamp() ) ] );
544 $row = $queryBuilder->caller( $fname )->fetchRow();
545 if ( $row ) {
546 $fieldMap = $this->unprefixRow( $row, 'img_' );
547 } else {
548 # File may have been uploaded over in the meantime; check the old versions
549 $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr, [ 'omit-nonlazy' ] );
550 $row = $queryBuilder->where( [ 'oi_name' => $this->getName() ] )
551 ->andWhere( [ 'oi_timestamp' => $dbr->timestamp( $this->getTimestamp() ) ] )
552 ->caller( __METHOD__ )->fetchRow();
553 if ( $row ) {
554 $fieldMap = $this->unprefixRow( $row, 'oi_' );
555 }
556 }
557
558 return $fieldMap;
559 }
560
566 protected function unprefixRow( $row, $prefix = 'img_' ) {
567 $array = (array)$row;
568 $prefixLength = strlen( $prefix );
569
570 // Double check prefix once
571 if ( substr( array_key_first( $array ), 0, $prefixLength ) !== $prefix ) {
572 throw new InvalidArgumentException( __METHOD__ . ': incorrect $prefix parameter' );
573 }
574
575 $decoded = [];
576 foreach ( $array as $name => $value ) {
577 $decoded[substr( $name, $prefixLength )] = $value;
578 }
579
580 return $decoded;
581 }
582
598 public function loadFromRow( $row, $prefix = 'img_' ) {
599 $this->dataLoaded = true;
600
601 $unprefixed = $this->unprefixRow( $row, $prefix );
602
603 $this->name = $unprefixed['name'];
604 $this->media_type = $unprefixed['media_type'];
605
606 $services = MediaWikiServices::getInstance();
607 $this->description = $services->getCommentStore()
608 ->getComment( "{$prefix}description", $row )->text;
609
610 $this->user = $services->getUserFactory()->newFromAnyId(
611 $unprefixed['user'] ?? null,
612 $unprefixed['user_text'] ?? null,
613 $unprefixed['actor'] ?? null
614 );
615
616 $this->timestamp = wfTimestamp( TS_MW, $unprefixed['timestamp'] );
617
619 $this->repo->getReplicaDB(), $unprefixed['metadata'] );
620
621 if ( empty( $unprefixed['major_mime'] ) ) {
622 $this->major_mime = 'unknown';
623 $this->minor_mime = 'unknown';
624 $this->mime = 'unknown/unknown';
625 } else {
626 if ( !$unprefixed['minor_mime'] ) {
627 $unprefixed['minor_mime'] = 'unknown';
628 }
629 $this->major_mime = $unprefixed['major_mime'];
630 $this->minor_mime = $unprefixed['minor_mime'];
631 $this->mime = $unprefixed['major_mime'] . '/' . $unprefixed['minor_mime'];
632 }
633
634 // Trim zero padding from char/binary field
635 $this->sha1 = rtrim( $unprefixed['sha1'], "\0" );
636
637 // Normalize some fields to integer type, per their database definition.
638 // Use unary + so that overflows will be upgraded to double instead of
639 // being truncated as with intval(). This is important to allow > 2 GiB
640 // files on 32-bit systems.
641 $this->size = +$unprefixed['size'];
642 $this->width = +$unprefixed['width'];
643 $this->height = +$unprefixed['height'];
644 $this->bits = +$unprefixed['bits'];
645
646 // Check for extra fields (deprecated since MW 1.37)
647 $extraFields = array_diff(
648 array_keys( $unprefixed ),
649 [
650 'name', 'media_type', 'description_text', 'description_data',
651 'description_cid', 'user', 'user_text', 'actor', 'timestamp',
652 'metadata', 'major_mime', 'minor_mime', 'sha1', 'size', 'width',
653 'height', 'bits'
654 ]
655 );
656 if ( $extraFields ) {
658 'Passing extra fields (' .
659 implode( ', ', $extraFields )
660 . ') to ' . __METHOD__ . ' was deprecated in MediaWiki 1.37. ' .
661 'Property assignment will be removed in a later version.',
662 '1.37' );
663 foreach ( $extraFields as $field ) {
664 $this->$field = $unprefixed[$field];
665 }
666 }
667
668 $this->fileExists = true;
669 }
670
676 public function load( $flags = 0 ) {
677 if ( !$this->dataLoaded ) {
678 if ( $flags & IDBAccessObject::READ_LATEST ) {
679 $this->loadFromDB( $flags );
680 } else {
681 $this->loadFromCache();
682 }
683 }
684
685 if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
686 // @note: loads on name/timestamp to reduce race condition problems
687 $this->loadExtraFromDB();
688 }
689 }
690
695 public function maybeUpgradeRow() {
696 if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() || $this->upgrading ) {
697 return;
698 }
699
700 $upgrade = false;
701 $reserialize = false;
702 if ( $this->media_type === null || $this->mime == 'image/svg' ) {
703 $upgrade = true;
704 } else {
705 $handler = $this->getHandler();
706 if ( $handler ) {
707 $validity = $handler->isFileMetadataValid( $this );
708 if ( $validity === MediaHandler::METADATA_BAD ) {
709 $upgrade = true;
710 } elseif ( $validity === MediaHandler::METADATA_COMPATIBLE
711 && $this->repo->isMetadataUpdateEnabled()
712 ) {
713 $upgrade = true;
714 } elseif ( $this->repo->isJsonMetadataEnabled()
715 && $this->repo->isMetadataReserializeEnabled()
716 ) {
717 if ( $this->repo->isSplitMetadataEnabled() && $this->isMetadataOversize() ) {
718 $reserialize = true;
719 } elseif ( $this->metadataSerializationFormat !== self::MDS_EMPTY &&
720 $this->metadataSerializationFormat !== self::MDS_JSON ) {
721 $reserialize = true;
722 }
723 }
724 }
725 }
726
727 if ( $upgrade || $reserialize ) {
728 $this->upgrading = true;
729 // Defer updates unless in auto-commit CLI mode
730 DeferredUpdates::addCallableUpdate( function () use ( $upgrade ) {
731 $this->upgrading = false; // avoid duplicate updates
732 try {
733 if ( $upgrade ) {
734 $this->upgradeRow();
735 } else {
736 $this->reserializeMetadata();
737 }
738 } catch ( LocalFileLockError $e ) {
739 // let the other process handle it (or do it next time)
740 }
741 } );
742 }
743 }
744
748 public function getUpgraded() {
749 return $this->upgraded;
750 }
751
756 public function upgradeRow() {
757 $dbw = $this->repo->getPrimaryDB();
758
759 // Make a DB query condition that will fail to match the image row if the
760 // image was reuploaded while the upgrade was in process.
761 $freshnessCondition = [ 'img_timestamp' => $dbw->timestamp( $this->getTimestamp() ) ];
762
763 $this->loadFromFile();
764
765 # Don't destroy file info of missing files
766 if ( !$this->fileExists ) {
767 wfDebug( __METHOD__ . ": file does not exist, aborting" );
768
769 return;
770 }
771
772 [ $major, $minor ] = self::splitMime( $this->mime );
773
774 wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema" );
775
776 $dbw->newUpdateQueryBuilder()
777 ->update( 'image' )
778 ->set( [
779 'img_size' => $this->size,
780 'img_width' => $this->width,
781 'img_height' => $this->height,
782 'img_bits' => $this->bits,
783 'img_media_type' => $this->media_type,
784 'img_major_mime' => $major,
785 'img_minor_mime' => $minor,
786 'img_metadata' => $this->getMetadataForDb( $dbw ),
787 'img_sha1' => $this->sha1,
788 ] )
789 ->where( [ 'img_name' => $this->getName() ] )
790 ->andWhere( $freshnessCondition )
791 ->caller( __METHOD__ )->execute();
792
793 $this->invalidateCache();
794
795 $this->upgraded = true; // avoid rework/retries
796 }
797
802 protected function reserializeMetadata() {
803 if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
804 return;
805 }
806 $dbw = $this->repo->getPrimaryDB();
807 $dbw->newUpdateQueryBuilder()
808 ->update( 'image' )
809 ->set( [ 'img_metadata' => $this->getMetadataForDb( $dbw ) ] )
810 ->where( [
811 'img_name' => $this->name,
812 'img_timestamp' => $dbw->timestamp( $this->timestamp ),
813 ] )
814 ->caller( __METHOD__ )->execute();
815 $this->upgraded = true;
816 }
817
829 protected function setProps( $info ) {
830 $this->dataLoaded = true;
831 $fields = $this->getCacheFields( '' );
832 $fields[] = 'fileExists';
833
834 foreach ( $fields as $field ) {
835 if ( isset( $info[$field] ) ) {
836 $this->$field = $info[$field];
837 }
838 }
839
840 // Only our own cache sets these properties, so they both should be present.
841 if ( isset( $info['user'] ) &&
842 isset( $info['user_text'] ) &&
843 $info['user_text'] !== ''
844 ) {
845 $this->user = new UserIdentityValue( $info['user'], $info['user_text'] );
846 }
847
848 // Fix up mime fields
849 if ( isset( $info['major_mime'] ) ) {
850 $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
851 } elseif ( isset( $info['mime'] ) ) {
852 $this->mime = $info['mime'];
853 [ $this->major_mime, $this->minor_mime ] = self::splitMime( $this->mime );
854 }
855
856 if ( isset( $info['metadata'] ) ) {
857 if ( is_string( $info['metadata'] ) ) {
858 $this->loadMetadataFromString( $info['metadata'] );
859 } elseif ( is_array( $info['metadata'] ) ) {
860 $this->metadataArray = $info['metadata'];
861 if ( isset( $info['metadataBlobs'] ) ) {
862 $this->metadataBlobs = $info['metadataBlobs'];
863 $this->unloadedMetadataBlobs = array_diff_key(
864 $this->metadataBlobs,
865 $this->metadataArray
866 );
867 } else {
868 $this->metadataBlobs = [];
869 $this->unloadedMetadataBlobs = [];
870 }
871 } else {
872 $logger = LoggerFactory::getInstance( 'LocalFile' );
873 $logger->warning( __METHOD__ . ' given invalid metadata of type ' .
874 gettype( $info['metadata'] ) );
875 $this->metadataArray = [];
876 }
877 $this->extraDataLoaded = true;
878 }
879 }
880
896 public function isMissing() {
897 if ( $this->missing === null ) {
898 $fileExists = $this->repo->fileExists( $this->getVirtualUrl() );
899 $this->missing = !$fileExists;
900 }
901
902 return $this->missing;
903 }
904
912 public function getWidth( $page = 1 ) {
913 $page = (int)$page;
914 if ( $page < 1 ) {
915 $page = 1;
916 }
917
918 $this->load();
919
920 if ( $this->isMultipage() ) {
921 $handler = $this->getHandler();
922 if ( !$handler ) {
923 return 0;
924 }
925 $dim = $handler->getPageDimensions( $this, $page );
926 if ( $dim ) {
927 return $dim['width'];
928 } else {
929 // For non-paged media, the false goes through an
930 // intval, turning failure into 0, so do same here.
931 return 0;
932 }
933 } else {
934 return $this->width;
935 }
936 }
937
945 public function getHeight( $page = 1 ) {
946 $page = (int)$page;
947 if ( $page < 1 ) {
948 $page = 1;
949 }
950
951 $this->load();
952
953 if ( $this->isMultipage() ) {
954 $handler = $this->getHandler();
955 if ( !$handler ) {
956 return 0;
957 }
958 $dim = $handler->getPageDimensions( $this, $page );
959 if ( $dim ) {
960 return $dim['height'];
961 } else {
962 // For non-paged media, the false goes through an
963 // intval, turning failure into 0, so do same here.
964 return 0;
965 }
966 } else {
967 return $this->height;
968 }
969 }
970
978 public function getDescriptionShortUrl() {
979 if ( !$this->title ) {
980 return null; // Avoid hard failure when the file does not exist. T221812
981 }
982
983 $pageId = $this->title->getArticleID();
984
985 if ( $pageId ) {
986 $url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
987 if ( $url !== false ) {
988 return $url;
989 }
990 }
991 return null;
992 }
993
1000 public function getMetadata() {
1001 $data = $this->getMetadataArray();
1002 if ( !$data ) {
1003 return '';
1004 } elseif ( array_keys( $data ) === [ '_error' ] ) {
1005 // Legacy error encoding
1006 return $data['_error'];
1007 } else {
1008 return serialize( $this->getMetadataArray() );
1009 }
1010 }
1011
1018 public function getMetadataArray(): array {
1019 $this->load( self::LOAD_ALL );
1020 if ( $this->unloadedMetadataBlobs ) {
1021 return $this->getMetadataItems(
1022 array_unique( array_merge(
1023 array_keys( $this->metadataArray ),
1024 array_keys( $this->unloadedMetadataBlobs )
1025 ) )
1026 );
1027 }
1028 return $this->metadataArray;
1029 }
1030
1031 public function getMetadataItems( array $itemNames ): array {
1032 $this->load( self::LOAD_ALL );
1033 $result = [];
1034 $addresses = [];
1035 foreach ( $itemNames as $itemName ) {
1036 if ( array_key_exists( $itemName, $this->metadataArray ) ) {
1037 $result[$itemName] = $this->metadataArray[$itemName];
1038 } elseif ( isset( $this->unloadedMetadataBlobs[$itemName] ) ) {
1039 $addresses[$itemName] = $this->unloadedMetadataBlobs[$itemName];
1040 }
1041 }
1042
1043 if ( $addresses ) {
1044 $resultFromBlob = $this->metadataStorageHelper->getMetadataFromBlobStore( $addresses );
1045 foreach ( $addresses as $itemName => $address ) {
1046 unset( $this->unloadedMetadataBlobs[$itemName] );
1047 $value = $resultFromBlob[$itemName] ?? null;
1048 if ( $value !== null ) {
1049 $result[$itemName] = $value;
1050 $this->metadataArray[$itemName] = $value;
1051 }
1052 }
1053 }
1054 return $result;
1055 }
1056
1068 public function getMetadataForDb( IReadableDatabase $db ) {
1069 $this->load( self::LOAD_ALL );
1070 if ( !$this->metadataArray && !$this->metadataBlobs ) {
1071 $s = '';
1072 } elseif ( $this->repo->isJsonMetadataEnabled() ) {
1073 $s = $this->getJsonMetadata();
1074 } else {
1075 $s = serialize( $this->getMetadataArray() );
1076 }
1077 if ( !is_string( $s ) ) {
1078 throw new RuntimeException( 'Could not serialize image metadata value for DB' );
1079 }
1080 return $db->encodeBlob( $s );
1081 }
1082
1089 private function getJsonMetadata() {
1090 // Directly store data that is not already in BlobStore
1091 $envelope = [
1092 'data' => array_diff_key( $this->metadataArray, $this->metadataBlobs )
1093 ];
1094
1095 // Also store the blob addresses
1096 if ( $this->metadataBlobs ) {
1097 $envelope['blobs'] = $this->metadataBlobs;
1098 }
1099
1100 [ $s, $blobAddresses ] = $this->metadataStorageHelper->getJsonMetadata( $this, $envelope );
1101
1102 // Repeated calls to this function should not keep inserting more blobs
1103 $this->metadataBlobs += $blobAddresses;
1104
1105 return $s;
1106 }
1107
1114 private function isMetadataOversize() {
1115 if ( !$this->repo->isSplitMetadataEnabled() ) {
1116 return false;
1117 }
1118 $threshold = $this->repo->getSplitMetadataThreshold();
1119 $directItems = array_diff_key( $this->metadataArray, $this->metadataBlobs );
1120 foreach ( $directItems as $value ) {
1121 if ( strlen( $this->metadataStorageHelper->jsonEncode( $value ) ) > $threshold ) {
1122 return true;
1123 }
1124 }
1125 return false;
1126 }
1127
1136 protected function loadMetadataFromDbFieldValue( IReadableDatabase $db, $metadataBlob ) {
1137 $this->loadMetadataFromString( $db->decodeBlob( $metadataBlob ) );
1138 }
1139
1147 protected function loadMetadataFromString( $metadataString ) {
1148 $this->extraDataLoaded = true;
1149 $this->metadataArray = [];
1150 $this->metadataBlobs = [];
1151 $this->unloadedMetadataBlobs = [];
1152 $metadataString = (string)$metadataString;
1153 if ( $metadataString === '' ) {
1154 $this->metadataSerializationFormat = self::MDS_EMPTY;
1155 return;
1156 }
1157 if ( $metadataString[0] === '{' ) {
1158 $envelope = $this->metadataStorageHelper->jsonDecode( $metadataString );
1159 if ( !$envelope ) {
1160 // Legacy error encoding
1161 $this->metadataArray = [ '_error' => $metadataString ];
1162 $this->metadataSerializationFormat = self::MDS_LEGACY;
1163 } else {
1164 $this->metadataSerializationFormat = self::MDS_JSON;
1165 if ( isset( $envelope['data'] ) ) {
1166 $this->metadataArray = $envelope['data'];
1167 }
1168 if ( isset( $envelope['blobs'] ) ) {
1169 $this->metadataBlobs = $this->unloadedMetadataBlobs = $envelope['blobs'];
1170 }
1171 }
1172 } else {
1173 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1174 $data = @unserialize( $metadataString );
1175 if ( !is_array( $data ) ) {
1176 // Legacy error encoding
1177 $data = [ '_error' => $metadataString ];
1178 $this->metadataSerializationFormat = self::MDS_LEGACY;
1179 } else {
1180 $this->metadataSerializationFormat = self::MDS_PHP;
1181 }
1182 $this->metadataArray = $data;
1183 }
1184 }
1185
1190 public function getBitDepth() {
1191 $this->load();
1192
1193 return (int)$this->bits;
1194 }
1195
1201 public function getSize() {
1202 $this->load();
1203
1204 return $this->size;
1205 }
1206
1212 public function getMimeType() {
1213 $this->load();
1214
1215 return $this->mime;
1216 }
1217
1224 public function getMediaType() {
1225 $this->load();
1226
1227 return $this->media_type;
1228 }
1229
1241 public function exists() {
1242 $this->load();
1243
1244 return $this->fileExists;
1245 }
1246
1268 protected function getThumbnails( $archiveName = false ) {
1269 if ( $archiveName ) {
1270 $dir = $this->getArchiveThumbPath( $archiveName );
1271 } else {
1272 $dir = $this->getThumbPath();
1273 }
1274
1275 $backend = $this->repo->getBackend();
1276 $files = [ $dir ];
1277 try {
1278 $iterator = $backend->getFileList( [ 'dir' => $dir, 'forWrite' => true ] );
1279 if ( $iterator !== null ) {
1280 foreach ( $iterator as $file ) {
1281 $files[] = $file;
1282 }
1283 }
1284 } catch ( FileBackendError $e ) {
1285 } // suppress (T56674)
1286
1287 return $files;
1288 }
1289
1298 public function purgeCache( $options = [] ) {
1299 // Refresh metadata in memcached, but don't touch thumbnails or CDN
1300 $this->maybeUpgradeRow();
1301 $this->invalidateCache();
1302
1303 // Delete thumbnails
1304 $this->purgeThumbnails( $options );
1305
1306 // Purge CDN cache for this file
1307 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1308 $hcu->purgeUrls(
1309 $this->getUrl(),
1310 !empty( $options['forThumbRefresh'] )
1311 ? $hcu::PURGE_PRESEND // just a manual purge
1312 : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1313 );
1314 }
1315
1321 public function purgeOldThumbnails( $archiveName ) {
1322 // Get a list of old thumbnails
1323 $thumbs = $this->getThumbnails( $archiveName );
1324
1325 // Delete thumbnails from storage, and prevent the directory itself from being purged
1326 $dir = array_shift( $thumbs );
1327 $this->purgeThumbList( $dir, $thumbs );
1328
1329 $urls = [];
1330 foreach ( $thumbs as $thumb ) {
1331 $urls[] = $this->getArchiveThumbUrl( $archiveName, $thumb );
1332 }
1333
1334 // Purge any custom thumbnail caches
1335 $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, $archiveName, $urls );
1336
1337 // Purge the CDN
1338 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1339 $hcu->purgeUrls( $urls, $hcu::PURGE_PRESEND );
1340 }
1341
1348 public function purgeThumbnails( $options = [] ) {
1349 $thumbs = $this->getThumbnails();
1350
1351 // Delete thumbnails from storage, and prevent the directory itself from being purged
1352 $dir = array_shift( $thumbs );
1353 $this->purgeThumbList( $dir, $thumbs );
1354
1355 // Always purge all files from CDN regardless of handler filters
1356 $urls = [];
1357 foreach ( $thumbs as $thumb ) {
1358 $urls[] = $this->getThumbUrl( $thumb );
1359 }
1360
1361 // Give the media handler a chance to filter the file purge list
1362 if ( !empty( $options['forThumbRefresh'] ) ) {
1363 $handler = $this->getHandler();
1364 if ( $handler ) {
1365 $handler->filterThumbnailPurgeList( $thumbs, $options );
1366 }
1367 }
1368
1369 // Purge any custom thumbnail caches
1370 $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, false, $urls );
1371
1372 // Purge the CDN
1373 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1374 $hcu->purgeUrls(
1375 $urls,
1376 !empty( $options['forThumbRefresh'] )
1377 ? $hcu::PURGE_PRESEND // just a manual purge
1378 : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1379 );
1380 }
1381
1388 public function prerenderThumbnails() {
1389 $uploadThumbnailRenderMap = MediaWikiServices::getInstance()
1390 ->getMainConfig()->get( MainConfigNames::UploadThumbnailRenderMap );
1391
1392 $jobs = [];
1393
1394 $sizes = $uploadThumbnailRenderMap;
1395 rsort( $sizes );
1396
1397 foreach ( $sizes as $size ) {
1398 if ( $this->isMultipage() ) {
1399 // (T309114) Only trigger render jobs up to MAX_PAGE_RENDER_JOBS to avoid
1400 // a flood of jobs for huge files.
1401 $pageLimit = min( $this->pageCount(), self::MAX_PAGE_RENDER_JOBS );
1402
1403 $jobs[] = new ThumbnailRenderJob(
1404 $this->getTitle(),
1405 [
1406 'transformParams' => [ 'width' => $size, 'page' => 1 ],
1407 'enqueueNextPage' => true,
1408 'pageLimit' => $pageLimit
1409 ]
1410 );
1411 } elseif ( $this->isVectorized() || $this->getWidth() > $size ) {
1412 $jobs[] = new ThumbnailRenderJob(
1413 $this->getTitle(),
1414 [ 'transformParams' => [ 'width' => $size ] ]
1415 );
1416 }
1417 }
1418
1419 if ( $jobs ) {
1420 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs );
1421 }
1422 }
1423
1430 protected function purgeThumbList( $dir, $files ) {
1431 $fileListDebug = strtr(
1432 var_export( $files, true ),
1433 [ "\n" => '' ]
1434 );
1435 wfDebug( __METHOD__ . ": $fileListDebug" );
1436
1437 if ( $this->repo->supportsSha1URLs() ) {
1438 $reference = $this->getSha1();
1439 } else {
1440 $reference = $this->getName();
1441 }
1442
1443 $purgeList = [];
1444 foreach ( $files as $file ) {
1445 # Check that the reference (filename or sha1) is part of the thumb name
1446 # This is a basic check to avoid erasing unrelated directories
1447 if ( str_contains( $file, $reference )
1448 || str_contains( $file, "-thumbnail" ) // "short" thumb name
1449 ) {
1450 $purgeList[] = "{$dir}/{$file}";
1451 }
1452 }
1453
1454 # Delete the thumbnails
1455 $this->repo->quickPurgeBatch( $purgeList );
1456 # Clear out the thumbnail directory if empty
1457 $this->repo->quickCleanDir( $dir );
1458 }
1459
1471 public function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1472 if ( !$this->exists() ) {
1473 return []; // Avoid hard failure when the file does not exist. T221812
1474 }
1475
1476 $dbr = $this->repo->getReplicaDB();
1477 $oldFileQuery = OldLocalFile::getQueryInfo();
1478
1479 $tables = $oldFileQuery['tables'];
1480 $fields = $oldFileQuery['fields'];
1481 $join_conds = $oldFileQuery['joins'];
1482 $conds = $opts = [];
1483 $eq = $inc ? '=' : '';
1484 $conds[] = $dbr->expr( 'oi_name', '=', $this->title->getDBkey() );
1485
1486 if ( $start ) {
1487 $conds[] = $dbr->expr( 'oi_timestamp', "<$eq", $dbr->timestamp( $start ) );
1488 }
1489
1490 if ( $end ) {
1491 $conds[] = $dbr->expr( 'oi_timestamp', ">$eq", $dbr->timestamp( $end ) );
1492 }
1493
1494 if ( $limit ) {
1495 $opts['LIMIT'] = $limit;
1496 }
1497
1498 // Search backwards for time > x queries
1499 $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
1500 $opts['ORDER BY'] = "oi_timestamp $order";
1501 $opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1502
1503 $this->getHookRunner()->onLocalFile__getHistory( $this, $tables, $fields,
1504 $conds, $opts, $join_conds );
1505
1506 $res = $dbr->newSelectQueryBuilder()
1507 ->tables( $tables )
1508 ->fields( $fields )
1509 ->conds( $conds )
1510 ->caller( __METHOD__ )
1511 ->options( $opts )
1512 ->joinConds( $join_conds )
1513 ->fetchResultSet();
1514 $r = [];
1515
1516 foreach ( $res as $row ) {
1517 $r[] = $this->repo->newFileFromRow( $row );
1518 }
1519
1520 if ( $order == 'ASC' ) {
1521 $r = array_reverse( $r ); // make sure it ends up descending
1522 }
1523
1524 return $r;
1525 }
1526
1537 public function nextHistoryLine() {
1538 if ( !$this->exists() ) {
1539 return false; // Avoid hard failure when the file does not exist. T221812
1540 }
1541
1542 # Polymorphic function name to distinguish foreign and local fetches
1543 $fname = static::class . '::' . __FUNCTION__;
1544
1545 $dbr = $this->repo->getReplicaDB();
1546
1547 if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1548 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr );
1549
1550 $queryBuilder->fields( [ 'oi_archive_name' => $dbr->addQuotes( '' ), 'oi_deleted' => '0' ] )
1551 ->where( [ 'img_name' => $this->title->getDBkey() ] );
1552 $this->historyRes = $queryBuilder->caller( $fname )->fetchResultSet();
1553
1554 if ( $this->historyRes->numRows() == 0 ) {
1555 $this->historyRes = null;
1556
1557 return false;
1558 }
1559 } elseif ( $this->historyLine == 1 ) {
1560 $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr );
1561
1562 $this->historyRes = $queryBuilder->where( [ 'oi_name' => $this->title->getDBkey() ] )
1563 ->orderBy( 'oi_timestamp', SelectQueryBuilder::SORT_DESC )
1564 ->caller( $fname )->fetchResultSet();
1565 }
1566 $this->historyLine++;
1567
1568 return $this->historyRes->fetchObject();
1569 }
1570
1575 public function resetHistory() {
1576 $this->historyLine = 0;
1577
1578 if ( $this->historyRes !== null ) {
1579 $this->historyRes = null;
1580 }
1581 }
1582
1616 public function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1617 $timestamp = false, Authority $uploader = null, $tags = [],
1618 $createNullRevision = true, $revert = false
1619 ) {
1620 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1621 return $this->readOnlyFatalStatus();
1622 } elseif ( MediaWikiServices::getInstance()->getRevisionStore()->isReadOnly() ) {
1623 // Check this in advance to avoid writing to FileBackend and the file tables,
1624 // only to fail on insert the revision due to the text store being unavailable.
1625 return $this->readOnlyFatalStatus();
1626 }
1627
1628 $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1629 if ( !$props ) {
1630 if ( FileRepo::isVirtualUrl( $srcPath )
1631 || FileBackend::isStoragePath( $srcPath )
1632 ) {
1633 $props = $this->repo->getFileProps( $srcPath );
1634 } else {
1635 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
1636 $props = $mwProps->getPropsFromPath( $srcPath, true );
1637 }
1638 }
1639
1640 $options = [];
1641 $handler = MediaHandler::getHandler( $props['mime'] );
1642 if ( $handler ) {
1643 if ( is_string( $props['metadata'] ) ) {
1644 // This supports callers directly fabricating a metadata
1645 // property using serialize(). Normally the metadata property
1646 // comes from MWFileProps, in which case it won't be a string.
1647 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1648 $metadata = @unserialize( $props['metadata'] );
1649 } else {
1650 $metadata = $props['metadata'];
1651 }
1652
1653 if ( is_array( $metadata ) ) {
1654 $options['headers'] = $handler->getContentHeaders( $metadata );
1655 }
1656 } else {
1657 $options['headers'] = [];
1658 }
1659
1660 // Trim spaces on user supplied text
1661 $comment = trim( $comment );
1662
1663 $status = $this->publish( $src, $flags, $options );
1664
1665 if ( $status->successCount >= 2 ) {
1666 // There will be a copy+(one of move,copy,store).
1667 // The first succeeding does not commit us to updating the DB
1668 // since it simply copied the current version to a timestamped file name.
1669 // It is only *preferable* to avoid leaving such files orphaned.
1670 // Once the second operation goes through, then the current version was
1671 // updated and we must therefore update the DB too.
1672 $oldver = $status->value;
1673
1674 $uploadStatus = $this->recordUpload3(
1675 $oldver,
1676 $comment,
1677 $pageText,
1678 $uploader ?? RequestContext::getMain()->getAuthority(),
1679 $props,
1680 $timestamp,
1681 $tags,
1682 $createNullRevision,
1683 $revert
1684 );
1685 if ( !$uploadStatus->isOK() ) {
1686 if ( $uploadStatus->hasMessage( 'filenotfound' ) ) {
1687 // update filenotfound error with more specific path
1688 $status->fatal( 'filenotfound', $srcPath );
1689 } else {
1690 $status->merge( $uploadStatus );
1691 }
1692 }
1693 }
1694
1695 return $status;
1696 }
1697
1714 public function recordUpload3(
1715 string $oldver,
1716 string $comment,
1717 string $pageText,
1718 Authority $performer,
1719 $props = false,
1720 $timestamp = false,
1721 $tags = [],
1722 bool $createNullRevision = true,
1723 bool $revert = false
1724 ): Status {
1725 $dbw = $this->repo->getPrimaryDB();
1726
1727 # Imports or such might force a certain timestamp; otherwise we generate
1728 # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1729 if ( $timestamp === false ) {
1730 $timestamp = $dbw->timestamp();
1731 $allowTimeKludge = true;
1732 } else {
1733 $allowTimeKludge = false;
1734 }
1735
1736 $props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1737 $props['description'] = $comment;
1738 $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1739 $this->setProps( $props );
1740
1741 # Fail now if the file isn't there
1742 if ( !$this->fileExists ) {
1743 wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!" );
1744
1745 return Status::newFatal( 'filenotfound', $this->getRel() );
1746 }
1747
1748 $mimeAnalyzer = MediaWikiServices::getInstance()->getMimeAnalyzer();
1749 if ( !$mimeAnalyzer->isValidMajorMimeType( $this->major_mime ) ) {
1750 $this->major_mime = 'unknown';
1751 }
1752
1753 $actorNormalizaton = MediaWikiServices::getInstance()->getActorNormalization();
1754
1755 $dbw->startAtomic( __METHOD__ );
1756
1757 $actorId = $actorNormalizaton->acquireActorId( $performer->getUser(), $dbw );
1758 $this->user = $performer->getUser();
1759
1760 # Test to see if the row exists using INSERT IGNORE
1761 # This avoids race conditions by locking the row until the commit, and also
1762 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1763 $commentStore = MediaWikiServices::getInstance()->getCommentStore();
1764 $commentFields = $commentStore->insert( $dbw, 'img_description', $comment );
1765 $actorFields = [ 'img_actor' => $actorId ];
1766 $dbw->newInsertQueryBuilder()
1767 ->insertInto( 'image' )
1768 ->ignore()
1769 ->row( [
1770 'img_name' => $this->getName(),
1771 'img_size' => $this->size,
1772 'img_width' => intval( $this->width ),
1773 'img_height' => intval( $this->height ),
1774 'img_bits' => $this->bits,
1775 'img_media_type' => $this->media_type,
1776 'img_major_mime' => $this->major_mime,
1777 'img_minor_mime' => $this->minor_mime,
1778 'img_timestamp' => $dbw->timestamp( $timestamp ),
1779 'img_metadata' => $this->getMetadataForDb( $dbw ),
1780 'img_sha1' => $this->sha1
1781 ] + $commentFields + $actorFields )
1782 ->caller( __METHOD__ )->execute();
1783 $reupload = ( $dbw->affectedRows() == 0 );
1784
1785 if ( $reupload ) {
1786 $row = $dbw->newSelectQueryBuilder()
1787 ->select( [ 'img_timestamp', 'img_sha1' ] )
1788 ->from( 'image' )
1789 ->where( [ 'img_name' => $this->getName() ] )
1790 ->caller( __METHOD__ )->fetchRow();
1791
1792 if ( $row && $row->img_sha1 === $this->sha1 ) {
1793 $dbw->endAtomic( __METHOD__ );
1794 wfDebug( __METHOD__ . ": File " . $this->getRel() . " already exists!" );
1795 $title = Title::newFromText( $this->getName(), NS_FILE );
1796 return Status::newFatal( 'fileexists-no-change', $title->getPrefixedText() );
1797 }
1798
1799 if ( $allowTimeKludge ) {
1800 # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1801 $lUnixtime = $row ? (int)wfTimestamp( TS_UNIX, $row->img_timestamp ) : false;
1802 # Avoid a timestamp that is not newer than the last version
1803 # TODO: the image/oldimage tables should be like page/revision with an ID field
1804 if ( $lUnixtime && (int)wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
1805 sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1806 $timestamp = $dbw->timestamp( $lUnixtime + 1 );
1807 $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1808 }
1809 }
1810
1811 $tables = [ 'image' ];
1812 $fields = [
1813 'oi_name' => 'img_name',
1814 'oi_archive_name' => $dbw->addQuotes( $oldver ),
1815 'oi_size' => 'img_size',
1816 'oi_width' => 'img_width',
1817 'oi_height' => 'img_height',
1818 'oi_bits' => 'img_bits',
1819 'oi_description_id' => 'img_description_id',
1820 'oi_timestamp' => 'img_timestamp',
1821 'oi_metadata' => 'img_metadata',
1822 'oi_media_type' => 'img_media_type',
1823 'oi_major_mime' => 'img_major_mime',
1824 'oi_minor_mime' => 'img_minor_mime',
1825 'oi_sha1' => 'img_sha1',
1826 'oi_actor' => 'img_actor',
1827 ];
1828 $joins = [];
1829
1830 # (T36993) Note: $oldver can be empty here, if the previous
1831 # version of the file was broken. Allow registration of the new
1832 # version to continue anyway, because that's better than having
1833 # an image that's not fixable by user operations.
1834 # Collision, this is an update of a file
1835 # Insert previous contents into oldimage
1836 $dbw->insertSelect( 'oldimage', $tables, $fields,
1837 [ 'img_name' => $this->getName() ], __METHOD__, [], [], $joins );
1838
1839 # Update the current image row
1840 $dbw->newUpdateQueryBuilder()
1841 ->update( 'image' )
1842 ->set( [
1843 'img_size' => $this->size,
1844 'img_width' => intval( $this->width ),
1845 'img_height' => intval( $this->height ),
1846 'img_bits' => $this->bits,
1847 'img_media_type' => $this->media_type,
1848 'img_major_mime' => $this->major_mime,
1849 'img_minor_mime' => $this->minor_mime,
1850 'img_timestamp' => $dbw->timestamp( $timestamp ),
1851 'img_metadata' => $this->getMetadataForDb( $dbw ),
1852 'img_sha1' => $this->sha1
1853 ] + $commentFields + $actorFields )
1854 ->where( [ 'img_name' => $this->getName() ] )
1855 ->caller( __METHOD__ )->execute();
1856 }
1857
1858 $descTitle = $this->getTitle();
1859 $descId = $descTitle->getArticleID();
1860 $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $descTitle );
1861 if ( !$wikiPage instanceof WikiFilePage ) {
1862 throw new UnexpectedValueException( 'Cannot obtain instance of WikiFilePage for ' . $this->getName()
1863 . ', got instance of ' . get_class( $wikiPage ) );
1864 }
1865 $wikiPage->setFile( $this );
1866
1867 // Determine log action. If reupload is done by reverting, use a special log_action.
1868 if ( $revert ) {
1869 $logAction = 'revert';
1870 } elseif ( $reupload ) {
1871 $logAction = 'overwrite';
1872 } else {
1873 $logAction = 'upload';
1874 }
1875 // Add the log entry...
1876 $logEntry = new ManualLogEntry( 'upload', $logAction );
1877 $logEntry->setTimestamp( $this->timestamp );
1878 $logEntry->setPerformer( $performer->getUser() );
1879 $logEntry->setComment( $comment );
1880 $logEntry->setTarget( $descTitle );
1881 // Allow people using the api to associate log entries with the upload.
1882 // Log has a timestamp, but sometimes different from upload timestamp.
1883 $logEntry->setParameters(
1884 [
1885 'img_sha1' => $this->sha1,
1886 'img_timestamp' => $timestamp,
1887 ]
1888 );
1889 // Note we keep $logId around since during new image
1890 // creation, page doesn't exist yet, so log_page = 0
1891 // but we want it to point to the page we're making,
1892 // so we later modify the log entry.
1893 // For a similar reason, we avoid making an RC entry
1894 // now and wait until the page exists.
1895 $logId = $logEntry->insert();
1896
1897 if ( $descTitle->exists() ) {
1898 if ( $createNullRevision ) {
1899 $revStore = MediaWikiServices::getInstance()->getRevisionStore();
1900 // Use own context to get the action text in content language
1901 $formatter = LogFormatter::newFromEntry( $logEntry );
1902 $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1903 $editSummary = $formatter->getPlainActionText();
1904 $summary = CommentStoreComment::newUnsavedComment( $editSummary );
1905 $nullRevRecord = $revStore->newNullRevision(
1906 $dbw,
1907 $descTitle,
1908 $summary,
1909 false,
1910 $performer->getUser()
1911 );
1912
1913 if ( $nullRevRecord ) {
1914 $inserted = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
1915
1916 $this->getHookRunner()->onRevisionFromEditComplete(
1917 $wikiPage,
1918 $inserted,
1919 $inserted->getParentId(),
1920 $performer->getUser(),
1921 $tags
1922 );
1923
1924 $wikiPage->updateRevisionOn( $dbw, $inserted );
1925 // Associate null revision id
1926 $logEntry->setAssociatedRevId( $inserted->getId() );
1927 }
1928 }
1929
1930 $newPageContent = null;
1931 } else {
1932 // Make the description page and RC log entry post-commit
1933 $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1934 }
1935
1936 // NOTE: Even after ending this atomic section, we are probably still in the implicit
1937 // transaction started by any prior master query in the request. We cannot yet safely
1938 // schedule jobs, see T263301.
1939 $dbw->endAtomic( __METHOD__ );
1940 $fname = __METHOD__;
1941
1942 # Do some cache purges after final commit so that:
1943 # a) Changes are more likely to be seen post-purge
1944 # b) They won't cause rollback of the log publish/update above
1945 $purgeUpdate = new AutoCommitUpdate(
1946 $dbw,
1947 __METHOD__,
1948 function () use (
1949 $reupload, $wikiPage, $newPageContent, $comment, $performer,
1950 $logEntry, $logId, $descId, $tags, $fname
1951 ) {
1952 # Update memcache after the commit
1953 $this->invalidateCache();
1954
1955 $updateLogPage = false;
1956 if ( $newPageContent ) {
1957 # New file page; create the description page.
1958 # There's already a log entry, so don't make a second RC entry
1959 # CDN and file cache for the description page are purged by doUserEditContent.
1960 $status = $wikiPage->doUserEditContent(
1961 $newPageContent,
1962 $performer,
1963 $comment,
1965 );
1966
1967 $revRecord = $status->getNewRevision();
1968 if ( $revRecord ) {
1969 // Associate new page revision id
1970 $logEntry->setAssociatedRevId( $revRecord->getId() );
1971
1972 // This relies on the resetArticleID() call in WikiPage::insertOn(),
1973 // which is triggered on $descTitle by doUserEditContent() above.
1974 $updateLogPage = $revRecord->getPageId();
1975 }
1976 } else {
1977 # Existing file page: invalidate description page cache
1978 $title = $wikiPage->getTitle();
1979 $title->invalidateCache();
1980 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1981 $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
1982 # Allow the new file version to be patrolled from the page footer
1984 }
1985
1986 # Update associated rev id. This should be done by $logEntry->insert() earlier,
1987 # but setAssociatedRevId() wasn't called at that point yet...
1988 $logParams = $logEntry->getParameters();
1989 $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
1990 $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
1991 if ( $updateLogPage ) {
1992 # Also log page, in case where we just created it above
1993 $update['log_page'] = $updateLogPage;
1994 }
1995 $this->getRepo()->getPrimaryDB()->newUpdateQueryBuilder()
1996 ->update( 'logging' )
1997 ->set( $update )
1998 ->where( [ 'log_id' => $logId ] )
1999 ->caller( $fname )->execute();
2000
2001 $this->getRepo()->getPrimaryDB()->newInsertQueryBuilder()
2002 ->insertInto( 'log_search' )
2003 ->row( [
2004 'ls_field' => 'associated_rev_id',
2005 'ls_value' => (string)$logEntry->getAssociatedRevId(),
2006 'ls_log_id' => $logId,
2007 ] )
2008 ->caller( $fname )->execute();
2009
2010 # Add change tags, if any
2011 if ( $tags ) {
2012 $logEntry->addTags( $tags );
2013 }
2014
2015 # Uploads can be patrolled
2016 $logEntry->setIsPatrollable( true );
2017
2018 # Now that the log entry is up-to-date, make an RC entry.
2019 $logEntry->publish( $logId );
2020
2021 # Run hook for other updates (typically more cache purging)
2022 $this->getHookRunner()->onFileUpload( $this, $reupload, !$newPageContent );
2023
2024 if ( $reupload ) {
2025 # Delete old thumbnails
2026 $this->purgeThumbnails();
2027 # Remove the old file from the CDN cache
2028 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2029 $hcu->purgeUrls( $this->getUrl(), $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2030 } else {
2031 # Update backlink pages pointing to this title if created
2032 $blcFactory = MediaWikiServices::getInstance()->getBacklinkCacheFactory();
2033 LinksUpdate::queueRecursiveJobsForTable(
2034 $this->getTitle(),
2035 'imagelinks',
2036 'upload-image',
2037 $performer->getUser()->getName(),
2038 $blcFactory->getBacklinkCache( $this->getTitle() )
2039 );
2040 }
2041
2042 $this->prerenderThumbnails();
2043 }
2044 );
2045
2046 # Invalidate cache for all pages using this file
2047 $cacheUpdateJob = HTMLCacheUpdateJob::newForBacklinks(
2048 $this->getTitle(),
2049 'imagelinks',
2050 [ 'causeAction' => 'file-upload', 'causeAgent' => $performer->getUser()->getName() ]
2051 );
2052
2053 // NOTE: We are probably still in the implicit transaction started by DBO_TRX. We should
2054 // only schedule jobs after that transaction was committed, so a job queue failure
2055 // doesn't cause the upload to fail (T263301). Also, we should generally not schedule any
2056 // Jobs or the DeferredUpdates that assume the update is complete until after the
2057 // transaction has been committed and we are sure that the upload was indeed successful.
2058 $dbw->onTransactionCommitOrIdle( static function () use ( $reupload, $purgeUpdate, $cacheUpdateJob ) {
2059 DeferredUpdates::addUpdate( $purgeUpdate, DeferredUpdates::PRESEND );
2060
2061 if ( !$reupload ) {
2062 // This is a new file, so update the image count
2063 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2064 }
2065
2066 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $cacheUpdateJob );
2067 }, __METHOD__ );
2068
2069 return Status::newGood();
2070 }
2071
2088 public function publish( $src, $flags = 0, array $options = [] ) {
2089 return $this->publishTo( $src, $this->getRel(), $flags, $options );
2090 }
2091
2108 protected function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
2109 $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
2110
2111 $repo = $this->getRepo();
2112 if ( $repo->getReadOnlyReason() !== false ) {
2113 return $this->readOnlyFatalStatus();
2114 }
2115
2116 $status = $this->acquireFileLock();
2117 if ( !$status->isOK() ) {
2118 return $status;
2119 }
2120
2121 if ( $this->isOld() ) {
2122 $archiveRel = $dstRel;
2123 $archiveName = basename( $archiveRel );
2124 } else {
2125 $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
2126 $archiveRel = $this->getArchiveRel( $archiveName );
2127 }
2128
2129 if ( $repo->hasSha1Storage() ) {
2130 $sha1 = FileRepo::isVirtualUrl( $srcPath )
2131 ? $repo->getFileSha1( $srcPath )
2132 : FSFile::getSha1Base36FromPath( $srcPath );
2134 $wrapperBackend = $repo->getBackend();
2135 '@phan-var FileBackendDBRepoWrapper $wrapperBackend';
2136 $dst = $wrapperBackend->getPathForSHA1( $sha1 );
2137 $status = $repo->quickImport( $src, $dst );
2138 if ( $flags & File::DELETE_SOURCE ) {
2139 unlink( $srcPath );
2140 }
2141
2142 if ( $this->exists() ) {
2143 $status->value = $archiveName;
2144 }
2145 } else {
2146 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
2147 $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
2148
2149 if ( $status->value == 'new' ) {
2150 $status->value = '';
2151 } else {
2152 $status->value = $archiveName;
2153 }
2154 }
2155
2156 $this->releaseFileLock();
2157 return $status;
2158 }
2159
2178 public function move( $target ) {
2179 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2180 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2181 return $this->readOnlyFatalStatus();
2182 }
2183
2184 wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
2185 $batch = new LocalFileMoveBatch( $this, $target );
2186
2187 $status = $batch->addCurrent();
2188 if ( !$status->isOK() ) {
2189 return $status;
2190 }
2191 $archiveNames = $batch->addOlds();
2192 $status = $batch->execute();
2193
2194 wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
2195
2196 // Purge the source and target files outside the transaction...
2197 $oldTitleFile = $localRepo->newFile( $this->title );
2198 $newTitleFile = $localRepo->newFile( $target );
2199 DeferredUpdates::addUpdate(
2200 new AutoCommitUpdate(
2201 $this->getRepo()->getPrimaryDB(),
2202 __METHOD__,
2203 static function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
2204 $oldTitleFile->purgeEverything();
2205 foreach ( $archiveNames as $archiveName ) {
2207 '@phan-var OldLocalFile $oldTitleFile';
2208 $oldTitleFile->purgeOldThumbnails( $archiveName );
2209 }
2210 $newTitleFile->purgeEverything();
2211 }
2212 ),
2213 DeferredUpdates::PRESEND
2214 );
2215
2216 if ( $status->isOK() ) {
2217 // Now switch the object
2218 $this->title = $target;
2219 // Force regeneration of the name and hashpath
2220 $this->name = null;
2221 $this->hashPath = null;
2222 }
2223
2224 return $status;
2225 }
2226
2243 public function deleteFile( $reason, UserIdentity $user, $suppress = false ) {
2244 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2245 return $this->readOnlyFatalStatus();
2246 }
2247
2248 $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2249
2250 $batch->addCurrent();
2251 // Get old version relative paths
2252 $archiveNames = $batch->addOlds();
2253 $status = $batch->execute();
2254
2255 if ( $status->isOK() ) {
2256 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
2257 }
2258
2259 // To avoid slow purges in the transaction, move them outside...
2260 DeferredUpdates::addUpdate(
2261 new AutoCommitUpdate(
2262 $this->getRepo()->getPrimaryDB(),
2263 __METHOD__,
2264 function () use ( $archiveNames ) {
2265 $this->purgeEverything();
2266 foreach ( $archiveNames as $archiveName ) {
2267 $this->purgeOldThumbnails( $archiveName );
2268 }
2269 }
2270 ),
2271 DeferredUpdates::PRESEND
2272 );
2273
2274 // Purge the CDN
2275 $purgeUrls = [];
2276 foreach ( $archiveNames as $archiveName ) {
2277 $purgeUrls[] = $this->getArchiveUrl( $archiveName );
2278 }
2279
2280 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2281 $hcu->purgeUrls( $purgeUrls, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2282
2283 return $status;
2284 }
2285
2303 public function deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress = false ) {
2304 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2305 return $this->readOnlyFatalStatus();
2306 }
2307
2308 $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2309
2310 $batch->addOld( $archiveName );
2311 $status = $batch->execute();
2312
2313 $this->purgeOldThumbnails( $archiveName );
2314 if ( $status->isOK() ) {
2315 $this->purgeDescription();
2316 }
2317
2318 $url = $this->getArchiveUrl( $archiveName );
2319 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2320 $hcu->purgeUrls( $url, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2321
2322 return $status;
2323 }
2324
2337 public function restore( $versions = [], $unsuppress = false ) {
2338 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2339 return $this->readOnlyFatalStatus();
2340 }
2341
2342 $batch = new LocalFileRestoreBatch( $this, $unsuppress );
2343
2344 if ( !$versions ) {
2345 $batch->addAll();
2346 } else {
2347 $batch->addIds( $versions );
2348 }
2349 $status = $batch->execute();
2350 if ( $status->isGood() ) {
2351 $cleanupStatus = $batch->cleanup();
2352 $cleanupStatus->successCount = 0;
2353 $cleanupStatus->failCount = 0;
2354 $status->merge( $cleanupStatus );
2355 }
2356
2357 return $status;
2358 }
2359
2370 public function getDescriptionUrl() {
2371 // Avoid hard failure when the file does not exist. T221812
2372 return $this->title ? $this->title->getLocalURL() : false;
2373 }
2374
2384 public function getDescriptionText( Language $lang = null ) {
2385 if ( !$this->title ) {
2386 return false; // Avoid hard failure when the file does not exist. T221812
2387 }
2388
2389 $services = MediaWikiServices::getInstance();
2390 $page = $services->getPageStore()->getPageByReference( $this->getTitle() );
2391 if ( !$page ) {
2392 return false;
2393 }
2394
2395 if ( $lang ) {
2396 $parserOptions = ParserOptions::newFromUserAndLang(
2397 RequestContext::getMain()->getUser(),
2398 $lang
2399 );
2400 } else {
2401 $parserOptions = ParserOptions::newFromContext( RequestContext::getMain() );
2402 }
2403
2404 $parseStatus = $services->getParserOutputAccess()
2405 ->getParserOutput( $page, $parserOptions );
2406
2407 if ( !$parseStatus->isGood() ) {
2408 // Rendering failed.
2409 return false;
2410 }
2411 return $parseStatus->getValue()->getText();
2412 }
2413
2421 public function getUploader( int $audience = self::FOR_PUBLIC, Authority $performer = null ): ?UserIdentity {
2422 $this->load();
2423 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
2424 return null;
2425 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $performer ) ) {
2426 return null;
2427 } else {
2428 return $this->user;
2429 }
2430 }
2431
2438 public function getDescription( $audience = self::FOR_PUBLIC, Authority $performer = null ) {
2439 $this->load();
2440 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
2441 return '';
2442 } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $performer ) ) {
2443 return '';
2444 } else {
2445 return $this->description;
2446 }
2447 }
2448
2453 public function getTimestamp() {
2454 $this->load();
2455
2456 return $this->timestamp;
2457 }
2458
2463 public function getDescriptionTouched() {
2464 if ( !$this->exists() ) {
2465 return false; // Avoid hard failure when the file does not exist. T221812
2466 }
2467
2468 // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
2469 // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
2470 // need to differentiate between null (uninitialized) and false (failed to load).
2471 if ( $this->descriptionTouched === null ) {
2472 $touched = $this->repo->getReplicaDB()->newSelectQueryBuilder()
2473 ->select( 'page_touched' )
2474 ->from( 'page' )
2475 ->where( [ 'page_namespace' => $this->title->getNamespace() ] )
2476 ->andWhere( [ 'page_title' => $this->title->getDBkey() ] )
2477 ->caller( __METHOD__ )->fetchField();
2478 $this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
2479 }
2480
2481 return $this->descriptionTouched;
2482 }
2483
2488 public function getSha1() {
2489 $this->load();
2490 return $this->sha1;
2491 }
2492
2496 public function isCacheable() {
2497 $this->load();
2498
2499 // If extra data (metadata) was not loaded then it must have been large
2500 return $this->extraDataLoaded
2501 && strlen( serialize( $this->metadataArray ) ) <= self::CACHE_FIELD_MAX_LEN;
2502 }
2503
2512 public function acquireFileLock( $timeout = 0 ) {
2513 return Status::wrap( $this->getRepo()->getBackend()->lockFiles(
2514 [ $this->getPath() ], LockManager::LOCK_EX, $timeout
2515 ) );
2516 }
2517
2524 public function releaseFileLock() {
2525 return Status::wrap( $this->getRepo()->getBackend()->unlockFiles(
2526 [ $this->getPath() ], LockManager::LOCK_EX
2527 ) );
2528 }
2529
2540 public function lock() {
2541 if ( !$this->locked ) {
2542 $logger = LoggerFactory::getInstance( 'LocalFile' );
2543
2544 $dbw = $this->repo->getPrimaryDB();
2545 $makesTransaction = !$dbw->trxLevel();
2546 $dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
2547 // T56736: use simple lock to handle when the file does not exist.
2548 // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
2549 // Also, that would cause contention on INSERT of similarly named rows.
2550 $status = $this->acquireFileLock( 10 ); // represents all versions of the file
2551 if ( !$status->isGood() ) {
2552 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2553 $logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
2554
2555 throw new LocalFileLockError( $status );
2556 }
2557 // Release the lock *after* commit to avoid row-level contention.
2558 // Make sure it triggers on rollback() as well as commit() (T132921).
2559 $dbw->onTransactionResolution(
2560 function () use ( $logger ) {
2561 $status = $this->releaseFileLock();
2562 if ( !$status->isGood() ) {
2563 $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
2564 }
2565 },
2566 __METHOD__
2567 );
2568 // Callers might care if the SELECT snapshot is safely fresh
2569 $this->lockedOwnTrx = $makesTransaction;
2570 }
2571
2572 $this->locked++;
2573
2574 return $this->lockedOwnTrx;
2575 }
2576
2587 public function unlock() {
2588 if ( $this->locked ) {
2589 --$this->locked;
2590 if ( !$this->locked ) {
2591 $dbw = $this->repo->getPrimaryDB();
2592 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2593 $this->lockedOwnTrx = false;
2594 }
2595 }
2596 }
2597
2601 protected function readOnlyFatalStatus() {
2602 return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
2603 $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2604 }
2605
2609 public function __destruct() {
2610 $this->unlock();
2611 }
2612}
getUser()
getAuthority()
const NS_FILE
Definition Defines.php:71
const EDIT_SUPPRESS_RC
Definition Defines.php:130
const EDIT_NEW
Definition Defines.php:127
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.
getCacheKey()
Get the cache key used to store status.
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Definition Article.php:1423
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
Class representing a non-directory file on the file system.
Definition FSFile.php:32
File backend exception for checked exceptions (e.g.
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Definition FileRepo.php:288
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:74
MediaHandler $handler
Definition File.php:142
assertRepoDefined()
Assert that $this->repo is set to a valid FileRepo instance.
Definition File.php:2467
getName()
Return the name of this file.
Definition File.php:342
const DELETE_SOURCE
Definition File.php:91
getVirtualUrl( $suffix=false)
Get the public zone virtual URL for a current version source file.
Definition File.php:1930
assertTitleDefined()
Assert that $this->title is set to a Title.
Definition File.php:2476
FileRepo LocalRepo ForeignAPIRepo false $repo
Some member variables can be lazy-initialised using __get().
Definition File.php:121
isMultipage()
Returns 'true' if this file is a type which supports multiple pages, e.g.
Definition File.php:2164
Title string false $title
Definition File.php:124
getHandler()
Get a MediaHandler instance for this file.
Definition File.php:1549
string null $name
The name of a file from its title object.
Definition File.php:151
static newForBacklinks(PageReference $page, $table, $params=[])
Base class for language-specific code.
Definition Language.php:65
Helper class for file deletion.
Helper class for file movement.
Helper class for file undeletion.
Local file in the wiki's own database.
Definition LocalFile.php:69
exists()
canRender inherited
setProps( $info)
Set properties in this object to be equal to those given in the associative array $info.
maybeUpgradeRow()
Upgrade a row if it needs it.
static newFromKey( $sha1, $repo, $timestamp=false)
Create a LocalFile from a SHA-1 key Do not call this except from inside a repo class.
array $metadataArray
Unserialized metadata.
getMediaType()
Returns the type of the media in the file.
string[] $unloadedMetadataBlobs
Map of metadata item name to blob address for items that exist but have not yet been loaded into $thi...
deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress=false)
Delete an old version of the file.
move( $target)
getLinksTo inherited
lock()
Start an atomic DB section and lock the image for update or increments a reference counter if the loc...
loadFromRow( $row, $prefix='img_')
Load file metadata from a DB result row.
loadMetadataFromDbFieldValue(IReadableDatabase $db, $metadataBlob)
Unserialize a metadata blob which came from the database and store it in $this.
getCacheKey()
Get the memcached key for the main data for this file, or false if there is no access to the shared c...
getWidth( $page=1)
Return the width of the image.
__destruct()
Clean up any dangling locks.
string $mime
MIME type, determined by MimeAnalyzer::guessMimeType.
reserializeMetadata()
Write the metadata back to the database with the current serialization format.
isMissing()
splitMime inherited
getDescriptionUrl()
isMultipage inherited
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
static getQueryInfo(array $options=[])
Return the tables, fields, and join conditions to be selected to create a new localfile object.
releaseFileLock()
Release a lock acquired with acquireFileLock().
getUploader(int $audience=self::FOR_PUBLIC, Authority $performer=null)
loadFromDB( $flags=0)
Load file metadata from the DB.
load( $flags=0)
Load file metadata from cache or DB, unless already loaded.
loadMetadataFromString( $metadataString)
Unserialize a metadata string which came from some non-DB source, or is the return value of IReadable...
string $media_type
MEDIATYPE_xxx (bitmap, drawing, audio...)
deleteFile( $reason, UserIdentity $user, $suppress=false)
Delete all versions of the file.
acquireFileLock( $timeout=0)
Acquire an exclusive lock on the file, indicating an intention to write to the file backend.
purgeCache( $options=[])
Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
getDescriptionTouched()
loadFromFile( $path=null)
Load metadata from the file itself.
string null $metadataSerializationFormat
One of the MDS_* constants, giving the format of the metadata as stored in the DB,...
int $size
Size in bytes (loadFromXxx)
getDescriptionShortUrl()
Get short description URL for a file based on the page ID.
getThumbnails( $archiveName=false)
getTransformScript inherited
static newFromTitle( $title, $repo, $unused=null)
Create a LocalFile from a title Do not call this except from inside a repo class.
int $height
Image height.
Definition LocalFile.php:96
purgeOldThumbnails( $archiveName)
Delete cached transformed files for an archived version only.
publishTo( $src, $dstRel, $flags=0, array $options=[])
Move or copy a file to a specified location.
getMetadataForDb(IReadableDatabase $db)
Serialize the metadata array for insertion into img_metadata, oi_metadata or fa_metadata.
purgeThumbList( $dir, $files)
Delete a list of thumbnails visible at urls.
unlock()
Decrement the lock reference count and end the atomic section if it reaches zero.
getLazyCacheFields( $prefix='img_')
Returns the list of object properties that are included as-is in the cache, only when they're not too...
getSize()
Returns the size of the image file, in bytes.
invalidateCache()
Purge the file object/metadata cache.
getMimeType()
Returns the MIME type of the file.
bool $extraDataLoaded
Whether or not lazy-loaded data has been loaded from the database.
readOnlyFatalStatus()
string $sha1
SHA-1 base 36 content hash.
getDescription( $audience=self::FOR_PUBLIC, Authority $performer=null)
getHeight( $page=1)
Return the height of the image.
prerenderThumbnails()
Prerenders a configurable set of thumbnails.
resetHistory()
Reset the history pointer to the first element of the history.
unprefixRow( $row, $prefix='img_')
static newFromRow( $row, $repo)
Create a LocalFile from a title Do not call this except from inside a repo class.
publish( $src, $flags=0, array $options=[])
Move or copy a file to its public location.
restore( $versions=[], $unsuppress=false)
Restore all or specified deleted revisions to the given file.
getCacheFields( $prefix='img_')
Returns the list of object properties that are included as-is in the cache.
int $bits
Returned by getimagesize (loadFromXxx)
Definition LocalFile.php:99
getMetadataItems(array $itemNames)
Get multiple elements of the unserialized handler-specific metadata.
getDescriptionText(Language $lang=null)
Get the HTML text of the description page This is not used by ImagePage for local files,...
purgeThumbnails( $options=[])
Delete cached transformed files for the current version only.
loadExtraFromDB()
Load lazy file metadata from the DB.
string $repoClass
int $width
Image width.
Definition LocalFile.php:93
nextHistoryLine()
Returns the history of this file, line by line.
upgradeRow()
Fix assorted version-related problems with the image row by reloading it from the file.
int $deleted
Bitfield akin to rev_deleted.
getMetadata()
Get handler-specific metadata as a serialized string.
getMetadataArray()
Get unserialized handler-specific metadata.
__construct( $title, $repo)
Do not call this except from inside a repo class.
bool $dataLoaded
Whether or not core data has been loaded from the database (loadFromXxx)
bool $fileExists
Does the file exist on disk? (loadFromXxx)
Definition LocalFile.php:90
upload( $src, $comment, $pageText, $flags=0, $props=false, $timestamp=false, Authority $uploader=null, $tags=[], $createNullRevision=true, $revert=false)
getHashPath inherited
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)
string[] $metadataBlobs
Map of metadata item name to blob address.
static makeParamBlob( $params)
Create a blob from a parameter array.
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
MimeMagic helper wrapper.
Class for creating new log entries and inserting them into the database.
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,...
Value object for a comment stored by CommentStore.
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.
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Represents a title within MediaWiki.
Definition Title.php:78
Value object representing a user's identity.
Helper for storage of metadata.
Job for asynchronous rendering of thumbnails, e.g.
Special handling for representing file pages.
Base class for all file backend classes (including multi-write backends).
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.
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...