MediaWiki  1.27.3
LocalFile.php
Go to the documentation of this file.
1 <?php
27 define( 'MW_FILE_VERSION', 9 );
28 
46 class LocalFile extends File {
47  const CACHE_FIELD_MAX_LEN = 1000;
48 
50  protected $fileExists;
51 
53  protected $width;
54 
56  protected $height;
57 
59  protected $bits;
60 
62  protected $media_type;
63 
65  protected $mime;
66 
68  protected $size;
69 
71  protected $metadata;
72 
74  protected $sha1;
75 
77  protected $dataLoaded;
78 
80  protected $extraDataLoaded;
81 
83  protected $deleted;
84 
86  protected $repoClass = 'LocalRepo';
87 
89  private $historyLine;
90 
92  private $historyRes;
93 
95  private $major_mime;
96 
98  private $minor_mime;
99 
101  private $timestamp;
102 
104  private $user;
105 
107  private $user_text;
108 
110  private $description;
111 
114 
116  private $upgraded;
117 
119  private $locked;
120 
122  private $lockedOwnTrx;
123 
125  private $missing;
126 
127  // @note: higher than IDBAccessObject constants
128  const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
129 
142  static function newFromTitle( $title, $repo, $unused = null ) {
143  return new self( $title, $repo );
144  }
145 
155  static function newFromRow( $row, $repo ) {
156  $title = Title::makeTitle( NS_FILE, $row->img_name );
157  $file = new self( $title, $repo );
158  $file->loadFromRow( $row );
159 
160  return $file;
161  }
162 
172  static function newFromKey( $sha1, $repo, $timestamp = false ) {
173  $dbr = $repo->getSlaveDB();
174 
175  $conds = [ 'img_sha1' => $sha1 ];
176  if ( $timestamp ) {
177  $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
178  }
179 
180  $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
181  if ( $row ) {
182  return self::newFromRow( $row, $repo );
183  } else {
184  return false;
185  }
186  }
187 
192  static function selectFields() {
193  return [
194  'img_name',
195  'img_size',
196  'img_width',
197  'img_height',
198  'img_metadata',
199  'img_bits',
200  'img_media_type',
201  'img_major_mime',
202  'img_minor_mime',
203  'img_description',
204  'img_user',
205  'img_user_text',
206  'img_timestamp',
207  'img_sha1',
208  ];
209  }
210 
217  function __construct( $title, $repo ) {
218  parent::__construct( $title, $repo );
219 
220  $this->metadata = '';
221  $this->historyLine = 0;
222  $this->historyRes = null;
223  $this->dataLoaded = false;
224  $this->extraDataLoaded = false;
225 
226  $this->assertRepoDefined();
227  $this->assertTitleDefined();
228  }
229 
235  function getCacheKey() {
236  $hashedName = md5( $this->getName() );
237 
238  return $this->repo->getSharedCacheKey( 'file', $hashedName );
239  }
240 
245  private function loadFromCache() {
246  $this->dataLoaded = false;
247  $this->extraDataLoaded = false;
248  $key = $this->getCacheKey();
249 
250  if ( !$key ) {
251  return false;
252  }
253 
255  $cachedValues = $cache->get( $key );
256 
257  // Check if the key existed and belongs to this version of MediaWiki
258  if ( is_array( $cachedValues ) && $cachedValues['version'] == MW_FILE_VERSION ) {
259  $this->fileExists = $cachedValues['fileExists'];
260  if ( $this->fileExists ) {
261  $this->setProps( $cachedValues );
262  }
263  $this->dataLoaded = true;
264  $this->extraDataLoaded = true;
265  foreach ( $this->getLazyCacheFields( '' ) as $field ) {
266  $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
267  }
268  }
269 
270  return $this->dataLoaded;
271  }
272 
276  private function saveToCache() {
277  $this->load();
278 
279  $key = $this->getCacheKey();
280  if ( !$key ) {
281  return;
282  }
283 
284  $fields = $this->getCacheFields( '' );
285  $cacheVal = [ 'version' => MW_FILE_VERSION ];
286  $cacheVal['fileExists'] = $this->fileExists;
287 
288  if ( $this->fileExists ) {
289  foreach ( $fields as $field ) {
290  $cacheVal[$field] = $this->$field;
291  }
292  }
293 
294  // Strip off excessive entries from the subset of fields that can become large.
295  // If the cache value gets to large it will not fit in memcached and nothing will
296  // get cached at all, causing master queries for any file access.
297  foreach ( $this->getLazyCacheFields( '' ) as $field ) {
298  if ( isset( $cacheVal[$field] ) && strlen( $cacheVal[$field] ) > 100 * 1024 ) {
299  unset( $cacheVal[$field] ); // don't let the value get too big
300  }
301  }
302 
303  // Cache presence for 1 week and negatives for 1 day
304  $ttl = $this->fileExists ? 86400 * 7 : 86400;
305  $opts = Database::getCacheSetOptions( $this->repo->getSlaveDB() );
306  ObjectCache::getMainWANInstance()->set( $key, $cacheVal, $ttl, $opts );
307  }
308 
312  public function invalidateCache() {
313  $key = $this->getCacheKey();
314  if ( !$key ) {
315  return;
316  }
317 
318  $this->repo->getMasterDB()->onTransactionPreCommitOrIdle( function() use ( $key ) {
320  } );
321  }
322 
326  function loadFromFile() {
327  $props = $this->repo->getFileProps( $this->getVirtualUrl() );
328  $this->setProps( $props );
329  }
330 
335  function getCacheFields( $prefix = 'img_' ) {
336  static $fields = [ 'size', 'width', 'height', 'bits', 'media_type',
337  'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
338  'user_text', 'description' ];
339  static $results = [];
340 
341  if ( $prefix == '' ) {
342  return $fields;
343  }
344 
345  if ( !isset( $results[$prefix] ) ) {
346  $prefixedFields = [];
347  foreach ( $fields as $field ) {
348  $prefixedFields[] = $prefix . $field;
349  }
350  $results[$prefix] = $prefixedFields;
351  }
352 
353  return $results[$prefix];
354  }
355 
360  function getLazyCacheFields( $prefix = 'img_' ) {
361  static $fields = [ 'metadata' ];
362  static $results = [];
363 
364  if ( $prefix == '' ) {
365  return $fields;
366  }
367 
368  if ( !isset( $results[$prefix] ) ) {
369  $prefixedFields = [];
370  foreach ( $fields as $field ) {
371  $prefixedFields[] = $prefix . $field;
372  }
373  $results[$prefix] = $prefixedFields;
374  }
375 
376  return $results[$prefix];
377  }
378 
383  function loadFromDB( $flags = 0 ) {
384  $fname = get_class( $this ) . '::' . __FUNCTION__;
385 
386  # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
387  $this->dataLoaded = true;
388  $this->extraDataLoaded = true;
389 
390  $dbr = ( $flags & self::READ_LATEST )
391  ? $this->repo->getMasterDB()
392  : $this->repo->getSlaveDB();
393 
394  $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
395  [ 'img_name' => $this->getName() ], $fname );
396 
397  if ( $row ) {
398  $this->loadFromRow( $row );
399  } else {
400  $this->fileExists = false;
401  }
402  }
403 
408  protected function loadExtraFromDB() {
409  $fname = get_class( $this ) . '::' . __FUNCTION__;
410 
411  # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
412  $this->extraDataLoaded = true;
413 
414  $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getSlaveDB(), $fname );
415  if ( !$fieldMap ) {
416  $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getMasterDB(), $fname );
417  }
418 
419  if ( $fieldMap ) {
420  foreach ( $fieldMap as $name => $value ) {
421  $this->$name = $value;
422  }
423  } else {
424  throw new MWException( "Could not find data for image '{$this->getName()}'." );
425  }
426  }
427 
433  private function loadFieldsWithTimestamp( $dbr, $fname ) {
434  $fieldMap = false;
435 
436  $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ),
437  [ 'img_name' => $this->getName(), 'img_timestamp' => $this->getTimestamp() ],
438  $fname );
439  if ( $row ) {
440  $fieldMap = $this->unprefixRow( $row, 'img_' );
441  } else {
442  # File may have been uploaded over in the meantime; check the old versions
443  $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ),
444  [ 'oi_name' => $this->getName(), 'oi_timestamp' => $this->getTimestamp() ],
445  $fname );
446  if ( $row ) {
447  $fieldMap = $this->unprefixRow( $row, 'oi_' );
448  }
449  }
450 
451  return $fieldMap;
452  }
453 
460  protected function unprefixRow( $row, $prefix = 'img_' ) {
461  $array = (array)$row;
462  $prefixLength = strlen( $prefix );
463 
464  // Sanity check prefix once
465  if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
466  throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
467  }
468 
469  $decoded = [];
470  foreach ( $array as $name => $value ) {
471  $decoded[substr( $name, $prefixLength )] = $value;
472  }
473 
474  return $decoded;
475  }
476 
485  function decodeRow( $row, $prefix = 'img_' ) {
486  $decoded = $this->unprefixRow( $row, $prefix );
487 
488  $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
489 
490  $decoded['metadata'] = $this->repo->getSlaveDB()->decodeBlob( $decoded['metadata'] );
491 
492  if ( empty( $decoded['major_mime'] ) ) {
493  $decoded['mime'] = 'unknown/unknown';
494  } else {
495  if ( !$decoded['minor_mime'] ) {
496  $decoded['minor_mime'] = 'unknown';
497  }
498  $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
499  }
500 
501  // Trim zero padding from char/binary field
502  $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
503 
504  // Normalize some fields to integer type, per their database definition.
505  // Use unary + so that overflows will be upgraded to double instead of
506  // being trucated as with intval(). This is important to allow >2GB
507  // files on 32-bit systems.
508  foreach ( [ 'size', 'width', 'height', 'bits' ] as $field ) {
509  $decoded[$field] = +$decoded[$field];
510  }
511 
512  return $decoded;
513  }
514 
521  function loadFromRow( $row, $prefix = 'img_' ) {
522  $this->dataLoaded = true;
523  $this->extraDataLoaded = true;
524 
525  $array = $this->decodeRow( $row, $prefix );
526 
527  foreach ( $array as $name => $value ) {
528  $this->$name = $value;
529  }
530 
531  $this->fileExists = true;
532  $this->maybeUpgradeRow();
533  }
534 
539  function load( $flags = 0 ) {
540  if ( !$this->dataLoaded ) {
541  if ( ( $flags & self::READ_LATEST ) || !$this->loadFromCache() ) {
542  $this->loadFromDB( $flags );
543  $this->saveToCache();
544  }
545  $this->dataLoaded = true;
546  }
547  if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
548  // @note: loads on name/timestamp to reduce race condition problems
549  $this->loadExtraFromDB();
550  }
551  }
552 
556  function maybeUpgradeRow() {
558  if ( wfReadOnly() ) {
559  return;
560  }
561 
562  $upgrade = false;
563  if ( is_null( $this->media_type ) ||
564  $this->mime == 'image/svg'
565  ) {
566  $upgrade = true;
567  } else {
568  $handler = $this->getHandler();
569  if ( $handler ) {
570  $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
571  if ( $validity === MediaHandler::METADATA_BAD
572  || ( $validity === MediaHandler::METADATA_COMPATIBLE && $wgUpdateCompatibleMetadata )
573  ) {
574  $upgrade = true;
575  }
576  }
577  }
578 
579  if ( $upgrade ) {
580  try {
581  $this->upgradeRow();
582  } catch ( LocalFileLockError $e ) {
583  // let the other process handle it (or do it next time)
584  }
585  $this->upgraded = true; // avoid rework/retries
586  }
587  }
588 
589  function getUpgraded() {
590  return $this->upgraded;
591  }
592 
596  function upgradeRow() {
597  $this->lock(); // begin
598 
599  $this->loadFromFile();
600 
601  # Don't destroy file info of missing files
602  if ( !$this->fileExists ) {
603  $this->unlock();
604  wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
605 
606  return;
607  }
608 
609  $dbw = $this->repo->getMasterDB();
610  list( $major, $minor ) = self::splitMime( $this->mime );
611 
612  if ( wfReadOnly() ) {
613  $this->unlock();
614 
615  return;
616  }
617  wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
618 
619  $dbw->update( 'image',
620  [
621  'img_size' => $this->size, // sanity
622  'img_width' => $this->width,
623  'img_height' => $this->height,
624  'img_bits' => $this->bits,
625  'img_media_type' => $this->media_type,
626  'img_major_mime' => $major,
627  'img_minor_mime' => $minor,
628  'img_metadata' => $dbw->encodeBlob( $this->metadata ),
629  'img_sha1' => $this->sha1,
630  ],
631  [ 'img_name' => $this->getName() ],
632  __METHOD__
633  );
634 
635  $this->invalidateCache();
636 
637  $this->unlock(); // done
638 
639  }
640 
651  function setProps( $info ) {
652  $this->dataLoaded = true;
653  $fields = $this->getCacheFields( '' );
654  $fields[] = 'fileExists';
655 
656  foreach ( $fields as $field ) {
657  if ( isset( $info[$field] ) ) {
658  $this->$field = $info[$field];
659  }
660  }
661 
662  // Fix up mime fields
663  if ( isset( $info['major_mime'] ) ) {
664  $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
665  } elseif ( isset( $info['mime'] ) ) {
666  $this->mime = $info['mime'];
667  list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
668  }
669  }
670 
682  function isMissing() {
683  if ( $this->missing === null ) {
684  list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
685  $this->missing = !$fileExists;
686  }
687 
688  return $this->missing;
689  }
690 
697  public function getWidth( $page = 1 ) {
698  $this->load();
699 
700  if ( $this->isMultipage() ) {
701  $handler = $this->getHandler();
702  if ( !$handler ) {
703  return 0;
704  }
705  $dim = $handler->getPageDimensions( $this, $page );
706  if ( $dim ) {
707  return $dim['width'];
708  } else {
709  // For non-paged media, the false goes through an
710  // intval, turning failure into 0, so do same here.
711  return 0;
712  }
713  } else {
714  return $this->width;
715  }
716  }
717 
724  public function getHeight( $page = 1 ) {
725  $this->load();
726 
727  if ( $this->isMultipage() ) {
728  $handler = $this->getHandler();
729  if ( !$handler ) {
730  return 0;
731  }
732  $dim = $handler->getPageDimensions( $this, $page );
733  if ( $dim ) {
734  return $dim['height'];
735  } else {
736  // For non-paged media, the false goes through an
737  // intval, turning failure into 0, so do same here.
738  return 0;
739  }
740  } else {
741  return $this->height;
742  }
743  }
744 
751  function getUser( $type = 'text' ) {
752  $this->load();
753 
754  if ( $type == 'text' ) {
755  return $this->user_text;
756  } elseif ( $type == 'id' ) {
757  return (int)$this->user;
758  }
759  }
760 
768  public function getDescriptionShortUrl() {
769  $pageId = $this->title->getArticleID();
770 
771  if ( $pageId !== null ) {
772  $url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
773  if ( $url !== false ) {
774  return $url;
775  }
776  }
777  return null;
778  }
779 
784  function getMetadata() {
785  $this->load( self::LOAD_ALL ); // large metadata is loaded in another step
786  return $this->metadata;
787  }
788 
792  function getBitDepth() {
793  $this->load();
794 
795  return (int)$this->bits;
796  }
797 
802  public function getSize() {
803  $this->load();
804 
805  return $this->size;
806  }
807 
812  function getMimeType() {
813  $this->load();
814 
815  return $this->mime;
816  }
817 
823  function getMediaType() {
824  $this->load();
825 
826  return $this->media_type;
827  }
828 
839  public function exists() {
840  $this->load();
841 
842  return $this->fileExists;
843  }
844 
860  function getThumbnails( $archiveName = false ) {
861  if ( $archiveName ) {
862  $dir = $this->getArchiveThumbPath( $archiveName );
863  } else {
864  $dir = $this->getThumbPath();
865  }
866 
867  $backend = $this->repo->getBackend();
868  $files = [ $dir ];
869  try {
870  $iterator = $backend->getFileList( [ 'dir' => $dir ] );
871  foreach ( $iterator as $file ) {
872  $files[] = $file;
873  }
874  } catch ( FileBackendError $e ) {
875  } // suppress (bug 54674)
876 
877  return $files;
878  }
879 
883  function purgeMetadataCache() {
884  $this->invalidateCache();
885  }
886 
894  function purgeCache( $options = [] ) {
895  // Refresh metadata cache
896  $this->purgeMetadataCache();
897 
898  // Delete thumbnails
899  $this->purgeThumbnails( $options );
900 
901  // Purge CDN cache for this file
903  new CdnCacheUpdate( [ $this->getUrl() ] ),
905  );
906  }
907 
912  function purgeOldThumbnails( $archiveName ) {
913  // Get a list of old thumbnails and URLs
914  $files = $this->getThumbnails( $archiveName );
915 
916  // Purge any custom thumbnail caches
917  Hooks::run( 'LocalFilePurgeThumbnails', [ $this, $archiveName ] );
918 
919  // Delete thumbnails
920  $dir = array_shift( $files );
921  $this->purgeThumbList( $dir, $files );
922 
923  // Purge the CDN
924  $urls = [];
925  foreach ( $files as $file ) {
926  $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
927  }
929  }
930 
935  public function purgeThumbnails( $options = [] ) {
936  $files = $this->getThumbnails();
937  // Always purge all files from CDN regardless of handler filters
938  $urls = [];
939  foreach ( $files as $file ) {
940  $urls[] = $this->getThumbUrl( $file );
941  }
942  array_shift( $urls ); // don't purge directory
943 
944  // Give media handler a chance to filter the file purge list
945  if ( !empty( $options['forThumbRefresh'] ) ) {
946  $handler = $this->getHandler();
947  if ( $handler ) {
949  }
950  }
951 
952  // Purge any custom thumbnail caches
953  Hooks::run( 'LocalFilePurgeThumbnails', [ $this, false ] );
954 
955  // Delete thumbnails
956  $dir = array_shift( $files );
957  $this->purgeThumbList( $dir, $files );
958 
959  // Purge the CDN
961  }
962 
968  protected function purgeThumbList( $dir, $files ) {
969  $fileListDebug = strtr(
970  var_export( $files, true ),
971  [ "\n" => '' ]
972  );
973  wfDebug( __METHOD__ . ": $fileListDebug\n" );
974 
975  $purgeList = [];
976  foreach ( $files as $file ) {
977  # Check that the base file name is part of the thumb name
978  # This is a basic sanity check to avoid erasing unrelated directories
979  if ( strpos( $file, $this->getName() ) !== false
980  || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
981  ) {
982  $purgeList[] = "{$dir}/{$file}";
983  }
984  }
985 
986  # Delete the thumbnails
987  $this->repo->quickPurgeBatch( $purgeList );
988  # Clear out the thumbnail directory if empty
989  $this->repo->quickCleanDir( $dir );
990  }
991 
1002  function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1003  $dbr = $this->repo->getSlaveDB();
1004  $tables = [ 'oldimage' ];
1005  $fields = OldLocalFile::selectFields();
1006  $conds = $opts = $join_conds = [];
1007  $eq = $inc ? '=' : '';
1008  $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
1009 
1010  if ( $start ) {
1011  $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
1012  }
1013 
1014  if ( $end ) {
1015  $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
1016  }
1017 
1018  if ( $limit ) {
1019  $opts['LIMIT'] = $limit;
1020  }
1021 
1022  // Search backwards for time > x queries
1023  $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
1024  $opts['ORDER BY'] = "oi_timestamp $order";
1025  $opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1026 
1027  // Avoid PHP 7.1 warning from passing $this by reference
1028  $localFile = $this;
1029  Hooks::run( 'LocalFile::getHistory', [ &$localFile, &$tables, &$fields,
1030  &$conds, &$opts, &$join_conds ] );
1031 
1032  $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1033  $r = [];
1034 
1035  foreach ( $res as $row ) {
1036  $r[] = $this->repo->newFileFromRow( $row );
1037  }
1038 
1039  if ( $order == 'ASC' ) {
1040  $r = array_reverse( $r ); // make sure it ends up descending
1041  }
1042 
1043  return $r;
1044  }
1045 
1055  public function nextHistoryLine() {
1056  # Polymorphic function name to distinguish foreign and local fetches
1057  $fname = get_class( $this ) . '::' . __FUNCTION__;
1058 
1059  $dbr = $this->repo->getSlaveDB();
1060 
1061  if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1062  $this->historyRes = $dbr->select( 'image',
1063  [
1064  '*',
1065  "'' AS oi_archive_name",
1066  '0 as oi_deleted',
1067  'img_sha1'
1068  ],
1069  [ 'img_name' => $this->title->getDBkey() ],
1070  $fname
1071  );
1072 
1073  if ( 0 == $dbr->numRows( $this->historyRes ) ) {
1074  $this->historyRes = null;
1075 
1076  return false;
1077  }
1078  } elseif ( $this->historyLine == 1 ) {
1079  $this->historyRes = $dbr->select( 'oldimage', '*',
1080  [ 'oi_name' => $this->title->getDBkey() ],
1081  $fname,
1082  [ 'ORDER BY' => 'oi_timestamp DESC' ]
1083  );
1084  }
1085  $this->historyLine++;
1086 
1087  return $dbr->fetchObject( $this->historyRes );
1088  }
1089 
1093  public function resetHistory() {
1094  $this->historyLine = 0;
1095 
1096  if ( !is_null( $this->historyRes ) ) {
1097  $this->historyRes = null;
1098  }
1099  }
1100 
1131  function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1132  $timestamp = false, $user = null, $tags = []
1133  ) {
1135 
1136  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1137  return $this->readOnlyFatalStatus();
1138  }
1139 
1140  $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1141  if ( !$props ) {
1142  if ( $this->repo->isVirtualUrl( $srcPath )
1143  || FileBackend::isStoragePath( $srcPath )
1144  ) {
1145  $props = $this->repo->getFileProps( $srcPath );
1146  } else {
1147  $props = FSFile::getPropsFromPath( $srcPath );
1148  }
1149  }
1150 
1151  $options = [];
1152  $handler = MediaHandler::getHandler( $props['mime'] );
1153  if ( $handler ) {
1154  $options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
1155  } else {
1156  $options['headers'] = [];
1157  }
1158 
1159  // Trim spaces on user supplied text
1160  $comment = trim( $comment );
1161 
1162  // Truncate nicely or the DB will do it for us
1163  // non-nicely (dangling multi-byte chars, non-truncated version in cache).
1164  $comment = $wgContLang->truncate( $comment, 255 );
1165  $this->lock(); // begin
1166  $status = $this->publish( $src, $flags, $options );
1167 
1168  if ( $status->successCount >= 2 ) {
1169  // There will be a copy+(one of move,copy,store).
1170  // The first succeeding does not commit us to updating the DB
1171  // since it simply copied the current version to a timestamped file name.
1172  // It is only *preferable* to avoid leaving such files orphaned.
1173  // Once the second operation goes through, then the current version was
1174  // updated and we must therefore update the DB too.
1175  $oldver = $status->value;
1176  if ( !$this->recordUpload2( $oldver, $comment, $pageText, $props, $timestamp, $user, $tags ) ) {
1177  $status->fatal( 'filenotfound', $srcPath );
1178  }
1179  }
1180 
1181  $this->unlock(); // done
1182 
1183  return $status;
1184  }
1185 
1198  function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
1199  $watch = false, $timestamp = false, User $user = null ) {
1200  if ( !$user ) {
1201  global $wgUser;
1202  $user = $wgUser;
1203  }
1204 
1205  $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
1206 
1207  if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
1208  return false;
1209  }
1210 
1211  if ( $watch ) {
1212  $user->addWatch( $this->getTitle() );
1213  }
1214 
1215  return true;
1216  }
1217 
1229  function recordUpload2(
1230  $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
1231  ) {
1232  if ( is_null( $user ) ) {
1233  global $wgUser;
1234  $user = $wgUser;
1235  }
1236 
1237  $dbw = $this->repo->getMasterDB();
1238 
1239  # Imports or such might force a certain timestamp; otherwise we generate
1240  # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1241  if ( $timestamp === false ) {
1242  $timestamp = $dbw->timestamp();
1243  $allowTimeKludge = true;
1244  } else {
1245  $allowTimeKludge = false;
1246  }
1247 
1248  $props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1249  $props['description'] = $comment;
1250  $props['user'] = $user->getId();
1251  $props['user_text'] = $user->getName();
1252  $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1253  $this->setProps( $props );
1254 
1255  # Fail now if the file isn't there
1256  if ( !$this->fileExists ) {
1257  wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
1258 
1259  return false;
1260  }
1261 
1262  $dbw->startAtomic( __METHOD__ );
1263 
1264  # Test to see if the row exists using INSERT IGNORE
1265  # This avoids race conditions by locking the row until the commit, and also
1266  # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1267  $dbw->insert( 'image',
1268  [
1269  'img_name' => $this->getName(),
1270  'img_size' => $this->size,
1271  'img_width' => intval( $this->width ),
1272  'img_height' => intval( $this->height ),
1273  'img_bits' => $this->bits,
1274  'img_media_type' => $this->media_type,
1275  'img_major_mime' => $this->major_mime,
1276  'img_minor_mime' => $this->minor_mime,
1277  'img_timestamp' => $timestamp,
1278  'img_description' => $comment,
1279  'img_user' => $user->getId(),
1280  'img_user_text' => $user->getName(),
1281  'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1282  'img_sha1' => $this->sha1
1283  ],
1284  __METHOD__,
1285  'IGNORE'
1286  );
1287 
1288  $reupload = ( $dbw->affectedRows() == 0 );
1289  if ( $reupload ) {
1290  if ( $allowTimeKludge ) {
1291  # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1292  $ltimestamp = $dbw->selectField(
1293  'image',
1294  'img_timestamp',
1295  [ 'img_name' => $this->getName() ],
1296  __METHOD__,
1297  [ 'LOCK IN SHARE MODE' ]
1298  );
1299  $lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
1300  # Avoid a timestamp that is not newer than the last version
1301  # TODO: the image/oldimage tables should be like page/revision with an ID field
1302  if ( $lUnixtime && wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
1303  sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1304  $timestamp = $dbw->timestamp( $lUnixtime + 1 );
1305  $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1306  }
1307  }
1308 
1309  # (bug 34993) Note: $oldver can be empty here, if the previous
1310  # version of the file was broken. Allow registration of the new
1311  # version to continue anyway, because that's better than having
1312  # an image that's not fixable by user operations.
1313  # Collision, this is an update of a file
1314  # Insert previous contents into oldimage
1315  $dbw->insertSelect( 'oldimage', 'image',
1316  [
1317  'oi_name' => 'img_name',
1318  'oi_archive_name' => $dbw->addQuotes( $oldver ),
1319  'oi_size' => 'img_size',
1320  'oi_width' => 'img_width',
1321  'oi_height' => 'img_height',
1322  'oi_bits' => 'img_bits',
1323  'oi_timestamp' => 'img_timestamp',
1324  'oi_description' => 'img_description',
1325  'oi_user' => 'img_user',
1326  'oi_user_text' => 'img_user_text',
1327  'oi_metadata' => 'img_metadata',
1328  'oi_media_type' => 'img_media_type',
1329  'oi_major_mime' => 'img_major_mime',
1330  'oi_minor_mime' => 'img_minor_mime',
1331  'oi_sha1' => 'img_sha1'
1332  ],
1333  [ 'img_name' => $this->getName() ],
1334  __METHOD__
1335  );
1336 
1337  # Update the current image row
1338  $dbw->update( 'image',
1339  [
1340  'img_size' => $this->size,
1341  'img_width' => intval( $this->width ),
1342  'img_height' => intval( $this->height ),
1343  'img_bits' => $this->bits,
1344  'img_media_type' => $this->media_type,
1345  'img_major_mime' => $this->major_mime,
1346  'img_minor_mime' => $this->minor_mime,
1347  'img_timestamp' => $timestamp,
1348  'img_description' => $comment,
1349  'img_user' => $user->getId(),
1350  'img_user_text' => $user->getName(),
1351  'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1352  'img_sha1' => $this->sha1
1353  ],
1354  [ 'img_name' => $this->getName() ],
1355  __METHOD__
1356  );
1357  }
1358 
1359  $descTitle = $this->getTitle();
1360  $descId = $descTitle->getArticleID();
1361  $wikiPage = new WikiFilePage( $descTitle );
1362  $wikiPage->setFile( $this );
1363 
1364  // Add the log entry...
1365  $logEntry = new ManualLogEntry( 'upload', $reupload ? 'overwrite' : 'upload' );
1366  $logEntry->setTimestamp( $this->timestamp );
1367  $logEntry->setPerformer( $user );
1368  $logEntry->setComment( $comment );
1369  $logEntry->setTarget( $descTitle );
1370  // Allow people using the api to associate log entries with the upload.
1371  // Log has a timestamp, but sometimes different from upload timestamp.
1372  $logEntry->setParameters(
1373  [
1374  'img_sha1' => $this->sha1,
1375  'img_timestamp' => $timestamp,
1376  ]
1377  );
1378  // Note we keep $logId around since during new image
1379  // creation, page doesn't exist yet, so log_page = 0
1380  // but we want it to point to the page we're making,
1381  // so we later modify the log entry.
1382  // For a similar reason, we avoid making an RC entry
1383  // now and wait until the page exists.
1384  $logId = $logEntry->insert();
1385 
1386  if ( $descTitle->exists() ) {
1387  // Use own context to get the action text in content language
1388  $formatter = LogFormatter::newFromEntry( $logEntry );
1389  $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1390  $editSummary = $formatter->getPlainActionText();
1391 
1392  $nullRevision = Revision::newNullRevision(
1393  $dbw,
1394  $descId,
1395  $editSummary,
1396  false,
1397  $user
1398  );
1399  if ( $nullRevision ) {
1400  $nullRevision->insertOn( $dbw );
1401  Hooks::run(
1402  'NewRevisionFromEditComplete',
1403  [ $wikiPage, $nullRevision, $nullRevision->getParentId(), $user ]
1404  );
1405  $wikiPage->updateRevisionOn( $dbw, $nullRevision );
1406  // Associate null revision id
1407  $logEntry->setAssociatedRevId( $nullRevision->getId() );
1408  }
1409 
1410  $newPageContent = null;
1411  } else {
1412  // Make the description page and RC log entry post-commit
1413  $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1414  }
1415 
1416  # Defer purges, page creation, and link updates in case they error out.
1417  # The most important thing is that files and the DB registry stay synced.
1418  $dbw->endAtomic( __METHOD__ );
1419 
1420  # Do some cache purges after final commit so that:
1421  # a) Changes are more likely to be seen post-purge
1422  # b) They won't cause rollback of the log publish/update above
1423  $that = $this;
1424  $dbw->onTransactionIdle( function () use (
1425  $that, $reupload, $wikiPage, $newPageContent, $comment, $user, $logEntry, $logId, $descId, $tags
1426  ) {
1427  # Update memcache after the commit
1428  $that->invalidateCache();
1429 
1430  $updateLogPage = false;
1431  if ( $newPageContent ) {
1432  # New file page; create the description page.
1433  # There's already a log entry, so don't make a second RC entry
1434  # CDN and file cache for the description page are purged by doEditContent.
1435  $status = $wikiPage->doEditContent(
1436  $newPageContent,
1437  $comment,
1439  false,
1440  $user
1441  );
1442 
1443  if ( isset( $status->value['revision'] ) ) {
1444  // Associate new page revision id
1445  $logEntry->setAssociatedRevId( $status->value['revision']->getId() );
1446  }
1447  // This relies on the resetArticleID() call in WikiPage::insertOn(),
1448  // which is triggered on $descTitle by doEditContent() above.
1449  if ( isset( $status->value['revision'] ) ) {
1451  $rev = $status->value['revision'];
1452  $updateLogPage = $rev->getPage();
1453  }
1454  } else {
1455  # Existing file page: invalidate description page cache
1456  $wikiPage->getTitle()->invalidateCache();
1457  $wikiPage->getTitle()->purgeSquid();
1458  # Allow the new file version to be patrolled from the page footer
1460  }
1461 
1462  # Update associated rev id. This should be done by $logEntry->insert() earlier,
1463  # but setAssociatedRevId() wasn't called at that point yet...
1464  $logParams = $logEntry->getParameters();
1465  $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
1466  $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
1467  if ( $updateLogPage ) {
1468  # Also log page, in case where we just created it above
1469  $update['log_page'] = $updateLogPage;
1470  }
1471  $that->getRepo()->getMasterDB()->update(
1472  'logging',
1473  $update,
1474  [ 'log_id' => $logId ],
1475  __METHOD__
1476  );
1477  $that->getRepo()->getMasterDB()->insert(
1478  'log_search',
1479  [
1480  'ls_field' => 'associated_rev_id',
1481  'ls_value' => $logEntry->getAssociatedRevId(),
1482  'ls_log_id' => $logId,
1483  ],
1484  __METHOD__
1485  );
1486 
1487  # Add change tags, if any
1488  if ( $tags ) {
1489  $logEntry->setTags( $tags );
1490  }
1491 
1492  # Uploads can be patrolled
1493  $logEntry->setIsPatrollable( true );
1494 
1495  # Now that the log entry is up-to-date, make an RC entry.
1496  $logEntry->publish( $logId );
1497 
1498  # Run hook for other updates (typically more cache purging)
1499  Hooks::run( 'FileUpload', [ $that, $reupload, !$newPageContent ] );
1500 
1501  if ( $reupload ) {
1502  # Delete old thumbnails
1503  $that->purgeThumbnails();
1504  # Remove the old file from the CDN cache
1506  new CdnCacheUpdate( [ $that->getUrl() ] ),
1508  );
1509  } else {
1510  # Update backlink pages pointing to this title if created
1511  LinksUpdate::queueRecursiveJobsForTable( $that->getTitle(), 'imagelinks' );
1512  }
1513  } );
1514 
1515  if ( !$reupload ) {
1516  # This is a new file, so update the image count
1517  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
1518  }
1519 
1520  # Invalidate cache for all pages using this file
1521  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ) );
1522 
1523  return true;
1524  }
1525 
1541  function publish( $src, $flags = 0, array $options = [] ) {
1542  return $this->publishTo( $src, $this->getRel(), $flags, $options );
1543  }
1544 
1560  function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
1561  $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1562 
1563  $repo = $this->getRepo();
1564  if ( $repo->getReadOnlyReason() !== false ) {
1565  return $this->readOnlyFatalStatus();
1566  }
1567 
1568  $this->lock(); // begin
1569 
1570  $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
1571  $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
1572 
1573  if ( $repo->hasSha1Storage() ) {
1574  $sha1 = $repo->isVirtualUrl( $srcPath )
1575  ? $repo->getFileSha1( $srcPath )
1576  : FSFile::getSha1Base36FromPath( $srcPath );
1577  $dst = $repo->getBackend()->getPathForSHA1( $sha1 );
1578  $status = $repo->quickImport( $src, $dst );
1579  if ( $flags & File::DELETE_SOURCE ) {
1580  unlink( $srcPath );
1581  }
1582 
1583  if ( $this->exists() ) {
1584  $status->value = $archiveName;
1585  }
1586  } else {
1588  $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
1589 
1590  if ( $status->value == 'new' ) {
1591  $status->value = '';
1592  } else {
1593  $status->value = $archiveName;
1594  }
1595  }
1596 
1597  $this->unlock(); // done
1598 
1599  return $status;
1600  }
1601 
1619  function move( $target ) {
1620  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1621  return $this->readOnlyFatalStatus();
1622  }
1623 
1624  wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1625  $batch = new LocalFileMoveBatch( $this, $target );
1626 
1627  $this->lock(); // begin
1628  $batch->addCurrent();
1629  $archiveNames = $batch->addOlds();
1630  $status = $batch->execute();
1631  $this->unlock(); // done
1632 
1633  wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1634 
1635  // Purge the source and target files...
1636  $oldTitleFile = wfLocalFile( $this->title );
1637  $newTitleFile = wfLocalFile( $target );
1638  // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
1639  // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
1640  $this->getRepo()->getMasterDB()->onTransactionIdle(
1641  function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
1642  $oldTitleFile->purgeEverything();
1643  foreach ( $archiveNames as $archiveName ) {
1644  $oldTitleFile->purgeOldThumbnails( $archiveName );
1645  }
1646  $newTitleFile->purgeEverything();
1647  }
1648  );
1649 
1650  if ( $status->isOK() ) {
1651  // Now switch the object
1652  $this->title = $target;
1653  // Force regeneration of the name and hashpath
1654  unset( $this->name );
1655  unset( $this->hashPath );
1656  }
1657 
1658  return $status;
1659  }
1660 
1674  function delete( $reason, $suppress = false, $user = null ) {
1675  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1676  return $this->readOnlyFatalStatus();
1677  }
1678 
1679  $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1680 
1681  $this->lock(); // begin
1682  $batch->addCurrent();
1683  # Get old version relative paths
1684  $archiveNames = $batch->addOlds();
1685  $status = $batch->execute();
1686  $this->unlock(); // done
1687 
1688  if ( $status->isOK() ) {
1689  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
1690  }
1691 
1692  // Hack: the lock()/unlock() pair is nested in a transaction so the locking is not
1693  // tied to BEGIN/COMMIT. To avoid slow purges in the transaction, move them outside.
1694  $that = $this;
1695  $this->getRepo()->getMasterDB()->onTransactionIdle(
1696  function () use ( $that, $archiveNames ) {
1697  $that->purgeEverything();
1698  foreach ( $archiveNames as $archiveName ) {
1699  $that->purgeOldThumbnails( $archiveName );
1700  }
1701  }
1702  );
1703 
1704  // Purge the CDN
1705  $purgeUrls = [];
1706  foreach ( $archiveNames as $archiveName ) {
1707  $purgeUrls[] = $this->getArchiveUrl( $archiveName );
1708  }
1710 
1711  return $status;
1712  }
1713 
1729  function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
1730  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1731  return $this->readOnlyFatalStatus();
1732  }
1733 
1734  $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1735 
1736  $this->lock(); // begin
1737  $batch->addOld( $archiveName );
1738  $status = $batch->execute();
1739  $this->unlock(); // done
1740 
1741  $this->purgeOldThumbnails( $archiveName );
1742  if ( $status->isOK() ) {
1743  $this->purgeDescription();
1744  }
1745 
1747  new CdnCacheUpdate( [ $this->getArchiveUrl( $archiveName ) ] ),
1749  );
1750 
1751  return $status;
1752  }
1753 
1765  function restore( $versions = [], $unsuppress = false ) {
1766  if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1767  return $this->readOnlyFatalStatus();
1768  }
1769 
1770  $batch = new LocalFileRestoreBatch( $this, $unsuppress );
1771 
1772  $this->lock(); // begin
1773  if ( !$versions ) {
1774  $batch->addAll();
1775  } else {
1776  $batch->addIds( $versions );
1777  }
1778  $status = $batch->execute();
1779  if ( $status->isGood() ) {
1780  $cleanupStatus = $batch->cleanup();
1781  $cleanupStatus->successCount = 0;
1782  $cleanupStatus->failCount = 0;
1783  $status->merge( $cleanupStatus );
1784  }
1785  $this->unlock(); // done
1786 
1787  return $status;
1788  }
1789 
1799  function getDescriptionUrl() {
1800  return $this->title->getLocalURL();
1801  }
1802 
1811  function getDescriptionText( $lang = null ) {
1812  $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
1813  if ( !$revision ) {
1814  return false;
1815  }
1816  $content = $revision->getContent();
1817  if ( !$content ) {
1818  return false;
1819  }
1820  $pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
1821 
1822  return $pout->getText();
1823  }
1824 
1830  function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
1831  $this->load();
1832  if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
1833  return '';
1834  } elseif ( $audience == self::FOR_THIS_USER
1835  && !$this->userCan( self::DELETED_COMMENT, $user )
1836  ) {
1837  return '';
1838  } else {
1839  return $this->description;
1840  }
1841  }
1842 
1846  function getTimestamp() {
1847  $this->load();
1848 
1849  return $this->timestamp;
1850  }
1851 
1855  public function getDescriptionTouched() {
1856  // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
1857  // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
1858  // need to differentiate between null (uninitialized) and false (failed to load).
1859  if ( $this->descriptionTouched === null ) {
1860  $cond = [
1861  'page_namespace' => $this->title->getNamespace(),
1862  'page_title' => $this->title->getDBkey()
1863  ];
1864  $touched = $this->repo->getSlaveDB()->selectField( 'page', 'page_touched', $cond, __METHOD__ );
1865  $this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
1866  }
1867 
1869  }
1870 
1874  function getSha1() {
1875  $this->load();
1876  // Initialise now if necessary
1877  if ( $this->sha1 == '' && $this->fileExists ) {
1878  $this->lock(); // begin
1879 
1880  $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
1881  if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1882  $dbw = $this->repo->getMasterDB();
1883  $dbw->update( 'image',
1884  [ 'img_sha1' => $this->sha1 ],
1885  [ 'img_name' => $this->getName() ],
1886  __METHOD__ );
1887  $this->invalidateCache();
1888  }
1889 
1890  $this->unlock(); // done
1891  }
1892 
1893  return $this->sha1;
1894  }
1895 
1899  function isCacheable() {
1900  $this->load();
1901 
1902  // If extra data (metadata) was not loaded then it must have been large
1903  return $this->extraDataLoaded
1904  && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
1905  }
1906 
1913  function lock() {
1914  if ( !$this->locked ) {
1915  $dbw = $this->repo->getMasterDB();
1916  if ( !$dbw->trxLevel() ) {
1917  $dbw->begin( __METHOD__ );
1918  $this->lockedOwnTrx = true;
1919  }
1920  // Bug 54736: use simple lock to handle when the file does not exist.
1921  // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
1922  // Also, that would cause contention on INSERT of similarly named rows.
1923  $backend = $this->getRepo()->getBackend();
1924  $lockPaths = [ $this->getPath() ]; // represents all versions of the file
1925  $status = $backend->lockFiles( $lockPaths, LockManager::LOCK_EX, 5 );
1926  if ( !$status->isGood() ) {
1927  if ( $this->lockedOwnTrx ) {
1928  $dbw->rollback( __METHOD__ );
1929  }
1930  throw new LocalFileLockError( "Could not acquire lock for '{$this->getName()}.'" );
1931  }
1932  // Release the lock *after* commit to avoid row-level contention
1933  $this->locked++;
1934  $dbw->onTransactionIdle( function () use ( $backend, $lockPaths ) {
1935  $backend->unlockFiles( $lockPaths, LockManager::LOCK_EX );
1936  } );
1937  }
1938 
1939  return $this->lockedOwnTrx;
1940  }
1941 
1946  function unlock() {
1947  if ( $this->locked ) {
1948  --$this->locked;
1949  if ( !$this->locked && $this->lockedOwnTrx ) {
1950  $dbw = $this->repo->getMasterDB();
1951  $dbw->commit( __METHOD__ );
1952  $this->lockedOwnTrx = false;
1953  }
1954  }
1955  }
1956 
1960  function unlockAndRollback() {
1961  $this->locked = false;
1962  $dbw = $this->repo->getMasterDB();
1963  $dbw->rollback( __METHOD__ );
1964  $this->lockedOwnTrx = false;
1965  }
1966 
1970  protected function readOnlyFatalStatus() {
1971  return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
1972  $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
1973  }
1974 
1978  function __destruct() {
1979  $this->unlock();
1980  }
1981 } // LocalFile class
1982 
1983 # ------------------------------------------------------------------------------
1984 
1991  private $file;
1992 
1994  private $reason;
1995 
1997  private $srcRels = [];
1998 
2000  private $archiveUrls = [];
2001 
2004 
2006  private $suppress;
2007 
2009  private $status;
2010 
2012  private $user;
2013 
2020  function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
2021  $this->file = $file;
2022  $this->reason = $reason;
2023  $this->suppress = $suppress;
2024  if ( $user ) {
2025  $this->user = $user;
2026  } else {
2027  global $wgUser;
2028  $this->user = $wgUser;
2029  }
2030  $this->status = $file->repo->newGood();
2031  }
2032 
2033  public function addCurrent() {
2034  $this->srcRels['.'] = $this->file->getRel();
2035  }
2036 
2040  public function addOld( $oldName ) {
2041  $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
2042  $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
2043  }
2044 
2049  public function addOlds() {
2050  $archiveNames = [];
2051 
2052  $dbw = $this->file->repo->getMasterDB();
2053  $result = $dbw->select( 'oldimage',
2054  [ 'oi_archive_name' ],
2055  [ 'oi_name' => $this->file->getName() ],
2056  __METHOD__
2057  );
2058 
2059  foreach ( $result as $row ) {
2060  $this->addOld( $row->oi_archive_name );
2061  $archiveNames[] = $row->oi_archive_name;
2062  }
2063 
2064  return $archiveNames;
2065  }
2066 
2070  protected function getOldRels() {
2071  if ( !isset( $this->srcRels['.'] ) ) {
2072  $oldRels =& $this->srcRels;
2073  $deleteCurrent = false;
2074  } else {
2075  $oldRels = $this->srcRels;
2076  unset( $oldRels['.'] );
2077  $deleteCurrent = true;
2078  }
2079 
2080  return [ $oldRels, $deleteCurrent ];
2081  }
2082 
2086  protected function getHashes() {
2087  $hashes = [];
2088  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2089 
2090  if ( $deleteCurrent ) {
2091  $hashes['.'] = $this->file->getSha1();
2092  }
2093 
2094  if ( count( $oldRels ) ) {
2095  $dbw = $this->file->repo->getMasterDB();
2096  $res = $dbw->select(
2097  'oldimage',
2098  [ 'oi_archive_name', 'oi_sha1' ],
2099  [ 'oi_archive_name' => array_keys( $oldRels ),
2100  'oi_name' => $this->file->getName() ], // performance
2101  __METHOD__
2102  );
2103 
2104  foreach ( $res as $row ) {
2105  if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
2106  // Get the hash from the file
2107  $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
2108  $props = $this->file->repo->getFileProps( $oldUrl );
2109 
2110  if ( $props['fileExists'] ) {
2111  // Upgrade the oldimage row
2112  $dbw->update( 'oldimage',
2113  [ 'oi_sha1' => $props['sha1'] ],
2114  [ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
2115  __METHOD__ );
2116  $hashes[$row->oi_archive_name] = $props['sha1'];
2117  } else {
2118  $hashes[$row->oi_archive_name] = false;
2119  }
2120  } else {
2121  $hashes[$row->oi_archive_name] = $row->oi_sha1;
2122  }
2123  }
2124  }
2125 
2126  $missing = array_diff_key( $this->srcRels, $hashes );
2127 
2128  foreach ( $missing as $name => $rel ) {
2129  $this->status->error( 'filedelete-old-unregistered', $name );
2130  }
2131 
2132  foreach ( $hashes as $name => $hash ) {
2133  if ( !$hash ) {
2134  $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
2135  unset( $hashes[$name] );
2136  }
2137  }
2138 
2139  return $hashes;
2140  }
2141 
2142  protected function doDBInserts() {
2143  $dbw = $this->file->repo->getMasterDB();
2144  $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
2145  $encUserId = $dbw->addQuotes( $this->user->getId() );
2146  $encReason = $dbw->addQuotes( $this->reason );
2147  $encGroup = $dbw->addQuotes( 'deleted' );
2148  $ext = $this->file->getExtension();
2149  $dotExt = $ext === '' ? '' : ".$ext";
2150  $encExt = $dbw->addQuotes( $dotExt );
2151  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2152 
2153  // Bitfields to further suppress the content
2154  if ( $this->suppress ) {
2155  $bitfield = 0;
2156  // This should be 15...
2157  $bitfield |= Revision::DELETED_TEXT;
2158  $bitfield |= Revision::DELETED_COMMENT;
2159  $bitfield |= Revision::DELETED_USER;
2160  $bitfield |= Revision::DELETED_RESTRICTED;
2161  } else {
2162  $bitfield = 'oi_deleted';
2163  }
2164 
2165  if ( $deleteCurrent ) {
2166  $concat = $dbw->buildConcat( [ "img_sha1", $encExt ] );
2167  $where = [ 'img_name' => $this->file->getName() ];
2168  $dbw->insertSelect( 'filearchive', 'image',
2169  [
2170  'fa_storage_group' => $encGroup,
2171  'fa_storage_key' => $dbw->conditional(
2172  [ 'img_sha1' => '' ],
2173  $dbw->addQuotes( '' ),
2174  $concat
2175  ),
2176  'fa_deleted_user' => $encUserId,
2177  'fa_deleted_timestamp' => $encTimestamp,
2178  'fa_deleted_reason' => $encReason,
2179  'fa_deleted' => $this->suppress ? $bitfield : 0,
2180 
2181  'fa_name' => 'img_name',
2182  'fa_archive_name' => 'NULL',
2183  'fa_size' => 'img_size',
2184  'fa_width' => 'img_width',
2185  'fa_height' => 'img_height',
2186  'fa_metadata' => 'img_metadata',
2187  'fa_bits' => 'img_bits',
2188  'fa_media_type' => 'img_media_type',
2189  'fa_major_mime' => 'img_major_mime',
2190  'fa_minor_mime' => 'img_minor_mime',
2191  'fa_description' => 'img_description',
2192  'fa_user' => 'img_user',
2193  'fa_user_text' => 'img_user_text',
2194  'fa_timestamp' => 'img_timestamp',
2195  'fa_sha1' => 'img_sha1',
2196  ], $where, __METHOD__ );
2197  }
2198 
2199  if ( count( $oldRels ) ) {
2200  $concat = $dbw->buildConcat( [ "oi_sha1", $encExt ] );
2201  $where = [
2202  'oi_name' => $this->file->getName(),
2203  'oi_archive_name' => array_keys( $oldRels ) ];
2204  $dbw->insertSelect( 'filearchive', 'oldimage',
2205  [
2206  'fa_storage_group' => $encGroup,
2207  'fa_storage_key' => $dbw->conditional(
2208  [ 'oi_sha1' => '' ],
2209  $dbw->addQuotes( '' ),
2210  $concat
2211  ),
2212  'fa_deleted_user' => $encUserId,
2213  'fa_deleted_timestamp' => $encTimestamp,
2214  'fa_deleted_reason' => $encReason,
2215  'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
2216 
2217  'fa_name' => 'oi_name',
2218  'fa_archive_name' => 'oi_archive_name',
2219  'fa_size' => 'oi_size',
2220  'fa_width' => 'oi_width',
2221  'fa_height' => 'oi_height',
2222  'fa_metadata' => 'oi_metadata',
2223  'fa_bits' => 'oi_bits',
2224  'fa_media_type' => 'oi_media_type',
2225  'fa_major_mime' => 'oi_major_mime',
2226  'fa_minor_mime' => 'oi_minor_mime',
2227  'fa_description' => 'oi_description',
2228  'fa_user' => 'oi_user',
2229  'fa_user_text' => 'oi_user_text',
2230  'fa_timestamp' => 'oi_timestamp',
2231  'fa_sha1' => 'oi_sha1',
2232  ], $where, __METHOD__ );
2233  }
2234  }
2235 
2236  function doDBDeletes() {
2237  $dbw = $this->file->repo->getMasterDB();
2238  list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2239 
2240  if ( count( $oldRels ) ) {
2241  $dbw->delete( 'oldimage',
2242  [
2243  'oi_name' => $this->file->getName(),
2244  'oi_archive_name' => array_keys( $oldRels )
2245  ], __METHOD__ );
2246  }
2247 
2248  if ( $deleteCurrent ) {
2249  $dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
2250  }
2251  }
2252 
2257  public function execute() {
2258  $repo = $this->file->getRepo();
2259  $this->file->lock();
2260 
2261  // Prepare deletion batch
2262  $hashes = $this->getHashes();
2263  $this->deletionBatch = [];
2264  $ext = $this->file->getExtension();
2265  $dotExt = $ext === '' ? '' : ".$ext";
2266 
2267  foreach ( $this->srcRels as $name => $srcRel ) {
2268  // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
2269  if ( isset( $hashes[$name] ) ) {
2270  $hash = $hashes[$name];
2271  $key = $hash . $dotExt;
2272  $dstRel = $repo->getDeletedHashPath( $key ) . $key;
2273  $this->deletionBatch[$name] = [ $srcRel, $dstRel ];
2274  }
2275  }
2276 
2277  // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
2278  // We acquire this lock by running the inserts now, before the file operations.
2279  // This potentially has poor lock contention characteristics -- an alternative
2280  // scheme would be to insert stub filearchive entries with no fa_name and commit
2281  // them in a separate transaction, then run the file ops, then update the fa_name fields.
2282  $this->doDBInserts();
2283 
2284  if ( !$repo->hasSha1Storage() ) {
2285  // Removes non-existent file from the batch, so we don't get errors.
2286  // This also handles files in the 'deleted' zone deleted via revision deletion.
2287  $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
2288  if ( !$checkStatus->isGood() ) {
2289  $this->status->merge( $checkStatus );
2290  return $this->status;
2291  }
2292  $this->deletionBatch = $checkStatus->value;
2293 
2294  // Execute the file deletion batch
2295  $status = $this->file->repo->deleteBatch( $this->deletionBatch );
2296 
2297  if ( !$status->isGood() ) {
2298  $this->status->merge( $status );
2299  }
2300  }
2301 
2302  if ( !$this->status->isOK() ) {
2303  // Critical file deletion error
2304  // Roll back inserts, release lock and abort
2305  // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
2306  $this->file->unlockAndRollback();
2307 
2308  return $this->status;
2309  }
2310 
2311  // Delete image/oldimage rows
2312  $this->doDBDeletes();
2313 
2314  // Commit and return
2315  $this->file->unlock();
2316 
2317  return $this->status;
2318  }
2319 
2325  protected function removeNonexistentFiles( $batch ) {
2326  $files = $newBatch = [];
2327 
2328  foreach ( $batch as $batchItem ) {
2329  list( $src, ) = $batchItem;
2330  $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
2331  }
2332 
2333  $result = $this->file->repo->fileExistsBatch( $files );
2334  if ( in_array( null, $result, true ) ) {
2335  return Status::newFatal( 'backend-fail-internal',
2336  $this->file->repo->getBackend()->getName() );
2337  }
2338 
2339  foreach ( $batch as $batchItem ) {
2340  if ( $result[$batchItem[0]] ) {
2341  $newBatch[] = $batchItem;
2342  }
2343  }
2344 
2345  return Status::newGood( $newBatch );
2346  }
2347 }
2348 
2349 # ------------------------------------------------------------------------------
2350 
2357  private $file;
2358 
2360  private $cleanupBatch;
2361 
2363  private $ids;
2364 
2366  private $all;
2367 
2369  private $unsuppress = false;
2370 
2375  function __construct( File $file, $unsuppress = false ) {
2376  $this->file = $file;
2377  $this->cleanupBatch = $this->ids = [];
2378  $this->ids = [];
2379  $this->unsuppress = $unsuppress;
2380  }
2381 
2386  public function addId( $fa_id ) {
2387  $this->ids[] = $fa_id;
2388  }
2389 
2394  public function addIds( $ids ) {
2395  $this->ids = array_merge( $this->ids, $ids );
2396  }
2397 
2401  public function addAll() {
2402  $this->all = true;
2403  }
2404 
2413  public function execute() {
2414  global $wgLang;
2415 
2416  $repo = $this->file->getRepo();
2417  if ( !$this->all && !$this->ids ) {
2418  // Do nothing
2419  return $repo->newGood();
2420  }
2421 
2422  $lockOwnsTrx = $this->file->lock();
2423 
2424  $dbw = $this->file->repo->getMasterDB();
2425  $status = $this->file->repo->newGood();
2426 
2427  $exists = (bool)$dbw->selectField( 'image', '1',
2428  [ 'img_name' => $this->file->getName() ],
2429  __METHOD__,
2430  // The lock() should already prevents changes, but this still may need
2431  // to bypass any transaction snapshot. However, if lock() started the
2432  // trx (which it probably did) then snapshot is post-lock and up-to-date.
2433  $lockOwnsTrx ? [] : [ 'LOCK IN SHARE MODE' ]
2434  );
2435 
2436  // Fetch all or selected archived revisions for the file,
2437  // sorted from the most recent to the oldest.
2438  $conditions = [ 'fa_name' => $this->file->getName() ];
2439 
2440  if ( !$this->all ) {
2441  $conditions['fa_id'] = $this->ids;
2442  }
2443 
2444  $result = $dbw->select(
2445  'filearchive',
2447  $conditions,
2448  __METHOD__,
2449  [ 'ORDER BY' => 'fa_timestamp DESC' ]
2450  );
2451 
2452  $idsPresent = [];
2453  $storeBatch = [];
2454  $insertBatch = [];
2455  $insertCurrent = false;
2456  $deleteIds = [];
2457  $first = true;
2458  $archiveNames = [];
2459 
2460  foreach ( $result as $row ) {
2461  $idsPresent[] = $row->fa_id;
2462 
2463  if ( $row->fa_name != $this->file->getName() ) {
2464  $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
2465  $status->failCount++;
2466  continue;
2467  }
2468 
2469  if ( $row->fa_storage_key == '' ) {
2470  // Revision was missing pre-deletion
2471  $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
2472  $status->failCount++;
2473  continue;
2474  }
2475 
2476  $deletedRel = $repo->getDeletedHashPath( $row->fa_storage_key ) .
2477  $row->fa_storage_key;
2478  $deletedUrl = $repo->getVirtualUrl() . '/deleted/' . $deletedRel;
2479 
2480  if ( isset( $row->fa_sha1 ) ) {
2481  $sha1 = $row->fa_sha1;
2482  } else {
2483  // old row, populate from key
2484  $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
2485  }
2486 
2487  # Fix leading zero
2488  if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
2489  $sha1 = substr( $sha1, 1 );
2490  }
2491 
2492  if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
2493  || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
2494  || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
2495  || is_null( $row->fa_metadata )
2496  ) {
2497  // Refresh our metadata
2498  // Required for a new current revision; nice for older ones too. :)
2499  $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
2500  } else {
2501  $props = [
2502  'minor_mime' => $row->fa_minor_mime,
2503  'major_mime' => $row->fa_major_mime,
2504  'media_type' => $row->fa_media_type,
2505  'metadata' => $row->fa_metadata
2506  ];
2507  }
2508 
2509  if ( $first && !$exists ) {
2510  // This revision will be published as the new current version
2511  $destRel = $this->file->getRel();
2512  $insertCurrent = [
2513  'img_name' => $row->fa_name,
2514  'img_size' => $row->fa_size,
2515  'img_width' => $row->fa_width,
2516  'img_height' => $row->fa_height,
2517  'img_metadata' => $props['metadata'],
2518  'img_bits' => $row->fa_bits,
2519  'img_media_type' => $props['media_type'],
2520  'img_major_mime' => $props['major_mime'],
2521  'img_minor_mime' => $props['minor_mime'],
2522  'img_description' => $row->fa_description,
2523  'img_user' => $row->fa_user,
2524  'img_user_text' => $row->fa_user_text,
2525  'img_timestamp' => $row->fa_timestamp,
2526  'img_sha1' => $sha1
2527  ];
2528 
2529  // The live (current) version cannot be hidden!
2530  if ( !$this->unsuppress && $row->fa_deleted ) {
2531  $status->fatal( 'undeleterevdel' );
2532  $this->file->unlock();
2533  return $status;
2534  }
2535  } else {
2536  $archiveName = $row->fa_archive_name;
2537 
2538  if ( $archiveName == '' ) {
2539  // This was originally a current version; we
2540  // have to devise a new archive name for it.
2541  // Format is <timestamp of archiving>!<name>
2542  $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
2543 
2544  do {
2545  $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
2546  $timestamp++;
2547  } while ( isset( $archiveNames[$archiveName] ) );
2548  }
2549 
2550  $archiveNames[$archiveName] = true;
2551  $destRel = $this->file->getArchiveRel( $archiveName );
2552  $insertBatch[] = [
2553  'oi_name' => $row->fa_name,
2554  'oi_archive_name' => $archiveName,
2555  'oi_size' => $row->fa_size,
2556  'oi_width' => $row->fa_width,
2557  'oi_height' => $row->fa_height,
2558  'oi_bits' => $row->fa_bits,
2559  'oi_description' => $row->fa_description,
2560  'oi_user' => $row->fa_user,
2561  'oi_user_text' => $row->fa_user_text,
2562  'oi_timestamp' => $row->fa_timestamp,
2563  'oi_metadata' => $props['metadata'],
2564  'oi_media_type' => $props['media_type'],
2565  'oi_major_mime' => $props['major_mime'],
2566  'oi_minor_mime' => $props['minor_mime'],
2567  'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
2568  'oi_sha1' => $sha1 ];
2569  }
2570 
2571  $deleteIds[] = $row->fa_id;
2572 
2573  if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
2574  // private files can stay where they are
2575  $status->successCount++;
2576  } else {
2577  $storeBatch[] = [ $deletedUrl, 'public', $destRel ];
2578  $this->cleanupBatch[] = $row->fa_storage_key;
2579  }
2580 
2581  $first = false;
2582  }
2583 
2584  unset( $result );
2585 
2586  // Add a warning to the status object for missing IDs
2587  $missingIds = array_diff( $this->ids, $idsPresent );
2588 
2589  foreach ( $missingIds as $id ) {
2590  $status->error( 'undelete-missing-filearchive', $id );
2591  }
2592 
2593  if ( !$repo->hasSha1Storage() ) {
2594  // Remove missing files from batch, so we don't get errors when undeleting them
2595  $checkStatus = $this->removeNonexistentFiles( $storeBatch );
2596  if ( !$checkStatus->isGood() ) {
2597  $status->merge( $checkStatus );
2598  return $status;
2599  }
2600  $storeBatch = $checkStatus->value;
2601 
2602  // Run the store batch
2603  // Use the OVERWRITE_SAME flag to smooth over a common error
2604  $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
2605  $status->merge( $storeStatus );
2606 
2607  if ( !$status->isGood() ) {
2608  // Even if some files could be copied, fail entirely as that is the
2609  // easiest thing to do without data loss
2610  $this->cleanupFailedBatch( $storeStatus, $storeBatch );
2611  $status->ok = false;
2612  $this->file->unlock();
2613 
2614  return $status;
2615  }
2616  }
2617 
2618  // Run the DB updates
2619  // Because we have locked the image row, key conflicts should be rare.
2620  // If they do occur, we can roll back the transaction at this time with
2621  // no data loss, but leaving unregistered files scattered throughout the
2622  // public zone.
2623  // This is not ideal, which is why it's important to lock the image row.
2624  if ( $insertCurrent ) {
2625  $dbw->insert( 'image', $insertCurrent, __METHOD__ );
2626  }
2627 
2628  if ( $insertBatch ) {
2629  $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
2630  }
2631 
2632  if ( $deleteIds ) {
2633  $dbw->delete( 'filearchive',
2634  [ 'fa_id' => $deleteIds ],
2635  __METHOD__ );
2636  }
2637 
2638  // If store batch is empty (all files are missing), deletion is to be considered successful
2639  if ( $status->successCount > 0 || !$storeBatch || $repo->hasSha1Storage() ) {
2640  if ( !$exists ) {
2641  wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
2642 
2643  DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2644 
2645  $this->file->purgeEverything();
2646  } else {
2647  wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
2648  $this->file->purgeDescription();
2649  }
2650  }
2651 
2652  $this->file->unlock();
2653 
2654  return $status;
2655  }
2656 
2662  protected function removeNonexistentFiles( $triplets ) {
2663  $files = $filteredTriplets = [];
2664  foreach ( $triplets as $file ) {
2665  $files[$file[0]] = $file[0];
2666  }
2667 
2668  $result = $this->file->repo->fileExistsBatch( $files );
2669  if ( in_array( null, $result, true ) ) {
2670  return Status::newFatal( 'backend-fail-internal',
2671  $this->file->repo->getBackend()->getName() );
2672  }
2673 
2674  foreach ( $triplets as $file ) {
2675  if ( $result[$file[0]] ) {
2676  $filteredTriplets[] = $file;
2677  }
2678  }
2679 
2680  return Status::newGood( $filteredTriplets );
2681  }
2682 
2688  protected function removeNonexistentFromCleanup( $batch ) {
2689  $files = $newBatch = [];
2690  $repo = $this->file->repo;
2691 
2692  foreach ( $batch as $file ) {
2693  $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
2694  rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
2695  }
2696 
2697  $result = $repo->fileExistsBatch( $files );
2698 
2699  foreach ( $batch as $file ) {
2700  if ( $result[$file] ) {
2701  $newBatch[] = $file;
2702  }
2703  }
2704 
2705  return $newBatch;
2706  }
2707 
2713  public function cleanup() {
2714  if ( !$this->cleanupBatch ) {
2715  return $this->file->repo->newGood();
2716  }
2717 
2718  $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
2719 
2720  $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
2721 
2722  return $status;
2723  }
2724 
2732  protected function cleanupFailedBatch( $storeStatus, $storeBatch ) {
2733  $cleanupBatch = [];
2734 
2735  foreach ( $storeStatus->success as $i => $success ) {
2736  // Check if this item of the batch was successfully copied
2737  if ( $success ) {
2738  // Item was successfully copied and needs to be removed again
2739  // Extract ($dstZone, $dstRel) from the batch
2740  $cleanupBatch[] = [ $storeBatch[$i][1], $storeBatch[$i][2] ];
2741  }
2742  }
2743  $this->file->repo->cleanupBatch( $cleanupBatch );
2744  }
2745 }
2746 
2747 # ------------------------------------------------------------------------------
2748 
2755  protected $file;
2756 
2758  protected $target;
2759 
2760  protected $cur;
2761 
2762  protected $olds;
2763 
2764  protected $oldCount;
2765 
2766  protected $archive;
2767 
2769  protected $db;
2770 
2776  $this->file = $file;
2777  $this->target = $target;
2778  $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
2779  $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
2780  $this->oldName = $this->file->getName();
2781  $this->newName = $this->file->repo->getNameFromTitle( $this->target );
2782  $this->oldRel = $this->oldHash . $this->oldName;
2783  $this->newRel = $this->newHash . $this->newName;
2784  $this->db = $file->getRepo()->getMasterDB();
2785  }
2786 
2790  public function addCurrent() {
2791  $this->cur = [ $this->oldRel, $this->newRel ];
2792  }
2793 
2798  public function addOlds() {
2799  $archiveBase = 'archive';
2800  $this->olds = [];
2801  $this->oldCount = 0;
2802  $archiveNames = [];
2803 
2804  $result = $this->db->select( 'oldimage',
2805  [ 'oi_archive_name', 'oi_deleted' ],
2806  [ 'oi_name' => $this->oldName ],
2807  __METHOD__,
2808  [ 'LOCK IN SHARE MODE' ] // ignore snapshot
2809  );
2810 
2811  foreach ( $result as $row ) {
2812  $archiveNames[] = $row->oi_archive_name;
2813  $oldName = $row->oi_archive_name;
2814  $bits = explode( '!', $oldName, 2 );
2815 
2816  if ( count( $bits ) != 2 ) {
2817  wfDebug( "Old file name missing !: '$oldName' \n" );
2818  continue;
2819  }
2820 
2821  list( $timestamp, $filename ) = $bits;
2822 
2823  if ( $this->oldName != $filename ) {
2824  wfDebug( "Old file name doesn't match: '$oldName' \n" );
2825  continue;
2826  }
2827 
2828  $this->oldCount++;
2829 
2830  // Do we want to add those to oldCount?
2831  if ( $row->oi_deleted & File::DELETED_FILE ) {
2832  continue;
2833  }
2834 
2835  $this->olds[] = [
2836  "{$archiveBase}/{$this->oldHash}{$oldName}",
2837  "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
2838  ];
2839  }
2840 
2841  return $archiveNames;
2842  }
2843 
2848  public function execute() {
2849  $repo = $this->file->repo;
2850  $status = $repo->newGood();
2851 
2852  $triplets = $this->getMoveTriplets();
2853  $checkStatus = $this->removeNonexistentFiles( $triplets );
2854  if ( !$checkStatus->isGood() ) {
2855  $status->merge( $checkStatus );
2856  return $status;
2857  }
2858  $triplets = $checkStatus->value;
2859  $destFile = wfLocalFile( $this->target );
2860 
2861  $this->file->lock(); // begin
2862  $destFile->lock(); // quickly fail if destination is not available
2863  // Rename the file versions metadata in the DB.
2864  // This implicitly locks the destination file, which avoids race conditions.
2865  // If we moved the files from A -> C before DB updates, another process could
2866  // move files from B -> C at this point, causing storeBatch() to fail and thus
2867  // cleanupTarget() to trigger. It would delete the C files and cause data loss.
2868  $statusDb = $this->doDBUpdates();
2869  if ( !$statusDb->isGood() ) {
2870  $destFile->unlock();
2871  $this->file->unlockAndRollback();
2872  $statusDb->ok = false;
2873 
2874  return $statusDb;
2875  }
2876  wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
2877  "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
2878 
2879  if ( !$repo->hasSha1Storage() ) {
2880  // Copy the files into their new location.
2881  // If a prior process fataled copying or cleaning up files we tolerate any
2882  // of the existing files if they are identical to the ones being stored.
2883  $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
2884  wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
2885  "{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
2886  if ( !$statusMove->isGood() ) {
2887  // Delete any files copied over (while the destination is still locked)
2888  $this->cleanupTarget( $triplets );
2889  $destFile->unlock();
2890  $this->file->unlockAndRollback(); // unlocks the destination
2891  wfDebugLog( 'imagemove', "Error in moving files: "
2892  . $statusMove->getWikiText( false, false, 'en' ) );
2893  $statusMove->ok = false;
2894 
2895  return $statusMove;
2896  }
2897  $status->merge( $statusMove );
2898  }
2899 
2900  $destFile->unlock();
2901  $this->file->unlock(); // done
2902 
2903  // Everything went ok, remove the source files
2904  $this->cleanupSource( $triplets );
2905 
2906  $status->merge( $statusDb );
2907 
2908  return $status;
2909  }
2910 
2917  protected function doDBUpdates() {
2918  $repo = $this->file->repo;
2919  $status = $repo->newGood();
2920  $dbw = $this->db;
2921 
2922  // Update current image
2923  $dbw->update(
2924  'image',
2925  [ 'img_name' => $this->newName ],
2926  [ 'img_name' => $this->oldName ],
2927  __METHOD__
2928  );
2929 
2930  if ( $dbw->affectedRows() ) {
2931  $status->successCount++;
2932  } else {
2933  $status->failCount++;
2934  $status->fatal( 'imageinvalidfilename' );
2935 
2936  return $status;
2937  }
2938 
2939  // Update old images
2940  $dbw->update(
2941  'oldimage',
2942  [
2943  'oi_name' => $this->newName,
2944  'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
2945  $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
2946  ],
2947  [ 'oi_name' => $this->oldName ],
2948  __METHOD__
2949  );
2950 
2951  $affected = $dbw->affectedRows();
2952  $total = $this->oldCount;
2953  $status->successCount += $affected;
2954  // Bug 34934: $total is based on files that actually exist.
2955  // There may be more DB rows than such files, in which case $affected
2956  // can be greater than $total. We use max() to avoid negatives here.
2957  $status->failCount += max( 0, $total - $affected );
2958  if ( $status->failCount ) {
2959  $status->error( 'imageinvalidfilename' );
2960  }
2961 
2962  return $status;
2963  }
2964 
2969  protected function getMoveTriplets() {
2970  $moves = array_merge( [ $this->cur ], $this->olds );
2971  $triplets = []; // The format is: (srcUrl, destZone, destUrl)
2972 
2973  foreach ( $moves as $move ) {
2974  // $move: (oldRelativePath, newRelativePath)
2975  $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
2976  $triplets[] = [ $srcUrl, 'public', $move[1] ];
2977  wfDebugLog(
2978  'imagemove',
2979  "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
2980  );
2981  }
2982 
2983  return $triplets;
2984  }
2985 
2991  protected function removeNonexistentFiles( $triplets ) {
2992  $files = [];
2993 
2994  foreach ( $triplets as $file ) {
2995  $files[$file[0]] = $file[0];
2996  }
2997 
2998  $result = $this->file->repo->fileExistsBatch( $files );
2999  if ( in_array( null, $result, true ) ) {
3000  return Status::newFatal( 'backend-fail-internal',
3001  $this->file->repo->getBackend()->getName() );
3002  }
3003 
3004  $filteredTriplets = [];
3005  foreach ( $triplets as $file ) {
3006  if ( $result[$file[0]] ) {
3007  $filteredTriplets[] = $file;
3008  } else {
3009  wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
3010  }
3011  }
3012 
3013  return Status::newGood( $filteredTriplets );
3014  }
3015 
3021  protected function cleanupTarget( $triplets ) {
3022  // Create dest pairs from the triplets
3023  $pairs = [];
3024  foreach ( $triplets as $triplet ) {
3025  // $triplet: (old source virtual URL, dst zone, dest rel)
3026  $pairs[] = [ $triplet[1], $triplet[2] ];
3027  }
3028 
3029  $this->file->repo->cleanupBatch( $pairs );
3030  }
3031 
3037  protected function cleanupSource( $triplets ) {
3038  // Create source file names from the triplets
3039  $files = [];
3040  foreach ( $triplets as $triplet ) {
3041  $files[] = $triplet[0];
3042  }
3043 
3044  $this->file->repo->cleanupBatch( $files );
3045  }
3046 }
3047 
3049 
3050 }
static purgePatrolFooterCache($articleID)
Purge the cache used to check if it is worth showing the patrol footer For example, it is done during re-uploads when file patrol is used.
Definition: Article.php:1233
removeNonexistentFiles($batch)
Removes non-existent files from a deletion batch.
Definition: LocalFile.php:2325
getArchiveThumbPath($archiveName, $suffix=false)
Get the path of an archived file's thumbs, or a particular thumb if $suffix is specified.
Definition: File.php:1598
static getMainWANInstance()
Get the main WAN cache object.
exists()
canRender inherited
Definition: LocalFile.php:839
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
invalidateCache()
Purge the file object/metadata cache.
Definition: LocalFile.php:312
the array() calling protocol came about after MediaWiki 1.4rc1.
MediaHandler $handler
Definition: File.php:113
string $media_type
MEDIATYPE_xxx (bitmap, drawing, audio...)
Definition: LocalFile.php:62
recordUpload($oldver, $desc, $license= '', $copyStatus= '', $source= '', $watch=false, $timestamp=false, User $user=null)
Record a file upload in the upload log and the image table.
Definition: LocalFile.php:1198
magic word the default is to use $key to get the and $key value or $key value text $key value html to format the value $key
Definition: hooks.txt:2325
bool $extraDataLoaded
Whether or not lazy-loaded data has been loaded from the database.
Definition: LocalFile.php:80
if(count($args)==0) $dir
assertTitleDefined()
Assert that $this->title is set to a Title.
Definition: File.php:2258
$success
userCan($field, User $user=null)
Determine if the current user is allowed to view a particular field of this file, if it's marked as d...
Definition: File.php:2147
addAll()
Add all revisions of the file.
Definition: LocalFile.php:2401
cleanupTarget($triplets)
Cleanup a partially moved array of triplets by deleting the target files.
Definition: LocalFile.php:3021
cleanupSource($triplets)
Cleanup a fully moved array of triplets by deleting the source files.
Definition: LocalFile.php:3037
loadFromRow($row, $prefix= 'img_')
Load file metadata from a DB result row.
Definition: LocalFile.php:521
string $minor_mime
Minor MIME type.
Definition: LocalFile.php:98
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
getHistory($limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
Definition: LocalFile.php:1002
restore($versions=[], $unsuppress=false)
Restore all or specified deleted revisions to the given file.
Definition: LocalFile.php:1765
div flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException'returning false will NOT prevent logging $e
Definition: hooks.txt:1936
Set options of the Parser.
purgeMetadataCache()
Refresh metadata in memcached, but don't touch thumbnails or CDN.
Definition: LocalFile.php:883
const DELETE_SOURCE
Definition: File.php:65
if(!isset($args[0])) $lang
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:189
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
__construct(File $file, $reason= '', $suppress=false, $user=null)
Definition: LocalFile.php:2020
cleanup()
Delete unused files in the deleted zone.
Definition: LocalFile.php:2713
getUser($type= 'text')
Returns ID or name of user who uploaded the file.
Definition: LocalFile.php:751
getThumbPath($suffix=false)
Get the path of the thumbnail directory, or a particular file if $suffix is specified.
Definition: File.php:1611
width
recordUpload2($oldver, $comment, $pageText, $props=false, $timestamp=false, $user=null, $tags=[])
Record a file upload in the upload log and the image table.
Definition: LocalFile.php:1229
addCurrent()
Add the current image to the batch.
Definition: LocalFile.php:2790
const DELETE_SOURCE
Definition: FileRepo.php:38
getSize()
Returns the size of the image file, in bytes.
Definition: LocalFile.php:802
Handles purging appropriate CDN URLs given a title (or titles)
$comment
Helper class for file undeletion.
Definition: LocalFile.php:2355
unlock()
Decrement the lock reference count.
Definition: LocalFile.php:1946
string $major_mime
Major MIME type.
Definition: LocalFile.php:95
string $sha1
SHA-1 base 36 content hash.
Definition: LocalFile.php:74
$source
$value
isMissing()
splitMime inherited
Definition: LocalFile.php:682
getArchiveThumbUrl($archiveName, $suffix=false)
Get the URL of the archived file's thumbs, or a particular thumb if $suffix is specified.
Definition: File.php:1655
static isVirtualUrl($url)
Determine if a string is an mwrepo:// URL.
Definition: FileRepo.php:254
$files
assertRepoDefined()
Assert that $this->repo is set to a valid FileRepo instance.
Definition: File.php:2248
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition: hooks.txt:2552
getName()
Return the name of this file.
Definition: File.php:296
string $name
The name of a file from its title object.
Definition: File.php:122
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:172
getRepo()
Returns the repository.
Definition: File.php:1854
unprefixRow($row, $prefix= 'img_')
Definition: LocalFile.php:460
const MW_FILE_VERSION
Bump this number when serialized cache records may be incompatible.
Definition: LocalFile.php:27
array $cleanupBatch
List of file IDs to restore.
Definition: LocalFile.php:2360
int $user
User ID of uploader.
Definition: LocalFile.php:104
Represents a title within MediaWiki.
Definition: Title.php:34
setProps($info)
Set properties in this object to be equal to those given in the associative array $info...
Definition: LocalFile.php:651
when a variable name is used in a it is silently declared as a new local masking the global
Definition: design.txt:93
const METADATA_BAD
static makeParamBlob($params)
Create a blob from a parameter array.
Definition: LogEntry.php:140
static newFatal($message)
Factory function for fatal errors.
Definition: Status.php:89
int $bits
Returned by getimagesize (loadFromXxx)
Definition: LocalFile.php:59
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that's attached to a given link target...
Definition: Revision.php:117
publish($src, $flags=0, array $options=[])
Move or copy a file to its public location.
Definition: LocalFile.php:1541
bool $fileExists
Does the file exist on disk? (loadFromXxx)
Definition: LocalFile.php:50
wfLocalFile($title)
Get an object referring to a locally registered file.
doDBUpdates()
Do the database updates and return a new FileRepoStatus indicating how many rows where updated...
Definition: LocalFile.php:2917
getHeight($page=1)
Return the height of the image.
Definition: LocalFile.php:724
getTitle()
Return the associated title object.
Definition: File.php:325
Title string bool $title
Definition: File.php:98
getHashPath()
Get the filename hash component of the directory including trailing slash, e.g.
Definition: File.php:1497
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist & $tables
Definition: hooks.txt:969
__destruct()
Clean up any dangling locks.
Definition: LocalFile.php:1978
static queueRecursiveJobsForTable(Title $title, $table)
Queue a RefreshLinks job for any table.
wfDebug($text, $dest= 'all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
const DELETED_FILE
Definition: File.php:52
getBackend()
Get the file backend instance.
Definition: FileRepo.php:215
update($table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
Definition: Database.php:1506
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as $wgLang
Definition: design.txt:56
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message.Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item.Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page.Return false to stop further processing of the tag $reader:XMLReader object &$pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision.Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag.Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload.Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports.&$fullInterwikiPrefix:Interwiki prefix, may contain colons.&$pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable.Can be used to lazy-load the import sources list.&$importSources:The value of $wgImportSources.Modify as necessary.See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page.$context:IContextSource object &$pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect.&$title:Title object for the current page &$request:WebRequest &$ignoreRedirect:boolean to skip redirect check &$target:Title/string of redirect target &$article:Article object 'InternalParseBeforeLinks':during Parser's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InternalParseBeforeSanitize':during Parser's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings.Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments.&$parser:Parser object &$text:string containing partially parsed text &$stripState:Parser's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not.Return true without providing an interwiki to continue interwiki search.$prefix:interwiki prefix we are looking for.&$iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user's email has been invalidated successfully.$user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification.Callee may modify $url and $query, URL will be constructed as $url.$query &$url:URL to index.php &$query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) &$article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() &$ip:IP being check &$result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from &$allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn't match your organization.$addr:The e-mail address entered by the user &$result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user &$result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we're looking for a messages file for &$file:The messages file path, you can override this to change the location. 'LanguageGetMagic':DEPRECATED!Use $magicWords in a file listed in $wgExtensionMessagesFiles instead.Use this to define synonyms of magic words depending of the language &$magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces.Do not use this hook to add namespaces.Use CanonicalNamespaces for that.&$namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED!Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead.Use to define aliases of special pages names depending of the language &$specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names.&$names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page's language links.This is called in various places to allow extensions to define the effective language links for a page.$title:The page's Title.&$links:Associative array mapping language codes to prefixed links of the form"language:title".&$linkFlags:Associative array mapping prefixed links to arrays of flags.Currently unused, but planned to provide support for marking individual language links in the UI, e.g.for featured articles. 'LanguageSelector':Hook to change the language selector available on a page.$out:The output page.$cssClassName:CSS class name of the language selector. 'LinkBegin':Used when generating internal and interwiki links in Linker::link(), before processing starts.Return false to skip default processing and return $ret.See documentation for Linker::link() for details on the expected meanings of parameters.$skin:the Skin object $target:the Title that the link is pointing to &$html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1800
getThumbnails()
Get all thumbnail names previously generated for this file STUB Overridden by LocalFile.
Definition: File.php:1410
$batch
Definition: linkcache.txt:23
getDescriptionShortUrl()
Get short description URL for a file based on the page ID.
Definition: LocalFile.php:768
nextHistoryLine()
Returns the history of this file, line by line.
Definition: LocalFile.php:1055
wfTimestamp($outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
unlockAndRollback()
Roll back the DB transaction and mark the image unlocked.
Definition: LocalFile.php:1960
__construct(File $file, Title $target)
Definition: LocalFile.php:2775
getPath()
Return the storage path to the file.
Definition: File.php:416
wfDebugLog($logGroup, $text, $dest= 'all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not...
removeNonexistentFiles($triplets)
Removes non-existent files from move batch.
Definition: LocalFile.php:2991
int $historyRes
Result of the query for the file's history (nextHistoryLine)
Definition: LocalFile.php:92
bool $locked
True if the image row is locked.
Definition: LocalFile.php:119
getFileSha1($virtualUrl)
Get the sha1 (base 36) of a file with a given virtual URL/storage path.
Definition: FileRepo.php:1586
deleteOld($archiveName, $reason, $suppress=false, $user=null)
Delete an old version of the file.
Definition: LocalFile.php:1729
getReadOnlyReason()
Get an explanatory message if this repo is read-only.
Definition: FileRepo.php:225
__construct(File $file, $unsuppress=false)
Definition: LocalFile.php:2375
wfReadOnly()
Check whether the wiki is in read-only mode.
string $metadata
Handler-specific metadata.
Definition: LocalFile.php:71
string $descriptionTouched
TS_MW timestamp of the last change of the file description.
Definition: LocalFile.php:113
We ve cleaned up the code here by removing clumps of infrequently used code and moving them off somewhere else It s much easier for someone working with this code to see what s _really_ going and make changes or fix bugs In we can take all the code that deals with the little used title reversing we can concentrate it all in an extension file
Definition: hooks.txt:93
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an and does all the work of translating among various forms such as plain database key
Definition: design.txt:25
publishTo($src, $dstRel, $flags=0, array $options=[])
Move or copy a file to a specified location.
Definition: LocalFile.php:1560
string $repoClass
Definition: LocalFile.php:86
File backend exception for checked exceptions (e.g.
isDeleted($field)
Is this file a "deleted" file in a private archive? STUB.
Definition: File.php:1875
Class to invalidate the HTML cache of all the pages linking to a given title.
isMultipage()
Returns 'true' if this file is a type which supports multiple pages, e.g.
Definition: File.php:1959
const LOAD_ALL
Definition: LocalFile.php:128
getHandler()
Get a MediaHandler instance for this file.
Definition: File.php:1365
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:235
isMetadataValid($image, $metadata)
Check if the metadata string is valid for this handler.
static factory(array $deltas)
if($limit) $timestamp
static selectFields()
Fields in the filearchive table.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context $options
Definition: hooks.txt:1008
getDescriptionTouched()
Definition: LocalFile.php:1855
const LOCK_EX
Definition: LockManager.php:62
getRel()
Get the path of the file relative to the public zone root.
Definition: File.php:1512
$res
Definition: database.txt:21
static singleton()
Get a RepoGroup instance.
Definition: RepoGroup.php:59
static isStoragePath($path)
Check if a given path is a "mwstore://" path.
bool $unsuppress
Whether to remove all settings for suppressed fields.
Definition: LocalFile.php:2369
static newNullRevision($dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page's history.
Definition: Revision.php:1624
static selectFields()
Fields in the image table.
Definition: LocalFile.php:192
string $user_text
User name of uploader.
Definition: LocalFile.php:107
__construct($title, $repo)
Constructor.
Definition: LocalFile.php:217
const CACHE_FIELD_MAX_LEN
Definition: LocalFile.php:47
purgeThumbList($dir, $files)
Delete a list of thumbnails visible at urls.
Definition: LocalFile.php:968
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such and we might be restricted by PHP settings such as safe mode or open_basedir We cannot assume that the software even has read access anywhere useful Many shared hosts run all users web applications under the same user
Wikitext formatted, in the key only.
Definition: distributors.txt:9
DatabaseBase $db
Definition: LocalFile.php:2769
upload($src, $comment, $pageText, $flags=0, $props=false, $timestamp=false, $user=null, $tags=[])
getHashPath inherited
Definition: LocalFile.php:1131
loadExtraFromDB()
Load lazy file metadata from the DB.
Definition: LocalFile.php:408
$cache
Definition: mcc.php:33
const EDIT_SUPPRESS_RC
Definition: Defines.php:182
static newFromRow($row, $repo)
Create a LocalFile from a title Do not call this except from inside a repo class. ...
Definition: LocalFile.php:155
maybeUpgradeRow()
Upgrade a row if it needs it.
Definition: LocalFile.php:556
static getInitialPageText($comment= '', $license= '', $copyStatus= '', $source= '', Config $config=null)
Get the initial image page text based on a comment and optional file status information.
quickImport($src, $dst, $options=null)
Import a file from the local file system into the repo.
Definition: FileRepo.php:966
readOnlyFatalStatus()
Definition: LocalFile.php:1970
Helper class for file movement.
Definition: LocalFile.php:2753
bool $upgraded
Whether the row was upgraded on load.
Definition: LocalFile.php:116
static newExtraneousContext(Title $title, $request=[])
Create a new extraneous context.
static selectFields()
Fields in the oldimage table.
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:142
loadFieldsWithTimestamp($dbr, $fname)
Definition: LocalFile.php:433
execute()
Run the transaction.
Definition: LocalFile.php:2257
title
const DELETED_RESTRICTED
Definition: Revision.php:79
getVirtualUrl($suffix=false)
Get the public zone virtual URL for a current version source file.
Definition: File.php:1713
bool $all
Add all revisions of the file.
Definition: LocalFile.php:2366
decodeRow($row, $prefix= 'img_')
Decode a row from the database (either object or array) to an array with timestamps and MIME types de...
Definition: LocalFile.php:485
bool $lockedOwnTrx
True if the image row is locked with a lock initiated transaction.
Definition: LocalFile.php:122
getStreamHeaders($metadata)
Get useful response headers for GET/HEAD requests for a file with the given metadata.
static addUpdate(DeferrableUpdate $update, $type=self::POSTSEND)
Add an update to the deferred list.
getThumbnails($archiveName=false)
getTransformScript inherited
Definition: LocalFile.php:860
static run($event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:131
array $ids
List of file IDs to restore.
Definition: LocalFile.php:2363
bool $dataLoaded
Whether or not core data has been loaded from the database (loadFromXxx)
Definition: LocalFile.php:77
const NS_FILE
Definition: Defines.php:75
FileRepo LocalRepo ForeignAPIRepo bool $repo
Some member variables can be lazy-initialised using __get().
Definition: File.php:95
static makeContent($text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
getMetadata()
Get handler-specific metadata.
Definition: LocalFile.php:784
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1588
static getSha1Base36FromPath($path)
Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case encoding, zero padded to 31 digits.
Definition: FSFile.php:275
static getCacheSetOptions(IDatabase $db1)
Merge the result of getSessionLagStatus() for several DBs using the most pessimistic values to estima...
Definition: Database.php:2907
string $mime
MIME type, determined by MimeMagic::guessMimeType.
Definition: LocalFile.php:65
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
Special handling for file pages.
execute()
Perform the move.
Definition: LocalFile.php:2848
Helper class for file deletion.
Definition: LocalFile.php:1989
array $deletionBatch
Items to be processed in the deletion batch.
Definition: LocalFile.php:2003
purgeOldThumbnails($archiveName)
Delete cached transformed files for an archived version only.
Definition: LocalFile.php:912
const DELETED_TEXT
Definition: Revision.php:76
getWidth($page=1)
Return the width of the image.
Definition: LocalFile.php:697
FileRepoStatus $status
Definition: LocalFile.php:2009
removeNonexistentFiles($triplets)
Removes non-existent files from a store batch.
Definition: LocalFile.php:2662
const TS_MW
MediaWiki concatenated string timestamp (YYYYMMDDHHMMSS)
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
removeNonexistentFromCleanup($batch)
Removes non-existent files from a cleanup batch.
Definition: LocalFile.php:2688
const DELETED_USER
Definition: Revision.php:78
Class for creating log entries manually, to inject them into the database.
Definition: LogEntry.php:394
if(!defined( 'MEDIAWIKI')) $fname
This file is not a valid entry point, perform no further processing unless MEDIAWIKI is defined...
Definition: Setup.php:35
upgradeRow()
Fix assorted version-related problems with the image row by reloading it from the file...
Definition: LocalFile.php:596
Class representing a non-directory file on the file system.
Definition: FSFile.php:29
const OVERWRITE_SAME
Definition: FileRepo.php:40
static getHashFromKey($key)
Gets the SHA1 hash from a storage key.
Definition: LocalRepo.php:181
const EDIT_NEW
Definition: Defines.php:179
getCacheFields($prefix= 'img_')
Definition: LocalFile.php:335
cleanupFailedBatch($storeStatus, $storeBatch)
Cleanup a failed batch.
Definition: LocalFile.php:2732
lock()
Start a transaction and lock the image for update Increments a reference counter if the lock is alrea...
Definition: LocalFile.php:1913
getDescriptionText($lang=null)
Get the HTML text of the description page This is not used by ImagePage for local files...
Definition: LocalFile.php:1811
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content $content
Definition: hooks.txt:1008
string $url
The URL corresponding to one of the four basic zones.
Definition: File.php:116
int $deleted
Bitfield akin to rev_deleted.
Definition: LocalFile.php:83
move($target)
getLinksTo inherited
Definition: LocalFile.php:1619
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at name
Definition: design.txt:12
filterThumbnailPurgeList(&$files, $options)
Remove files from the purge list.
getUrl()
Return the URL of the file.
Definition: File.php:347
$license
bool $missing
True if file is not present in file system.
Definition: LocalFile.php:125
static getHandler($type)
Get a MediaHandler for a given MIME type from the instance cache.
const METADATA_COMPATIBLE
getDescriptionUrl()
isMultipage inherited
Definition: LocalFile.php:1799
getPageDimensions(File $image, $page)
Get an associative array of page dimensions Currently "width" and "height" are understood, but this might be expanded in the future.
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set and then return false from the hook function Ensure you consume the ChangeTagAfterDelete hook to carry out custom deletion actions as context called by AbstractContent::getParserOutput May be used to override the normal model specific rendering of page content as context as context the output can only depend on parameters provided to this hook not on global state indicating whether full HTML should be generated If generation of HTML may be but other information should still be present in the ParserOutput object to manipulate or replace but no entry for that model exists in $wgContentHandlers if desired whether it is OK to use $contentModel on $title Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok inclusive $limit
Definition: hooks.txt:1008
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition: design.txt:56
getDescription($audience=self::FOR_PUBLIC, User $user=null)
Definition: LocalFile.php:1830
Class to represent a local file in the wiki's own database.
Definition: LocalFile.php:46
addOlds()
Add the old versions of the image to the batch.
Definition: LocalFile.php:2798
getThumbUrl($suffix=false)
Get the URL of the thumbnail directory, or a particular file if $suffix is specified.
Definition: File.php:1693
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist e g Watchlist removed from all revisions and log entries to which it was applied This gives extensions a chance to take it off their books as the deletion has already been partly carried out by this point or something similar the user will be unable to create the tag set $status
Definition: hooks.txt:1008
getMoveTriplets()
Generate triplets for FileRepo::storeBatch().
Definition: LocalFile.php:2969
string $timestamp
Upload timestamp.
Definition: LocalFile.php:101
loadFromDB($flags=0)
Load file metadata from the DB.
Definition: LocalFile.php:383
getLazyCacheFields($prefix= 'img_')
Definition: LocalFile.php:360
publish($src, $dstRel, $archiveRel, $flags=0, array $options=[])
Copy or move a file either from a storage path, virtual URL, or file system path, into this repositor...
Definition: FileRepo.php:1170
int $height
Image height.
Definition: LocalFile.php:56
loadFromFile()
Load metadata from the file itself.
Definition: LocalFile.php:326
const TS_UNIX
Unix time - the number of seconds since 1970-01-01 00:00:00 UTC.
serialize()
Definition: ApiMessage.php:94
const DELETED_COMMENT
Definition: Revision.php:77
string $description
Description of current revision of the file.
Definition: LocalFile.php:110
load($flags=0)
Load file metadata from cache or DB, unless already loaded.
Definition: LocalFile.php:539
purgeCache($options=[])
Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN...
Definition: LocalFile.php:894
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:50
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Definition: Status.php:132
bool $suppress
Whether to suppress all suppressable fields when deleting.
Definition: LocalFile.php:2006
getArchiveUrl($suffix=false)
Get the URL of the archive directory, or a particular file if $suffix is specified.
Definition: File.php:1635
getMediaType()
Returns the type of the media in the file.
Definition: LocalFile.php:823
loadFromCache()
Try to load file metadata from memcached.
Definition: LocalFile.php:245
int $width
Image width.
Definition: LocalFile.php:53
purgeThumbnails($options=[])
Delete cached transformed files for the current version only.
Definition: LocalFile.php:935
getMimeType()
Returns the MIME type of the file.
Definition: LocalFile.php:812
resetHistory()
Reset the history pointer to the first element of the history.
Definition: LocalFile.php:1093
static getPropsFromPath($path, $ext=true)
Get an associative array containing information about a file in the local filesystem.
Definition: FSFile.php:259
int $size
Size in bytes (loadFromXxx)
Definition: LocalFile.php:68
hasSha1Storage()
Returns whether or not storage is SHA-1 based.
Definition: FileRepo.php:1911
addOlds()
Add the old versions of the image to the batch.
Definition: LocalFile.php:2049
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached one of or reset my talk my contributions etc etc otherwise the built in rate limiting checks are if enabled allows for interception of redirect as a string mapping parameter names to values & $type
Definition: hooks.txt:2342
purgeDescription()
Purge the file description page, but don't go after pages using the file.
Definition: File.php:1429
static & makeTitle($ns, $title, $fragment= '', $interwiki= '')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:524
$wgUpdateCompatibleMetadata
If to automatically update the img_metadata field if the metadata field is outdated but compatible wi...
addIds($ids)
Add a whole lot of files by ID.
Definition: LocalFile.php:2394
static newGood($value=null)
Factory function for good results.
Definition: Status.php:101
do that in ParserLimitReportFormat instead use this to modify the parameters of the image and a DIV can begin in one section and end in another Make sure your code can handle that case gracefully See the EditSectionClearerLink extension for an example zero but section is usually empty its values are the globals values before the output is cached $page
Definition: hooks.txt:2342
saveToCache()
Save the file metadata to memcached.
Definition: LocalFile.php:276
int $historyLine
Number of line to return by nextHistoryLine() (constructor)
Definition: LocalFile.php:89
execute()
Run the transaction, except the cleanup batch.
Definition: LocalFile.php:2413
$wgUser
Definition: Setup.php:794
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:314
addId($fa_id)
Add a file by ID.
Definition: LocalFile.php:2386