MediaWiki master
LocalFile.php
Go to the documentation of this file.
1<?php
48
75class LocalFile extends File {
76 private const VERSION = 13; // cache version
77
78 private const CACHE_FIELD_MAX_LEN = 1000;
79
81 private const MDS_EMPTY = 'empty';
82
84 private const MDS_LEGACY = 'legacy';
85
87 private const MDS_PHP = 'php';
88
90 private const MDS_JSON = 'json';
91
93 private const MAX_PAGE_RENDER_JOBS = 50;
94
96 protected $fileExists;
97
99 protected $width;
100
102 protected $height;
103
105 protected $bits;
106
108 protected $media_type;
109
111 protected $mime;
112
114 protected $size;
115
117 protected $metadataArray = [];
118
126
128 protected $metadataBlobs = [];
129
136 protected $unloadedMetadataBlobs = [];
137
139 protected $sha1;
140
142 protected $dataLoaded = false;
143
145 protected $extraDataLoaded = false;
146
148 protected $deleted;
149
151 protected $repoClass = LocalRepo::class;
152
154 private $historyLine = 0;
155
157 private $historyRes = null;
158
160 private $major_mime;
161
163 private $minor_mime;
164
166 private $timestamp;
167
169 private $user;
170
172 private $description;
173
175 private $descriptionTouched;
176
178 private $upgraded;
179
181 private $upgrading;
182
184 private $locked;
185
187 private $lockedOwnTrx;
188
190 private $missing;
191
193 private $metadataStorageHelper;
194
195 // @note: higher than IDBAccessObject constants
196 private const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
197
198 private const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
199
214 public static function newFromTitle( $title, $repo, $unused = null ) {
215 return new static( $title, $repo );
216 }
217
229 public static function newFromRow( $row, $repo ) {
230 $title = Title::makeTitle( NS_FILE, $row->img_name );
231 $file = new static( $title, $repo );
232 $file->loadFromRow( $row );
233
234 return $file;
235 }
236
248 public static function newFromKey( $sha1, $repo, $timestamp = false ) {
249 $dbr = $repo->getReplicaDB();
250 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr );
251
252 $queryBuilder->where( [ 'img_sha1' => $sha1 ] );
253
254 if ( $timestamp ) {
255 $queryBuilder->andWhere( [ 'img_timestamp' => $dbr->timestamp( $timestamp ) ] );
256 }
257
258 $row = $queryBuilder->caller( __METHOD__ )->fetchRow();
259 if ( $row ) {
260 return static::newFromRow( $row, $repo );
261 } else {
262 return false;
263 }
264 }
265
286 public static function getQueryInfo( array $options = [] ) {
287 $dbr = MediaWikiServices::getInstance()->getConnectionProvider()->getReplicaDatabase();
288 $queryInfo = FileSelectQueryBuilder::newForFile( $dbr, $options )->getQueryInfo();
289 // needs remapping...
290 return [
291 'tables' => $queryInfo['tables'],
292 'fields' => $queryInfo['fields'],
293 'joins' => $queryInfo['join_conds'],
294 ];
295 }
296
304 public function __construct( $title, $repo ) {
305 parent::__construct( $title, $repo );
306 $this->metadataStorageHelper = new MetadataStorageHelper( $repo );
307
308 $this->assertRepoDefined();
309 $this->assertTitleDefined();
310 }
311
315 public function getRepo() {
316 return $this->repo;
317 }
318
325 protected function getCacheKey() {
326 return $this->repo->getSharedCacheKey( 'file', sha1( $this->getName() ) );
327 }
328
332 private function loadFromCache() {
333 $this->dataLoaded = false;
334 $this->extraDataLoaded = false;
335
336 $key = $this->getCacheKey();
337 if ( !$key ) {
338 $this->loadFromDB( IDBAccessObject::READ_NORMAL );
339
340 return;
341 }
342
343 $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
344 $cachedValues = $cache->getWithSetCallback(
345 $key,
346 $cache::TTL_WEEK,
347 function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
348 $setOpts += Database::getCacheSetOptions( $this->repo->getReplicaDB() );
349
350 $this->loadFromDB( IDBAccessObject::READ_NORMAL );
351
352 $fields = $this->getCacheFields( '' );
353 $cacheVal = [];
354 $cacheVal['fileExists'] = $this->fileExists;
355 if ( $this->fileExists ) {
356 foreach ( $fields as $field ) {
357 $cacheVal[$field] = $this->$field;
358 }
359 }
360 if ( $this->user ) {
361 $cacheVal['user'] = $this->user->getId();
362 $cacheVal['user_text'] = $this->user->getName();
363 }
364
365 // Don't cache metadata items stored as blobs, since they tend to be large
366 if ( $this->metadataBlobs ) {
367 $cacheVal['metadata'] = array_diff_key(
368 $this->metadataArray, $this->metadataBlobs );
369 // Save the blob addresses
370 $cacheVal['metadataBlobs'] = $this->metadataBlobs;
371 } else {
372 $cacheVal['metadata'] = $this->metadataArray;
373 }
374
375 // Strip off excessive entries from the subset of fields that can become large.
376 // If the cache value gets too large and might not fit in the cache,
377 // causing repeat database queries for each access to the file.
378 foreach ( $this->getLazyCacheFields( '' ) as $field ) {
379 if ( isset( $cacheVal[$field] )
380 && strlen( serialize( $cacheVal[$field] ) ) > 100 * 1024
381 ) {
382 unset( $cacheVal[$field] ); // don't let the value get too big
383 if ( $field === 'metadata' ) {
384 unset( $cacheVal['metadataBlobs'] );
385 }
386 }
387 }
388
389 if ( $this->fileExists ) {
390 $ttl = $cache->adaptiveTTL( (int)wfTimestamp( TS_UNIX, $this->timestamp ), $ttl );
391 } else {
392 $ttl = $cache::TTL_DAY;
393 }
394
395 return $cacheVal;
396 },
397 [ 'version' => self::VERSION ]
398 );
399
400 $this->fileExists = $cachedValues['fileExists'];
401 if ( $this->fileExists ) {
402 $this->setProps( $cachedValues );
403 }
404
405 $this->dataLoaded = true;
406 $this->extraDataLoaded = true;
407 foreach ( $this->getLazyCacheFields( '' ) as $field ) {
408 $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
409 }
410 }
411
415 public function invalidateCache() {
416 $key = $this->getCacheKey();
417 if ( !$key ) {
418 return;
419 }
420
421 $this->repo->getPrimaryDB()->onTransactionPreCommitOrIdle(
422 static function () use ( $key ) {
423 MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key );
424 },
425 __METHOD__
426 );
427 }
428
436 public function loadFromFile( $path = null ) {
437 $props = $this->repo->getFileProps( $path ?? $this->getVirtualUrl() );
438 $this->setProps( $props );
439 }
440
448 protected function getCacheFields( $prefix = 'img_' ) {
449 if ( $prefix !== '' ) {
450 throw new InvalidArgumentException(
451 __METHOD__ . ' with a non-empty prefix is no longer supported.'
452 );
453 }
454
455 // See self::getQueryInfo() for the fetching of the data from the DB,
456 // self::loadFromRow() for the loading of the object from the DB row,
457 // and self::loadFromCache() for the caching, and self::setProps() for
458 // populating the object from an array of data.
459 return [ 'size', 'width', 'height', 'bits', 'media_type',
460 'major_mime', 'minor_mime', 'timestamp', 'sha1', 'description' ];
461 }
462
470 protected function getLazyCacheFields( $prefix = 'img_' ) {
471 if ( $prefix !== '' ) {
472 throw new InvalidArgumentException(
473 __METHOD__ . ' with a non-empty prefix is no longer supported.'
474 );
475 }
476
477 // Keep this in sync with the omit-lazy option in self::getQueryInfo().
478 return [ 'metadata' ];
479 }
480
486 protected function loadFromDB( $flags = 0 ) {
487 $fname = static::class . '::' . __FUNCTION__;
488
489 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
490 $this->dataLoaded = true;
491 $this->extraDataLoaded = true;
492
493 $dbr = ( $flags & IDBAccessObject::READ_LATEST )
494 ? $this->repo->getPrimaryDB()
495 : $this->repo->getReplicaDB();
496 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr );
497
498 $queryBuilder->where( [ 'img_name' => $this->getName() ] );
499 $row = $queryBuilder->caller( $fname )->fetchRow();
500
501 if ( $row ) {
502 $this->loadFromRow( $row );
503 } else {
504 $this->fileExists = false;
505 }
506 }
507
513 protected function loadExtraFromDB() {
514 if ( !$this->title ) {
515 return; // Avoid hard failure when the file does not exist. T221812
516 }
517
518 $fname = static::class . '::' . __FUNCTION__;
519
520 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
521 $this->extraDataLoaded = true;
522
523 $db = $this->repo->getReplicaDB();
524 $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
525 if ( !$fieldMap ) {
526 $db = $this->repo->getPrimaryDB();
527 $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
528 }
529
530 if ( $fieldMap ) {
531 if ( isset( $fieldMap['metadata'] ) ) {
532 $this->loadMetadataFromDbFieldValue( $db, $fieldMap['metadata'] );
533 }
534 } else {
535 throw new RuntimeException( "Could not find data for image '{$this->getName()}'." );
536 }
537 }
538
544 private function loadExtraFieldsWithTimestamp( IReadableDatabase $dbr, $fname ) {
545 $fieldMap = false;
546
547 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr, [ 'omit-nonlazy' ] );
548 $queryBuilder->where( [ 'img_name' => $this->getName() ] )
549 ->andWhere( [ 'img_timestamp' => $dbr->timestamp( $this->getTimestamp() ) ] );
550 $row = $queryBuilder->caller( $fname )->fetchRow();
551 if ( $row ) {
552 $fieldMap = $this->unprefixRow( $row, 'img_' );
553 } else {
554 # File may have been uploaded over in the meantime; check the old versions
555 $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr, [ 'omit-nonlazy' ] );
556 $row = $queryBuilder->where( [ 'oi_name' => $this->getName() ] )
557 ->andWhere( [ 'oi_timestamp' => $dbr->timestamp( $this->getTimestamp() ) ] )
558 ->caller( __METHOD__ )->fetchRow();
559 if ( $row ) {
560 $fieldMap = $this->unprefixRow( $row, 'oi_' );
561 }
562 }
563
564 return $fieldMap;
565 }
566
572 protected function unprefixRow( $row, $prefix = 'img_' ) {
573 $array = (array)$row;
574 $prefixLength = strlen( $prefix );
575
576 // Double check prefix once
577 if ( substr( array_key_first( $array ), 0, $prefixLength ) !== $prefix ) {
578 throw new InvalidArgumentException( __METHOD__ . ': incorrect $prefix parameter' );
579 }
580
581 $decoded = [];
582 foreach ( $array as $name => $value ) {
583 $decoded[substr( $name, $prefixLength )] = $value;
584 }
585
586 return $decoded;
587 }
588
604 public function loadFromRow( $row, $prefix = 'img_' ) {
605 $this->dataLoaded = true;
606
607 $unprefixed = $this->unprefixRow( $row, $prefix );
608
609 $this->name = $unprefixed['name'];
610 $this->media_type = $unprefixed['media_type'];
611
612 $services = MediaWikiServices::getInstance();
613 $this->description = $services->getCommentStore()
614 ->getComment( "{$prefix}description", $row )->text;
615
616 $this->user = $services->getUserFactory()->newFromAnyId(
617 $unprefixed['user'] ?? null,
618 $unprefixed['user_text'] ?? null,
619 $unprefixed['actor'] ?? null
620 );
621
622 $this->timestamp = wfTimestamp( TS_MW, $unprefixed['timestamp'] );
623
625 $this->repo->getReplicaDB(), $unprefixed['metadata'] );
626
627 if ( empty( $unprefixed['major_mime'] ) ) {
628 $this->major_mime = 'unknown';
629 $this->minor_mime = 'unknown';
630 $this->mime = 'unknown/unknown';
631 } else {
632 if ( !$unprefixed['minor_mime'] ) {
633 $unprefixed['minor_mime'] = 'unknown';
634 }
635 $this->major_mime = $unprefixed['major_mime'];
636 $this->minor_mime = $unprefixed['minor_mime'];
637 $this->mime = $unprefixed['major_mime'] . '/' . $unprefixed['minor_mime'];
638 }
639
640 // Trim zero padding from char/binary field
641 $this->sha1 = rtrim( $unprefixed['sha1'], "\0" );
642
643 // Normalize some fields to integer type, per their database definition.
644 // Use unary + so that overflows will be upgraded to double instead of
645 // being truncated as with intval(). This is important to allow > 2 GiB
646 // files on 32-bit systems.
647 $this->size = +$unprefixed['size'];
648 $this->width = +$unprefixed['width'];
649 $this->height = +$unprefixed['height'];
650 $this->bits = +$unprefixed['bits'];
651
652 // Check for extra fields (deprecated since MW 1.37)
653 $extraFields = array_diff(
654 array_keys( $unprefixed ),
655 [
656 'name', 'media_type', 'description_text', 'description_data',
657 'description_cid', 'user', 'user_text', 'actor', 'timestamp',
658 'metadata', 'major_mime', 'minor_mime', 'sha1', 'size', 'width',
659 'height', 'bits'
660 ]
661 );
662 if ( $extraFields ) {
664 'Passing extra fields (' .
665 implode( ', ', $extraFields )
666 . ') to ' . __METHOD__ . ' was deprecated in MediaWiki 1.37. ' .
667 'Property assignment will be removed in a later version.',
668 '1.37' );
669 foreach ( $extraFields as $field ) {
670 $this->$field = $unprefixed[$field];
671 }
672 }
673
674 $this->fileExists = true;
675 }
676
682 public function load( $flags = 0 ) {
683 if ( !$this->dataLoaded ) {
684 if ( $flags & IDBAccessObject::READ_LATEST ) {
685 $this->loadFromDB( $flags );
686 } else {
687 $this->loadFromCache();
688 }
689 }
690
691 if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
692 // @note: loads on name/timestamp to reduce race condition problems
693 $this->loadExtraFromDB();
694 }
695 }
696
701 public function maybeUpgradeRow() {
702 if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() || $this->upgrading ) {
703 return;
704 }
705
706 $upgrade = false;
707 $reserialize = false;
708 if ( $this->media_type === null || $this->mime == 'image/svg' ) {
709 $upgrade = true;
710 } else {
711 $handler = $this->getHandler();
712 if ( $handler ) {
713 $validity = $handler->isFileMetadataValid( $this );
714 if ( $validity === MediaHandler::METADATA_BAD ) {
715 $upgrade = true;
716 } elseif ( $validity === MediaHandler::METADATA_COMPATIBLE
717 && $this->repo->isMetadataUpdateEnabled()
718 ) {
719 $upgrade = true;
720 } elseif ( $this->repo->isJsonMetadataEnabled()
721 && $this->repo->isMetadataReserializeEnabled()
722 ) {
723 if ( $this->repo->isSplitMetadataEnabled() && $this->isMetadataOversize() ) {
724 $reserialize = true;
725 } elseif ( $this->metadataSerializationFormat !== self::MDS_EMPTY &&
726 $this->metadataSerializationFormat !== self::MDS_JSON ) {
727 $reserialize = true;
728 }
729 }
730 }
731 }
732
733 if ( $upgrade || $reserialize ) {
734 $this->upgrading = true;
735 // Defer updates unless in auto-commit CLI mode
736 DeferredUpdates::addCallableUpdate( function () use ( $upgrade ) {
737 $this->upgrading = false; // avoid duplicate updates
738 try {
739 if ( $upgrade ) {
740 $this->upgradeRow();
741 } else {
742 $this->reserializeMetadata();
743 }
744 } catch ( LocalFileLockError $e ) {
745 // let the other process handle it (or do it next time)
746 }
747 } );
748 }
749 }
750
754 public function getUpgraded() {
755 return $this->upgraded;
756 }
757
762 public function upgradeRow() {
763 $dbw = $this->repo->getPrimaryDB();
764
765 // Make a DB query condition that will fail to match the image row if the
766 // image was reuploaded while the upgrade was in process.
767 $freshnessCondition = [ 'img_timestamp' => $dbw->timestamp( $this->getTimestamp() ) ];
768
769 $this->loadFromFile();
770
771 # Don't destroy file info of missing files
772 if ( !$this->fileExists ) {
773 wfDebug( __METHOD__ . ": file does not exist, aborting" );
774
775 return;
776 }
777
778 [ $major, $minor ] = self::splitMime( $this->mime );
779
780 wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema" );
781
782 $dbw->newUpdateQueryBuilder()
783 ->update( 'image' )
784 ->set( [
785 'img_size' => $this->size,
786 'img_width' => $this->width,
787 'img_height' => $this->height,
788 'img_bits' => $this->bits,
789 'img_media_type' => $this->media_type,
790 'img_major_mime' => $major,
791 'img_minor_mime' => $minor,
792 'img_metadata' => $this->getMetadataForDb( $dbw ),
793 'img_sha1' => $this->sha1,
794 ] )
795 ->where( [ 'img_name' => $this->getName() ] )
796 ->andWhere( $freshnessCondition )
797 ->caller( __METHOD__ )->execute();
798
799 $this->invalidateCache();
800
801 $this->upgraded = true; // avoid rework/retries
802 }
803
808 protected function reserializeMetadata() {
809 if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
810 return;
811 }
812 $dbw = $this->repo->getPrimaryDB();
813 $dbw->newUpdateQueryBuilder()
814 ->update( 'image' )
815 ->set( [ 'img_metadata' => $this->getMetadataForDb( $dbw ) ] )
816 ->where( [
817 'img_name' => $this->name,
818 'img_timestamp' => $dbw->timestamp( $this->timestamp ),
819 ] )
820 ->caller( __METHOD__ )->execute();
821 $this->upgraded = true;
822 }
823
835 protected function setProps( $info ) {
836 $this->dataLoaded = true;
837 $fields = $this->getCacheFields( '' );
838 $fields[] = 'fileExists';
839
840 foreach ( $fields as $field ) {
841 if ( isset( $info[$field] ) ) {
842 $this->$field = $info[$field];
843 }
844 }
845
846 // Only our own cache sets these properties, so they both should be present.
847 if ( isset( $info['user'] ) &&
848 isset( $info['user_text'] ) &&
849 $info['user_text'] !== ''
850 ) {
851 $this->user = new UserIdentityValue( $info['user'], $info['user_text'] );
852 }
853
854 // Fix up mime fields
855 if ( isset( $info['major_mime'] ) ) {
856 $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
857 } elseif ( isset( $info['mime'] ) ) {
858 $this->mime = $info['mime'];
859 [ $this->major_mime, $this->minor_mime ] = self::splitMime( $this->mime );
860 }
861
862 if ( isset( $info['metadata'] ) ) {
863 if ( is_string( $info['metadata'] ) ) {
864 $this->loadMetadataFromString( $info['metadata'] );
865 } elseif ( is_array( $info['metadata'] ) ) {
866 $this->metadataArray = $info['metadata'];
867 if ( isset( $info['metadataBlobs'] ) ) {
868 $this->metadataBlobs = $info['metadataBlobs'];
869 $this->unloadedMetadataBlobs = array_diff_key(
870 $this->metadataBlobs,
871 $this->metadataArray
872 );
873 } else {
874 $this->metadataBlobs = [];
875 $this->unloadedMetadataBlobs = [];
876 }
877 } else {
878 $logger = LoggerFactory::getInstance( 'LocalFile' );
879 $logger->warning( __METHOD__ . ' given invalid metadata of type ' .
880 get_debug_type( $info['metadata'] ) );
881 $this->metadataArray = [];
882 }
883 $this->extraDataLoaded = true;
884 }
885 }
886
902 public function isMissing() {
903 if ( $this->missing === null ) {
904 $fileExists = $this->repo->fileExists( $this->getVirtualUrl() );
905 $this->missing = !$fileExists;
906 }
907
908 return $this->missing;
909 }
910
918 public function getWidth( $page = 1 ) {
919 $page = (int)$page;
920 if ( $page < 1 ) {
921 $page = 1;
922 }
923
924 $this->load();
925
926 if ( $this->isMultipage() ) {
927 $handler = $this->getHandler();
928 if ( !$handler ) {
929 return 0;
930 }
931 $dim = $handler->getPageDimensions( $this, $page );
932 if ( $dim ) {
933 return $dim['width'];
934 } else {
935 // For non-paged media, the false goes through an
936 // intval, turning failure into 0, so do same here.
937 return 0;
938 }
939 } else {
940 return $this->width;
941 }
942 }
943
951 public function getHeight( $page = 1 ) {
952 $page = (int)$page;
953 if ( $page < 1 ) {
954 $page = 1;
955 }
956
957 $this->load();
958
959 if ( $this->isMultipage() ) {
960 $handler = $this->getHandler();
961 if ( !$handler ) {
962 return 0;
963 }
964 $dim = $handler->getPageDimensions( $this, $page );
965 if ( $dim ) {
966 return $dim['height'];
967 } else {
968 // For non-paged media, the false goes through an
969 // intval, turning failure into 0, so do same here.
970 return 0;
971 }
972 } else {
973 return $this->height;
974 }
975 }
976
984 public function getDescriptionShortUrl() {
985 if ( !$this->title ) {
986 return null; // Avoid hard failure when the file does not exist. T221812
987 }
988
989 $pageId = $this->title->getArticleID();
990
991 if ( $pageId ) {
992 $url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
993 if ( $url !== false ) {
994 return $url;
995 }
996 }
997 return null;
998 }
999
1006 public function getMetadata() {
1007 $data = $this->getMetadataArray();
1008 if ( !$data ) {
1009 return '';
1010 } elseif ( array_keys( $data ) === [ '_error' ] ) {
1011 // Legacy error encoding
1012 return $data['_error'];
1013 } else {
1014 return serialize( $this->getMetadataArray() );
1015 }
1016 }
1017
1024 public function getMetadataArray(): array {
1025 $this->load( self::LOAD_ALL );
1026 if ( $this->unloadedMetadataBlobs ) {
1027 return $this->getMetadataItems(
1028 array_unique( array_merge(
1029 array_keys( $this->metadataArray ),
1030 array_keys( $this->unloadedMetadataBlobs )
1031 ) )
1032 );
1033 }
1034 return $this->metadataArray;
1035 }
1036
1037 public function getMetadataItems( array $itemNames ): array {
1038 $this->load( self::LOAD_ALL );
1039 $result = [];
1040 $addresses = [];
1041 foreach ( $itemNames as $itemName ) {
1042 if ( array_key_exists( $itemName, $this->metadataArray ) ) {
1043 $result[$itemName] = $this->metadataArray[$itemName];
1044 } elseif ( isset( $this->unloadedMetadataBlobs[$itemName] ) ) {
1045 $addresses[$itemName] = $this->unloadedMetadataBlobs[$itemName];
1046 }
1047 }
1048
1049 if ( $addresses ) {
1050 $resultFromBlob = $this->metadataStorageHelper->getMetadataFromBlobStore( $addresses );
1051 foreach ( $addresses as $itemName => $address ) {
1052 unset( $this->unloadedMetadataBlobs[$itemName] );
1053 $value = $resultFromBlob[$itemName] ?? null;
1054 if ( $value !== null ) {
1055 $result[$itemName] = $value;
1056 $this->metadataArray[$itemName] = $value;
1057 }
1058 }
1059 }
1060 return $result;
1061 }
1062
1074 public function getMetadataForDb( IReadableDatabase $db ) {
1075 $this->load( self::LOAD_ALL );
1076 if ( !$this->metadataArray && !$this->metadataBlobs ) {
1077 $s = '';
1078 } elseif ( $this->repo->isJsonMetadataEnabled() ) {
1079 $s = $this->getJsonMetadata();
1080 } else {
1081 $s = serialize( $this->getMetadataArray() );
1082 }
1083 if ( !is_string( $s ) ) {
1084 throw new RuntimeException( 'Could not serialize image metadata value for DB' );
1085 }
1086 return $db->encodeBlob( $s );
1087 }
1088
1095 private function getJsonMetadata() {
1096 // Directly store data that is not already in BlobStore
1097 $envelope = [
1098 'data' => array_diff_key( $this->metadataArray, $this->metadataBlobs )
1099 ];
1100
1101 // Also store the blob addresses
1102 if ( $this->metadataBlobs ) {
1103 $envelope['blobs'] = $this->metadataBlobs;
1104 }
1105
1106 [ $s, $blobAddresses ] = $this->metadataStorageHelper->getJsonMetadata( $this, $envelope );
1107
1108 // Repeated calls to this function should not keep inserting more blobs
1109 $this->metadataBlobs += $blobAddresses;
1110
1111 return $s;
1112 }
1113
1120 private function isMetadataOversize() {
1121 if ( !$this->repo->isSplitMetadataEnabled() ) {
1122 return false;
1123 }
1124 $threshold = $this->repo->getSplitMetadataThreshold();
1125 $directItems = array_diff_key( $this->metadataArray, $this->metadataBlobs );
1126 foreach ( $directItems as $value ) {
1127 if ( strlen( $this->metadataStorageHelper->jsonEncode( $value ) ) > $threshold ) {
1128 return true;
1129 }
1130 }
1131 return false;
1132 }
1133
1142 protected function loadMetadataFromDbFieldValue( IReadableDatabase $db, $metadataBlob ) {
1143 $this->loadMetadataFromString( $db->decodeBlob( $metadataBlob ) );
1144 }
1145
1153 protected function loadMetadataFromString( $metadataString ) {
1154 $this->extraDataLoaded = true;
1155 $this->metadataArray = [];
1156 $this->metadataBlobs = [];
1157 $this->unloadedMetadataBlobs = [];
1158 $metadataString = (string)$metadataString;
1159 if ( $metadataString === '' ) {
1160 $this->metadataSerializationFormat = self::MDS_EMPTY;
1161 return;
1162 }
1163 if ( $metadataString[0] === '{' ) {
1164 $envelope = $this->metadataStorageHelper->jsonDecode( $metadataString );
1165 if ( !$envelope ) {
1166 // Legacy error encoding
1167 $this->metadataArray = [ '_error' => $metadataString ];
1168 $this->metadataSerializationFormat = self::MDS_LEGACY;
1169 } else {
1170 $this->metadataSerializationFormat = self::MDS_JSON;
1171 if ( isset( $envelope['data'] ) ) {
1172 $this->metadataArray = $envelope['data'];
1173 }
1174 if ( isset( $envelope['blobs'] ) ) {
1175 $this->metadataBlobs = $this->unloadedMetadataBlobs = $envelope['blobs'];
1176 }
1177 }
1178 } else {
1179 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1180 $data = @unserialize( $metadataString );
1181 if ( !is_array( $data ) ) {
1182 // Legacy error encoding
1183 $data = [ '_error' => $metadataString ];
1184 $this->metadataSerializationFormat = self::MDS_LEGACY;
1185 } else {
1186 $this->metadataSerializationFormat = self::MDS_PHP;
1187 }
1188 $this->metadataArray = $data;
1189 }
1190 }
1191
1196 public function getBitDepth() {
1197 $this->load();
1198
1199 return (int)$this->bits;
1200 }
1201
1207 public function getSize() {
1208 $this->load();
1209
1210 return $this->size;
1211 }
1212
1218 public function getMimeType() {
1219 $this->load();
1220
1221 return $this->mime;
1222 }
1223
1230 public function getMediaType() {
1231 $this->load();
1232
1233 return $this->media_type;
1234 }
1235
1247 public function exists() {
1248 $this->load();
1249
1250 return $this->fileExists;
1251 }
1252
1274 protected function getThumbnails( $archiveName = false ) {
1275 if ( $archiveName ) {
1276 $dir = $this->getArchiveThumbPath( $archiveName );
1277 } else {
1278 $dir = $this->getThumbPath();
1279 }
1280
1281 $backend = $this->repo->getBackend();
1282 $files = [ $dir ];
1283 try {
1284 $iterator = $backend->getFileList( [ 'dir' => $dir, 'forWrite' => true ] );
1285 if ( $iterator !== null ) {
1286 foreach ( $iterator as $file ) {
1287 $files[] = $file;
1288 }
1289 }
1290 } catch ( FileBackendError $e ) {
1291 } // suppress (T56674)
1292
1293 return $files;
1294 }
1295
1304 public function purgeCache( $options = [] ) {
1305 // Refresh metadata in memcached, but don't touch thumbnails or CDN
1306 $this->maybeUpgradeRow();
1307 $this->invalidateCache();
1308
1309 // Delete thumbnails
1310 $this->purgeThumbnails( $options );
1311
1312 // Purge CDN cache for this file
1313 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1314 $hcu->purgeUrls(
1315 $this->getUrl(),
1316 !empty( $options['forThumbRefresh'] )
1317 ? $hcu::PURGE_PRESEND // just a manual purge
1318 : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1319 );
1320 }
1321
1327 public function purgeOldThumbnails( $archiveName ) {
1328 // Get a list of old thumbnails
1329 $thumbs = $this->getThumbnails( $archiveName );
1330
1331 // Delete thumbnails from storage, and prevent the directory itself from being purged
1332 $dir = array_shift( $thumbs );
1333 $this->purgeThumbList( $dir, $thumbs );
1334
1335 $urls = [];
1336 foreach ( $thumbs as $thumb ) {
1337 $urls[] = $this->getArchiveThumbUrl( $archiveName, $thumb );
1338 }
1339
1340 // Purge any custom thumbnail caches
1341 $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, $archiveName, $urls );
1342
1343 // Purge the CDN
1344 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1345 $hcu->purgeUrls( $urls, $hcu::PURGE_PRESEND );
1346 }
1347
1354 public function purgeThumbnails( $options = [] ) {
1355 $thumbs = $this->getThumbnails();
1356
1357 // Delete thumbnails from storage, and prevent the directory itself from being purged
1358 $dir = array_shift( $thumbs );
1359 $this->purgeThumbList( $dir, $thumbs );
1360
1361 // Always purge all files from CDN regardless of handler filters
1362 $urls = [];
1363 foreach ( $thumbs as $thumb ) {
1364 $urls[] = $this->getThumbUrl( $thumb );
1365 }
1366
1367 // Give the media handler a chance to filter the file purge list
1368 if ( !empty( $options['forThumbRefresh'] ) ) {
1369 $handler = $this->getHandler();
1370 if ( $handler ) {
1371 $handler->filterThumbnailPurgeList( $thumbs, $options );
1372 }
1373 }
1374
1375 // Purge any custom thumbnail caches
1376 $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, false, $urls );
1377
1378 // Purge the CDN
1379 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1380 $hcu->purgeUrls(
1381 $urls,
1382 !empty( $options['forThumbRefresh'] )
1383 ? $hcu::PURGE_PRESEND // just a manual purge
1384 : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1385 );
1386 }
1387
1394 public function prerenderThumbnails() {
1395 $uploadThumbnailRenderMap = MediaWikiServices::getInstance()
1396 ->getMainConfig()->get( MainConfigNames::UploadThumbnailRenderMap );
1397
1398 $jobs = [];
1399
1400 $sizes = $uploadThumbnailRenderMap;
1401 rsort( $sizes );
1402
1403 foreach ( $sizes as $size ) {
1404 if ( $this->isMultipage() ) {
1405 // (T309114) Only trigger render jobs up to MAX_PAGE_RENDER_JOBS to avoid
1406 // a flood of jobs for huge files.
1407 $pageLimit = min( $this->pageCount(), self::MAX_PAGE_RENDER_JOBS );
1408
1409 $jobs[] = new ThumbnailRenderJob(
1410 $this->getTitle(),
1411 [
1412 'transformParams' => [ 'width' => $size, 'page' => 1 ],
1413 'enqueueNextPage' => true,
1414 'pageLimit' => $pageLimit
1415 ]
1416 );
1417 } elseif ( $this->isVectorized() || $this->getWidth() > $size ) {
1418 $jobs[] = new ThumbnailRenderJob(
1419 $this->getTitle(),
1420 [ 'transformParams' => [ 'width' => $size ] ]
1421 );
1422 }
1423 }
1424
1425 if ( $jobs ) {
1426 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs );
1427 }
1428 }
1429
1436 protected function purgeThumbList( $dir, $files ) {
1437 $fileListDebug = strtr(
1438 var_export( $files, true ),
1439 [ "\n" => '' ]
1440 );
1441 wfDebug( __METHOD__ . ": $fileListDebug" );
1442
1443 if ( $this->repo->supportsSha1URLs() ) {
1444 $reference = $this->getSha1();
1445 } else {
1446 $reference = $this->getName();
1447 }
1448
1449 $purgeList = [];
1450 foreach ( $files as $file ) {
1451 # Check that the reference (filename or sha1) is part of the thumb name
1452 # This is a basic check to avoid erasing unrelated directories
1453 if ( str_contains( $file, $reference )
1454 || str_contains( $file, "-thumbnail" ) // "short" thumb name
1455 ) {
1456 $purgeList[] = "{$dir}/{$file}";
1457 }
1458 }
1459
1460 # Delete the thumbnails
1461 $this->repo->quickPurgeBatch( $purgeList );
1462 # Clear out the thumbnail directory if empty
1463 $this->repo->quickCleanDir( $dir );
1464 }
1465
1477 public function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1478 if ( !$this->exists() ) {
1479 return []; // Avoid hard failure when the file does not exist. T221812
1480 }
1481
1482 $dbr = $this->repo->getReplicaDB();
1483 $oldFileQuery = OldLocalFile::getQueryInfo();
1484
1485 $tables = $oldFileQuery['tables'];
1486 $fields = $oldFileQuery['fields'];
1487 $join_conds = $oldFileQuery['joins'];
1488 $conds = $opts = [];
1489 $eq = $inc ? '=' : '';
1490 $conds[] = $dbr->expr( 'oi_name', '=', $this->title->getDBkey() );
1491
1492 if ( $start ) {
1493 $conds[] = $dbr->expr( 'oi_timestamp', "<$eq", $dbr->timestamp( $start ) );
1494 }
1495
1496 if ( $end ) {
1497 $conds[] = $dbr->expr( 'oi_timestamp', ">$eq", $dbr->timestamp( $end ) );
1498 }
1499
1500 if ( $limit ) {
1501 $opts['LIMIT'] = $limit;
1502 }
1503
1504 // Search backwards for time > x queries
1505 $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
1506 $opts['ORDER BY'] = "oi_timestamp $order";
1507 $opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1508
1509 $this->getHookRunner()->onLocalFile__getHistory( $this, $tables, $fields,
1510 $conds, $opts, $join_conds );
1511
1512 $res = $dbr->newSelectQueryBuilder()
1513 ->tables( $tables )
1514 ->fields( $fields )
1515 ->conds( $conds )
1516 ->caller( __METHOD__ )
1517 ->options( $opts )
1518 ->joinConds( $join_conds )
1519 ->fetchResultSet();
1520 $r = [];
1521
1522 foreach ( $res as $row ) {
1523 $r[] = $this->repo->newFileFromRow( $row );
1524 }
1525
1526 if ( $order == 'ASC' ) {
1527 $r = array_reverse( $r ); // make sure it ends up descending
1528 }
1529
1530 return $r;
1531 }
1532
1543 public function nextHistoryLine() {
1544 if ( !$this->exists() ) {
1545 return false; // Avoid hard failure when the file does not exist. T221812
1546 }
1547
1548 # Polymorphic function name to distinguish foreign and local fetches
1549 $fname = static::class . '::' . __FUNCTION__;
1550
1551 $dbr = $this->repo->getReplicaDB();
1552
1553 if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1554 $queryBuilder = FileSelectQueryBuilder::newForFile( $dbr );
1555
1556 $queryBuilder->fields( [ 'oi_archive_name' => $dbr->addQuotes( '' ), 'oi_deleted' => '0' ] )
1557 ->where( [ 'img_name' => $this->title->getDBkey() ] );
1558 $this->historyRes = $queryBuilder->caller( $fname )->fetchResultSet();
1559
1560 if ( $this->historyRes->numRows() == 0 ) {
1561 $this->historyRes = null;
1562
1563 return false;
1564 }
1565 } elseif ( $this->historyLine == 1 ) {
1566 $queryBuilder = FileSelectQueryBuilder::newForOldFile( $dbr );
1567
1568 $this->historyRes = $queryBuilder->where( [ 'oi_name' => $this->title->getDBkey() ] )
1569 ->orderBy( 'oi_timestamp', SelectQueryBuilder::SORT_DESC )
1570 ->caller( $fname )->fetchResultSet();
1571 }
1572 $this->historyLine++;
1573
1574 return $this->historyRes->fetchObject();
1575 }
1576
1581 public function resetHistory() {
1582 $this->historyLine = 0;
1583
1584 if ( $this->historyRes !== null ) {
1585 $this->historyRes = null;
1586 }
1587 }
1588
1622 public function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1623 $timestamp = false, ?Authority $uploader = null, $tags = [],
1624 $createNullRevision = true, $revert = false
1625 ) {
1626 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1627 return $this->readOnlyFatalStatus();
1628 } elseif ( MediaWikiServices::getInstance()->getRevisionStore()->isReadOnly() ) {
1629 // Check this in advance to avoid writing to FileBackend and the file tables,
1630 // only to fail on insert the revision due to the text store being unavailable.
1631 return $this->readOnlyFatalStatus();
1632 }
1633
1634 $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1635 if ( !$props ) {
1636 if ( FileRepo::isVirtualUrl( $srcPath )
1637 || FileBackend::isStoragePath( $srcPath )
1638 ) {
1639 $props = $this->repo->getFileProps( $srcPath );
1640 } else {
1641 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
1642 $props = $mwProps->getPropsFromPath( $srcPath, true );
1643 }
1644 }
1645
1646 $options = [];
1647 $handler = MediaHandler::getHandler( $props['mime'] );
1648 if ( $handler ) {
1649 if ( is_string( $props['metadata'] ) ) {
1650 // This supports callers directly fabricating a metadata
1651 // property using serialize(). Normally the metadata property
1652 // comes from MWFileProps, in which case it won't be a string.
1653 // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1654 $metadata = @unserialize( $props['metadata'] );
1655 } else {
1656 $metadata = $props['metadata'];
1657 }
1658
1659 if ( is_array( $metadata ) ) {
1660 $options['headers'] = $handler->getContentHeaders( $metadata );
1661 }
1662 } else {
1663 $options['headers'] = [];
1664 }
1665
1666 // Trim spaces on user supplied text
1667 $comment = trim( $comment );
1668
1669 $status = $this->publish( $src, $flags, $options );
1670
1671 if ( $status->successCount >= 2 ) {
1672 // There will be a copy+(one of move,copy,store).
1673 // The first succeeding does not commit us to updating the DB
1674 // since it simply copied the current version to a timestamped file name.
1675 // It is only *preferable* to avoid leaving such files orphaned.
1676 // Once the second operation goes through, then the current version was
1677 // updated and we must therefore update the DB too.
1678 $oldver = $status->value;
1679
1680 $uploadStatus = $this->recordUpload3(
1681 $oldver,
1682 $comment,
1683 $pageText,
1684 $uploader ?? RequestContext::getMain()->getAuthority(),
1685 $props,
1686 $timestamp,
1687 $tags,
1688 $createNullRevision,
1689 $revert
1690 );
1691 if ( !$uploadStatus->isOK() ) {
1692 if ( $uploadStatus->hasMessage( 'filenotfound' ) ) {
1693 // update filenotfound error with more specific path
1694 $status->fatal( 'filenotfound', $srcPath );
1695 } else {
1696 $status->merge( $uploadStatus );
1697 }
1698 }
1699 }
1700
1701 return $status;
1702 }
1703
1720 public function recordUpload3(
1721 string $oldver,
1722 string $comment,
1723 string $pageText,
1724 Authority $performer,
1725 $props = false,
1726 $timestamp = false,
1727 $tags = [],
1728 bool $createNullRevision = true,
1729 bool $revert = false
1730 ): Status {
1731 $dbw = $this->repo->getPrimaryDB();
1732
1733 # Imports or such might force a certain timestamp; otherwise we generate
1734 # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1735 if ( $timestamp === false ) {
1736 $timestamp = $dbw->timestamp();
1737 $allowTimeKludge = true;
1738 } else {
1739 $allowTimeKludge = false;
1740 }
1741
1742 $props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1743 $props['description'] = $comment;
1744 $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1745 $this->setProps( $props );
1746
1747 # Fail now if the file isn't there
1748 if ( !$this->fileExists ) {
1749 wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!" );
1750
1751 return Status::newFatal( 'filenotfound', $this->getRel() );
1752 }
1753
1754 $mimeAnalyzer = MediaWikiServices::getInstance()->getMimeAnalyzer();
1755 if ( !$mimeAnalyzer->isValidMajorMimeType( $this->major_mime ) ) {
1756 $this->major_mime = 'unknown';
1757 }
1758
1759 $actorNormalizaton = MediaWikiServices::getInstance()->getActorNormalization();
1760
1761 $dbw->startAtomic( __METHOD__ );
1762
1763 $actorId = $actorNormalizaton->acquireActorId( $performer->getUser(), $dbw );
1764 $this->user = $performer->getUser();
1765
1766 # Test to see if the row exists using INSERT IGNORE
1767 # This avoids race conditions by locking the row until the commit, and also
1768 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1769 $commentStore = MediaWikiServices::getInstance()->getCommentStore();
1770 $commentFields = $commentStore->insert( $dbw, 'img_description', $comment );
1771 $actorFields = [ 'img_actor' => $actorId ];
1772 $dbw->newInsertQueryBuilder()
1773 ->insertInto( 'image' )
1774 ->ignore()
1775 ->row( [
1776 'img_name' => $this->getName(),
1777 'img_size' => $this->size,
1778 'img_width' => intval( $this->width ),
1779 'img_height' => intval( $this->height ),
1780 'img_bits' => $this->bits,
1781 'img_media_type' => $this->media_type,
1782 'img_major_mime' => $this->major_mime,
1783 'img_minor_mime' => $this->minor_mime,
1784 'img_timestamp' => $dbw->timestamp( $timestamp ),
1785 'img_metadata' => $this->getMetadataForDb( $dbw ),
1786 'img_sha1' => $this->sha1
1787 ] + $commentFields + $actorFields )
1788 ->caller( __METHOD__ )->execute();
1789 $reupload = ( $dbw->affectedRows() == 0 );
1790
1791 if ( $reupload ) {
1792 $row = $dbw->newSelectQueryBuilder()
1793 ->select( [ 'img_timestamp', 'img_sha1' ] )
1794 ->from( 'image' )
1795 ->where( [ 'img_name' => $this->getName() ] )
1796 ->caller( __METHOD__ )->fetchRow();
1797
1798 if ( $row && $row->img_sha1 === $this->sha1 ) {
1799 $dbw->endAtomic( __METHOD__ );
1800 wfDebug( __METHOD__ . ": File " . $this->getRel() . " already exists!" );
1801 $title = Title::newFromText( $this->getName(), NS_FILE );
1802 return Status::newFatal( 'fileexists-no-change', $title->getPrefixedText() );
1803 }
1804
1805 if ( $allowTimeKludge ) {
1806 # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1807 $lUnixtime = $row ? (int)wfTimestamp( TS_UNIX, $row->img_timestamp ) : false;
1808 # Avoid a timestamp that is not newer than the last version
1809 # TODO: the image/oldimage tables should be like page/revision with an ID field
1810 if ( $lUnixtime && (int)wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
1811 sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1812 $timestamp = $dbw->timestamp( $lUnixtime + 1 );
1813 $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1814 }
1815 }
1816
1817 $tables = [ 'image' ];
1818 $fields = [
1819 'oi_name' => 'img_name',
1820 'oi_archive_name' => $dbw->addQuotes( $oldver ),
1821 'oi_size' => 'img_size',
1822 'oi_width' => 'img_width',
1823 'oi_height' => 'img_height',
1824 'oi_bits' => 'img_bits',
1825 'oi_description_id' => 'img_description_id',
1826 'oi_timestamp' => 'img_timestamp',
1827 'oi_metadata' => 'img_metadata',
1828 'oi_media_type' => 'img_media_type',
1829 'oi_major_mime' => 'img_major_mime',
1830 'oi_minor_mime' => 'img_minor_mime',
1831 'oi_sha1' => 'img_sha1',
1832 'oi_actor' => 'img_actor',
1833 ];
1834 $joins = [];
1835
1836 # (T36993) Note: $oldver can be empty here, if the previous
1837 # version of the file was broken. Allow registration of the new
1838 # version to continue anyway, because that's better than having
1839 # an image that's not fixable by user operations.
1840 # Collision, this is an update of a file
1841 # Insert previous contents into oldimage
1842 $dbw->insertSelect( 'oldimage', $tables, $fields,
1843 [ 'img_name' => $this->getName() ], __METHOD__, [], [], $joins );
1844
1845 # Update the current image row
1846 $dbw->newUpdateQueryBuilder()
1847 ->update( 'image' )
1848 ->set( [
1849 'img_size' => $this->size,
1850 'img_width' => intval( $this->width ),
1851 'img_height' => intval( $this->height ),
1852 'img_bits' => $this->bits,
1853 'img_media_type' => $this->media_type,
1854 'img_major_mime' => $this->major_mime,
1855 'img_minor_mime' => $this->minor_mime,
1856 'img_timestamp' => $dbw->timestamp( $timestamp ),
1857 'img_metadata' => $this->getMetadataForDb( $dbw ),
1858 'img_sha1' => $this->sha1
1859 ] + $commentFields + $actorFields )
1860 ->where( [ 'img_name' => $this->getName() ] )
1861 ->caller( __METHOD__ )->execute();
1862 }
1863
1864 $descTitle = $this->getTitle();
1865 $descId = $descTitle->getArticleID();
1866 $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $descTitle );
1867 if ( !$wikiPage instanceof WikiFilePage ) {
1868 throw new UnexpectedValueException( 'Cannot obtain instance of WikiFilePage for ' . $this->getName()
1869 . ', got instance of ' . get_class( $wikiPage ) );
1870 }
1871 $wikiPage->setFile( $this );
1872
1873 // Determine log action. If reupload is done by reverting, use a special log_action.
1874 if ( $revert ) {
1875 $logAction = 'revert';
1876 } elseif ( $reupload ) {
1877 $logAction = 'overwrite';
1878 } else {
1879 $logAction = 'upload';
1880 }
1881 // Add the log entry...
1882 $logEntry = new ManualLogEntry( 'upload', $logAction );
1883 $logEntry->setTimestamp( $this->timestamp );
1884 $logEntry->setPerformer( $performer->getUser() );
1885 $logEntry->setComment( $comment );
1886 $logEntry->setTarget( $descTitle );
1887 // Allow people using the api to associate log entries with the upload.
1888 // Log has a timestamp, but sometimes different from upload timestamp.
1889 $logEntry->setParameters(
1890 [
1891 'img_sha1' => $this->sha1,
1892 'img_timestamp' => $timestamp,
1893 ]
1894 );
1895 // Note we keep $logId around since during new image
1896 // creation, page doesn't exist yet, so log_page = 0
1897 // but we want it to point to the page we're making,
1898 // so we later modify the log entry.
1899 // For a similar reason, we avoid making an RC entry
1900 // now and wait until the page exists.
1901 $logId = $logEntry->insert();
1902
1903 if ( $descTitle->exists() ) {
1904 if ( $createNullRevision ) {
1905 $services = MediaWikiServices::getInstance();
1906 $revStore = $services->getRevisionStore();
1907 // Use own context to get the action text in content language
1908 $formatter = $services->getLogFormatterFactory()->newFromEntry( $logEntry );
1909 $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1910 $editSummary = $formatter->getPlainActionText();
1911 $summary = CommentStoreComment::newUnsavedComment( $editSummary );
1912 $nullRevRecord = $revStore->newNullRevision(
1913 $dbw,
1914 $descTitle,
1915 $summary,
1916 false,
1917 $performer->getUser()
1918 );
1919
1920 if ( $nullRevRecord ) {
1921 $inserted = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
1922
1923 $this->getHookRunner()->onRevisionFromEditComplete(
1924 $wikiPage,
1925 $inserted,
1926 $inserted->getParentId(),
1927 $performer->getUser(),
1928 $tags
1929 );
1930
1931 $wikiPage->updateRevisionOn( $dbw, $inserted );
1932 // Associate null revision id
1933 $logEntry->setAssociatedRevId( $inserted->getId() );
1934 }
1935 }
1936
1937 $newPageContent = null;
1938 } else {
1939 // Make the description page and RC log entry post-commit
1940 $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1941 }
1942
1943 // NOTE: Even after ending this atomic section, we are probably still in the implicit
1944 // transaction started by any prior master query in the request. We cannot yet safely
1945 // schedule jobs, see T263301.
1946 $dbw->endAtomic( __METHOD__ );
1947 $fname = __METHOD__;
1948
1949 # Do some cache purges after final commit so that:
1950 # a) Changes are more likely to be seen post-purge
1951 # b) They won't cause rollback of the log publish/update above
1952 $purgeUpdate = new AutoCommitUpdate(
1953 $dbw,
1954 __METHOD__,
1955 function () use (
1956 $reupload, $wikiPage, $newPageContent, $comment, $performer,
1957 $logEntry, $logId, $descId, $tags, $fname
1958 ) {
1959 # Update memcache after the commit
1960 $this->invalidateCache();
1961
1962 $updateLogPage = false;
1963 if ( $newPageContent ) {
1964 # New file page; create the description page.
1965 # There's already a log entry, so don't make a second RC entry
1966 # CDN and file cache for the description page are purged by doUserEditContent.
1967 $status = $wikiPage->doUserEditContent(
1968 $newPageContent,
1969 $performer,
1970 $comment,
1972 );
1973
1974 $revRecord = $status->getNewRevision();
1975 if ( $revRecord ) {
1976 // Associate new page revision id
1977 $logEntry->setAssociatedRevId( $revRecord->getId() );
1978
1979 // This relies on the resetArticleID() call in WikiPage::insertOn(),
1980 // which is triggered on $descTitle by doUserEditContent() above.
1981 $updateLogPage = $revRecord->getPageId();
1982 }
1983 } else {
1984 # Existing file page: invalidate description page cache
1985 $title = $wikiPage->getTitle();
1986 $title->invalidateCache();
1987 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1988 $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
1989 # Allow the new file version to be patrolled from the page footer
1991 }
1992
1993 # Update associated rev id. This should be done by $logEntry->insert() earlier,
1994 # but setAssociatedRevId() wasn't called at that point yet...
1995 $logParams = $logEntry->getParameters();
1996 $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
1997 $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
1998 if ( $updateLogPage ) {
1999 # Also log page, in case where we just created it above
2000 $update['log_page'] = $updateLogPage;
2001 }
2002 $this->getRepo()->getPrimaryDB()->newUpdateQueryBuilder()
2003 ->update( 'logging' )
2004 ->set( $update )
2005 ->where( [ 'log_id' => $logId ] )
2006 ->caller( $fname )->execute();
2007
2008 $this->getRepo()->getPrimaryDB()->newInsertQueryBuilder()
2009 ->insertInto( 'log_search' )
2010 ->row( [
2011 'ls_field' => 'associated_rev_id',
2012 'ls_value' => (string)$logEntry->getAssociatedRevId(),
2013 'ls_log_id' => $logId,
2014 ] )
2015 ->caller( $fname )->execute();
2016
2017 # Add change tags, if any
2018 if ( $tags ) {
2019 $logEntry->addTags( $tags );
2020 }
2021
2022 # Uploads can be patrolled
2023 $logEntry->setIsPatrollable( true );
2024
2025 # Now that the log entry is up-to-date, make an RC entry.
2026 $logEntry->publish( $logId );
2027
2028 # Run hook for other updates (typically more cache purging)
2029 $this->getHookRunner()->onFileUpload( $this, $reupload, !$newPageContent );
2030
2031 if ( $reupload ) {
2032 # Delete old thumbnails
2033 $this->purgeThumbnails();
2034 # Remove the old file from the CDN cache
2035 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2036 $hcu->purgeUrls( $this->getUrl(), $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2037 } else {
2038 # Update backlink pages pointing to this title if created
2039 $blcFactory = MediaWikiServices::getInstance()->getBacklinkCacheFactory();
2040 LinksUpdate::queueRecursiveJobsForTable(
2041 $this->getTitle(),
2042 'imagelinks',
2043 'upload-image',
2044 $performer->getUser()->getName(),
2045 $blcFactory->getBacklinkCache( $this->getTitle() )
2046 );
2047 }
2048
2049 $this->prerenderThumbnails();
2050 }
2051 );
2052
2053 # Invalidate cache for all pages using this file
2054 $cacheUpdateJob = HTMLCacheUpdateJob::newForBacklinks(
2055 $this->getTitle(),
2056 'imagelinks',
2057 [ 'causeAction' => 'file-upload', 'causeAgent' => $performer->getUser()->getName() ]
2058 );
2059
2060 // NOTE: We are probably still in the implicit transaction started by DBO_TRX. We should
2061 // only schedule jobs after that transaction was committed, so a job queue failure
2062 // doesn't cause the upload to fail (T263301). Also, we should generally not schedule any
2063 // Jobs or the DeferredUpdates that assume the update is complete until after the
2064 // transaction has been committed and we are sure that the upload was indeed successful.
2065 $dbw->onTransactionCommitOrIdle( static function () use ( $reupload, $purgeUpdate, $cacheUpdateJob ) {
2066 DeferredUpdates::addUpdate( $purgeUpdate, DeferredUpdates::PRESEND );
2067
2068 if ( !$reupload ) {
2069 // This is a new file, so update the image count
2070 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2071 }
2072
2073 MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $cacheUpdateJob );
2074 }, __METHOD__ );
2075
2076 return Status::newGood();
2077 }
2078
2095 public function publish( $src, $flags = 0, array $options = [] ) {
2096 return $this->publishTo( $src, $this->getRel(), $flags, $options );
2097 }
2098
2115 protected function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
2116 $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
2117
2118 $repo = $this->getRepo();
2119 if ( $repo->getReadOnlyReason() !== false ) {
2120 return $this->readOnlyFatalStatus();
2121 }
2122
2123 $status = $this->acquireFileLock();
2124 if ( !$status->isOK() ) {
2125 return $status;
2126 }
2127
2128 if ( $this->isOld() ) {
2129 $archiveRel = $dstRel;
2130 $archiveName = basename( $archiveRel );
2131 } else {
2132 $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
2133 $archiveRel = $this->getArchiveRel( $archiveName );
2134 }
2135
2136 if ( $repo->hasSha1Storage() ) {
2137 $sha1 = FileRepo::isVirtualUrl( $srcPath )
2138 ? $repo->getFileSha1( $srcPath )
2139 : FSFile::getSha1Base36FromPath( $srcPath );
2141 $wrapperBackend = $repo->getBackend();
2142 '@phan-var FileBackendDBRepoWrapper $wrapperBackend';
2143 $dst = $wrapperBackend->getPathForSHA1( $sha1 );
2144 $status = $repo->quickImport( $src, $dst );
2145 if ( $flags & File::DELETE_SOURCE ) {
2146 unlink( $srcPath );
2147 }
2148
2149 if ( $this->exists() ) {
2150 $status->value = $archiveName;
2151 }
2152 } else {
2153 $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
2154 $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
2155
2156 if ( $status->value == 'new' ) {
2157 $status->value = '';
2158 } else {
2159 $status->value = $archiveName;
2160 }
2161 }
2162
2163 $this->releaseFileLock();
2164 return $status;
2165 }
2166
2185 public function move( $target ) {
2186 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2187 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2188 return $this->readOnlyFatalStatus();
2189 }
2190
2191 wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
2192 $batch = new LocalFileMoveBatch( $this, $target );
2193
2194 $status = $batch->addCurrent();
2195 if ( !$status->isOK() ) {
2196 return $status;
2197 }
2198 $archiveNames = $batch->addOlds();
2199 $status = $batch->execute();
2200
2201 wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
2202
2203 // Purge the source and target files outside the transaction...
2204 $oldTitleFile = $localRepo->newFile( $this->title );
2205 $newTitleFile = $localRepo->newFile( $target );
2206 DeferredUpdates::addUpdate(
2207 new AutoCommitUpdate(
2208 $this->getRepo()->getPrimaryDB(),
2209 __METHOD__,
2210 static function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
2211 $oldTitleFile->purgeEverything();
2212 foreach ( $archiveNames as $archiveName ) {
2214 '@phan-var OldLocalFile $oldTitleFile';
2215 $oldTitleFile->purgeOldThumbnails( $archiveName );
2216 }
2217 $newTitleFile->purgeEverything();
2218 }
2219 ),
2220 DeferredUpdates::PRESEND
2221 );
2222
2223 if ( $status->isOK() ) {
2224 // Now switch the object
2225 $this->title = $target;
2226 // Force regeneration of the name and hashpath
2227 $this->name = null;
2228 $this->hashPath = null;
2229 }
2230
2231 return $status;
2232 }
2233
2250 public function deleteFile( $reason, UserIdentity $user, $suppress = false ) {
2251 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2252 return $this->readOnlyFatalStatus();
2253 }
2254
2255 $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2256
2257 $batch->addCurrent();
2258 // Get old version relative paths
2259 $archiveNames = $batch->addOlds();
2260 $status = $batch->execute();
2261
2262 if ( $status->isOK() ) {
2263 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
2264 }
2265
2266 // To avoid slow purges in the transaction, move them outside...
2267 DeferredUpdates::addUpdate(
2268 new AutoCommitUpdate(
2269 $this->getRepo()->getPrimaryDB(),
2270 __METHOD__,
2271 function () use ( $archiveNames ) {
2272 $this->purgeEverything();
2273 foreach ( $archiveNames as $archiveName ) {
2274 $this->purgeOldThumbnails( $archiveName );
2275 }
2276 }
2277 ),
2278 DeferredUpdates::PRESEND
2279 );
2280
2281 // Purge the CDN
2282 $purgeUrls = [];
2283 foreach ( $archiveNames as $archiveName ) {
2284 $purgeUrls[] = $this->getArchiveUrl( $archiveName );
2285 }
2286
2287 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2288 $hcu->purgeUrls( $purgeUrls, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2289
2290 return $status;
2291 }
2292
2310 public function deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress = false ) {
2311 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2312 return $this->readOnlyFatalStatus();
2313 }
2314
2315 $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2316
2317 $batch->addOld( $archiveName );
2318 $status = $batch->execute();
2319
2320 $this->purgeOldThumbnails( $archiveName );
2321 if ( $status->isOK() ) {
2322 $this->purgeDescription();
2323 }
2324
2325 $url = $this->getArchiveUrl( $archiveName );
2326 $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2327 $hcu->purgeUrls( $url, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2328
2329 return $status;
2330 }
2331
2344 public function restore( $versions = [], $unsuppress = false ) {
2345 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2346 return $this->readOnlyFatalStatus();
2347 }
2348
2349 $batch = new LocalFileRestoreBatch( $this, $unsuppress );
2350
2351 if ( !$versions ) {
2352 $batch->addAll();
2353 } else {
2354 $batch->addIds( $versions );
2355 }
2356 $status = $batch->execute();
2357 if ( $status->isGood() ) {
2358 $cleanupStatus = $batch->cleanup();
2359 $cleanupStatus->successCount = 0;
2360 $cleanupStatus->failCount = 0;
2361 $status->merge( $cleanupStatus );
2362 }
2363
2364 return $status;
2365 }
2366
2377 public function getDescriptionUrl() {
2378 // Avoid hard failure when the file does not exist. T221812
2379 return $this->title ? $this->title->getLocalURL() : false;
2380 }
2381
2391 public function getDescriptionText( ?Language $lang = null ) {
2392 if ( !$this->title ) {
2393 return false; // Avoid hard failure when the file does not exist. T221812
2394 }
2395
2396 $services = MediaWikiServices::getInstance();
2397 $page = $services->getPageStore()->getPageByReference( $this->getTitle() );
2398 if ( !$page ) {
2399 return false;
2400 }
2401
2402 if ( $lang ) {
2403 $parserOptions = ParserOptions::newFromUserAndLang(
2404 RequestContext::getMain()->getUser(),
2405 $lang
2406 );
2407 } else {
2408 $parserOptions = ParserOptions::newFromContext( RequestContext::getMain() );
2409 }
2410
2411 $parseStatus = $services->getParserOutputAccess()
2412 ->getParserOutput( $page, $parserOptions );
2413
2414 if ( !$parseStatus->isGood() ) {
2415 // Rendering failed.
2416 return false;
2417 }
2418 return $parseStatus->getValue()->getText();
2419 }
2420
2428 public function getUploader( int $audience = self::FOR_PUBLIC, ?Authority $performer = null ): ?UserIdentity {
2429 $this->load();
2430 if ( $audience === self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
2431 return null;
2432 } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $performer ) ) {
2433 return null;
2434 } else {
2435 return $this->user;
2436 }
2437 }
2438
2445 public function getDescription( $audience = self::FOR_PUBLIC, ?Authority $performer = null ) {
2446 $this->load();
2447 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
2448 return '';
2449 } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $performer ) ) {
2450 return '';
2451 } else {
2452 return $this->description;
2453 }
2454 }
2455
2460 public function getTimestamp() {
2461 $this->load();
2462
2463 return $this->timestamp;
2464 }
2465
2470 public function getDescriptionTouched() {
2471 if ( !$this->exists() ) {
2472 return false; // Avoid hard failure when the file does not exist. T221812
2473 }
2474
2475 // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
2476 // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
2477 // need to differentiate between null (uninitialized) and false (failed to load).
2478 if ( $this->descriptionTouched === null ) {
2479 $touched = $this->repo->getReplicaDB()->newSelectQueryBuilder()
2480 ->select( 'page_touched' )
2481 ->from( 'page' )
2482 ->where( [ 'page_namespace' => $this->title->getNamespace() ] )
2483 ->andWhere( [ 'page_title' => $this->title->getDBkey() ] )
2484 ->caller( __METHOD__ )->fetchField();
2485 $this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
2486 }
2487
2488 return $this->descriptionTouched;
2489 }
2490
2495 public function getSha1() {
2496 $this->load();
2497 return $this->sha1;
2498 }
2499
2503 public function isCacheable() {
2504 $this->load();
2505
2506 // If extra data (metadata) was not loaded then it must have been large
2507 return $this->extraDataLoaded
2508 && strlen( serialize( $this->metadataArray ) ) <= self::CACHE_FIELD_MAX_LEN;
2509 }
2510
2519 public function acquireFileLock( $timeout = 0 ) {
2520 return Status::wrap( $this->getRepo()->getBackend()->lockFiles(
2521 [ $this->getPath() ], LockManager::LOCK_EX, $timeout
2522 ) );
2523 }
2524
2531 public function releaseFileLock() {
2532 return Status::wrap( $this->getRepo()->getBackend()->unlockFiles(
2533 [ $this->getPath() ], LockManager::LOCK_EX
2534 ) );
2535 }
2536
2547 public function lock() {
2548 if ( !$this->locked ) {
2549 $logger = LoggerFactory::getInstance( 'LocalFile' );
2550
2551 $dbw = $this->repo->getPrimaryDB();
2552 $makesTransaction = !$dbw->trxLevel();
2553 $dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
2554 // T56736: use simple lock to handle when the file does not exist.
2555 // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
2556 // Also, that would cause contention on INSERT of similarly named rows.
2557 $status = $this->acquireFileLock( 10 ); // represents all versions of the file
2558 if ( !$status->isGood() ) {
2559 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2560 $logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
2561
2562 throw new LocalFileLockError( $status );
2563 }
2564 // Release the lock *after* commit to avoid row-level contention.
2565 // Make sure it triggers on rollback() as well as commit() (T132921).
2566 $dbw->onTransactionResolution(
2567 function () use ( $logger ) {
2568 $status = $this->releaseFileLock();
2569 if ( !$status->isGood() ) {
2570 $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
2571 }
2572 },
2573 __METHOD__
2574 );
2575 // Callers might care if the SELECT snapshot is safely fresh
2576 $this->lockedOwnTrx = $makesTransaction;
2577 }
2578
2579 $this->locked++;
2580
2581 return $this->lockedOwnTrx;
2582 }
2583
2594 public function unlock() {
2595 if ( $this->locked ) {
2596 --$this->locked;
2597 if ( !$this->locked ) {
2598 $dbw = $this->repo->getPrimaryDB();
2599 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2600 $this->lockedOwnTrx = false;
2601 }
2602 }
2603 }
2604
2608 protected function readOnlyFatalStatus() {
2609 return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
2610 $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2611 }
2612
2616 public function __destruct() {
2617 $this->unlock();
2618 }
2619}
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:1484
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Definition FileRepo.php:293
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:79
MediaHandler $handler
Definition File.php:147
assertRepoDefined()
Assert that $this->repo is set to a valid FileRepo instance.
Definition File.php:2487
getName()
Return the name of this file.
Definition File.php:347
const DELETE_SOURCE
Definition File.php:96
getVirtualUrl( $suffix=false)
Get the public zone virtual URL for a current version source file.
Definition File.php:1950
assertTitleDefined()
Assert that $this->title is set to a Title.
Definition File.php:2496
FileRepo LocalRepo ForeignAPIRepo false $repo
Some member variables can be lazy-initialised using __get().
Definition File.php:126
isMultipage()
Returns 'true' if this file is a type which supports multiple pages, e.g.
Definition File.php:2184
Title string false $title
Definition File.php:129
getHandler()
Get a MediaHandler instance for this file.
Definition File.php:1569
string null $name
The name of a file from its title object.
Definition File.php:156
static newForBacklinks(PageReference $page, $table, $params=[])
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:75
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().
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.
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.
getDescriptionText(?Language $lang=null)
Get the HTML text of the description page This is not used by ImagePage for local files,...
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.
getHeight( $page=1)
Return the height of the image.
prerenderThumbnails()
Prerenders a configurable set of thumbnails.
getDescription( $audience=self::FOR_PUBLIC, ?Authority $performer=null)
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)
getMetadataItems(array $itemNames)
Get multiple elements of the unserialized handler-specific metadata.
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:99
nextHistoryLine()
Returns the history of this file, line by line.
upload( $src, $comment, $pageText, $flags=0, $props=false, $timestamp=false, ?Authority $uploader=null, $tags=[], $createNullRevision=true, $revert=false)
getHashPath inherited
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.
getUploader(int $audience=self::FOR_PUBLIC, ?Authority $performer=null)
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:96
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.
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.
A content handler knows how do deal with a specific type of content on a wiki page.
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.
Base class for language-specific code.
Definition Language.php:80
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Set options of the Parser.
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.
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).
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...