MediaWiki  master
LocalFile.php
Go to the documentation of this file.
1 <?php
34 
61 class LocalFile extends File {
62  private const VERSION = 13; // cache version
63 
64  private const CACHE_FIELD_MAX_LEN = 1000;
65 
67  private const MDS_EMPTY = 'empty';
68 
70  private const MDS_LEGACY = 'legacy';
71 
73  private const MDS_PHP = 'php';
74 
76  private const MDS_JSON = 'json';
77 
79  private const MAX_PAGE_RENDER_JOBS = 50;
80 
82  protected $fileExists;
83 
85  protected $width;
86 
88  protected $height;
89 
91  protected $bits;
92 
94  protected $media_type;
95 
97  protected $mime;
98 
100  protected $size;
101 
103  protected $metadataArray = [];
104 
112 
114  protected $metadataBlobs = [];
115 
122  protected $unloadedMetadataBlobs = [];
123 
125  protected $sha1;
126 
128  protected $dataLoaded = false;
129 
131  protected $extraDataLoaded = false;
132 
134  protected $deleted;
135 
137  protected $repoClass = LocalRepo::class;
138 
140  private $historyLine = 0;
141 
143  private $historyRes = null;
144 
146  private $major_mime;
147 
149  private $minor_mime;
150 
152  private $timestamp;
153 
155  private $user;
156 
158  private $description;
159 
161  private $descriptionTouched;
162 
164  private $upgraded;
165 
167  private $upgrading;
168 
170  private $locked;
171 
173  private $lockedOwnTrx;
174 
176  private $missing;
177 
179  private $metadataStorageHelper;
180 
181  // @note: higher than IDBAccessObject constants
182  private const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
183 
184  private const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
185 
200  public static function newFromTitle( $title, $repo, $unused = null ) {
201  return new static( $title, $repo );
202  }
203 
215  public static function newFromRow( $row, $repo ) {
216  $title = Title::makeTitle( NS_FILE, $row->img_name );
217  $file = new static( $title, $repo );
218  $file->loadFromRow( $row );
219 
220  return $file;
221  }
222 
234  public static function newFromKey( $sha1, $repo, $timestamp = false ) {
235  $dbr = $repo->getReplicaDB();
236 
237  $conds = [ 'img_sha1' => $sha1 ];
238  if ( $timestamp ) {
239  $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
240  }
241 
242  $fileQuery = static::getQueryInfo();
243  $row = $dbr->selectRow(
244  $fileQuery['tables'], $fileQuery['fields'], $conds, __METHOD__, [], $fileQuery['joins']
245  );
246  if ( $row ) {
247  return static::newFromRow( $row, $repo );
248  } else {
249  return false;
250  }
251  }
252 
272  public static function getQueryInfo( array $options = [] ) {
273  $commentQuery = MediaWikiServices::getInstance()->getCommentStore()->getJoin( 'img_description' );
274  $ret = [
275  'tables' => [
276  'image',
277  'image_actor' => 'actor'
278  ] + $commentQuery['tables'],
279  'fields' => [
280  'img_name',
281  'img_size',
282  'img_width',
283  'img_height',
284  'img_metadata',
285  'img_bits',
286  'img_media_type',
287  'img_major_mime',
288  'img_minor_mime',
289  'img_timestamp',
290  'img_sha1',
291  'img_actor',
292  'img_user' => 'image_actor.actor_user',
293  'img_user_text' => 'image_actor.actor_name',
294  ] + $commentQuery['fields'],
295  'joins' => [
296  'image_actor' => [ 'JOIN', 'actor_id=img_actor' ]
297  ] + $commentQuery['joins'],
298  ];
299 
300  if ( in_array( 'omit-nonlazy', $options, true ) ) {
301  // Internal use only for getting only the lazy fields
302  $ret['fields'] = [];
303  }
304  if ( !in_array( 'omit-lazy', $options, true ) ) {
305  // Note: Keep this in sync with self::getLazyCacheFields() and
306  // self::loadExtraFromDB()
307  $ret['fields'][] = 'img_metadata';
308  }
309 
310  return $ret;
311  }
312 
320  public function __construct( $title, $repo ) {
321  parent::__construct( $title, $repo );
322  $this->metadataStorageHelper = new MetadataStorageHelper( $repo );
323 
324  $this->assertRepoDefined();
325  $this->assertTitleDefined();
326  }
327 
331  public function getRepo() {
332  return $this->repo;
333  }
334 
341  protected function getCacheKey() {
342  return $this->repo->getSharedCacheKey( 'file', sha1( $this->getName() ) );
343  }
344 
348  private function loadFromCache() {
349  $this->dataLoaded = false;
350  $this->extraDataLoaded = false;
351 
352  $key = $this->getCacheKey();
353  if ( !$key ) {
354  $this->loadFromDB( self::READ_NORMAL );
355 
356  return;
357  }
358 
359  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
360  $cachedValues = $cache->getWithSetCallback(
361  $key,
362  $cache::TTL_WEEK,
363  function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
364  $setOpts += Database::getCacheSetOptions( $this->repo->getReplicaDB() );
365 
366  $this->loadFromDB( self::READ_NORMAL );
367 
368  $fields = $this->getCacheFields( '' );
369  $cacheVal = [];
370  $cacheVal['fileExists'] = $this->fileExists;
371  if ( $this->fileExists ) {
372  foreach ( $fields as $field ) {
373  $cacheVal[$field] = $this->$field;
374  }
375  }
376  if ( $this->user ) {
377  $cacheVal['user'] = $this->user->getId();
378  $cacheVal['user_text'] = $this->user->getName();
379  }
380 
381  // Don't cache metadata items stored as blobs, since they tend to be large
382  if ( $this->metadataBlobs ) {
383  $cacheVal['metadata'] = array_diff_key(
384  $this->metadataArray, $this->metadataBlobs );
385  // Save the blob addresses
386  $cacheVal['metadataBlobs'] = $this->metadataBlobs;
387  } else {
388  $cacheVal['metadata'] = $this->metadataArray;
389  }
390 
391  // Strip off excessive entries from the subset of fields that can become large.
392  // If the cache value gets too large and might not fit in the cache,
393  // causing repeat database queries for each access to the file.
394  foreach ( $this->getLazyCacheFields( '' ) as $field ) {
395  if ( isset( $cacheVal[$field] )
396  && strlen( serialize( $cacheVal[$field] ) ) > 100 * 1024
397  ) {
398  unset( $cacheVal[$field] ); // don't let the value get too big
399  if ( $field === 'metadata' ) {
400  unset( $cacheVal['metadataBlobs'] );
401  }
402  }
403  }
404 
405  if ( $this->fileExists ) {
406  $ttl = $cache->adaptiveTTL( (int)wfTimestamp( TS_UNIX, $this->timestamp ), $ttl );
407  } else {
408  $ttl = $cache::TTL_DAY;
409  }
410 
411  return $cacheVal;
412  },
413  [ 'version' => self::VERSION ]
414  );
415 
416  $this->fileExists = $cachedValues['fileExists'];
417  if ( $this->fileExists ) {
418  $this->setProps( $cachedValues );
419  }
420 
421  $this->dataLoaded = true;
422  $this->extraDataLoaded = true;
423  foreach ( $this->getLazyCacheFields( '' ) as $field ) {
424  $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
425  }
426  }
427 
431  public function invalidateCache() {
432  $key = $this->getCacheKey();
433  if ( !$key ) {
434  return;
435  }
436 
437  $this->repo->getPrimaryDB()->onTransactionPreCommitOrIdle(
438  static function () use ( $key ) {
439  MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key );
440  },
441  __METHOD__
442  );
443  }
444 
452  public function loadFromFile( $path = null ) {
453  $props = $this->repo->getFileProps( $path ?? $this->getVirtualUrl() );
454  $this->setProps( $props );
455  }
456 
464  protected function getCacheFields( $prefix = 'img_' ) {
465  if ( $prefix !== '' ) {
466  throw new InvalidArgumentException(
467  __METHOD__ . ' with a non-empty prefix is no longer supported.'
468  );
469  }
470 
471  // See self::getQueryInfo() for the fetching of the data from the DB,
472  // self::loadFromRow() for the loading of the object from the DB row,
473  // and self::loadFromCache() for the caching, and self::setProps() for
474  // populating the object from an array of data.
475  return [ 'size', 'width', 'height', 'bits', 'media_type',
476  'major_mime', 'minor_mime', 'timestamp', 'sha1', 'description' ];
477  }
478 
486  protected function getLazyCacheFields( $prefix = 'img_' ) {
487  if ( $prefix !== '' ) {
488  throw new InvalidArgumentException(
489  __METHOD__ . ' with a non-empty prefix is no longer supported.'
490  );
491  }
492 
493  // Keep this in sync with the omit-lazy option in self::getQueryInfo().
494  return [ 'metadata' ];
495  }
496 
502  protected function loadFromDB( $flags = 0 ) {
503  $fname = static::class . '::' . __FUNCTION__;
504 
505  # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
506  $this->dataLoaded = true;
507  $this->extraDataLoaded = true;
508 
509  $dbr = ( $flags & self::READ_LATEST )
510  ? $this->repo->getPrimaryDB()
511  : $this->repo->getReplicaDB();
512 
513  $fileQuery = static::getQueryInfo();
514  $row = $dbr->selectRow(
515  $fileQuery['tables'],
516  $fileQuery['fields'],
517  [ 'img_name' => $this->getName() ],
518  $fname,
519  [],
520  $fileQuery['joins']
521  );
522 
523  if ( $row ) {
524  $this->loadFromRow( $row );
525  } else {
526  $this->fileExists = false;
527  }
528  }
529 
535  protected function loadExtraFromDB() {
536  if ( !$this->title ) {
537  return; // Avoid hard failure when the file does not exist. T221812
538  }
539 
540  $fname = static::class . '::' . __FUNCTION__;
541 
542  # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
543  $this->extraDataLoaded = true;
544 
545  $db = $this->repo->getReplicaDB();
546  $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
547  if ( !$fieldMap ) {
548  $db = $this->repo->getPrimaryDB();
549  $fieldMap = $this->loadExtraFieldsWithTimestamp( $db, $fname );
550  }
551 
552  if ( $fieldMap ) {
553  if ( isset( $fieldMap['metadata'] ) ) {
554  $this->loadMetadataFromDbFieldValue( $db, $fieldMap['metadata'] );
555  }
556  } else {
557  throw new MWException( "Could not find data for image '{$this->getName()}'." );
558  }
559  }
560 
566  private function loadExtraFieldsWithTimestamp( $dbr, $fname ) {
567  $fieldMap = false;
568 
569  $fileQuery = self::getQueryInfo( [ 'omit-nonlazy' ] );
570  $row = $dbr->selectRow(
571  $fileQuery['tables'],
572  $fileQuery['fields'],
573  [
574  'img_name' => $this->getName(),
575  'img_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
576  ],
577  $fname,
578  [],
579  $fileQuery['joins']
580  );
581  if ( $row ) {
582  $fieldMap = $this->unprefixRow( $row, 'img_' );
583  } else {
584  # File may have been uploaded over in the meantime; check the old versions
585  $fileQuery = OldLocalFile::getQueryInfo( [ 'omit-nonlazy' ] );
586  $row = $dbr->selectRow(
587  $fileQuery['tables'],
588  $fileQuery['fields'],
589  [
590  'oi_name' => $this->getName(),
591  'oi_timestamp' => $dbr->timestamp( $this->getTimestamp() ),
592  ],
593  $fname,
594  [],
595  $fileQuery['joins']
596  );
597  if ( $row ) {
598  $fieldMap = $this->unprefixRow( $row, 'oi_' );
599  }
600  }
601 
602  return $fieldMap;
603  }
604 
611  protected function unprefixRow( $row, $prefix = 'img_' ) {
612  $array = (array)$row;
613  $prefixLength = strlen( $prefix );
614 
615  // Double check prefix once
616  if ( substr( array_key_first( $array ), 0, $prefixLength ) !== $prefix ) {
617  throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
618  }
619 
620  $decoded = [];
621  foreach ( $array as $name => $value ) {
622  $decoded[substr( $name, $prefixLength )] = $value;
623  }
624 
625  return $decoded;
626  }
627 
643  public function loadFromRow( $row, $prefix = 'img_' ) {
644  $this->dataLoaded = true;
645 
646  $unprefixed = $this->unprefixRow( $row, $prefix );
647 
648  $this->name = $unprefixed['name'];
649  $this->media_type = $unprefixed['media_type'];
650 
651  $services = MediaWikiServices::getInstance();
652  $this->description = $services->getCommentStore()
653  ->getComment( "{$prefix}description", $row )->text;
654 
655  $this->user = $services->getUserFactory()->newFromAnyId(
656  $unprefixed['user'] ?? null,
657  $unprefixed['user_text'] ?? null,
658  $unprefixed['actor'] ?? null
659  );
660 
661  $this->timestamp = wfTimestamp( TS_MW, $unprefixed['timestamp'] );
662 
664  $this->repo->getReplicaDB(), $unprefixed['metadata'] );
665 
666  if ( empty( $unprefixed['major_mime'] ) ) {
667  $this->major_mime = 'unknown';
668  $this->minor_mime = 'unknown';
669  $this->mime = 'unknown/unknown';
670  } else {
671  if ( !$unprefixed['minor_mime'] ) {
672  $unprefixed['minor_mime'] = 'unknown';
673  }
674  $this->major_mime = $unprefixed['major_mime'];
675  $this->minor_mime = $unprefixed['minor_mime'];
676  $this->mime = $unprefixed['major_mime'] . '/' . $unprefixed['minor_mime'];
677  }
678 
679  // Trim zero padding from char/binary field
680  $this->sha1 = rtrim( $unprefixed['sha1'], "\0" );
681 
682  // Normalize some fields to integer type, per their database definition.
683  // Use unary + so that overflows will be upgraded to double instead of
684  // being truncated as with intval(). This is important to allow > 2 GiB
685  // files on 32-bit systems.
686  $this->size = +$unprefixed['size'];
687  $this->width = +$unprefixed['width'];
688  $this->height = +$unprefixed['height'];
689  $this->bits = +$unprefixed['bits'];
690 
691  // Check for extra fields (deprecated since MW 1.37)
692  $extraFields = array_diff(
693  array_keys( $unprefixed ),
694  [
695  'name', 'media_type', 'description_text', 'description_data',
696  'description_cid', 'user', 'user_text', 'actor', 'timestamp',
697  'metadata', 'major_mime', 'minor_mime', 'sha1', 'size', 'width',
698  'height', 'bits'
699  ]
700  );
701  if ( $extraFields ) {
703  'Passing extra fields (' .
704  implode( ', ', $extraFields )
705  . ') to ' . __METHOD__ . ' was deprecated in MediaWiki 1.37. ' .
706  'Property assignment will be removed in a later version.',
707  '1.37' );
708  foreach ( $extraFields as $field ) {
709  $this->$field = $unprefixed[$field];
710  }
711  }
712 
713  $this->fileExists = true;
714  }
715 
721  public function load( $flags = 0 ) {
722  if ( !$this->dataLoaded ) {
723  if ( $flags & self::READ_LATEST ) {
724  $this->loadFromDB( $flags );
725  } else {
726  $this->loadFromCache();
727  }
728  }
729 
730  if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
731  // @note: loads on name/timestamp to reduce race condition problems
732  $this->loadExtraFromDB();
733  }
734  }
735 
740  public function maybeUpgradeRow() {
741  if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() || $this->upgrading ) {
742  return;
743  }
744 
745  $upgrade = false;
746  $reserialize = false;
747  if ( $this->media_type === null || $this->mime == 'image/svg' ) {
748  $upgrade = true;
749  } else {
750  $handler = $this->getHandler();
751  if ( $handler ) {
752  $validity = $handler->isFileMetadataValid( $this );
753  if ( $validity === MediaHandler::METADATA_BAD ) {
754  $upgrade = true;
755  } elseif ( $validity === MediaHandler::METADATA_COMPATIBLE
756  && $this->repo->isMetadataUpdateEnabled()
757  ) {
758  $upgrade = true;
759  } elseif ( $this->repo->isJsonMetadataEnabled()
760  && $this->repo->isMetadataReserializeEnabled()
761  ) {
762  if ( $this->repo->isSplitMetadataEnabled() && $this->isMetadataOversize() ) {
763  $reserialize = true;
764  } elseif ( $this->metadataSerializationFormat !== self::MDS_EMPTY &&
765  $this->metadataSerializationFormat !== self::MDS_JSON ) {
766  $reserialize = true;
767  }
768  }
769  }
770  }
771 
772  if ( $upgrade || $reserialize ) {
773  $this->upgrading = true;
774  // Defer updates unless in auto-commit CLI mode
775  DeferredUpdates::addCallableUpdate( function () use ( $upgrade ) {
776  $this->upgrading = false; // avoid duplicate updates
777  try {
778  if ( $upgrade ) {
779  $this->upgradeRow();
780  } else {
781  $this->reserializeMetadata();
782  }
783  } catch ( LocalFileLockError $e ) {
784  // let the other process handle it (or do it next time)
785  }
786  } );
787  }
788  }
789 
793  public function getUpgraded() {
794  return $this->upgraded;
795  }
796 
801  public function upgradeRow() {
802  $dbw = $this->repo->getPrimaryDB();
803 
804  // Make a DB query condition that will fail to match the image row if the
805  // image was reuploaded while the upgrade was in process.
806  $freshnessCondition = [ 'img_timestamp' => $dbw->timestamp( $this->getTimestamp() ) ];
807 
808  $this->loadFromFile();
809 
810  # Don't destroy file info of missing files
811  if ( !$this->fileExists ) {
812  wfDebug( __METHOD__ . ": file does not exist, aborting" );
813 
814  return;
815  }
816 
817  [ $major, $minor ] = self::splitMime( $this->mime );
818 
819  wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema" );
820 
821  $dbw->update( 'image',
822  [
823  'img_size' => $this->size,
824  'img_width' => $this->width,
825  'img_height' => $this->height,
826  'img_bits' => $this->bits,
827  'img_media_type' => $this->media_type,
828  'img_major_mime' => $major,
829  'img_minor_mime' => $minor,
830  'img_metadata' => $this->getMetadataForDb( $dbw ),
831  'img_sha1' => $this->sha1,
832  ],
833  array_merge(
834  [ 'img_name' => $this->getName() ],
835  $freshnessCondition
836  ),
837  __METHOD__
838  );
839 
840  $this->invalidateCache();
841 
842  $this->upgraded = true; // avoid rework/retries
843  }
844 
849  protected function reserializeMetadata() {
850  if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) {
851  return;
852  }
853  $dbw = $this->repo->getPrimaryDB();
854  $dbw->update(
855  'image',
856  [ 'img_metadata' => $this->getMetadataForDb( $dbw ) ],
857  [
858  'img_name' => $this->name,
859  'img_timestamp' => $dbw->timestamp( $this->timestamp ),
860  ],
861  __METHOD__
862  );
863  $this->upgraded = true;
864  }
865 
877  protected function setProps( $info ) {
878  $this->dataLoaded = true;
879  $fields = $this->getCacheFields( '' );
880  $fields[] = 'fileExists';
881 
882  foreach ( $fields as $field ) {
883  if ( isset( $info[$field] ) ) {
884  $this->$field = $info[$field];
885  }
886  }
887 
888  // Only our own cache sets these properties, so they both should be present.
889  if ( isset( $info['user'] ) &&
890  isset( $info['user_text'] ) &&
891  $info['user_text'] !== ''
892  ) {
893  $this->user = new UserIdentityValue( $info['user'], $info['user_text'] );
894  }
895 
896  // Fix up mime fields
897  if ( isset( $info['major_mime'] ) ) {
898  $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
899  } elseif ( isset( $info['mime'] ) ) {
900  $this->mime = $info['mime'];
901  [ $this->major_mime, $this->minor_mime ] = self::splitMime( $this->mime );
902  }
903 
904  if ( isset( $info['metadata'] ) ) {
905  if ( is_string( $info['metadata'] ) ) {
906  $this->loadMetadataFromString( $info['metadata'] );
907  } elseif ( is_array( $info['metadata'] ) ) {
908  $this->metadataArray = $info['metadata'];
909  if ( isset( $info['metadataBlobs'] ) ) {
910  $this->metadataBlobs = $info['metadataBlobs'];
911  $this->unloadedMetadataBlobs = array_diff_key(
912  $this->metadataBlobs,
913  $this->metadataArray
914  );
915  } else {
916  $this->metadataBlobs = [];
917  $this->unloadedMetadataBlobs = [];
918  }
919  } else {
920  $logger = LoggerFactory::getInstance( 'LocalFile' );
921  $logger->warning( __METHOD__ . ' given invalid metadata of type ' .
922  gettype( $info['metadata'] ) );
923  $this->metadataArray = [];
924  }
925  $this->extraDataLoaded = true;
926  }
927  }
928 
944  public function isMissing() {
945  if ( $this->missing === null ) {
946  $fileExists = $this->repo->fileExists( $this->getVirtualUrl() );
947  $this->missing = !$fileExists;
948  }
949 
950  return $this->missing;
951  }
952 
960  public function getWidth( $page = 1 ) {
961  $page = (int)$page;
962  if ( $page < 1 ) {
963  $page = 1;
964  }
965 
966  $this->load();
967 
968  if ( $this->isMultipage() ) {
969  $handler = $this->getHandler();
970  if ( !$handler ) {
971  return 0;
972  }
973  $dim = $handler->getPageDimensions( $this, $page );
974  if ( $dim ) {
975  return $dim['width'];
976  } else {
977  // For non-paged media, the false goes through an
978  // intval, turning failure into 0, so do same here.
979  return 0;
980  }
981  } else {
982  return $this->width;
983  }
984  }
985 
993  public function getHeight( $page = 1 ) {
994  $page = (int)$page;
995  if ( $page < 1 ) {
996  $page = 1;
997  }
998 
999  $this->load();
1000 
1001  if ( $this->isMultipage() ) {
1002  $handler = $this->getHandler();
1003  if ( !$handler ) {
1004  return 0;
1005  }
1006  $dim = $handler->getPageDimensions( $this, $page );
1007  if ( $dim ) {
1008  return $dim['height'];
1009  } else {
1010  // For non-paged media, the false goes through an
1011  // intval, turning failure into 0, so do same here.
1012  return 0;
1013  }
1014  } else {
1015  return $this->height;
1016  }
1017  }
1018 
1026  public function getDescriptionShortUrl() {
1027  if ( !$this->title ) {
1028  return null; // Avoid hard failure when the file does not exist. T221812
1029  }
1030 
1031  $pageId = $this->title->getArticleID();
1032 
1033  if ( $pageId ) {
1034  $url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
1035  if ( $url !== false ) {
1036  return $url;
1037  }
1038  }
1039  return null;
1040  }
1041 
1048  public function getMetadata() {
1049  $data = $this->getMetadataArray();
1050  if ( !$data ) {
1051  return '';
1052  } elseif ( array_keys( $data ) === [ '_error' ] ) {
1053  // Legacy error encoding
1054  return $data['_error'];
1055  } else {
1056  return serialize( $this->getMetadataArray() );
1057  }
1058  }
1059 
1066  public function getMetadataArray(): array {
1067  $this->load( self::LOAD_ALL );
1068  if ( $this->unloadedMetadataBlobs ) {
1069  return $this->getMetadataItems(
1070  array_unique( array_merge(
1071  array_keys( $this->metadataArray ),
1072  array_keys( $this->unloadedMetadataBlobs )
1073  ) )
1074  );
1075  }
1076  return $this->metadataArray;
1077  }
1078 
1079  public function getMetadataItems( array $itemNames ): array {
1080  $this->load( self::LOAD_ALL );
1081  $result = [];
1082  $addresses = [];
1083  foreach ( $itemNames as $itemName ) {
1084  if ( array_key_exists( $itemName, $this->metadataArray ) ) {
1085  $result[$itemName] = $this->metadataArray[$itemName];
1086  } elseif ( isset( $this->unloadedMetadataBlobs[$itemName] ) ) {
1087  $addresses[$itemName] = $this->unloadedMetadataBlobs[$itemName];
1088  }
1089  }
1090 
1091  if ( $addresses ) {
1092  $resultFromBlob = $this->metadataStorageHelper->getMetadataFromBlobStore( $addresses );
1093  foreach ( $addresses as $itemName => $address ) {
1094  unset( $this->unloadedMetadataBlobs[$itemName] );
1095  $value = $resultFromBlob[$itemName] ?? null;
1096  if ( $value !== null ) {
1097  $result[$itemName] = $value;
1098  $this->metadataArray[$itemName] = $value;
1099  }
1100  }
1101  }
1102  return $result;
1103  }
1104 
1116  public function getMetadataForDb( IDatabase $db ) {
1117  $this->load( self::LOAD_ALL );
1118  if ( !$this->metadataArray && !$this->metadataBlobs ) {
1119  $s = '';
1120  } elseif ( $this->repo->isJsonMetadataEnabled() ) {
1121  $s = $this->getJsonMetadata();
1122  } else {
1123  $s = serialize( $this->getMetadataArray() );
1124  }
1125  if ( !is_string( $s ) ) {
1126  throw new MWException( 'Could not serialize image metadata value for DB' );
1127  }
1128  return $db->encodeBlob( $s );
1129  }
1130 
1137  private function getJsonMetadata() {
1138  // Directly store data that is not already in BlobStore
1139  $envelope = [
1140  'data' => array_diff_key( $this->metadataArray, $this->metadataBlobs )
1141  ];
1142 
1143  // Also store the blob addresses
1144  if ( $this->metadataBlobs ) {
1145  $envelope['blobs'] = $this->metadataBlobs;
1146  }
1147 
1148  [ $s, $blobAddresses ] = $this->metadataStorageHelper->getJsonMetadata( $this, $envelope );
1149 
1150  // Repeated calls to this function should not keep inserting more blobs
1151  $this->metadataBlobs += $blobAddresses;
1152 
1153  return $s;
1154  }
1155 
1162  private function isMetadataOversize() {
1163  if ( !$this->repo->isSplitMetadataEnabled() ) {
1164  return false;
1165  }
1166  $threshold = $this->repo->getSplitMetadataThreshold();
1167  $directItems = array_diff_key( $this->metadataArray, $this->metadataBlobs );
1168  foreach ( $directItems as $value ) {
1169  if ( strlen( $this->metadataStorageHelper->jsonEncode( $value ) ) > $threshold ) {
1170  return true;
1171  }
1172  }
1173  return false;
1174  }
1175 
1184  protected function loadMetadataFromDbFieldValue( IDatabase $db, $metadataBlob ) {
1185  $this->loadMetadataFromString( $db->decodeBlob( $metadataBlob ) );
1186  }
1187 
1195  protected function loadMetadataFromString( $metadataString ) {
1196  $this->extraDataLoaded = true;
1197  $this->metadataArray = [];
1198  $this->metadataBlobs = [];
1199  $this->unloadedMetadataBlobs = [];
1200  $metadataString = (string)$metadataString;
1201  if ( $metadataString === '' ) {
1202  $this->metadataSerializationFormat = self::MDS_EMPTY;
1203  return;
1204  }
1205  if ( $metadataString[0] === '{' ) {
1206  $envelope = $this->metadataStorageHelper->jsonDecode( $metadataString );
1207  if ( !$envelope ) {
1208  // Legacy error encoding
1209  $this->metadataArray = [ '_error' => $metadataString ];
1210  $this->metadataSerializationFormat = self::MDS_LEGACY;
1211  } else {
1212  $this->metadataSerializationFormat = self::MDS_JSON;
1213  if ( isset( $envelope['data'] ) ) {
1214  $this->metadataArray = $envelope['data'];
1215  }
1216  if ( isset( $envelope['blobs'] ) ) {
1217  $this->metadataBlobs = $this->unloadedMetadataBlobs = $envelope['blobs'];
1218  }
1219  }
1220  } else {
1221  // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1222  $data = @unserialize( $metadataString );
1223  if ( !is_array( $data ) ) {
1224  // Legacy error encoding
1225  $data = [ '_error' => $metadataString ];
1226  $this->metadataSerializationFormat = self::MDS_LEGACY;
1227  } else {
1228  $this->metadataSerializationFormat = self::MDS_PHP;
1229  }
1230  $this->metadataArray = $data;
1231  }
1232  }
1233 
1238  public function getBitDepth() {
1239  $this->load();
1240 
1241  return (int)$this->bits;
1242  }
1243 
1249  public function getSize() {
1250  $this->load();
1251 
1252  return $this->size;
1253  }
1254 
1260  public function getMimeType() {
1261  $this->load();
1262 
1263  return $this->mime;
1264  }
1265 
1272  public function getMediaType() {
1273  $this->load();
1274 
1275  return $this->media_type;
1276  }
1277 
1289  public function exists() {
1290  $this->load();
1291 
1292  return $this->fileExists;
1293  }
1294 
1316  protected function getThumbnails( $archiveName = false ) {
1317  if ( $archiveName ) {
1318  $dir = $this->getArchiveThumbPath( $archiveName );
1319  } else {
1320  $dir = $this->getThumbPath();
1321  }
1322 
1323  $backend = $this->repo->getBackend();
1324  $files = [ $dir ];
1325  try {
1326  $iterator = $backend->getFileList( [ 'dir' => $dir, 'forWrite' => true ] );
1327  if ( $iterator !== null ) {
1328  foreach ( $iterator as $file ) {
1329  $files[] = $file;
1330  }
1331  }
1332  } catch ( FileBackendError $e ) {
1333  } // suppress (T56674)
1334 
1335  return $files;
1336  }
1337 
1341  private function purgeMetadataCache() {
1342  $this->invalidateCache();
1343  }
1344 
1353  public function purgeCache( $options = [] ) {
1354  // Refresh metadata cache
1355  $this->maybeUpgradeRow();
1356  $this->purgeMetadataCache();
1357 
1358  // Delete thumbnails
1359  $this->purgeThumbnails( $options );
1360 
1361  // Purge CDN cache for this file
1362  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1363  $hcu->purgeUrls(
1364  $this->getUrl(),
1365  !empty( $options['forThumbRefresh'] )
1366  ? $hcu::PURGE_PRESEND // just a manual purge
1367  : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1368  );
1369  }
1370 
1376  public function purgeOldThumbnails( $archiveName ) {
1377  // Get a list of old thumbnails
1378  $thumbs = $this->getThumbnails( $archiveName );
1379 
1380  // Delete thumbnails from storage, and prevent the directory itself from being purged
1381  $dir = array_shift( $thumbs );
1382  $this->purgeThumbList( $dir, $thumbs );
1383 
1384  $urls = [];
1385  foreach ( $thumbs as $thumb ) {
1386  $urls[] = $this->getArchiveThumbUrl( $archiveName, $thumb );
1387  }
1388 
1389  // Purge any custom thumbnail caches
1390  $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, $archiveName, $urls );
1391 
1392  // Purge the CDN
1393  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1394  $hcu->purgeUrls( $urls, $hcu::PURGE_PRESEND );
1395  }
1396 
1403  public function purgeThumbnails( $options = [] ) {
1404  $thumbs = $this->getThumbnails();
1405 
1406  // Delete thumbnails from storage, and prevent the directory itself from being purged
1407  $dir = array_shift( $thumbs );
1408  $this->purgeThumbList( $dir, $thumbs );
1409 
1410  // Always purge all files from CDN regardless of handler filters
1411  $urls = [];
1412  foreach ( $thumbs as $thumb ) {
1413  $urls[] = $this->getThumbUrl( $thumb );
1414  }
1415 
1416  // Give the media handler a chance to filter the file purge list
1417  if ( !empty( $options['forThumbRefresh'] ) ) {
1418  $handler = $this->getHandler();
1419  if ( $handler ) {
1420  $handler->filterThumbnailPurgeList( $thumbs, $options );
1421  }
1422  }
1423 
1424  // Purge any custom thumbnail caches
1425  $this->getHookRunner()->onLocalFilePurgeThumbnails( $this, false, $urls );
1426 
1427  // Purge the CDN
1428  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
1429  $hcu->purgeUrls(
1430  $urls,
1431  !empty( $options['forThumbRefresh'] )
1432  ? $hcu::PURGE_PRESEND // just a manual purge
1433  : $hcu::PURGE_INTENT_TXROUND_REFLECTED
1434  );
1435  }
1436 
1443  public function prerenderThumbnails() {
1444  $uploadThumbnailRenderMap = MediaWikiServices::getInstance()
1445  ->getMainConfig()->get( MainConfigNames::UploadThumbnailRenderMap );
1446 
1447  $jobs = [];
1448 
1449  $sizes = $uploadThumbnailRenderMap;
1450  rsort( $sizes );
1451 
1452  foreach ( $sizes as $size ) {
1453  if ( $this->isMultipage() ) {
1454  // (T309114) Only trigger render jobs up to MAX_PAGE_RENDER_JOBS to avoid
1455  // a flood of jobs for huge files.
1456  $pageLimit = min( $this->pageCount(), self::MAX_PAGE_RENDER_JOBS );
1457 
1458  for ( $page = 1; $page <= $pageLimit; $page++ ) {
1459  $jobs[] = new ThumbnailRenderJob(
1460  $this->getTitle(),
1461  [ 'transformParams' => [
1462  'width' => $size,
1463  'page' => $page,
1464  ] ]
1465  );
1466  }
1467  } elseif ( $this->isVectorized() || $this->getWidth() > $size ) {
1468  $jobs[] = new ThumbnailRenderJob(
1469  $this->getTitle(),
1470  [ 'transformParams' => [ 'width' => $size ] ]
1471  );
1472  }
1473  }
1474 
1475  if ( $jobs ) {
1476  MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs );
1477  }
1478  }
1479 
1486  protected function purgeThumbList( $dir, $files ) {
1487  $fileListDebug = strtr(
1488  var_export( $files, true ),
1489  [ "\n" => '' ]
1490  );
1491  wfDebug( __METHOD__ . ": $fileListDebug" );
1492 
1493  if ( $this->repo->supportsSha1URLs() ) {
1494  $reference = $this->getSha1();
1495  } else {
1496  $reference = $this->getName();
1497  }
1498 
1499  $purgeList = [];
1500  foreach ( $files as $file ) {
1501  # Check that the reference (filename or sha1) is part of the thumb name
1502  # This is a basic check to avoid erasing unrelated directories
1503  if ( str_contains( $file, $reference )
1504  || str_contains( $file, "-thumbnail" ) // "short" thumb name
1505  ) {
1506  $purgeList[] = "{$dir}/{$file}";
1507  }
1508  }
1509 
1510  # Delete the thumbnails
1511  $this->repo->quickPurgeBatch( $purgeList );
1512  # Clear out the thumbnail directory if empty
1513  $this->repo->quickCleanDir( $dir );
1514  }
1515 
1527  public function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1528  if ( !$this->exists() ) {
1529  return []; // Avoid hard failure when the file does not exist. T221812
1530  }
1531 
1532  $dbr = $this->repo->getReplicaDB();
1533  $oldFileQuery = OldLocalFile::getQueryInfo();
1534 
1535  $tables = $oldFileQuery['tables'];
1536  $fields = $oldFileQuery['fields'];
1537  $join_conds = $oldFileQuery['joins'];
1538  $conds = $opts = [];
1539  $eq = $inc ? '=' : '';
1540  $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
1541 
1542  if ( $start ) {
1543  $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
1544  }
1545 
1546  if ( $end ) {
1547  $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
1548  }
1549 
1550  if ( $limit ) {
1551  $opts['LIMIT'] = $limit;
1552  }
1553 
1554  // Search backwards for time > x queries
1555  $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
1556  $opts['ORDER BY'] = "oi_timestamp $order";
1557  $opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1558 
1559  $this->getHookRunner()->onLocalFile__getHistory( $this, $tables, $fields,
1560  $conds, $opts, $join_conds );
1561 
1562  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1563  $r = [];
1564 
1565  foreach ( $res as $row ) {
1566  $r[] = $this->repo->newFileFromRow( $row );
1567  }
1568 
1569  if ( $order == 'ASC' ) {
1570  $r = array_reverse( $r ); // make sure it ends up descending
1571  }
1572 
1573  return $r;
1574  }
1575 
1586  public function nextHistoryLine() {
1587  if ( !$this->exists() ) {
1588  return false; // Avoid hard failure when the file does not exist. T221812
1589  }
1590 
1591  # Polymorphic function name to distinguish foreign and local fetches
1592  $fname = static::class . '::' . __FUNCTION__;
1593 
1594  $dbr = $this->repo->getReplicaDB();
1595 
1596  if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1597  $fileQuery = self::getQueryInfo();
1598  $this->historyRes = $dbr->select( $fileQuery['tables'],
1599  $fileQuery['fields'] + [
1600  'oi_archive_name' => $dbr->addQuotes( '' ),
1601  'oi_deleted' => 0,
1602  ],
1603  [ 'img_name' => $this->title->getDBkey() ],
1604  $fname,
1605  [],
1606  $fileQuery['joins']
1607  );
1608 
1609  if ( $this->historyRes->numRows() == 0 ) {
1610  $this->historyRes = null;
1611 
1612  return false;
1613  }
1614  } elseif ( $this->historyLine == 1 ) {
1615  $fileQuery = OldLocalFile::getQueryInfo();
1616  $this->historyRes = $dbr->select(
1617  $fileQuery['tables'],
1618  $fileQuery['fields'],
1619  [ 'oi_name' => $this->title->getDBkey() ],
1620  $fname,
1621  [ 'ORDER BY' => 'oi_timestamp DESC' ],
1622  $fileQuery['joins']
1623  );
1624  }
1625  $this->historyLine++;
1626 
1627  return $this->historyRes->fetchObject();
1628  }
1629 
1634  public function resetHistory() {
1635  $this->historyLine = 0;
1636 
1637  if ( $this->historyRes !== null ) {
1638  $this->historyRes = null;
1639  }
1640  }
1641 
1675  public function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1676  $timestamp = false, Authority $uploader = null, $tags = [],
1677  $createNullRevision = true, $revert = false
1678  ) {
1679  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1680  return $this->readOnlyFatalStatus();
1681  } elseif ( MediaWikiServices::getInstance()->getRevisionStore()->isReadOnly() ) {
1682  // Check this in advance to avoid writing to FileBackend and the file tables,
1683  // only to fail on insert the revision due to the text store being unavailable.
1684  return $this->readOnlyFatalStatus();
1685  }
1686 
1687  $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1688  if ( !$props ) {
1689  if ( FileRepo::isVirtualUrl( $srcPath )
1690  || FileBackend::isStoragePath( $srcPath )
1691  ) {
1692  $props = $this->repo->getFileProps( $srcPath );
1693  } else {
1694  $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
1695  $props = $mwProps->getPropsFromPath( $srcPath, true );
1696  }
1697  }
1698 
1699  $options = [];
1700  $handler = MediaHandler::getHandler( $props['mime'] );
1701  if ( $handler ) {
1702  if ( is_string( $props['metadata'] ) ) {
1703  // This supports callers directly fabricating a metadata
1704  // property using serialize(). Normally the metadata property
1705  // comes from MWFileProps, in which case it won't be a string.
1706  // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged
1707  $metadata = @unserialize( $props['metadata'] );
1708  } else {
1709  $metadata = $props['metadata'];
1710  }
1711 
1712  if ( is_array( $metadata ) ) {
1713  $options['headers'] = $handler->getContentHeaders( $metadata );
1714  }
1715  } else {
1716  $options['headers'] = [];
1717  }
1718 
1719  // Trim spaces on user supplied text
1720  $comment = trim( $comment );
1721 
1722  $status = $this->publish( $src, $flags, $options );
1723 
1724  if ( $status->successCount >= 2 ) {
1725  // There will be a copy+(one of move,copy,store).
1726  // The first succeeding does not commit us to updating the DB
1727  // since it simply copied the current version to a timestamped file name.
1728  // It is only *preferable* to avoid leaving such files orphaned.
1729  // Once the second operation goes through, then the current version was
1730  // updated and we must therefore update the DB too.
1731  $oldver = $status->value;
1732 
1733  $uploadStatus = $this->recordUpload3(
1734  $oldver,
1735  $comment,
1736  $pageText,
1737  $uploader ?? RequestContext::getMain()->getAuthority(),
1738  $props,
1739  $timestamp,
1740  $tags,
1741  $createNullRevision,
1742  $revert
1743  );
1744  if ( !$uploadStatus->isOK() ) {
1745  if ( $uploadStatus->hasMessage( 'filenotfound' ) ) {
1746  // update filenotfound error with more specific path
1747  $status->fatal( 'filenotfound', $srcPath );
1748  } else {
1749  $status->merge( $uploadStatus );
1750  }
1751  }
1752  }
1753 
1754  return $status;
1755  }
1756 
1773  public function recordUpload3(
1774  string $oldver,
1775  string $comment,
1776  string $pageText,
1777  Authority $performer,
1778  $props = false,
1779  $timestamp = false,
1780  $tags = [],
1781  bool $createNullRevision = true,
1782  bool $revert = false
1783  ): Status {
1784  $dbw = $this->repo->getPrimaryDB();
1785 
1786  # Imports or such might force a certain timestamp; otherwise we generate
1787  # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1788  if ( $timestamp === false ) {
1789  $timestamp = $dbw->timestamp();
1790  $allowTimeKludge = true;
1791  } else {
1792  $allowTimeKludge = false;
1793  }
1794 
1795  $props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1796  $props['description'] = $comment;
1797  $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1798  $this->setProps( $props );
1799 
1800  # Fail now if the file isn't there
1801  if ( !$this->fileExists ) {
1802  wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!" );
1803 
1804  return Status::newFatal( 'filenotfound', $this->getRel() );
1805  }
1806 
1807  $actorNormalizaton = MediaWikiServices::getInstance()->getActorNormalization();
1808 
1809  $dbw->startAtomic( __METHOD__ );
1810 
1811  $actorId = $actorNormalizaton->acquireActorId( $performer->getUser(), $dbw );
1812  $this->user = $performer->getUser();
1813 
1814  # Test to see if the row exists using INSERT IGNORE
1815  # This avoids race conditions by locking the row until the commit, and also
1816  # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1817  $commentStore = MediaWikiServices::getInstance()->getCommentStore();
1818  $commentFields = $commentStore->insert( $dbw, 'img_description', $comment );
1819  $actorFields = [ 'img_actor' => $actorId ];
1820  $dbw->insert( 'image',
1821  [
1822  'img_name' => $this->getName(),
1823  'img_size' => $this->size,
1824  'img_width' => intval( $this->width ),
1825  'img_height' => intval( $this->height ),
1826  'img_bits' => $this->bits,
1827  'img_media_type' => $this->media_type,
1828  'img_major_mime' => $this->major_mime,
1829  'img_minor_mime' => $this->minor_mime,
1830  'img_timestamp' => $dbw->timestamp( $timestamp ),
1831  'img_metadata' => $this->getMetadataForDb( $dbw ),
1832  'img_sha1' => $this->sha1
1833  ] + $commentFields + $actorFields,
1834  __METHOD__,
1835  [ 'IGNORE' ]
1836  );
1837  $reupload = ( $dbw->affectedRows() == 0 );
1838 
1839  if ( $reupload ) {
1840  $row = $dbw->selectRow(
1841  'image',
1842  [ 'img_timestamp', 'img_sha1' ],
1843  [ 'img_name' => $this->getName() ],
1844  __METHOD__,
1845  [ 'LOCK IN SHARE MODE' ]
1846  );
1847 
1848  if ( $row && $row->img_sha1 === $this->sha1 ) {
1849  $dbw->endAtomic( __METHOD__ );
1850  wfDebug( __METHOD__ . ": File " . $this->getRel() . " already exists!" );
1851  $title = Title::newFromText( $this->getName(), NS_FILE );
1852  return Status::newFatal( 'fileexists-no-change', $title->getPrefixedText() );
1853  }
1854 
1855  if ( $allowTimeKludge ) {
1856  # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1857  $lUnixtime = $row ? (int)wfTimestamp( TS_UNIX, $row->img_timestamp ) : false;
1858  # Avoid a timestamp that is not newer than the last version
1859  # TODO: the image/oldimage tables should be like page/revision with an ID field
1860  if ( $lUnixtime && (int)wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
1861  sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1862  $timestamp = $dbw->timestamp( $lUnixtime + 1 );
1863  $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1864  }
1865  }
1866 
1867  $tables = [ 'image' ];
1868  $fields = [
1869  'oi_name' => 'img_name',
1870  'oi_archive_name' => $dbw->addQuotes( $oldver ),
1871  'oi_size' => 'img_size',
1872  'oi_width' => 'img_width',
1873  'oi_height' => 'img_height',
1874  'oi_bits' => 'img_bits',
1875  'oi_description_id' => 'img_description_id',
1876  'oi_timestamp' => 'img_timestamp',
1877  'oi_metadata' => 'img_metadata',
1878  'oi_media_type' => 'img_media_type',
1879  'oi_major_mime' => 'img_major_mime',
1880  'oi_minor_mime' => 'img_minor_mime',
1881  'oi_sha1' => 'img_sha1',
1882  'oi_actor' => 'img_actor',
1883  ];
1884  $joins = [];
1885 
1886  # (T36993) Note: $oldver can be empty here, if the previous
1887  # version of the file was broken. Allow registration of the new
1888  # version to continue anyway, because that's better than having
1889  # an image that's not fixable by user operations.
1890  # Collision, this is an update of a file
1891  # Insert previous contents into oldimage
1892  $dbw->insertSelect( 'oldimage', $tables, $fields,
1893  [ 'img_name' => $this->getName() ], __METHOD__, [], [], $joins );
1894 
1895  # Update the current image row
1896  $dbw->update( 'image',
1897  [
1898  'img_size' => $this->size,
1899  'img_width' => intval( $this->width ),
1900  'img_height' => intval( $this->height ),
1901  'img_bits' => $this->bits,
1902  'img_media_type' => $this->media_type,
1903  'img_major_mime' => $this->major_mime,
1904  'img_minor_mime' => $this->minor_mime,
1905  'img_timestamp' => $dbw->timestamp( $timestamp ),
1906  'img_metadata' => $this->getMetadataForDb( $dbw ),
1907  'img_sha1' => $this->sha1
1908  ] + $commentFields + $actorFields,
1909  [ 'img_name' => $this->getName() ],
1910  __METHOD__
1911  );
1912  }
1913 
1914  $descTitle = $this->getTitle();
1915  $descId = $descTitle->getArticleID();
1916  $wikiPage = MediaWikiServices::getInstance()->getWikiPageFactory()->newFromTitle( $descTitle );
1917  if ( !$wikiPage instanceof WikiFilePage ) {
1918  throw new MWException( 'Cannot instance WikiFilePage for ' . $this->getName()
1919  . ', got instance of ' . get_class( $wikiPage ) );
1920  }
1921  $wikiPage->setFile( $this );
1922 
1923  // Determine log action. If reupload is done by reverting, use a special log_action.
1924  if ( $revert ) {
1925  $logAction = 'revert';
1926  } elseif ( $reupload ) {
1927  $logAction = 'overwrite';
1928  } else {
1929  $logAction = 'upload';
1930  }
1931  // Add the log entry...
1932  $logEntry = new ManualLogEntry( 'upload', $logAction );
1933  $logEntry->setTimestamp( $this->timestamp );
1934  $logEntry->setPerformer( $performer->getUser() );
1935  $logEntry->setComment( $comment );
1936  $logEntry->setTarget( $descTitle );
1937  // Allow people using the api to associate log entries with the upload.
1938  // Log has a timestamp, but sometimes different from upload timestamp.
1939  $logEntry->setParameters(
1940  [
1941  'img_sha1' => $this->sha1,
1942  'img_timestamp' => $timestamp,
1943  ]
1944  );
1945  // Note we keep $logId around since during new image
1946  // creation, page doesn't exist yet, so log_page = 0
1947  // but we want it to point to the page we're making,
1948  // so we later modify the log entry.
1949  // For a similar reason, we avoid making an RC entry
1950  // now and wait until the page exists.
1951  $logId = $logEntry->insert();
1952 
1953  if ( $descTitle->exists() ) {
1954  if ( $createNullRevision ) {
1955  $revStore = MediaWikiServices::getInstance()->getRevisionStore();
1956  // Use own context to get the action text in content language
1957  $formatter = LogFormatter::newFromEntry( $logEntry );
1958  $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1959  $editSummary = $formatter->getPlainActionText();
1960  $summary = CommentStoreComment::newUnsavedComment( $editSummary );
1961  $nullRevRecord = $revStore->newNullRevision(
1962  $dbw,
1963  $descTitle,
1964  $summary,
1965  false,
1966  $performer->getUser()
1967  );
1968 
1969  if ( $nullRevRecord ) {
1970  $inserted = $revStore->insertRevisionOn( $nullRevRecord, $dbw );
1971 
1972  $this->getHookRunner()->onRevisionFromEditComplete(
1973  $wikiPage,
1974  $inserted,
1975  $inserted->getParentId(),
1976  $performer->getUser(),
1977  $tags
1978  );
1979 
1980  $wikiPage->updateRevisionOn( $dbw, $inserted );
1981  // Associate null revision id
1982  $logEntry->setAssociatedRevId( $inserted->getId() );
1983  }
1984  }
1985 
1986  $newPageContent = null;
1987  } else {
1988  // Make the description page and RC log entry post-commit
1989  $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1990  }
1991 
1992  // NOTE: Even after ending this atomic section, we are probably still in the implicit
1993  // transaction started by any prior master query in the request. We cannot yet safely
1994  // schedule jobs, see T263301.
1995  $dbw->endAtomic( __METHOD__ );
1996  $fname = __METHOD__;
1997 
1998  # Do some cache purges after final commit so that:
1999  # a) Changes are more likely to be seen post-purge
2000  # b) They won't cause rollback of the log publish/update above
2001  $purgeUpdate = new AutoCommitUpdate(
2002  $dbw,
2003  __METHOD__,
2004  function () use (
2005  $reupload, $wikiPage, $newPageContent, $comment, $performer,
2006  $logEntry, $logId, $descId, $tags, $fname
2007  ) {
2008  # Update memcache after the commit
2009  $this->invalidateCache();
2010 
2011  $updateLogPage = false;
2012  if ( $newPageContent ) {
2013  # New file page; create the description page.
2014  # There's already a log entry, so don't make a second RC entry
2015  # CDN and file cache for the description page are purged by doUserEditContent.
2016  $status = $wikiPage->doUserEditContent(
2017  $newPageContent,
2018  $performer,
2019  $comment,
2021  );
2022 
2023  $revRecord = $status->getNewRevision();
2024  if ( $revRecord ) {
2025  // Associate new page revision id
2026  $logEntry->setAssociatedRevId( $revRecord->getId() );
2027 
2028  // This relies on the resetArticleID() call in WikiPage::insertOn(),
2029  // which is triggered on $descTitle by doUserEditContent() above.
2030  $updateLogPage = $revRecord->getPageId();
2031  }
2032  } else {
2033  # Existing file page: invalidate description page cache
2034  $title = $wikiPage->getTitle();
2035  $title->invalidateCache();
2036  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2037  $hcu->purgeTitleUrls( $title, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2038  # Allow the new file version to be patrolled from the page footer
2040  }
2041 
2042  # Update associated rev id. This should be done by $logEntry->insert() earlier,
2043  # but setAssociatedRevId() wasn't called at that point yet...
2044  $logParams = $logEntry->getParameters();
2045  $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
2046  $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
2047  if ( $updateLogPage ) {
2048  # Also log page, in case where we just created it above
2049  $update['log_page'] = $updateLogPage;
2050  }
2051  $this->getRepo()->getPrimaryDB()->update(
2052  'logging',
2053  $update,
2054  [ 'log_id' => $logId ],
2055  $fname
2056  );
2057  $this->getRepo()->getPrimaryDB()->insert(
2058  'log_search',
2059  [
2060  'ls_field' => 'associated_rev_id',
2061  'ls_value' => (string)$logEntry->getAssociatedRevId(),
2062  'ls_log_id' => $logId,
2063  ],
2064  $fname
2065  );
2066 
2067  # Add change tags, if any
2068  if ( $tags ) {
2069  $logEntry->addTags( $tags );
2070  }
2071 
2072  # Uploads can be patrolled
2073  $logEntry->setIsPatrollable( true );
2074 
2075  # Now that the log entry is up-to-date, make an RC entry.
2076  $logEntry->publish( $logId );
2077 
2078  # Run hook for other updates (typically more cache purging)
2079  $this->getHookRunner()->onFileUpload( $this, $reupload, !$newPageContent );
2080 
2081  if ( $reupload ) {
2082  # Delete old thumbnails
2083  $this->purgeThumbnails();
2084  # Remove the old file from the CDN cache
2085  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2086  $hcu->purgeUrls( $this->getUrl(), $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2087  } else {
2088  # Update backlink pages pointing to this title if created
2089  $blcFactory = MediaWikiServices::getInstance()->getBacklinkCacheFactory();
2090  LinksUpdate::queueRecursiveJobsForTable(
2091  $this->getTitle(),
2092  'imagelinks',
2093  'upload-image',
2094  $performer->getUser()->getName(),
2095  $blcFactory->getBacklinkCache( $this->getTitle() )
2096  );
2097  }
2098 
2099  $this->prerenderThumbnails();
2100  }
2101  );
2102 
2103  # Invalidate cache for all pages using this file
2104  $cacheUpdateJob = HTMLCacheUpdateJob::newForBacklinks(
2105  $this->getTitle(),
2106  'imagelinks',
2107  [ 'causeAction' => 'file-upload', 'causeAgent' => $performer->getUser()->getName() ]
2108  );
2109 
2110  // NOTE: We are probably still in the implicit transaction started by DBO_TRX. We should
2111  // only schedule jobs after that transaction was committed, so a job queue failure
2112  // doesn't cause the upload to fail (T263301). Also, we should generally not schedule any
2113  // Jobs or the DeferredUpdates that assume the update is complete until after the
2114  // transaction has been committed and we are sure that the upload was indeed successful.
2115  $dbw->onTransactionCommitOrIdle( static function () use ( $reupload, $purgeUpdate, $cacheUpdateJob ) {
2116  DeferredUpdates::addUpdate( $purgeUpdate, DeferredUpdates::PRESEND );
2117 
2118  if ( !$reupload ) {
2119  // This is a new file, so update the image count
2120  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2121  }
2122 
2123  MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $cacheUpdateJob );
2124  }, __METHOD__ );
2125 
2126  return Status::newGood();
2127  }
2128 
2145  public function publish( $src, $flags = 0, array $options = [] ) {
2146  return $this->publishTo( $src, $this->getRel(), $flags, $options );
2147  }
2148 
2165  protected function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
2166  $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
2167 
2168  $repo = $this->getRepo();
2169  if ( $repo->getReadOnlyReason() !== false ) {
2170  return $this->readOnlyFatalStatus();
2171  }
2172 
2173  $status = $this->acquireFileLock();
2174  if ( !$status->isOK() ) {
2175  return $status;
2176  }
2177 
2178  if ( $this->isOld() ) {
2179  $archiveRel = $dstRel;
2180  $archiveName = basename( $archiveRel );
2181  } else {
2182  $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
2183  $archiveRel = $this->getArchiveRel( $archiveName );
2184  }
2185 
2186  if ( $repo->hasSha1Storage() ) {
2187  $sha1 = FileRepo::isVirtualUrl( $srcPath )
2188  ? $repo->getFileSha1( $srcPath )
2189  : FSFile::getSha1Base36FromPath( $srcPath );
2191  $wrapperBackend = $repo->getBackend();
2192  '@phan-var FileBackendDBRepoWrapper $wrapperBackend';
2193  $dst = $wrapperBackend->getPathForSHA1( $sha1 );
2194  $status = $repo->quickImport( $src, $dst );
2195  if ( $flags & File::DELETE_SOURCE ) {
2196  unlink( $srcPath );
2197  }
2198 
2199  if ( $this->exists() ) {
2200  $status->value = $archiveName;
2201  }
2202  } else {
2203  $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
2204  $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
2205 
2206  if ( $status->value == 'new' ) {
2207  $status->value = '';
2208  } else {
2209  $status->value = $archiveName;
2210  }
2211  }
2212 
2213  $this->releaseFileLock();
2214  return $status;
2215  }
2216 
2235  public function move( $target ) {
2236  $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2237  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2238  return $this->readOnlyFatalStatus();
2239  }
2240 
2241  wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
2242  $batch = new LocalFileMoveBatch( $this, $target );
2243 
2244  $status = $batch->addCurrent();
2245  if ( !$status->isOK() ) {
2246  return $status;
2247  }
2248  $archiveNames = $batch->addOlds();
2249  $status = $batch->execute();
2250 
2251  wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
2252 
2253  // Purge the source and target files outside the transaction...
2254  $oldTitleFile = $localRepo->newFile( $this->title );
2255  $newTitleFile = $localRepo->newFile( $target );
2257  new AutoCommitUpdate(
2258  $this->getRepo()->getPrimaryDB(),
2259  __METHOD__,
2260  static function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
2261  $oldTitleFile->purgeEverything();
2262  foreach ( $archiveNames as $archiveName ) {
2264  '@phan-var OldLocalFile $oldTitleFile';
2265  $oldTitleFile->purgeOldThumbnails( $archiveName );
2266  }
2267  $newTitleFile->purgeEverything();
2268  }
2269  ),
2270  DeferredUpdates::PRESEND
2271  );
2272 
2273  if ( $status->isOK() ) {
2274  // Now switch the object
2275  $this->title = $target;
2276  // Force regeneration of the name and hashpath
2277  $this->name = null;
2278  $this->hashPath = null;
2279  }
2280 
2281  return $status;
2282  }
2283 
2300  public function deleteFile( $reason, UserIdentity $user, $suppress = false ) {
2301  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2302  return $this->readOnlyFatalStatus();
2303  }
2304 
2305  $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2306 
2307  $batch->addCurrent();
2308  // Get old version relative paths
2309  $archiveNames = $batch->addOlds();
2310  $status = $batch->execute();
2311 
2312  if ( $status->isOK() ) {
2313  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
2314  }
2315 
2316  // To avoid slow purges in the transaction, move them outside...
2318  new AutoCommitUpdate(
2319  $this->getRepo()->getPrimaryDB(),
2320  __METHOD__,
2321  function () use ( $archiveNames ) {
2322  $this->purgeEverything();
2323  foreach ( $archiveNames as $archiveName ) {
2324  $this->purgeOldThumbnails( $archiveName );
2325  }
2326  }
2327  ),
2328  DeferredUpdates::PRESEND
2329  );
2330 
2331  // Purge the CDN
2332  $purgeUrls = [];
2333  foreach ( $archiveNames as $archiveName ) {
2334  $purgeUrls[] = $this->getArchiveUrl( $archiveName );
2335  }
2336 
2337  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2338  $hcu->purgeUrls( $purgeUrls, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2339 
2340  return $status;
2341  }
2342 
2361  public function deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress = false ) {
2362  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2363  return $this->readOnlyFatalStatus();
2364  }
2365 
2366  $batch = new LocalFileDeleteBatch( $this, $user, $reason, $suppress );
2367 
2368  $batch->addOld( $archiveName );
2369  $status = $batch->execute();
2370 
2371  $this->purgeOldThumbnails( $archiveName );
2372  if ( $status->isOK() ) {
2373  $this->purgeDescription();
2374  }
2375 
2376  $url = $this->getArchiveUrl( $archiveName );
2377  $hcu = MediaWikiServices::getInstance()->getHtmlCacheUpdater();
2378  $hcu->purgeUrls( $url, $hcu::PURGE_INTENT_TXROUND_REFLECTED );
2379 
2380  return $status;
2381  }
2382 
2395  public function restore( $versions = [], $unsuppress = false ) {
2396  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
2397  return $this->readOnlyFatalStatus();
2398  }
2399 
2400  $batch = new LocalFileRestoreBatch( $this, $unsuppress );
2401 
2402  if ( !$versions ) {
2403  $batch->addAll();
2404  } else {
2405  $batch->addIds( $versions );
2406  }
2407  $status = $batch->execute();
2408  if ( $status->isGood() ) {
2409  $cleanupStatus = $batch->cleanup();
2410  $cleanupStatus->successCount = 0;
2411  $cleanupStatus->failCount = 0;
2412  $status->merge( $cleanupStatus );
2413  }
2414 
2415  return $status;
2416  }
2417 
2428  public function getDescriptionUrl() {
2429  // Avoid hard failure when the file does not exist. T221812
2430  return $this->title ? $this->title->getLocalURL() : false;
2431  }
2432 
2442  public function getDescriptionText( Language $lang = null ) {
2443  if ( !$this->title ) {
2444  return false; // Avoid hard failure when the file does not exist. T221812
2445  }
2446 
2447  $services = MediaWikiServices::getInstance();
2448  $page = $services->getPageStore()->getPageByReference( $this->getTitle() );
2449  if ( !$page ) {
2450  return false;
2451  }
2452 
2453  if ( $lang ) {
2454  $parserOptions = ParserOptions::newFromUserAndLang(
2456  $lang
2457  );
2458  } else {
2460  }
2461 
2462  $parseStatus = $services->getParserOutputAccess()
2463  ->getParserOutput( $page, $parserOptions );
2464 
2465  if ( !$parseStatus->isGood() ) {
2466  // Rendering failed.
2467  return false;
2468  }
2469  return $parseStatus->getValue()->getText();
2470  }
2471 
2479  public function getUploader( int $audience = self::FOR_PUBLIC, Authority $performer = null ): ?UserIdentity {
2480  $this->load();
2481  if ( $audience === self::FOR_PUBLIC && $this->isDeleted( self::DELETED_USER ) ) {
2482  return null;
2483  } elseif ( $audience === self::FOR_THIS_USER && !$this->userCan( self::DELETED_USER, $performer ) ) {
2484  return null;
2485  } else {
2486  return $this->user;
2487  }
2488  }
2489 
2496  public function getDescription( $audience = self::FOR_PUBLIC, Authority $performer = null ) {
2497  $this->load();
2498  if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
2499  return '';
2500  } elseif ( $audience == self::FOR_THIS_USER && !$this->userCan( self::DELETED_COMMENT, $performer ) ) {
2501  return '';
2502  } else {
2503  return $this->description;
2504  }
2505  }
2506 
2511  public function getTimestamp() {
2512  $this->load();
2513 
2514  return $this->timestamp;
2515  }
2516 
2521  public function getDescriptionTouched() {
2522  if ( !$this->exists() ) {
2523  return false; // Avoid hard failure when the file does not exist. T221812
2524  }
2525 
2526  // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
2527  // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
2528  // need to differentiate between null (uninitialized) and false (failed to load).
2529  if ( $this->descriptionTouched === null ) {
2530  $cond = [
2531  'page_namespace' => $this->title->getNamespace(),
2532  'page_title' => $this->title->getDBkey()
2533  ];
2534  $touched = $this->repo->getReplicaDB()->selectField( 'page', 'page_touched', $cond, __METHOD__ );
2535  $this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
2536  }
2537 
2538  return $this->descriptionTouched;
2539  }
2540 
2545  public function getSha1() {
2546  $this->load();
2547  return $this->sha1;
2548  }
2549 
2553  public function isCacheable() {
2554  $this->load();
2555 
2556  // If extra data (metadata) was not loaded then it must have been large
2557  return $this->extraDataLoaded
2558  && strlen( serialize( $this->metadataArray ) ) <= self::CACHE_FIELD_MAX_LEN;
2559  }
2560 
2569  public function acquireFileLock( $timeout = 0 ) {
2570  return Status::wrap( $this->getRepo()->getBackend()->lockFiles(
2571  [ $this->getPath() ], LockManager::LOCK_EX, $timeout
2572  ) );
2573  }
2574 
2581  public function releaseFileLock() {
2582  return Status::wrap( $this->getRepo()->getBackend()->unlockFiles(
2583  [ $this->getPath() ], LockManager::LOCK_EX
2584  ) );
2585  }
2586 
2597  public function lock() {
2598  if ( !$this->locked ) {
2599  $logger = LoggerFactory::getInstance( 'LocalFile' );
2600 
2601  $dbw = $this->repo->getPrimaryDB();
2602  $makesTransaction = !$dbw->trxLevel();
2603  $dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
2604  // T56736: use simple lock to handle when the file does not exist.
2605  // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
2606  // Also, that would cause contention on INSERT of similarly named rows.
2607  $status = $this->acquireFileLock( 10 ); // represents all versions of the file
2608  if ( !$status->isGood() ) {
2609  $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2610  $logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
2611 
2612  throw new LocalFileLockError( $status );
2613  }
2614  // Release the lock *after* commit to avoid row-level contention.
2615  // Make sure it triggers on rollback() as well as commit() (T132921).
2616  $dbw->onTransactionResolution(
2617  function () use ( $logger ) {
2618  $status = $this->releaseFileLock();
2619  if ( !$status->isGood() ) {
2620  $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
2621  }
2622  },
2623  __METHOD__
2624  );
2625  // Callers might care if the SELECT snapshot is safely fresh
2626  $this->lockedOwnTrx = $makesTransaction;
2627  }
2628 
2629  $this->locked++;
2630 
2631  return $this->lockedOwnTrx;
2632  }
2633 
2644  public function unlock() {
2645  if ( $this->locked ) {
2646  --$this->locked;
2647  if ( !$this->locked ) {
2648  $dbw = $this->repo->getPrimaryDB();
2649  $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2650  $this->lockedOwnTrx = false;
2651  }
2652  }
2653  }
2654 
2658  protected function readOnlyFatalStatus() {
2659  return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
2660  $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2661  }
2662 
2666  public function __destruct() {
2667  $this->unlock();
2668  }
2669 }
getUser()
getAuthority()
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:1368
Deferrable Update for closure/callback updates that should use auto-commit mode.
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:49
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Definition: FileRepo.php:286
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:2459
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:1921
assertTitleDefined()
Assert that $this->title is set to a Title.
Definition: File.php:2469
FileRepo LocalRepo ForeignAPIRepo false $repo
Some member variables can be lazy-initialised using __get().
Definition: File.php:115
isMultipage()
Returns 'true' if this file is a type which supports multiple pages, e.g.
Definition: File.php:2156
Title string false $title
Definition: File.php:118
string $path
The storage path corresponding to one of the zones.
Definition: File.php:145
getHandler()
Get a MediaHandler instance for this file.
Definition: File.php:1540
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:57
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:61
exists()
canRender inherited
Definition: LocalFile.php:1289
setProps( $info)
Set properties in this object to be equal to those given in the associative array $info.
Definition: LocalFile.php:877
maybeUpgradeRow()
Upgrade a row if it needs it.
Definition: LocalFile.php:740
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:234
array $metadataArray
Unserialized metadata.
Definition: LocalFile.php:103
getMediaType()
Returns the type of the media in the file.
Definition: LocalFile.php:1272
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:122
deleteOldFile( $archiveName, $reason, UserIdentity $user, $suppress=false)
Delete an old version of the file.
Definition: LocalFile.php:2361
move( $target)
getLinksTo inherited
Definition: LocalFile.php:2235
lock()
Start an atomic DB section and lock the image for update or increments a reference counter if the loc...
Definition: LocalFile.php:2597
loadFromRow( $row, $prefix='img_')
Load file metadata from a DB result row.
Definition: LocalFile.php:643
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:341
getWidth( $page=1)
Return the width of the image @stable to override.
Definition: LocalFile.php:960
__destruct()
Clean up any dangling locks.
Definition: LocalFile.php:2666
string $mime
MIME type, determined by MimeAnalyzer::guessMimeType.
Definition: LocalFile.php:97
reserializeMetadata()
Write the metadata back to the database with the current serialization format.
Definition: LocalFile.php:849
isMissing()
splitMime inherited
Definition: LocalFile.php:944
getDescriptionUrl()
isMultipage inherited
Definition: LocalFile.php:2428
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
Definition: LocalFile.php:1527
static getQueryInfo(array $options=[])
Return the tables, fields, and join conditions to be selected to create a new localfile object.
Definition: LocalFile.php:272
releaseFileLock()
Release a lock acquired with acquireFileLock().
Definition: LocalFile.php:2581
getUploader(int $audience=self::FOR_PUBLIC, Authority $performer=null)
Definition: LocalFile.php:2479
loadMetadataFromDbFieldValue(IDatabase $db, $metadataBlob)
Unserialize a metadata blob which came from the database and store it in $this.
Definition: LocalFile.php:1184
loadFromDB( $flags=0)
Load file metadata from the DB.
Definition: LocalFile.php:502
load( $flags=0)
Load file metadata from cache or DB, unless already loaded.
Definition: LocalFile.php:721
loadMetadataFromString( $metadataString)
Unserialize a metadata string which came from some non-DB source, or is the return value of IDatabase...
Definition: LocalFile.php:1195
string $media_type
MEDIATYPE_xxx (bitmap, drawing, audio...)
Definition: LocalFile.php:94
deleteFile( $reason, UserIdentity $user, $suppress=false)
Delete all versions of the file.
Definition: LocalFile.php:2300
acquireFileLock( $timeout=0)
Acquire an exclusive lock on the file, indicating an intention to write to the file backend.
Definition: LocalFile.php:2569
purgeCache( $options=[])
Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
Definition: LocalFile.php:1353
getDescriptionTouched()
Definition: LocalFile.php:2521
loadFromFile( $path=null)
Load metadata from the file itself.
Definition: LocalFile.php:452
getBitDepth()
@stable to override
Definition: LocalFile.php:1238
string null $metadataSerializationFormat
One of the MDS_* constants, giving the format of the metadata as stored in the DB,...
Definition: LocalFile.php:111
int $size
Size in bytes (loadFromXxx)
Definition: LocalFile.php:100
getDescriptionShortUrl()
Get short description URL for a file based on the page ID.
Definition: LocalFile.php:1026
getThumbnails( $archiveName=false)
getTransformScript inherited
Definition: LocalFile.php:1316
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:200
getMetadataForDb(IDatabase $db)
Serialize the metadata array for insertion into img_metadata, oi_metadata or fa_metadata.
Definition: LocalFile.php:1116
int $height
Image height.
Definition: LocalFile.php:88
purgeOldThumbnails( $archiveName)
Delete cached transformed files for an archived version only.
Definition: LocalFile.php:1376
publishTo( $src, $dstRel, $flags=0, array $options=[])
Move or copy a file to a specified location.
Definition: LocalFile.php:2165
purgeThumbList( $dir, $files)
Delete a list of thumbnails visible at urls.
Definition: LocalFile.php:1486
unlock()
Decrement the lock reference count and end the atomic section if it reaches zero.
Definition: LocalFile.php:2644
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:486
getSize()
Returns the size of the image file, in bytes @stable to override.
Definition: LocalFile.php:1249
invalidateCache()
Purge the file object/metadata cache.
Definition: LocalFile.php:431
getMimeType()
Returns the MIME type of the file.
Definition: LocalFile.php:1260
bool $extraDataLoaded
Whether or not lazy-loaded data has been loaded from the database.
Definition: LocalFile.php:131
readOnlyFatalStatus()
Definition: LocalFile.php:2658
string $sha1
SHA-1 base 36 content hash.
Definition: LocalFile.php:125
getDescription( $audience=self::FOR_PUBLIC, Authority $performer=null)
Definition: LocalFile.php:2496
getHeight( $page=1)
Return the height of the image @stable to override.
Definition: LocalFile.php:993
prerenderThumbnails()
Prerenders a configurable set of thumbnails.
Definition: LocalFile.php:1443
resetHistory()
Reset the history pointer to the first element of the history.
Definition: LocalFile.php:1634
unprefixRow( $row, $prefix='img_')
Definition: LocalFile.php:611
static newFromRow( $row, $repo)
Create a LocalFile from a title Do not call this except from inside a repo class.
Definition: LocalFile.php:215
publish( $src, $flags=0, array $options=[])
Move or copy a file to its public location.
Definition: LocalFile.php:2145
restore( $versions=[], $unsuppress=false)
Restore all or specified deleted revisions to the given file.
Definition: LocalFile.php:2395
getCacheFields( $prefix='img_')
Returns the list of object properties that are included as-is in the cache.
Definition: LocalFile.php:464
int $bits
Returned by getimagesize (loadFromXxx)
Definition: LocalFile.php:91
getMetadataItems(array $itemNames)
Get multiple elements of the unserialized handler-specific metadata.
Definition: LocalFile.php:1079
getDescriptionText(Language $lang=null)
Get the HTML text of the description page This is not used by ImagePage for local files,...
Definition: LocalFile.php:2442
purgeThumbnails( $options=[])
Delete cached transformed files for the current version only.
Definition: LocalFile.php:1403
loadExtraFromDB()
Load lazy file metadata from the DB.
Definition: LocalFile.php:535
string $repoClass
Definition: LocalFile.php:137
int $width
Image width.
Definition: LocalFile.php:85
nextHistoryLine()
Returns the history of this file, line by line.
Definition: LocalFile.php:1586
upgradeRow()
Fix assorted version-related problems with the image row by reloading it from the file.
Definition: LocalFile.php:801
int $deleted
Bitfield akin to rev_deleted.
Definition: LocalFile.php:134
getMetadata()
Get handler-specific metadata as a serialized string.
Definition: LocalFile.php:1048
getMetadataArray()
Get unserialized handler-specific metadata.
Definition: LocalFile.php:1066
__construct( $title, $repo)
Do not call this except from inside a repo class.
Definition: LocalFile.php:320
bool $dataLoaded
Whether or not core data has been loaded from the database (loadFromXxx)
Definition: LocalFile.php:128
bool $fileExists
Does the file exist on disk? (loadFromXxx)
Definition: LocalFile.php:78
upload( $src, $comment, $pageText, $flags=0, $props=false, $timestamp=false, Authority $uploader=null, $tags=[], $createNullRevision=true, $revert=false)
getHashPath inherited
Definition: LocalFile.php:1675
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:1773
string[] $metadataBlobs
Map of metadata item name to blob address.
Definition: LocalFile.php:114
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:32
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,...
Value object for a comment stored by CommentStore.
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.
Represents a title within MediaWiki.
Definition: Title.php:82
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:46
static wrap( $sv)
Succinct helper method to wrap a StatusValue.
Definition: Status.php:64
Job for asynchronous rendering of thumbnails, e.g.
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:36
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.
Result wrapper for grabbing data queried from an IDatabase object.
$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