MediaWiki  master
LocalFile.php
Go to the documentation of this file.
1 <?php
33 
60 class LocalFile extends File {
61  private const VERSION = 13; // cache version
62 
63  private const CACHE_FIELD_MAX_LEN = 1000;
64 
66  private const MDS_EMPTY = 'empty';
67 
69  private const MDS_LEGACY = 'legacy';
70 
72  private const MDS_PHP = 'php';
73 
75  private const MDS_JSON = 'json';
76 
78  private const MAX_PAGE_RENDER_JOBS = 50;
79 
81  protected $fileExists;
82 
84  protected $width;
85 
87  protected $height;
88 
90  protected $bits;
91 
93  protected $media_type;
94 
96  protected $mime;
97 
99  protected $size;
100 
102  protected $metadataArray = [];
103 
111 
113  protected $metadataBlobs = [];
114 
121  protected $unloadedMetadataBlobs = [];
122 
124  protected $sha1;
125 
127  protected $dataLoaded = false;
128 
130  protected $extraDataLoaded = false;
131 
133  protected $deleted;
134 
136  protected $repoClass = LocalRepo::class;
137 
139  private $historyLine = 0;
140 
142  private $historyRes = null;
143 
145  private $major_mime;
146 
148  private $minor_mime;
149 
151  private $timestamp;
152 
154  private $user;
155 
157  private $description;
158 
160  private $descriptionTouched;
161 
163  private $upgraded;
164 
166  private $upgrading;
167 
169  private $locked;
170 
172  private $lockedOwnTrx;
173 
175  private $missing;
176 
178  private $metadataStorageHelper;
179 
180  // @note: higher than IDBAccessObject constants
181  private const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
182 
183  private const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
184 
199  public static function newFromTitle( $title, $repo, $unused = null ) {
200  return new static( $title, $repo );
201  }
202 
214  public static function newFromRow( $row, $repo ) {
215  $title = Title::makeTitle( NS_FILE, $row->img_name );
216  $file = new static( $title, $repo );
217  $file->loadFromRow( $row );
218 
219  return $file;
220  }
221 
233  public static function newFromKey( $sha1, $repo, $timestamp = false ) {
234  $dbr = $repo->getReplicaDB();
235 
236  $conds = [ 'img_sha1' => $sha1 ];
237  if ( $timestamp ) {
238  $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
239  }
240 
241  $fileQuery = static::getQueryInfo();
242  $row = $dbr->selectRow(
243  $fileQuery['tables'], $fileQuery['fields'], $conds, __METHOD__, [], $fileQuery['joins']
244  );
245  if ( $row ) {
246  return static::newFromRow( $row, $repo );
247  } else {
248  return false;
249  }
250  }
251 
271  public static function getQueryInfo( array $options = [] ) {
272  $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'img_description' );
273  $ret = [
274  'tables' => [
275  'image',
276  'image_actor' => 'actor'
277  ] + $commentQuery['tables'],
278  'fields' => [
279  'img_name',
280  'img_size',
281  'img_width',
282  'img_height',
283  'img_metadata',
284  'img_bits',
285  'img_media_type',
286  'img_major_mime',
287  'img_minor_mime',
288  'img_timestamp',
289  'img_sha1',
290  'img_actor',
291  'img_user' => 'image_actor.actor_user',
292  'img_user_text' => 'image_actor.actor_name',
293  ] + $commentQuery['fields'],
294  'joins' => [
295  'image_actor' => [ 'JOIN', 'actor_id=img_actor' ]
296  ] + $commentQuery['joins'],
297  ];
298 
299  if ( in_array( 'omit-nonlazy', $options, true ) ) {
300  // Internal use only for getting only the lazy fields
301  $ret['fields'] = [];
302  }
303  if ( !in_array( 'omit-lazy', $options, true ) ) {
304  // Note: Keep this in sync with self::getLazyCacheFields() and
305  // self::loadExtraFromDB()
306  $ret['fields'][] = 'img_metadata';
307  }
308 
309  return $ret;
310  }
311 
319  public function __construct( $title, $repo ) {
320  parent::__construct( $title, $repo );
321  $this->metadataStorageHelper = new MetadataStorageHelper( $repo );
322 
323  $this->assertRepoDefined();
324  $this->assertTitleDefined();
325  }
326 
330  public function getRepo() {
331  return $this->repo;
332  }
333 
340  protected function getCacheKey() {
341  return $this->repo->getSharedCacheKey( 'file', sha1( $this->getName() ) );
342  }
343 
350  return [ $this->getCacheKey() ];
351  }
352 
356  private function loadFromCache() {
357  $this->dataLoaded = false;
358  $this->extraDataLoaded = false;
359 
360  $key = $this->getCacheKey();
361  if ( !$key ) {
362  $this->loadFromDB( self::READ_NORMAL );
363 
364  return;
365  }
366 
367  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
368  $cachedValues = $cache->getWithSetCallback(
369  $key,
370  $cache::TTL_WEEK,
371  function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
372  $setOpts += Database::getCacheSetOptions( $this->repo->getReplicaDB() );
373 
374  $this->loadFromDB( self::READ_NORMAL );
375 
376  $fields = $this->getCacheFields( '' );
377  $cacheVal = [];
378  $cacheVal['fileExists'] = $this->fileExists;
379  if ( $this->fileExists ) {
380  foreach ( $fields as $field ) {
381  $cacheVal[$field] = $this->$field;
382  }
383  }
384  if ( $this->user ) {
385  $cacheVal['user'] = $this->user->getId();
386  $cacheVal['user_text'] = $this->user->getName();
387  }
388 
389  // Don't cache metadata items stored as blobs, since they tend to be large
390  if ( $this->metadataBlobs ) {
391  $cacheVal['metadata'] = array_diff_key(
392  $this->metadataArray, $this->metadataBlobs );
393  // Save the blob addresses
394  $cacheVal['metadataBlobs'] = $this->metadataBlobs;
395  } else {
396  $cacheVal['metadata'] = $this->metadataArray;
397  }
398 
399  // Strip off excessive entries from the subset of fields that can become large.
400  // If the cache value gets too large and might not fit in the cache,
401  // causing repeat database queries for each access to the file.
402  foreach ( $this->getLazyCacheFields( '' ) as $field ) {
403  if ( isset( $cacheVal[$field] )
404  && strlen( serialize( $cacheVal[$field] ) ) > 100 * 1024
405  ) {
406  unset( $cacheVal[$field] ); // don't let the value get too big
407  if ( $field === 'metadata' ) {
408  unset( $cacheVal['metadataBlobs'] );
409  }
410  }
411  }
412 
413  if ( $this->fileExists ) {
414  $ttl = $cache->adaptiveTTL( (int)wfTimestamp( TS_UNIX, $this->timestamp ), $ttl );
415  } else {
416  $ttl = $cache::TTL_DAY;
417  }
418 
419  return $cacheVal;
420  },
421  [ 'version' => self::VERSION ]
422  );
423 
424  $this->fileExists = $cachedValues['fileExists'];
425  if ( $this->fileExists ) {
426  $this->setProps( $cachedValues );
427  }
428 
429  $this->dataLoaded = true;
430  $this->extraDataLoaded = true;
431  foreach ( $this->getLazyCacheFields( '' ) as $field ) {
432  $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
433  }
434  }
435 
439  public function invalidateCache() {
440  $key = $this->getCacheKey();
441  if ( !$key ) {
442  return;
443  }
444 
445  $this->repo->getPrimaryDB()->onTransactionPreCommitOrIdle(
446  static function () use ( $key ) {
447  MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key );
448  },
449  __METHOD__
450  );
451  }
452 
460  public function loadFromFile( $path = null ) {
461  $props = $this->repo->getFileProps( $path ?? $this->getVirtualUrl() );
462  $this->setProps( $props );
463  }
464 
472  protected function getCacheFields( $prefix = 'img_' ) {
473  if ( $prefix !== '' ) {
474  throw new InvalidArgumentException(
475  __METHOD__ . ' with a non-empty prefix is no longer supported.'
476  );
477  }
478 
479  // See self::getQueryInfo() for the fetching of the data from the DB,
480  // self::loadFromRow() for the loading of the object from the DB row,
481  // and self::loadFromCache() for the caching, and self::setProps() for
482  // populating the object from an array of data.
483  return [ 'size', 'width', 'height', 'bits', 'media_type',
484  'major_mime', 'minor_mime', 'timestamp', 'sha1', 'description' ];
485  }
486 
494  protected function getLazyCacheFields( $prefix = 'img_' ) {
495  if ( $prefix !== '' ) {
496  throw new InvalidArgumentException(
497  __METHOD__ . ' with a non-empty prefix is no longer supported.'
498  );
499  }
500 
501  // Keep this in sync with the omit-lazy option in self::getQueryInfo().
502  return [ 'metadata' ];
503  }
504 
510  protected function loadFromDB( $flags = 0 ) {
511  $fname = static::class . '::' . __FUNCTION__;
512 
513  # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
514  $this->dataLoaded = true;
515  $this->extraDataLoaded = true;
516 
517  $dbr = ( $flags & self::READ_LATEST )
518  ? $this->repo->getPrimaryDB()
519  : $this->repo->getReplicaDB();
520 
521  $fileQuery = static::getQueryInfo();
522  $row = $dbr->selectRow(
523  $fileQuery['tables'],
524  $fileQuery['fields'],
525  [ 'img_name' => $this->getName() ],
526  $fname,
527  [],
528  $fileQuery['joins']
529  );
530 
531  if ( $row ) {
532  $this->loadFromRow( $row );
533  } else {
534  $this->fileExists = false;
535  }
536  }
537 
543  protected function loadExtraFromDB() {
544  if ( !$this->title ) {
545  return; // Avoid hard failure when the file does not exist. T221812
546  }
547 
548  $fname = static::class . '::' . __FUNCTION__;
549 
550  # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
551  $this->extraDataLoaded = true;
552 
553  $db = $this->repo->getReplicaDB();
554  $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
555  if ( !$fieldMap ) {
556  $db = $this->repo->getPrimaryDB();
557  $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
558  }
559 
560  if ( $fieldMap ) {
561  if ( isset( $fieldMap['metadata'] ) ) {
562  $this->loadMetadataFromDbFieldValue( $db, $fieldMap['metadata'] );
563  }
564  } else {
565  throw new MWException( "Could not find data for image '{$this->getName()}'." );
566  }
567  }
568 
574  private function loadExtraFieldsWithTimestamp( $dbr, $fname ) {
575  $fieldMap = false;
576 
577  $fileQuery = self::getQueryInfo( [ 'omit-nonlazy' ] );
578  $row = $dbr->selectRow(
579  $fileQuery['tables'],
580  $fileQuery['fields'],
581  [
582  'img_name' => $this->getName(),
583  'img_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
584  ],
585  $fname,
586  [],
587  $fileQuery['joins']
588  );
589  if ( $row ) {
590  $fieldMap = $this->unprefixRow( $row, 'img_' );
591  } else {
592  # File may have been uploaded over in the meantime; check the old versions
593  $fileQuery = OldLocalFile::getQueryInfo( [ 'omit-nonlazy' ] );
594  $row = $dbr->selectRow(
595  $fileQuery['tables'],
596  $fileQuery['fields'],
597  [
598  'oi_name' => $this->getName(),
599  'oi_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
600  ],
601  $fname,
602  [],
603  $fileQuery['joins']
604  );
605  if ( $row ) {
606  $fieldMap = $this->unprefixRow( $row, 'oi_' );
607  }
608  }
609 
610  return $fieldMap;
611  }
612 
619  protected function unprefixRow( $row, $prefix = 'img_' ) {
620  $array = (array)$row;
621  $prefixLength = strlen( $prefix );
622 
623  // Double check prefix once
624  if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
625  throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
626  }
627 
628  $decoded = [];
629  foreach ( $array as $name => $value ) {
630  $decoded[substr( $name, $prefixLength )] = $value;
631  }
632 
633  return $decoded;
634  }
635 
651  public function loadFromRow( $row, $prefix = 'img_' ) {
652  $this->dataLoaded = true;
653 
654  $unprefixed = $this->unprefixRow( $row, $prefix );
655 
656  $this->name = $unprefixed['name'];
657  $this->media_type = $unprefixed['media_type'];
658 
659  $services = MediaWikiServices::getInstance();
660  $this->description = $services->getCommentStore()
661  ->getComment( "{$prefix}description", $row )->text;
662 
663  $this->user = $services->getUserFactory()->newFromAnyId(
664  $unprefixed['user'] ?? null,
665  $unprefixed['user_text'] ?? null,
666  $unprefixed['actor'] ?? null
667  );
668 
669  $this->timestamp = wfTimestamp( TS_MW, $unprefixed['timestamp'] );
670 
672  $this->repo->getReplicaDB(), $unprefixed['metadata'] );
673 
674  if ( empty( $unprefixed['major_mime'] ) ) {
675  $this->major_mime = 'unknown';
676  $this->minor_mime = 'unknown';
677  $this->mime = 'unknown/unknown';
678  } else {
679  if ( !$unprefixed['minor_mime'] ) {
680  $unprefixed['minor_mime'] = 'unknown';
681  }
682  $this->major_mime = $unprefixed['major_mime'];
683  $this->minor_mime = $unprefixed['minor_mime'];
684  $this->mime = $unprefixed['major_mime'] . '/' . $unprefixed['minor_mime'];
685  }
686 
687  // Trim zero padding from char/binary field
688  $this->sha1 = rtrim( $unprefixed['sha1'], "\0" );
689 
690  // Normalize some fields to integer type, per their database definition.
691  // Use unary + so that overflows will be upgraded to double instead of
692  // being truncated as with intval(). This is important to allow > 2 GiB
693  // files on 32-bit systems.
694  $this->size = +$unprefixed['size'];
695  $this->width = +$unprefixed['width'];
696  $this->height = +$unprefixed['height'];
697  $this->bits = +$unprefixed['bits'];
698 
699  // Check for extra fields (deprecated since MW 1.37)
700  $extraFields = array_diff(
701  array_keys( $unprefixed ),
702  [
703  'name', 'media_type', 'description_text', 'description_data',
704  'description_cid', 'user', 'user_text', 'actor', 'timestamp',
705  'metadata', 'major_mime', 'minor_mime', 'sha1', 'size', 'width',
706  'height', 'bits'
707  ]
708  );
709  if ( $extraFields ) {
711  'Passing extra fields (' .
712  implode( ', ', $extraFields )
713  . ') to ' . __METHOD__ . ' was deprecated in MediaWiki 1.37. ' .
714  'Property assignment will be removed in a later version.',
715  '1.37' );
716  foreach ( $extraFields as $field ) {
717  $this->$field = $unprefixed[$field];
718  }
719  }
720 
721  $this->fileExists = true;
722  }
723 
729  public function load( $flags = 0 ) {
730  if ( !$this->dataLoaded ) {
731  if ( $flags & self::READ_LATEST ) {
732  $this->loadFromDB( $flags );
733  } else {
734  $this->loadFromCache();
735  }
736  }
737 
738  if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
739  // @note: loads on name/timestamp to reduce race condition problems
740  $this->loadExtraFromDB();
741  }
742  }
743 
748  public function maybeUpgradeRow() {
749  if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() || $this->upgrading ) {
750  return;
751  }
752 
753  $upgrade = false;
754  $reserialize = false;
755  if ( $this->media_type === null || $this->mime == 'image/svg' ) {
756  $upgrade = true;
757  } else {
758  $handler = $this->getHandler();
759  if ( $handler ) {
760  $validity = $handler->isFileMetadataValid( $this );
761  if ( $validity === MediaHandler::METADATA_BAD ) {
762  $upgrade = true;
763  } elseif ( $validity === MediaHandler::METADATA_COMPATIBLE
764  && $this->repo->isMetadataUpdateEnabled()
765  ) {
766  $upgrade = true;
767  } elseif ( $this->repo->isJsonMetadataEnabled()
768  && $this->repo->isMetadataReserializeEnabled()
769  ) {
770  if ( $this->repo->isSplitMetadataEnabled() && $this->isMetadataOversize() ) {
771  $reserialize = true;
772  } elseif ( $this->metadataSerializationFormat !== self::MDS_EMPTY &&
773  $this->metadataSerializationFormat !== self::MDS_JSON ) {
774  $reserialize = true;
775  }
776  }
777  }
778  }
779 
780  if ( $upgrade || $reserialize ) {
781  $this->upgrading = true;
782  // Defer updates unless in auto-commit CLI mode
783  DeferredUpdates::addCallableUpdate( function () use ( $upgrade ) {
784  $this->upgrading = false; // avoid duplicate updates
785  try {
786  if ( $upgrade ) {
787  $this->upgradeRow();
788  } else {
789  $this->reserializeMetadata();
790  }
791  } catch ( LocalFileLockError $e ) {
792  // let the other process handle it (or do it next time)
793  }
794  } );
795  }
796  }
797 
801  public function getUpgraded() {
802  return $this->upgraded;
803  }
804 
809  public function upgradeRow() {
810  $dbw = $this->repo->getPrimaryDB();
811 
812  // Make a DB query condition that will fail to match the image row if the
813  // image was reuploaded while the upgrade was in process.
814  $freshnessCondition = [ 'img_timestamp' => $dbw->timestamp( $this->getTimestamp() ) ];
815 
816  $this->loadFromFile();
817 
818  # Don't destroy file info of missing files
819  if ( !$this->fileExists ) {
820  wfDebug( __METHOD__ . ": file does not exist, aborting" );
821 
822  return;
823  }
824 
825  list( $major, $minor ) = self::splitMime( $this->mime );
826 
827  wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema" );
828 
829  $dbw->update( 'image',
830  [
831  'img_size' => $this->size,
832  'img_width' => $this->width,
833  'img_height' => $this->height,
834  'img_bits' => $this->bits,
835  'img_media_type' => $this->media_type,
836  'img_major_mime' => $major,
837  'img_minor_mime' => $minor,
838  'img_metadata' => $this->getMetadataForDb( $dbw ),
839  'img_sha1' => $this->sha1,
840  ],
841  array_merge(
842  [ 'img_name' => $this->getName() ],
843  $freshnessCondition
844  ),
845  __METHOD__
846  );
847 
848  $this->invalidateCache();
849 
850  $this->upgraded = true; // avoid rework/retries
851  }
852 
857  protected function reserializeMetadata() {
858  if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
859  return;
860  }
861  $dbw = $this->repo->getPrimaryDB();
862  $dbw->update(
863  'image',
864  [ 'img_metadata' => $this->getMetadataForDb( $dbw ) ],
865  [
866  'img_name' => $this->name,
867  'img_timestamp' => $dbw->timestamp( $this->timestamp ),
868  ],
869  __METHOD__
870  );
871  $this->upgraded = true;
872  }
873 
885  protected function setProps( $info ) {
886  $this->dataLoaded = true;
887  $fields = $this->getCacheFields( '' );
888  $fields[] = 'fileExists';
889 
890  foreach ( $fields as $field ) {
891  if ( isset( $info[$field] ) ) {
892  $this->$field = $info[$field];
893  }
894  }
895 
896  // Only our own cache sets these properties, so they both should be present.
897  if ( isset( $info['user'] ) &&
898  isset( $info['user_text'] ) &&
899  $info['user_text'] !== ''
900  ) {
901  $this->user = new UserIdentityValue( $info['user'], $info['user_text'] );
902  }
903 
904  // Fix up mime fields
905  if ( isset( $info['major_mime'] ) ) {
906  $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
907  } elseif ( isset( $info['mime'] ) ) {
908  $this->mime = $info['mime'];
909  list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
910  }
911 
912  if ( isset( $info['metadata'] ) ) {
913  if ( is_string( $info['metadata'] ) ) {
914  $this->loadMetadataFromString( $info['metadata'] );
915  } elseif ( is_array( $info['metadata'] ) ) {
916  $this->metadataArray = $info['metadata'];
917  if ( isset( $info['metadataBlobs'] ) ) {
918  $this->metadataBlobs = $info['metadataBlobs'];
919  $this->unloadedMetadataBlobs = array_diff_key(
920  $this->metadataBlobs,
921  $this->metadataArray
922  );
923  } else {
924  $this->metadataBlobs = [];
925  $this->unloadedMetadataBlobs = [];
926  }
927  } else {
928  $logger = LoggerFactory::getInstance( 'LocalFile' );
929  $logger->warning( __METHOD__ . ' given invalid metadata of type ' .
930  gettype( $info['metadata'] ) );
931  $this->metadataArray = [];
932  }
933  $this->extraDataLoaded = true;
934  }
935  }
936 
952  public function isMissing() {
953  if ( $this->missing === null ) {
954  $fileExists = $this->repo->fileExists( $this->getVirtualUrl() );
955  $this->missing = !$fileExists;
956  }
957 
958  return $this->missing;
959  }
960 
968  public function getWidth( $page = 1 ) {
969  $page = (int)$page;
970  if ( $page < 1 ) {
971  $page = 1;
972  }
973 
974  $this->load();
975 
976  if ( $this->isMultipage() ) {
977  $handler = $this->getHandler();
978  if ( !$handler ) {
979  return 0;
980  }
981  $dim = $handler->getPageDimensions( $this, $page );
982  if ( $dim ) {
983  return $dim['width'];
984  } else {
985  // For non-paged media, the false goes through an
986  // intval, turning failure into 0, so do same here.
987  return 0;
988  }
989  } else {
990  return $this->width;
991  }
992  }
993 
1001  public function getHeight( $page = 1 ) {
1002  $page = (int)$page;
1003  if ( $page < 1 ) {
1004  $page = 1;
1005  }
1006 
1007  $this->load();
1008 
1009  if ( $this->isMultipage() ) {
1010  $handler = $this->getHandler();
1011  if ( !$handler ) {
1012  return 0;
1013  }
1014  $dim = $handler->getPageDimensions( $this, $page );
1015  if ( $dim ) {
1016  return $dim['height'];
1017  } else {
1018  // For non-paged media, the false goes through an
1019  // intval, turning failure into 0, so do same here.
1020  return 0;
1021  }
1022  } else {
1023  return $this->height;
1024  }
1025  }
1026 
1034  public function getDescriptionShortUrl() {
1035  if ( !$this->title ) {
1036  return null; // Avoid hard failure when the file does not exist. T221812
1037  }
1038 
1039  $pageId = $this->title->getArticleID();
1040 
1041  if ( $pageId ) {
1042  $url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
1043  if ( $url !== false ) {
1044  return $url;
1045  }
1046  }
1047  return null;
1048  }
1049 
1056  public function getMetadata() {
1057  $data = $this->getMetadataArray();
1058  if ( !$data ) {
1059  return '';
1060  } elseif ( array_keys( $data ) === [ '_error' ] ) {
1061  // Legacy error encoding
1062  return $data['_error'];
1063  } else {
1064  return serialize( $this->getMetadataArray() );
1065  }
1066  }
1067 
1074  public function getMetadataArray(): array {
1075  $this->load( self::LOAD_ALL );
1076  if ( $this->unloadedMetadataBlobs ) {
1077  return $this->getMetadataItems(
1078  array_unique( array_merge(
1079  array_keys( $this->metadataArray ),
1080  array_keys( $this->unloadedMetadataBlobs )
1081  ) )
1082  );
1083  }
1084  return $this->metadataArray;
1085  }
1086 
1087  public function getMetadataItems( array $itemNames ): array {
1088  $this->load( self::LOAD_ALL );
1089  $result = [];
1090  $addresses = [];
1091  foreach ( $itemNames as $itemName ) {
1092  if ( array_key_exists( $itemName, $this->metadataArray ) ) {
1093  $result[$itemName] = $this->metadataArray[$itemName];
1094  } elseif ( isset( $this->unloadedMetadataBlobs[$itemName] ) ) {
1095  $addresses[$itemName] = $this->unloadedMetadataBlobs[$itemName];
1096  }
1097  }
1098 
1099  if ( $addresses ) {
1100  $resultFromBlob = $this->metadataStorageHelper->getMetadataFromBlobStore( $addresses );
1101  foreach ( $addresses as $itemName => $address ) {
1102  unset( $this->unloadedMetadataBlobs[$itemName] );
1103  $value = $resultFromBlob[$itemName] ?? null;
1104  if ( $value !== null ) {
1105  $result[$itemName] = $value;
1106  $this->metadataArray[$itemName] = $value;
1107  }
1108  }
1109  }
1110  return $result;
1111  }
1112 
1124  public function getMetadataForDb( IDatabase $db ) {
1125  $this->load( self::LOAD_ALL );
1126  if ( !$this->metadataArray && !$this->metadataBlobs ) {
1127  $s = '';
1128  } elseif ( $this->repo->isJsonMetadataEnabled() ) {
1129  $s = $this->getJsonMetadata();
1130  } else {
1131  $s = serialize( $this->getMetadataArray() );
1132  }
1133  if ( !is_string( $s ) ) {
1134  throw new MWException( 'Could not serialize image metadata value for DB' );
1135  }
1136  return $db->encodeBlob( $s );
1137  }
1138 
1145  private function getJsonMetadata() {
1146  // Directly store data that is not already in BlobStore
1147  $envelope = [
1148  'data' => array_diff_key( $this->metadataArray, $this->metadataBlobs )
1149  ];
1150 
1151  // Also store the blob addresses
1152  if ( $this->metadataBlobs ) {
1153  $envelope['blobs'] = $this->metadataBlobs;
1154  }
1155 
1156  list( $s, $blobAddresses ) = $this->metadataStorageHelper->getJsonMetadata( $this, $envelope );
1157 
1158  // Repeated calls to this function should not keep inserting more blobs
1159  $this->metadataBlobs += $blobAddresses;
1160 
1161  return $s;
1162  }
1163 
1170  private function isMetadataOversize() {
1171  if ( !$this->repo->isSplitMetadataEnabled() ) {
1172  return false;
1173  }
1174  $threshold = $this->repo->getSplitMetadataThreshold();
1175  $directItems = array_diff_key( $this->metadataArray, $this->metadataBlobs );
1176  foreach ( $directItems as $value ) {
1177  if ( strlen( $this->metadataStorageHelper->jsonEncode( $value ) ) > $threshold ) {
1178  return true;
1179  }
1180  }
1181  return false;
1182  }
1183 
1192  protected function loadMetadataFromDbFieldValue( IDatabase $db, $metadataBlob ) {
1193  $this->loadMetadataFromString( $db->decodeBlob( $metadataBlob ) );
1194  }
1195 
1203  protected function loadMetadataFromString( $metadataString ) {
1204  $this->extraDataLoaded = true;
1205  $this->metadataArray = [];
1206  $this->metadataBlobs = [];
1207  $this->unloadedMetadataBlobs = [];
1208  $metadataString = (string)$metadataString;
1209  if ( $metadataString === '' ) {
1210  $this->metadataSerializationFormat = self::MDS_EMPTY;
1211  return;
1212  }
1213  if ( $metadataString[0] === '{' ) {
1214  $envelope = $this->metadataStorageHelper->jsonDecode( $metadataString );
1215  if ( !$envelope ) {
1216  // Legacy error encoding
1217  $this->metadataArray = [ '_error' => $metadataString ];
1218  $this->metadataSerializationFormat = self::MDS_LEGACY;
1219  } else {
1220  $this->metadataSerializationFormat = self::MDS_JSON;
1221  if ( isset( $envelope['data'] ) ) {
1222  $this->metadataArray = $envelope['data'];
1223  }
1224  if ( isset( $envelope['blobs'] ) ) {
1225  $this->metadataBlobs = $this->unloadedMetadataBlobs = $envelope['blobs'];
1226  }
1227  }
1228  } else {
1229  // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1230  $data = @unserialize( $metadataString );
1231  if ( !is_array( $data ) ) {
1232  // Legacy error encoding
1233  $data = [ '_error' => $metadataString ];
1234  $this->metadataSerializationFormat = self::MDS_LEGACY;
1235  } else {
1236  $this->metadataSerializationFormat = self::MDS_PHP;
1237  }
1238  $this->metadataArray = $data;
1239  }
1240  }
1241 
1246  public function getBitDepth() {
1247  $this->load();
1248 
1249  return (int)$this->bits;
1250  }
1251 
1257  public function getSize() {
1258  $this->load();
1259 
1260  return $this->size;
1261  }
1262 
1268  public function getMimeType() {
1269  $this->load();
1270 
1271  return $this->mime;
1272  }
1273 
1280  public function getMediaType() {
1281  $this->load();
1282 
1283  return $this->media_type;
1284  }
1285 
1297  public function exists() {
1298  $this->load();
1299 
1300  return $this->fileExists;
1301  }
1302 
1319  protected function getThumbnails( $archiveName = false ) {
1320  if ( $archiveName ) {
1321  $dir = $this->getArchiveThumbPath( $archiveName );
1322  } else {
1323  $dir = $this->getThumbPath();
1324  }
1325 
1326  $backend = $this->repo->getBackend();
1327  $files = [ $dir ];
1328  try {
1329  $iterator = $backend->getFileList( [ 'dir' => $dir ] );
1330  if ( $iterator !== null ) {
1331  foreach ( $iterator as $file ) {
1332  $files[] = $file;
1333  }
1334  }
1335  } catch ( FileBackendError $e ) {
1336  } // suppress (T56674)
1337 
1338  return $files;
1339  }
1340 
1344  private function purgeMetadataCache() {
1345  $this->invalidateCache();
1346  }
1347 
1356  public function purgeCache( $options = [] ) {
1357  // Refresh metadata cache
1358  $this->maybeUpgradeRow();
1359  $this->purgeMetadataCache();
1360 
1361  // Delete thumbnails
1362  $this->purgeThumbnails( $options );
1363 
1364  // Purge CDN cache for this file
1365  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1366  $hcu->purgeUrls(
1367  $this->getUrl(),
1368  !empty( $options['forThumbRefresh'] )
1369  ? $hcu::PURGE_PRESEND // just a manual purge
1370  : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1371  );
1372  }
1373 
1379  public function purgeOldThumbnails( $archiveName ) {
1380  // Get a list of old thumbnails
1381  $thumbs = $this->getThumbnails( $archiveName );
1382 
1383  // Delete thumbnails from storage, and prevent the directory itself from being purged
1384  $dir = array_shift( $thumbs );
1385  $this->purgeThumbList( $dir, $thumbs );
1386 
1387  $urls = [];
1388  foreach ( $thumbs as $thumb ) {
1389  $urls[] = $this->getArchiveThumbUrl( $archiveName, $thumb );
1390  }
1391 
1392  // Purge any custom thumbnail caches
1393  $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, $archiveName, $urls );
1394 
1395  // Purge the CDN
1396  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1397  $hcu->purgeUrls( $urls, $hcu::PURGE_PRESEND );
1398  }
1399 
1406  public function purgeThumbnails( $options = [] ) {
1407  $thumbs = $this->getThumbnails();
1408 
1409  // Delete thumbnails from storage, and prevent the directory itself from being purged
1410  $dir = array_shift( $thumbs );
1411  $this->purgeThumbList( $dir, $thumbs );
1412 
1413  // Always purge all files from CDN regardless of handler filters
1414  $urls = [];
1415  foreach ( $thumbs as $thumb ) {
1416  $urls[] = $this->getThumbUrl( $thumb );
1417  }
1418 
1419  // Give the media handler a chance to filter the file purge list
1420  if ( !empty( $options['forThumbRefresh'] ) ) {
1421  $handler = $this->getHandler();
1422  if ( $handler ) {
1423  $handler->filterThumbnailPurgeList( $thumbs, $options );
1424  }
1425  }
1426 
1427  // Purge any custom thumbnail caches
1428  $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, false, $urls );
1429 
1430  // Purge the CDN
1431  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1432  $hcu->purgeUrls(
1433  $urls,
1434  !empty( $options['forThumbRefresh'] )
1435  ? $hcu::PURGE_PRESEND // just a manual purge
1436  : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1437  );
1438  }
1439 
1446  public function prerenderThumbnails() {
1447  $uploadThumbnailRenderMap = MediaWikiServices::getInstance()
1448  ->getMainConfig()->get( MainConfigNames::UploadThumbnailRenderMap );
1449 
1450  $jobs = [];
1451 
1452  $sizes = $uploadThumbnailRenderMap;
1453  rsort( $sizes );
1454 
1455  foreach ( $sizes as $size ) {
1456  if ( $this->isMultipage() ) {
1457  // (T309114) Only trigger render jobs up to MAX_PAGE_RENDER_JOBS to avoid
1458  // a flood of jobs for huge files.
1459  $pageLimit = min( $this->pageCount(), self::MAX_PAGE_RENDER_JOBS );
1460 
1461  for ( $page = 1; $page <= $pageLimit; $page++ ) {
1462  $jobs[] = new ThumbnailRenderJob(
1463  $this->getTitle(),
1464  [ 'transformParams' => [
1465  'width' => $size,
1466  'page' => $page,
1467  ] ]
1468  );
1469  }
1470  } elseif ( $this->isVectorized() || $this->getWidth() > $size ) {
1471  $jobs[] = new ThumbnailRenderJob(
1472  $this->getTitle(),
1473  [ 'transformParams' => [ 'width' => $size ] ]
1474  );
1475  }
1476  }
1477 
1478  if ( $jobs ) {
1479  MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs );
1480  }
1481  }
1482 
1489  protected function purgeThumbList( $dir, $files ) {
1490  $fileListDebug = strtr(
1491  var_export( $files, true ),
1492  [ "\n" => '' ]
1493  );
1494  wfDebug( __METHOD__ . ": $fileListDebug" );
1495 
1496  if ( $this->repo->supportsSha1URLs() ) {
1497  $reference = $this->getSha1();
1498  } else {
1499  $reference = $this->getName();
1500  }
1501 
1502  $purgeList = [];
1503  foreach ( $files as $file ) {
1504  # Check that the reference (filename or sha1) is part of the thumb name
1505  # This is a basic check to avoid erasing unrelated directories
1506  if ( strpos( $file, $reference ) !== false
1507  || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
1508  ) {
1509  $purgeList[] = "{$dir}/{$file}";
1510  }
1511  }
1512 
1513  # Delete the thumbnails
1514  $this->repo->quickPurgeBatch( $purgeList );
1515  # Clear out the thumbnail directory if empty
1516  $this->repo->quickCleanDir( $dir );
1517  }
1518 
1530  public function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1531  if ( !$this->exists() ) {
1532  return []; // Avoid hard failure when the file does not exist. T221812
1533  }
1534 
1535  $dbr = $this->repo->getReplicaDB();
1536  $oldFileQuery = OldLocalFile::getQueryInfo();
1537 
1538  $tables = $oldFileQuery['tables'];
1539  $fields = $oldFileQuery['fields'];
1540  $join_conds = $oldFileQuery['joins'];
1541  $conds = $opts = [];
1542  $eq = $inc ? '=' : '';
1543  $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
1544 
1545  if ( $start ) {
1546  $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
1547  }
1548 
1549  if ( $end ) {
1550  $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
1551  }
1552 
1553  if ( $limit ) {
1554  $opts['LIMIT'] = $limit;
1555  }
1556 
1557  // Search backwards for time > x queries
1558  $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
1559  $opts['ORDER BY'] = "oi_timestamp $order";
1560  $opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1561 
1562  $this->getHookRunner()->onLocalFile__getHistory( $this, $tables, $fields,
1563  $conds, $opts, $join_conds );
1564 
1565  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1566  $r = [];
1567 
1568  foreach ( $res as $row ) {
1569  $r[] = $this->repo->newFileFromRow( $row );
1570  }
1571 
1572  if ( $order == 'ASC' ) {
1573  $r = array_reverse( $r ); // make sure it ends up descending
1574  }
1575 
1576  return $r;
1577  }
1578 
1589  public function nextHistoryLine() {
1590  if ( !$this->exists() ) {
1591  return false; // Avoid hard failure when the file does not exist. T221812
1592  }
1593 
1594  # Polymorphic function name to distinguish foreign and local fetches
1595  $fname = static::class . '::' . __FUNCTION__;
1596 
1597  $dbr = $this->repo->getReplicaDB();
1598 
1599  if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1600  $fileQuery = self::getQueryInfo();
1601  $this->historyRes = $dbr->select( $fileQuery['tables'],
1602  $fileQuery['fields'] + [
1603  'oi_archive_name' => $dbr->addQuotes( '' ),
1604  'oi_deleted' => 0,
1605  ],
1606  [ 'img_name' => $this->title->getDBkey() ],
1607  $fname,
1608  [],
1609  $fileQuery['joins']
1610  );
1611 
1612  if ( $this->historyRes->numRows() == 0 ) {
1613  $this->historyRes = null;
1614 
1615  return false;
1616  }
1617  } elseif ( $this->historyLine == 1 ) {
1618  $fileQuery = OldLocalFile::getQueryInfo();
1619  $this->historyRes = $dbr->select(
1620  $fileQuery['tables'],
1621  $fileQuery['fields'],
1622  [ 'oi_name' => $this->title->getDBkey() ],
1623  $fname,
1624  [ 'ORDER BY' => 'oi_timestamp DESC' ],
1625  $fileQuery['joins']
1626  );
1627  }
1628  $this->historyLine++;
1629 
1630  return $this->historyRes->fetchObject();
1631  }
1632 
1637  public function resetHistory() {
1638  $this->historyLine = 0;
1639 
1640  if ( $this->historyRes !== null ) {
1641  $this->historyRes = null;
1642  }
1643  }
1644 
1678  public function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1679  $timestamp = false, Authority $uploader = null, $tags = [],
1680  $createNullRevision = true, $revert = false
1681  ) {
1682  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1683  return $this->readOnlyFatalStatus();
1684  } elseif ( MediaWikiServices::getInstance()->getRevisionStore()->isReadOnly() ) {
1685  // Check this in advance to avoid writing to FileBackend and the file tables,
1686  // only to fail on insert the revision due to the text store being unavailable.
1687  return $this->readOnlyFatalStatus();
1688  }
1689 
1690  $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1691  if ( !$props ) {
1692  if ( FileRepo::isVirtualUrl( $srcPath )
1693  || FileBackend::isStoragePath( $srcPath )
1694  ) {
1695  $props = $this->repo->getFileProps( $srcPath );
1696  } else {
1697  $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
1698  $props = $mwProps->getPropsFromPath( $srcPath, true );
1699  }
1700  }
1701 
1702  $options = [];
1703  $handler = MediaHandler::getHandler( $props['mime'] );
1704  if ( $handler ) {
1705  if ( is_string( $props['metadata'] ) ) {
1706  // This supports callers directly fabricating a metadata
1707  // property using serialize(). Normally the metadata property
1708  // comes from MWFileProps, in which case it won't be a string.
1709  // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1710  $metadata = @unserialize( $props['metadata'] );
1711  } else {
1712  $metadata = $props['metadata'];
1713  }
1714 
1715  if ( is_array( $metadata ) ) {
1716  $options['headers'] = $handler->getContentHeaders( $metadata );
1717  }
1718  } else {
1719  $options['headers'] = [];
1720  }
1721 
1722  // Trim spaces on user supplied text
1723  $comment = trim( $comment );
1724 
1725  $status = $this->publish( $src, $flags, $options );
1726 
1727  if ( $status->successCount >= 2 ) {
1728  // There will be a copy+(one of move,copy,store).
1729  // The first succeeding does not commit us to updating the DB
1730  // since it simply copied the current version to a timestamped file name.
1731  // It is only *preferable* to avoid leaving such files orphaned.
1732  // Once the second operation goes through, then the current version was
1733  // updated and we must therefore update the DB too.
1734  $oldver = $status->value;
1735 
1736  if ( $uploader === null ) {
1737  // Uploader argument is optional, fall back to the context authority
1738  $uploader = RequestContext::getMain()->getAuthority();
1739  }
1740 
1741  $uploadStatus = $this->recordUpload3(
1742  $oldver,
1743  $comment,
1744  $pageText,
1745  $uploader,
1746  $props,
1747  $timestamp,
1748  $tags,
1749  $createNullRevision,
1750  $revert
1751  );
1752  if ( !$uploadStatus->isOK() ) {
1753  if ( $uploadStatus->hasMessage( 'filenotfound' ) ) {
1754  // update filenotfound error with more specific path
1755  $status->fatal( 'filenotfound', $srcPath );
1756  } else {
1757  $status->merge( $uploadStatus );
1758  }
1759  }
1760  }
1761 
1762  return $status;
1763  }
1764 
1781  public function recordUpload3(
1782  string $oldver,
1783  string $comment,
1784  string $pageText,
1785  Authority $performer,
1786  $props = false,
1787  $timestamp = false,
1788  $tags = [],
1789  bool $createNullRevision = true,
1790  bool $revert = false
1791  ): Status {
1792  $dbw = $this->repo->getPrimaryDB();
1793 
1794  # Imports or such might force a certain timestamp; otherwise we generate
1795  # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1796  if ( $timestamp === false ) {
1797  $timestamp = $dbw->timestamp();
1798  $allowTimeKludge = true;
1799  } else {
1800  $allowTimeKludge = false;
1801  }
1802 
1803  $props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1804  $props['description'] = $comment;
1805  $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1806  $this->setProps( $props );
1807 
1808  # Fail now if the file isn't there
1809  if ( !$this->fileExists ) {
1810  wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!" );
1811 
1812  return Status::newFatal( 'filenotfound', $this->getRel() );
1813  }
1814 
1815  $actorNormalizaton = MediaWikiServices::getInstance()->getActorNormalization();
1816 
1817  $dbw->startAtomic( __METHOD__ );
1818 
1819  $actorId = $actorNormalizaton->acquireActorId( $performer->getUser(), $dbw );
1820  $this->user = $performer->getUser();
1821 
1822  # Test to see if the row exists using INSERT IGNORE
1823  # This avoids race conditions by locking the row until the commit, and also
1824  # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1825  $commentStore = MediaWikiServices::getInstance()->getCommentStore();
1826  $commentFields = $commentStore->insert( $dbw, 'img_description', $comment );
1827  $actorFields = [ 'img_actor' => $actorId ];
1828  $dbw->insert( 'image',
1829  [
1830  'img_name' => $this->getName(),
1831  'img_size' => $this->size,
1832  'img_width' => intval( $this->width ),
1833  'img_height' => intval( $this->height ),
1834  'img_bits' => $this->bits,
1835  'img_media_type' => $this->media_type,
1836  'img_major_mime' => $this->major_mime,
1837  'img_minor_mime' => $this->minor_mime,
1838  'img_timestamp' => $timestamp,
1839  'img_metadata' => $this->getMetadataForDb( $dbw ),
1840  'img_sha1' => $this->sha1
1841  ] + $commentFields + $actorFields,
1842  __METHOD__,
1843  [ 'IGNORE' ]
1844  );
1845  $reupload = ( $dbw->affectedRows() == 0 );
1846 
1847  if ( $reupload ) {
1848  $row = $dbw->selectRow(
1849  'image',
1850  [ 'img_timestamp', 'img_sha1' ],
1851  [ 'img_name' => $this->getName() ],
1852  __METHOD__,
1853  [ 'LOCK IN SHARE MODE' ]
1854  );
1855 
1856  if ( $row && $row->img_sha1 === $this->sha1 ) {
1857  $dbw->endAtomic( __METHOD__ );
1858  wfDebug( __METHOD__ . ": File " . $this->getRel() . " already exists!" );
1859  $title = Title::newFromText( $this->getName(), NS_FILE );
1860  return Status::newFatal( 'fileexists-no-change', $title->getPrefixedText() );
1861  }
1862 
1863  if ( $allowTimeKludge ) {
1864  # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1865  $lUnixtime = $row ? (int)wfTimestamp( TS_UNIX, $row->img_timestamp ) : false;
1866  # Avoid a timestamp that is not newer than the last version
1867  # TODO: the image/oldimage tables should be like page/revision with an ID field
1868  if ( $lUnixtime && (int)wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
1869  sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1870  $timestamp = $dbw->timestamp( $lUnixtime + 1 );
1871  $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1872  }
1873  }
1874 
1875  $tables = [ 'image' ];
1876  $fields = [
1877  'oi_name' => 'img_name',
1878  'oi_archive_name' => $dbw->addQuotes( $oldver ),
1879  'oi_size' => 'img_size',
1880  'oi_width' => 'img_width',
1881  'oi_height' => 'img_height',
1882  'oi_bits' => 'img_bits',
1883  'oi_description_id' => 'img_description_id',
1884  'oi_timestamp' => 'img_timestamp',
1885  'oi_metadata' => 'img_metadata',
1886  'oi_media_type' => 'img_media_type',
1887  'oi_major_mime' => 'img_major_mime',
1888  'oi_minor_mime' => 'img_minor_mime',
1889  'oi_sha1' => 'img_sha1',
1890  'oi_actor' => 'img_actor',
1891  ];
1892  $joins = [];
1893 
1894  # (T36993) Note: $oldver can be empty here, if the previous
1895  # version of the file was broken. Allow registration of the new
1896  # version to continue anyway, because that's better than having
1897  # an image that's not fixable by user operations.
1898  # Collision, this is an update of a file
1899  # Insert previous contents into oldimage
1900  $dbw->insertSelect( 'oldimage', $tables, $fields,
1901  [ 'img_name' => $this->getName() ], __METHOD__, [], [], $joins );
1902 
1903  # Update the current image row
1904  $dbw->update( 'image',
1905  [
1906  'img_size' => $this->size,
1907  'img_width' => intval( $this->width ),
1908  'img_height' => intval( $this->height ),
1909  'img_bits' => $this->bits,
1910  'img_media_type' => $this->media_type,
1911  'img_major_mime' => $this->major_mime,
1912  'img_minor_mime' => $this->minor_mime,
1913  'img_timestamp' => $timestamp,
1914  'img_metadata' => $this->getMetadataForDb( $dbw ),
1915  'img_sha1' => $this->sha1
1916  ] + $commentFields + $actorFields,
1917  [ 'img_name' => $this->getName() ],
1918  __METHOD__
1919  );
1920  }
1921 
1922  $descTitle = $this->getTitle();
1923  $descId = $descTitle->getArticleID();
1924  $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $descTitle );
1925  if ( !$wikiPage instanceof WikiFilePage ) {
1926  throw new MWException( 'Cannot instance WikiFilePage for ' . $this->getName()
1927  . ', got instance of ' . get_class( $wikiPage ) );
1928  }
1929  $wikiPage->setFile( $this );
1930 
1931  // Determine log action. If reupload is done by reverting, use a special log_action.
1932  if ( $revert ) {
1933  $logAction = 'revert';
1934  } elseif ( $reupload ) {
1935  $logAction = 'overwrite';
1936  } else {
1937  $logAction = 'upload';
1938  }
1939  // Add the log entry...
1940  $logEntry = new ManualLogEntry( 'upload', $logAction );
1941  $logEntry->setTimestamp( $this->timestamp );
1942  $logEntry->setPerformer( $performer->getUser() );
1943  $logEntry->setComment( $comment );
1944  $logEntry->setTarget( $descTitle );
1945  // Allow people using the api to associate log entries with the upload.
1946  // Log has a timestamp, but sometimes different from upload timestamp.
1947  $logEntry->setParameters(
1948  [
1949  'img_sha1' => $this->sha1,
1950  'img_timestamp' => $timestamp,
1951  ]
1952  );
1953  // Note we keep $logId around since during new image
1954  // creation, page doesn't exist yet, so log_page = 0
1955  // but we want it to point to the page we're making,
1956  // so we later modify the log entry.
1957  // For a similar reason, we avoid making an RC entry
1958  // now and wait until the page exists.
1959  $logId = $logEntry->insert();
1960 
1961  if ( $descTitle->exists() ) {
1962  if ( $createNullRevision ) {
1963  $revStore = MediaWikiServices::getInstance()->getRevisionStore();
1964  // Use own context to get the action text in content language
1965  $formatter = LogFormatter::newFromEntry( $logEntry );
1966  $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1967  $editSummary = $formatter->getPlainActionText();
1968  $summary = CommentStoreComment::newUnsavedComment( $editSummary );
1969  $nullRevRecord = $revStore->newNullRevision(
1970  $dbw,
1971  $descTitle,
1972  $summary,
1973  false,
1974  $performer->getUser()
1975  );
1976 
1977  if ( $nullRevRecord ) {
1978  $inserted = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
1979 
1980  $this->getHookRunner()->onRevisionFromEditComplete(
1981  $wikiPage,
1982  $inserted,
1983  $inserted->getParentId(),
1984  $performer->getUser(),
1985  $tags
1986  );
1987 
1988  $wikiPage->updateRevisionOn( $dbw, $inserted );
1989  // Associate null revision id
1990  $logEntry->setAssociatedRevId( $inserted->getId() );
1991  }
1992  }
1993 
1994  $newPageContent = null;
1995  } else {
1996  // Make the description page and RC log entry post-commit
1997  $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1998  }
1999 
2000  // NOTE: Even after ending this atomic section, we are probably still in the implicit
2001  // transaction started by any prior master query in the request. We cannot yet safely
2002  // schedule jobs, see T263301.
2003  $dbw->endAtomic( __METHOD__ );
2004  $fname = __METHOD__;
2005 
2006  # Do some cache purges after final commit so that:
2007  # a) Changes are more likely to be seen post-purge
2008  # b) They won't cause rollback of the log publish/update above
2009  $purgeUpdate = new AutoCommitUpdate(
2010  $dbw,
2011  __METHOD__,
2013  function () use (
2014  $reupload, $wikiPage, $newPageContent, $comment, $performer,
2015  $logEntry, $logId, $descId, $tags, $fname
2016  ) {
2017  # Update memcache after the commit
2018  $this->invalidateCache();
2019 
2020  $updateLogPage = false;
2021  if ( $newPageContent ) {
2022  # New file page; create the description page.
2023  # There's already a log entry, so don't make a second RC entry
2024  # CDN and file cache for the description page are purged by doUserEditContent.
2025  $status = $wikiPage->doUserEditContent(
2026  $newPageContent,
2027  $performer,
2028  $comment,
2030  );
2031 
2032  if ( isset( $status->value['revision-record'] ) ) {
2034  $revRecord = $status->value['revision-record'];
2035  // Associate new page revision id
2036  $logEntry->setAssociatedRevId( $revRecord->getId() );
2037  }
2038  // This relies on the resetArticleID() call in WikiPage::insertOn(),
2039  // which is triggered on $descTitle by doUserEditContent() above.
2040  if ( isset( $status->value['revision-record'] ) ) {
2042  $revRecord = $status->value['revision-record'];
2043  $updateLogPage = $revRecord->getPageId();
2044  }
2045  } else {
2046  # Existing file page: invalidate description page cache
2047  $title = $wikiPage->getTitle();
2048  $title->invalidateCache();
2049  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2050  $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2051  # Allow the new file version to be patrolled from the page footer
2053  }
2054 
2055  # Update associated rev id. This should be done by $logEntry->insert() earlier,
2056  # but setAssociatedRevId() wasn't called at that point yet...
2057  $logParams = $logEntry->getParameters();
2058  $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
2059  $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
2060  if ( $updateLogPage ) {
2061  # Also log page, in case where we just created it above
2062  $update['log_page'] = $updateLogPage;
2063  }
2064  $this->getRepo()->getPrimaryDB()->update(
2065  'logging',
2066  $update,
2067  [ 'log_id' => $logId ],
2068  $fname
2069  );
2070  $this->getRepo()->getPrimaryDB()->insert(
2071  'log_search',
2072  [
2073  'ls_field' => 'associated_rev_id',
2074  'ls_value' => (string)$logEntry->getAssociatedRevId(),
2075  'ls_log_id' => $logId,
2076  ],
2077  $fname
2078  );
2079 
2080  # Add change tags, if any
2081  if ( $tags ) {
2082  $logEntry->addTags( $tags );
2083  }
2084 
2085  # Uploads can be patrolled
2086  $logEntry->setIsPatrollable( true );
2087 
2088  # Now that the log entry is up-to-date, make an RC entry.
2089  $logEntry->publish( $logId );
2090 
2091  # Run hook for other updates (typically more cache purging)
2092  $this->getHookRunner()->onFileUpload( $this, $reupload, !$newPageContent );
2093 
2094  if ( $reupload ) {
2095  # Delete old thumbnails
2096  $this->purgeThumbnails();
2097  # Remove the old file from the CDN cache
2098  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2099  $hcu->purgeUrls( $this->getUrl(), $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2100  } else {
2101  # Update backlink pages pointing to this title if created
2102  $blcFactory = MediaWikiServices::getInstance()->getBacklinkCacheFactory();
2103  LinksUpdate::queueRecursiveJobsForTable(
2104  $this->getTitle(),
2105  'imagelinks',
2106  'upload-image',
2107  $performer->getUser()->getName(),
2108  $blcFactory->getBacklinkCache( $this->getTitle() )
2109  );
2110  }
2111 
2112  $this->prerenderThumbnails();
2113  }
2114  );
2115 
2116  # Invalidate cache for all pages using this file
2117  $cacheUpdateJob = HTMLCacheUpdateJob::newForBacklinks(
2118  $this->getTitle(),
2119  'imagelinks',
2120  [ 'causeAction' => 'file-upload', 'causeAgent' => $performer->getUser()->getName() ]
2121  );
2122 
2123  // NOTE: We are probably still in the implicit transaction started by DBO_TRX. We should
2124  // only schedule jobs after that transaction was committed, so a job queue failure
2125  // doesn't cause the upload to fail (T263301). Also, we should generally not schedule any
2126  // Jobs or the DeferredUpdates that assume the update is complete until after the
2127  // transaction has been committed and we are sure that the upload was indeed successful.
2128  $dbw->onTransactionCommitOrIdle( static function () use ( $reupload, $purgeUpdate, $cacheUpdateJob ) {
2129  DeferredUpdates::addUpdate( $purgeUpdate, DeferredUpdates::PRESEND );
2130 
2131  if ( !$reupload ) {
2132  // This is a new file, so update the image count
2133  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2134  }
2135 
2136  MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $cacheUpdateJob );
2137  }, __METHOD__ );
2138 
2139  return Status::newGood();
2140  }
2141 
2158  public function publish( $src, $flags = 0, array $options = [] ) {
2159  return $this->publishTo( $src, $this->getRel(), $flags, $options );
2160  }
2161 
2178  protected function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
2179  $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
2180 
2181  $repo = $this->getRepo();
2182  if ( $repo->getReadOnlyReason() !== false ) {
2183  return $this->readOnlyFatalStatus();
2184  }
2185 
2186  $status = $this->acquireFileLock();
2187  if ( !$status->isOK() ) {
2188  return $status;
2189  }
2190 
2191  if ( $this->isOld() ) {
2192  $archiveRel = $dstRel;
2193  $archiveName = basename( $archiveRel );
2194  } else {
2195  $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
2196  $archiveRel = $this->getArchiveRel( $archiveName );
2197  }
2198 
2199  if ( $repo->hasSha1Storage() ) {
2200  $sha1 = FileRepo::isVirtualUrl( $srcPath )
2201  ? $repo->getFileSha1( $srcPath )
2202  : FSFile::getSha1Base36FromPath( $srcPath );
2204  $wrapperBackend = $repo->getBackend();
2205  '@phan-var FileBackendDBRepoWrapper $wrapperBackend';
2206  $dst = $wrapperBackend->getPathForSHA1( $sha1 );
2207  $status = $repo->quickImport( $src, $dst );
2208  if ( $flags & File::DELETE_SOURCE ) {
2209  unlink( $srcPath );
2210  }
2211 
2212  if ( $this->exists() ) {
2213  $status->value = $archiveName;
2214  }
2215  } else {
2216  $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
2217  $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
2218 
2219  if ( $status->value == 'new' ) {
2220  $status->value = '';
2221  } else {
2222  $status->value = $archiveName;
2223  }
2224  }
2225 
2226  $this->releaseFileLock();
2227  return $status;
2228  }
2229 
2248  public function move( $target ) {
2249  $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2250  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2251  return $this->readOnlyFatalStatus();
2252  }
2253 
2254  wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
2255  $batch = new LocalFileMoveBatch( $this, $target );
2256 
2257  $status = $batch->addCurrent();
2258  if ( !$status->isOK() ) {
2259  return $status;
2260  }
2261  $archiveNames = $batch->addOlds();
2262  $status = $batch->execute();
2263 
2264  wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
2265 
2266  // Purge the source and target files outside the transaction...
2267  $oldTitleFile = $localRepo->newFile( $this->title );
2268  $newTitleFile = $localRepo->newFile( $target );
2270  new AutoCommitUpdate(
2271  $this->getRepo()->getPrimaryDB(),
2272  __METHOD__,
2273  static function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
2274  $oldTitleFile->purgeEverything();
2275  foreach ( $archiveNames as $archiveName ) {
2277  '@phan-var OldLocalFile $oldTitleFile';
2278  $oldTitleFile->purgeOldThumbnails( $archiveName );
2279  }
2280  $newTitleFile->purgeEverything();
2281  }
2282  ),
2283  DeferredUpdates::PRESEND
2284  );
2285 
2286  if ( $status->isOK() ) {
2287  // Now switch the object
2288  $this->title = $target;
2289  // Force regeneration of the name and hashpath
2290  $this->name = null;
2291  $this->hashPath = null;
2292  }
2293 
2294  return $status;
2295  }
2296 
2313  public function deleteFile( $reason, UserIdentity $user, $suppress = false ) {
2314  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2315  return $this->readOnlyFatalStatus();
2316  }
2317 
2318  $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2319 
2320  $batch->addCurrent();
2321  // Get old version relative paths
2322  $archiveNames = $batch->addOlds();
2323  $status = $batch->execute();
2324 
2325  if ( $status->isOK() ) {
2326  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
2327  }
2328 
2329  // To avoid slow purges in the transaction, move them outside...
2331  new AutoCommitUpdate(
2332  $this->getRepo()->getPrimaryDB(),
2333  __METHOD__,
2334  function () use ( $archiveNames ) {
2335  $this->purgeEverything();
2336  foreach ( $archiveNames as $archiveName ) {
2337  $this->purgeOldThumbnails( $archiveName );
2338  }
2339  }
2340  ),
2341  DeferredUpdates::PRESEND
2342  );
2343 
2344  // Purge the CDN
2345  $purgeUrls = [];
2346  foreach ( $archiveNames as $archiveName ) {
2347  $purgeUrls[] = $this->getArchiveUrl( $archiveName );
2348  }
2349 
2350  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2351  $hcu->purgeUrls( $purgeUrls, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2352 
2353  return $status;
2354  }
2355 
2374  public function deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress = false ) {
2375  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2376  return $this->readOnlyFatalStatus();
2377  }
2378 
2379  $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2380 
2381  $batch->addOld( $archiveName );
2382  $status = $batch->execute();
2383 
2384  $this->purgeOldThumbnails( $archiveName );
2385  if ( $status->isOK() ) {
2386  $this->purgeDescription();
2387  }
2388 
2389  $url = $this->getArchiveUrl( $archiveName );
2390  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2391  $hcu->purgeUrls( $url, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2392 
2393  return $status;
2394  }
2395 
2408  public function restore( $versions = [], $unsuppress = false ) {
2409  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2410  return $this->readOnlyFatalStatus();
2411  }
2412 
2413  $batch = new LocalFileRestoreBatch( $this, $unsuppress );
2414 
2415  if ( !$versions ) {
2416  $batch->addAll();
2417  } else {
2418  $batch->addIds( $versions );
2419  }
2420  $status = $batch->execute();
2421  if ( $status->isGood() ) {
2422  $cleanupStatus = $batch->cleanup();
2423  $cleanupStatus->successCount = 0;
2424  $cleanupStatus->failCount = 0;
2425  $status->merge( $cleanupStatus );
2426  }
2427 
2428  return $status;
2429  }
2430 
2441  public function getDescriptionUrl() {
2442  if ( !$this->title ) {
2443  return false; // Avoid hard failure when the file does not exist. T221812
2444  }
2445 
2446  return $this->title->getLocalURL();
2447  }
2448 
2458  public function getDescriptionText( Language $lang = null ) {
2459  if ( !$this->title ) {
2460  return false; // Avoid hard failure when the file does not exist. T221812
2461  }
2462 
2463  $services = MediaWikiServices::getInstance();
2464  $page = $services->getPageStore()->getPageByReference( $this->getTitle() );
2465  if ( !$page ) {
2466  return false;
2467  }
2468 
2469  if ( $lang ) {
2470  $parserOptions = ParserOptions::newFromUserAndLang(
2472  $lang
2473  );
2474  } else {
2476  }
2477 
2478  $parseStatus = $services->getParserOutputAccess()
2479  ->getParserOutput( $page, $parserOptions );
2480 
2481  if ( !$parseStatus->isGood() ) {
2482  // Rendering failed.
2483  return false;
2484  }
2485  return $parseStatus->getValue()->getText();
2486  }
2487 
2495  public function getUploader( int $audience = self::FOR_PUBLIC, Authority $performer = null ): ?UserIdentity {
2496  $this->load();
2497  if ( $audience === self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
2498  return null;
2499  } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $performer ) ) {
2500  return null;
2501  } else {
2502  return $this->user;
2503  }
2504  }
2505 
2512  public function getDescription( $audience = self::FOR_PUBLIC, Authority $performer = null ) {
2513  $this->load();
2514  if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
2515  return '';
2516  } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $performer ) ) {
2517  return '';
2518  } else {
2519  return $this->description;
2520  }
2521  }
2522 
2527  public function getTimestamp() {
2528  $this->load();
2529 
2530  return $this->timestamp;
2531  }
2532 
2537  public function getDescriptionTouched() {
2538  if ( !$this->exists() ) {
2539  return false; // Avoid hard failure when the file does not exist. T221812
2540  }
2541 
2542  // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
2543  // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
2544  // need to differentiate between null (uninitialized) and false (failed to load).
2545  if ( $this->descriptionTouched === null ) {
2546  $cond = [
2547  'page_namespace' => $this->title->getNamespace(),
2548  'page_title' => $this->title->getDBkey()
2549  ];
2550  $touched = $this->repo->getReplicaDB()->selectField( 'page', 'page_touched', $cond, __METHOD__ );
2551  $this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
2552  }
2553 
2554  return $this->descriptionTouched;
2555  }
2556 
2561  public function getSha1() {
2562  $this->load();
2563  return $this->sha1;
2564  }
2565 
2569  public function isCacheable() {
2570  $this->load();
2571 
2572  // If extra data (metadata) was not loaded then it must have been large
2573  return $this->extraDataLoaded
2574  && strlen( serialize( $this->metadataArray ) ) <= self::CACHE_FIELD_MAX_LEN;
2575  }
2576 
2585  public function acquireFileLock( $timeout = 0 ) {
2586  return Status::wrap( $this->getRepo()->getBackend()->lockFiles(
2587  [ $this->getPath() ], LockManager::LOCK_EX, $timeout
2588  ) );
2589  }
2590 
2597  public function releaseFileLock() {
2598  return Status::wrap( $this->getRepo()->getBackend()->unlockFiles(
2599  [ $this->getPath() ], LockManager::LOCK_EX
2600  ) );
2601  }
2602 
2613  public function lock() {
2614  if ( !$this->locked ) {
2615  $logger = LoggerFactory::getInstance( 'LocalFile' );
2616 
2617  $dbw = $this->repo->getPrimaryDB();
2618  $makesTransaction = !$dbw->trxLevel();
2619  $dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
2620  // T56736: use simple lock to handle when the file does not exist.
2621  // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
2622  // Also, that would cause contention on INSERT of similarly named rows.
2623  $status = $this->acquireFileLock( 10 ); // represents all versions of the file
2624  if ( !$status->isGood() ) {
2625  $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2626  $logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
2627 
2628  throw new LocalFileLockError( $status );
2629  }
2630  // Release the lock *after* commit to avoid row-level contention.
2631  // Make sure it triggers on rollback() as well as commit() (T132921).
2632  $dbw->onTransactionResolution(
2633  function () use ( $logger ) {
2634  $status = $this->releaseFileLock();
2635  if ( !$status->isGood() ) {
2636  $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
2637  }
2638  },
2639  __METHOD__
2640  );
2641  // Callers might care if the SELECT snapshot is safely fresh
2642  $this->lockedOwnTrx = $makesTransaction;
2643  }
2644 
2645  $this->locked++;
2646 
2647  return $this->lockedOwnTrx;
2648  }
2649 
2660  public function unlock() {
2661  if ( $this->locked ) {
2662  --$this->locked;
2663  if ( !$this->locked ) {
2664  $dbw = $this->repo->getPrimaryDB();
2665  $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2666  $this->lockedOwnTrx = false;
2667  }
2668  }
2669  }
2670 
2674  protected function readOnlyFatalStatus() {
2675  return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
2676  $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2677  }
2678 
2682  public function __destruct() {
2683  $this->unlock();
2684  }
2685 }
getUser()
serialize()
unserialize( $serialized)
const NS_FILE
Definition: Defines.php:70
const EDIT_SUPPRESS_RC
Definition: Defines.php:129
const EDIT_NEW
Definition: Defines.php:126
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.
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Definition: Article.php:1318
Deferrable Update for closure/callback updates that should use auto-commit mode.
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the pending update queue for execution at the appropriate time.
static addCallableUpdate( $callable, $stage=self::POSTSEND, $dbw=null)
Add an update to the pending update queue that invokes the specified callback when run.
Class representing a non-directory file on the file system.
Definition: FSFile.php:32
static getSha1Base36FromPath( $path)
Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case encoding,...
Definition: FSFile.php:225
File backend exception for checked exceptions (e.g.
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
const DELETE_SOURCE
Definition: FileRepo.php:48
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Definition: FileRepo.php:285
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:68
string $url
The URL corresponding to one of the four basic zones.
Definition: File.php:136
MediaHandler $handler
Definition: File.php:133
static splitMime(?string $mime)
Split an internet media type into its two components; if not a two-part name, set the minor type to '...
Definition: File.php:307
assertRepoDefined()
Assert that $this->repo is set to a valid FileRepo instance.
Definition: File.php:2464
getName()
Return the name of this file.
Definition: File.php:334
const DELETE_SOURCE
Definition: File.php:85
getVirtualUrl( $suffix=false)
Get the public zone virtual URL for a current version source file.
Definition: File.php:1923
assertTitleDefined()
Assert that $this->title is set to a Title.
Definition: File.php:2474
isMultipage()
Returns 'true' if this file is a type which supports multiple pages, e.g.
Definition: File.php:2161
FileRepo LocalRepo ForeignAPIRepo bool $repo
Some member variables can be lazy-initialised using __get().
Definition: File.php:115
string $path
The storage path corresponding to one of the zones.
Definition: File.php:145
Title string bool $title
Definition: File.php:118
getHandler()
Get a MediaHandler instance for this file.
Definition: File.php:1542
string null $name
The name of a file from its title object.
Definition: File.php:142
static newForBacklinks(PageReference $page, $table, $params=[])
Base class for language-specific code.
Definition: Language.php:53
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:60
exists()
canRender inherited
Definition: LocalFile.php:1297
setProps( $info)
Set properties in this object to be equal to those given in the associative array $info.
Definition: LocalFile.php:885
maybeUpgradeRow()
Upgrade a row if it needs it.
Definition: LocalFile.php:748
static newFromKey( $sha1, $repo, $timestamp=false)
Create a LocalFile from a SHA-1 key Do not call this except from inside a repo class.
Definition: LocalFile.php:233
array $metadataArray
Unserialized metadata.
Definition: LocalFile.php:102
getMediaType()
Returns the type of the media in the file.
Definition: LocalFile.php:1280
string[] $unloadedMetadataBlobs
Map of metadata item name to blob address for items that exist but have not yet been loaded into $thi...
Definition: LocalFile.php:121
deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress=false)
Delete an old version of the file.
Definition: LocalFile.php:2374
move( $target)
getLinksTo inherited
Definition: LocalFile.php:2248
lock()
Start an atomic DB section and lock the image for update or increments a reference counter if the loc...
Definition: LocalFile.php:2613
loadFromRow( $row, $prefix='img_')
Load file metadata from a DB result row.
Definition: LocalFile.php:651
getCacheKey()
Get the memcached key for the main data for this file, or false if there is no access to the shared c...
Definition: LocalFile.php:340
getWidth( $page=1)
Return the width of the image @stable to override.
Definition: LocalFile.php:968
__destruct()
Clean up any dangling locks.
Definition: LocalFile.php:2682
string $mime
MIME type, determined by MimeAnalyzer::guessMimeType.
Definition: LocalFile.php:96
reserializeMetadata()
Write the metadata back to the database with the current serialization format.
Definition: LocalFile.php:857
isMissing()
splitMime inherited
Definition: LocalFile.php:952
getDescriptionUrl()
isMultipage inherited
Definition: LocalFile.php:2441
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
Definition: LocalFile.php:1530
getMutableCacheKeys(WANObjectCache $cache)
Definition: LocalFile.php:349
static getQueryInfo(array $options=[])
Return the tables, fields, and join conditions to be selected to create a new localfile object.
Definition: LocalFile.php:271
releaseFileLock()
Release a lock acquired with acquireFileLock().
Definition: LocalFile.php:2597
getUploader(int $audience=self::FOR_PUBLIC, Authority $performer=null)
Definition: LocalFile.php:2495
loadMetadataFromDbFieldValue(IDatabase $db, $metadataBlob)
Unserialize a metadata blob which came from the database and store it in $this.
Definition: LocalFile.php:1192
loadFromDB( $flags=0)
Load file metadata from the DB.
Definition: LocalFile.php:510
load( $flags=0)
Load file metadata from cache or DB, unless already loaded.
Definition: LocalFile.php:729
loadMetadataFromString( $metadataString)
Unserialize a metadata string which came from some non-DB source, or is the return value of IDatabase...
Definition: LocalFile.php:1203
string $media_type
MEDIATYPE_xxx (bitmap, drawing, audio...)
Definition: LocalFile.php:93
deleteFile( $reason, UserIdentity $user, $suppress=false)
Delete all versions of the file.
Definition: LocalFile.php:2313
acquireFileLock( $timeout=0)
Acquire an exclusive lock on the file, indicating an intention to write to the file backend.
Definition: LocalFile.php:2585
purgeCache( $options=[])
Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
Definition: LocalFile.php:1356
getDescriptionTouched()
Definition: LocalFile.php:2537
loadFromFile( $path=null)
Load metadata from the file itself.
Definition: LocalFile.php:460
getBitDepth()
@stable to override
Definition: LocalFile.php:1246
string null $metadataSerializationFormat
One of the MDS_* constants, giving the format of the metadata as stored in the DB,...
Definition: LocalFile.php:110
int $size
Size in bytes (loadFromXxx)
Definition: LocalFile.php:99
getDescriptionShortUrl()
Get short description URL for a file based on the page ID.
Definition: LocalFile.php:1034
getThumbnails( $archiveName=false)
getTransformScript inherited
Definition: LocalFile.php:1319
static newFromTitle( $title, $repo, $unused=null)
Create a LocalFile from a title Do not call this except from inside a repo class.
Definition: LocalFile.php:199
getMetadataForDb(IDatabase $db)
Serialize the metadata array for insertion into img_metadata, oi_metadata or fa_metadata.
Definition: LocalFile.php:1124
int $height
Image height.
Definition: LocalFile.php:87
purgeOldThumbnails( $archiveName)
Delete cached transformed files for an archived version only.
Definition: LocalFile.php:1379
publishTo( $src, $dstRel, $flags=0, array $options=[])
Move or copy a file to a specified location.
Definition: LocalFile.php:2178
purgeThumbList( $dir, $files)
Delete a list of thumbnails visible at urls.
Definition: LocalFile.php:1489
unlock()
Decrement the lock reference count and end the atomic section if it reaches zero.
Definition: LocalFile.php:2660
getLazyCacheFields( $prefix='img_')
Returns the list of object properties that are included as-is in the cache, only when they're not too...
Definition: LocalFile.php:494
getSize()
Returns the size of the image file, in bytes @stable to override.
Definition: LocalFile.php:1257
invalidateCache()
Purge the file object/metadata cache.
Definition: LocalFile.php:439
getMimeType()
Returns the MIME type of the file.
Definition: LocalFile.php:1268
bool $extraDataLoaded
Whether or not lazy-loaded data has been loaded from the database.
Definition: LocalFile.php:130
readOnlyFatalStatus()
Definition: LocalFile.php:2674
string $sha1
SHA-1 base 36 content hash.
Definition: LocalFile.php:124
getDescription( $audience=self::FOR_PUBLIC, Authority $performer=null)
Definition: LocalFile.php:2512
getHeight( $page=1)
Return the height of the image @stable to override.
Definition: LocalFile.php:1001
prerenderThumbnails()
Prerenders a configurable set of thumbnails.
Definition: LocalFile.php:1446
resetHistory()
Reset the history pointer to the first element of the history.
Definition: LocalFile.php:1637
unprefixRow( $row, $prefix='img_')
Definition: LocalFile.php:619
static newFromRow( $row, $repo)
Create a LocalFile from a title Do not call this except from inside a repo class.
Definition: LocalFile.php:214
publish( $src, $flags=0, array $options=[])
Move or copy a file to its public location.
Definition: LocalFile.php:2158
restore( $versions=[], $unsuppress=false)
Restore all or specified deleted revisions to the given file.
Definition: LocalFile.php:2408
getCacheFields( $prefix='img_')
Returns the list of object properties that are included as-is in the cache.
Definition: LocalFile.php:472
int $bits
Returned by getimagesize (loadFromXxx)
Definition: LocalFile.php:90
getMetadataItems(array $itemNames)
Get multiple elements of the unserialized handler-specific metadata.
Definition: LocalFile.php:1087
getDescriptionText(Language $lang=null)
Get the HTML text of the description page This is not used by ImagePage for local files,...
Definition: LocalFile.php:2458
purgeThumbnails( $options=[])
Delete cached transformed files for the current version only.
Definition: LocalFile.php:1406
loadExtraFromDB()
Load lazy file metadata from the DB.
Definition: LocalFile.php:543
string $repoClass
Definition: LocalFile.php:136
int $width
Image width.
Definition: LocalFile.php:84
nextHistoryLine()
Returns the history of this file, line by line.
Definition: LocalFile.php:1589
upgradeRow()
Fix assorted version-related problems with the image row by reloading it from the file.
Definition: LocalFile.php:809
int $deleted
Bitfield akin to rev_deleted.
Definition: LocalFile.php:133
getMetadata()
Get handler-specific metadata as a serialized string.
Definition: LocalFile.php:1056
getMetadataArray()
Get unserialized handler-specific metadata.
Definition: LocalFile.php:1074
__construct( $title, $repo)
Do not call this except from inside a repo class.
Definition: LocalFile.php:319
bool $dataLoaded
Whether or not core data has been loaded from the database (loadFromXxx)
Definition: LocalFile.php:127
bool $fileExists
Does the file exist on disk? (loadFromXxx)
Definition: LocalFile.php:77
upload( $src, $comment, $pageText, $flags=0, $props=false, $timestamp=false, Authority $uploader=null, $tags=[], $createNullRevision=true, $revert=false)
getHashPath inherited
Definition: LocalFile.php:1678
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)
Definition: LocalFile.php:1781
string[] $metadataBlobs
Map of metadata item name to blob address.
Definition: LocalFile.php:113
const LOCK_EX
Definition: LockManager.php:70
static makeParamBlob( $params)
Create a blob from a parameter array.
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
MediaWiki exception.
Definition: MWException.php:29
MimeMagic helper wrapper.
Definition: MWFileProps.php:28
Class for creating new log entries and inserting them into the database.
const METADATA_COMPATIBLE
static getHandler( $type)
Get a MediaHandler for a given MIME type from the instance cache.
isFileMetadataValid( $image)
Check if the metadata is valid for this handler.
const METADATA_BAD
getPageDimensions(File $image, $page)
Get an associative array of page dimensions Currently "width" and "height" are understood,...
Class the manages updates of *_link tables as well as similar extension-managed tables.
Definition: LinksUpdate.php:55
PSR-3 logger instance factory.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
Page revision base class.
Value object representing a user's identity.
Helper for storage of metadata.
static getQueryInfo(array $options=[])
Return the tables, fields, and join conditions to be selected to create a new oldlocalfile object.
static newFromUserAndLang(UserIdentity $user, Language $lang)
Get a ParserOptions object from a given user and language.
static newFromContext(IContextSource $context)
Get a ParserOptions object from a IContextSource object.
static newExtraneousContext(Title $title, $request=[])
Create a new extraneous context.
static getMain()
Get the RequestContext object associated with the main request.
static factory(array $deltas)
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition: Status.php:44
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:62
Job for asynchronous rendering of thumbnails, e.g.
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:370
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:638
Multi-datacenter aware caching interface.
Special handling for representing file pages.
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
getUser()
Returns the performer of the actions associated with this authority.
Interface for objects representing user identity.
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:39
decodeBlob( $b)
Some DBMSs return a special placeholder object representing blob fields in result objects.
encodeBlob( $b)
Some DBMSs have a special format for inserting into blob fields, they don't allow simple quoted strin...
Result wrapper for grabbing data queried from an IDatabase object.
$cache
Definition: mcc.php:33
foreach( $mmfl['setupFiles'] as $fileName) if( $queue) if(empty( $mmfl['quiet'])) $s
$mime
Definition: router.php:60
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
if(!isset( $args[0])) $lang
$revStore