MediaWiki REL1_29
LocalFile.php
Go to the documentation of this file.
1<?php
27
45class LocalFile extends File {
46 const VERSION = 10; // cache version
47
48 const CACHE_FIELD_MAX_LEN = 1000;
49
51 protected $fileExists;
52
54 protected $width;
55
57 protected $height;
58
60 protected $bits;
61
63 protected $media_type;
64
66 protected $mime;
67
69 protected $size;
70
72 protected $metadata;
73
75 protected $sha1;
76
78 protected $dataLoaded;
79
82
84 protected $deleted;
85
87 protected $repoClass = 'LocalRepo';
88
90 private $historyLine;
91
93 private $historyRes;
94
96 private $major_mime;
97
99 private $minor_mime;
100
102 private $timestamp;
103
105 private $user;
106
108 private $user_text;
109
112
115
117 private $upgraded;
118
120 private $upgrading;
121
123 private $locked;
124
127
129 private $missing;
130
131 // @note: higher than IDBAccessObject constants
132 const LOAD_ALL = 16; // integer; load all the lazy fields too (like metadata)
133
134 const ATOMIC_SECTION_LOCK = 'LocalFile::lockingTransaction';
135
148 static function newFromTitle( $title, $repo, $unused = null ) {
149 return new self( $title, $repo );
150 }
151
161 static function newFromRow( $row, $repo ) {
162 $title = Title::makeTitle( NS_FILE, $row->img_name );
163 $file = new self( $title, $repo );
164 $file->loadFromRow( $row );
165
166 return $file;
167 }
168
178 static function newFromKey( $sha1, $repo, $timestamp = false ) {
179 $dbr = $repo->getReplicaDB();
180
181 $conds = [ 'img_sha1' => $sha1 ];
182 if ( $timestamp ) {
183 $conds['img_timestamp'] = $dbr->timestamp( $timestamp );
184 }
185
186 $row = $dbr->selectRow( 'image', self::selectFields(), $conds, __METHOD__ );
187 if ( $row ) {
188 return self::newFromRow( $row, $repo );
189 } else {
190 return false;
191 }
192 }
193
198 static function selectFields() {
199 return [
200 'img_name',
201 'img_size',
202 'img_width',
203 'img_height',
204 'img_metadata',
205 'img_bits',
206 'img_media_type',
207 'img_major_mime',
208 'img_minor_mime',
209 'img_description',
210 'img_user',
211 'img_user_text',
212 'img_timestamp',
213 'img_sha1',
214 ];
215 }
216
223 function __construct( $title, $repo ) {
224 parent::__construct( $title, $repo );
225
226 $this->metadata = '';
227 $this->historyLine = 0;
228 $this->historyRes = null;
229 $this->dataLoaded = false;
230 $this->extraDataLoaded = false;
231
232 $this->assertRepoDefined();
233 $this->assertTitleDefined();
234 }
235
241 function getCacheKey() {
242 return $this->repo->getSharedCacheKey( 'file', sha1( $this->getName() ) );
243 }
244
251 return [ $this->getCacheKey() ];
252 }
253
257 private function loadFromCache() {
258 $this->dataLoaded = false;
259 $this->extraDataLoaded = false;
260
261 $key = $this->getCacheKey();
262 if ( !$key ) {
263 $this->loadFromDB( self::READ_NORMAL );
264
265 return;
266 }
267
268 $cache = ObjectCache::getMainWANInstance();
269 $cachedValues = $cache->getWithSetCallback(
270 $key,
271 $cache::TTL_WEEK,
272 function ( $oldValue, &$ttl, array &$setOpts ) use ( $cache ) {
273 $setOpts += Database::getCacheSetOptions( $this->repo->getReplicaDB() );
274
275 $this->loadFromDB( self::READ_NORMAL );
276
277 $fields = $this->getCacheFields( '' );
278 $cacheVal['fileExists'] = $this->fileExists;
279 if ( $this->fileExists ) {
280 foreach ( $fields as $field ) {
281 $cacheVal[$field] = $this->$field;
282 }
283 }
284 // Strip off excessive entries from the subset of fields that can become large.
285 // If the cache value gets to large it will not fit in memcached and nothing will
286 // get cached at all, causing master queries for any file access.
287 foreach ( $this->getLazyCacheFields( '' ) as $field ) {
288 if ( isset( $cacheVal[$field] )
289 && strlen( $cacheVal[$field] ) > 100 * 1024
290 ) {
291 unset( $cacheVal[$field] ); // don't let the value get too big
292 }
293 }
294
295 if ( $this->fileExists ) {
296 $ttl = $cache->adaptiveTTL( wfTimestamp( TS_UNIX, $this->timestamp ), $ttl );
297 } else {
298 $ttl = $cache::TTL_DAY;
299 }
300
301 return $cacheVal;
302 },
303 [ 'version' => self::VERSION ]
304 );
305
306 $this->fileExists = $cachedValues['fileExists'];
307 if ( $this->fileExists ) {
308 $this->setProps( $cachedValues );
309 }
310
311 $this->dataLoaded = true;
312 $this->extraDataLoaded = true;
313 foreach ( $this->getLazyCacheFields( '' ) as $field ) {
314 $this->extraDataLoaded = $this->extraDataLoaded && isset( $cachedValues[$field] );
315 }
316 }
317
321 public function invalidateCache() {
322 $key = $this->getCacheKey();
323 if ( !$key ) {
324 return;
325 }
326
327 $this->repo->getMasterDB()->onTransactionPreCommitOrIdle(
328 function () use ( $key ) {
329 ObjectCache::getMainWANInstance()->delete( $key );
330 },
331 __METHOD__
332 );
333 }
334
338 function loadFromFile() {
339 $props = $this->repo->getFileProps( $this->getVirtualUrl() );
340 $this->setProps( $props );
341 }
342
347 function getCacheFields( $prefix = 'img_' ) {
348 static $fields = [ 'size', 'width', 'height', 'bits', 'media_type',
349 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user',
350 'user_text', 'description' ];
351 static $results = [];
352
353 if ( $prefix == '' ) {
354 return $fields;
355 }
356
357 if ( !isset( $results[$prefix] ) ) {
358 $prefixedFields = [];
359 foreach ( $fields as $field ) {
360 $prefixedFields[] = $prefix . $field;
361 }
362 $results[$prefix] = $prefixedFields;
363 }
364
365 return $results[$prefix];
366 }
367
372 function getLazyCacheFields( $prefix = 'img_' ) {
373 static $fields = [ 'metadata' ];
374 static $results = [];
375
376 if ( $prefix == '' ) {
377 return $fields;
378 }
379
380 if ( !isset( $results[$prefix] ) ) {
381 $prefixedFields = [];
382 foreach ( $fields as $field ) {
383 $prefixedFields[] = $prefix . $field;
384 }
385 $results[$prefix] = $prefixedFields;
386 }
387
388 return $results[$prefix];
389 }
390
395 function loadFromDB( $flags = 0 ) {
396 $fname = static::class . '::' . __FUNCTION__;
397
398 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
399 $this->dataLoaded = true;
400 $this->extraDataLoaded = true;
401
402 $dbr = ( $flags & self::READ_LATEST )
403 ? $this->repo->getMasterDB()
404 : $this->repo->getReplicaDB();
405
406 $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
407 [ 'img_name' => $this->getName() ], $fname );
408
409 if ( $row ) {
410 $this->loadFromRow( $row );
411 } else {
412 $this->fileExists = false;
413 }
414 }
415
420 protected function loadExtraFromDB() {
421 $fname = static::class . '::' . __FUNCTION__;
422
423 # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
424 $this->extraDataLoaded = true;
425
426 $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getReplicaDB(), $fname );
427 if ( !$fieldMap ) {
428 $fieldMap = $this->loadFieldsWithTimestamp( $this->repo->getMasterDB(), $fname );
429 }
430
431 if ( $fieldMap ) {
432 foreach ( $fieldMap as $name => $value ) {
433 $this->$name = $value;
434 }
435 } else {
436 throw new MWException( "Could not find data for image '{$this->getName()}'." );
437 }
438 }
439
445 private function loadFieldsWithTimestamp( $dbr, $fname ) {
446 $fieldMap = false;
447
448 $row = $dbr->selectRow( 'image', $this->getLazyCacheFields( 'img_' ), [
449 'img_name' => $this->getName(),
450 'img_timestamp' => $dbr->timestamp( $this->getTimestamp() )
451 ], $fname );
452 if ( $row ) {
453 $fieldMap = $this->unprefixRow( $row, 'img_' );
454 } else {
455 # File may have been uploaded over in the meantime; check the old versions
456 $row = $dbr->selectRow( 'oldimage', $this->getLazyCacheFields( 'oi_' ), [
457 'oi_name' => $this->getName(),
458 'oi_timestamp' => $dbr->timestamp( $this->getTimestamp() )
459 ], $fname );
460 if ( $row ) {
461 $fieldMap = $this->unprefixRow( $row, 'oi_' );
462 }
463 }
464
465 return $fieldMap;
466 }
467
474 protected function unprefixRow( $row, $prefix = 'img_' ) {
475 $array = (array)$row;
476 $prefixLength = strlen( $prefix );
477
478 // Sanity check prefix once
479 if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
480 throw new MWException( __METHOD__ . ': incorrect $prefix parameter' );
481 }
482
483 $decoded = [];
484 foreach ( $array as $name => $value ) {
485 $decoded[substr( $name, $prefixLength )] = $value;
486 }
487
488 return $decoded;
489 }
490
499 function decodeRow( $row, $prefix = 'img_' ) {
500 $decoded = $this->unprefixRow( $row, $prefix );
501
502 $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
503
504 $decoded['metadata'] = $this->repo->getReplicaDB()->decodeBlob( $decoded['metadata'] );
505
506 if ( empty( $decoded['major_mime'] ) ) {
507 $decoded['mime'] = 'unknown/unknown';
508 } else {
509 if ( !$decoded['minor_mime'] ) {
510 $decoded['minor_mime'] = 'unknown';
511 }
512 $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
513 }
514
515 // Trim zero padding from char/binary field
516 $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
517
518 // Normalize some fields to integer type, per their database definition.
519 // Use unary + so that overflows will be upgraded to double instead of
520 // being trucated as with intval(). This is important to allow >2GB
521 // files on 32-bit systems.
522 foreach ( [ 'size', 'width', 'height', 'bits' ] as $field ) {
523 $decoded[$field] = +$decoded[$field];
524 }
525
526 return $decoded;
527 }
528
535 function loadFromRow( $row, $prefix = 'img_' ) {
536 $this->dataLoaded = true;
537 $this->extraDataLoaded = true;
538
539 $array = $this->decodeRow( $row, $prefix );
540
541 foreach ( $array as $name => $value ) {
542 $this->$name = $value;
543 }
544
545 $this->fileExists = true;
546 $this->maybeUpgradeRow();
547 }
548
553 function load( $flags = 0 ) {
554 if ( !$this->dataLoaded ) {
555 if ( $flags & self::READ_LATEST ) {
556 $this->loadFromDB( $flags );
557 } else {
558 $this->loadFromCache();
559 }
560 }
561
562 if ( ( $flags & self::LOAD_ALL ) && !$this->extraDataLoaded ) {
563 // @note: loads on name/timestamp to reduce race condition problems
564 $this->loadExtraFromDB();
565 }
566 }
567
571 function maybeUpgradeRow() {
573
574 if ( wfReadOnly() || $this->upgrading ) {
575 return;
576 }
577
578 $upgrade = false;
579 if ( is_null( $this->media_type ) || $this->mime == 'image/svg' ) {
580 $upgrade = true;
581 } else {
582 $handler = $this->getHandler();
583 if ( $handler ) {
584 $validity = $handler->isMetadataValid( $this, $this->getMetadata() );
585 if ( $validity === MediaHandler::METADATA_BAD ) {
586 $upgrade = true;
587 } elseif ( $validity === MediaHandler::METADATA_COMPATIBLE ) {
589 }
590 }
591 }
592
593 if ( $upgrade ) {
594 $this->upgrading = true;
595 // Defer updates unless in auto-commit CLI mode
596 DeferredUpdates::addCallableUpdate( function() {
597 $this->upgrading = false; // avoid duplicate updates
598 try {
599 $this->upgradeRow();
600 } catch ( LocalFileLockError $e ) {
601 // let the other process handle it (or do it next time)
602 }
603 } );
604 }
605 }
606
610 function getUpgraded() {
611 return $this->upgraded;
612 }
613
617 function upgradeRow() {
618 $this->lock(); // begin
619
620 $this->loadFromFile();
621
622 # Don't destroy file info of missing files
623 if ( !$this->fileExists ) {
624 $this->unlock();
625 wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
626
627 return;
628 }
629
630 $dbw = $this->repo->getMasterDB();
631 list( $major, $minor ) = self::splitMime( $this->mime );
632
633 if ( wfReadOnly() ) {
634 $this->unlock();
635
636 return;
637 }
638 wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
639
640 $dbw->update( 'image',
641 [
642 'img_size' => $this->size, // sanity
643 'img_width' => $this->width,
644 'img_height' => $this->height,
645 'img_bits' => $this->bits,
646 'img_media_type' => $this->media_type,
647 'img_major_mime' => $major,
648 'img_minor_mime' => $minor,
649 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
650 'img_sha1' => $this->sha1,
651 ],
652 [ 'img_name' => $this->getName() ],
653 __METHOD__
654 );
655
656 $this->invalidateCache();
657
658 $this->unlock(); // done
659 $this->upgraded = true; // avoid rework/retries
660 }
661
672 function setProps( $info ) {
673 $this->dataLoaded = true;
674 $fields = $this->getCacheFields( '' );
675 $fields[] = 'fileExists';
676
677 foreach ( $fields as $field ) {
678 if ( isset( $info[$field] ) ) {
679 $this->$field = $info[$field];
680 }
681 }
682
683 // Fix up mime fields
684 if ( isset( $info['major_mime'] ) ) {
685 $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
686 } elseif ( isset( $info['mime'] ) ) {
687 $this->mime = $info['mime'];
688 list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
689 }
690 }
691
703 function isMissing() {
704 if ( $this->missing === null ) {
705 list( $fileExists ) = $this->repo->fileExists( $this->getVirtualUrl() );
706 $this->missing = !$fileExists;
707 }
708
709 return $this->missing;
710 }
711
718 public function getWidth( $page = 1 ) {
719 $this->load();
720
721 if ( $this->isMultipage() ) {
722 $handler = $this->getHandler();
723 if ( !$handler ) {
724 return 0;
725 }
726 $dim = $handler->getPageDimensions( $this, $page );
727 if ( $dim ) {
728 return $dim['width'];
729 } else {
730 // For non-paged media, the false goes through an
731 // intval, turning failure into 0, so do same here.
732 return 0;
733 }
734 } else {
735 return $this->width;
736 }
737 }
738
745 public function getHeight( $page = 1 ) {
746 $this->load();
747
748 if ( $this->isMultipage() ) {
749 $handler = $this->getHandler();
750 if ( !$handler ) {
751 return 0;
752 }
753 $dim = $handler->getPageDimensions( $this, $page );
754 if ( $dim ) {
755 return $dim['height'];
756 } else {
757 // For non-paged media, the false goes through an
758 // intval, turning failure into 0, so do same here.
759 return 0;
760 }
761 } else {
762 return $this->height;
763 }
764 }
765
772 function getUser( $type = 'text' ) {
773 $this->load();
774
775 if ( $type == 'text' ) {
776 return $this->user_text;
777 } else { // id
778 return (int)$this->user;
779 }
780 }
781
789 public function getDescriptionShortUrl() {
790 $pageId = $this->title->getArticleID();
791
792 if ( $pageId !== null ) {
793 $url = $this->repo->makeUrl( [ 'curid' => $pageId ] );
794 if ( $url !== false ) {
795 return $url;
796 }
797 }
798 return null;
799 }
800
805 function getMetadata() {
806 $this->load( self::LOAD_ALL ); // large metadata is loaded in another step
807 return $this->metadata;
808 }
809
813 function getBitDepth() {
814 $this->load();
815
816 return (int)$this->bits;
817 }
818
823 public function getSize() {
824 $this->load();
825
826 return $this->size;
827 }
828
833 function getMimeType() {
834 $this->load();
835
836 return $this->mime;
837 }
838
844 function getMediaType() {
845 $this->load();
846
847 return $this->media_type;
848 }
849
860 public function exists() {
861 $this->load();
862
863 return $this->fileExists;
864 }
865
881 function getThumbnails( $archiveName = false ) {
882 if ( $archiveName ) {
883 $dir = $this->getArchiveThumbPath( $archiveName );
884 } else {
885 $dir = $this->getThumbPath();
886 }
887
888 $backend = $this->repo->getBackend();
889 $files = [ $dir ];
890 try {
891 $iterator = $backend->getFileList( [ 'dir' => $dir ] );
892 foreach ( $iterator as $file ) {
893 $files[] = $file;
894 }
895 } catch ( FileBackendError $e ) {
896 } // suppress (T56674)
897
898 return $files;
899 }
900
905 $this->invalidateCache();
906 }
907
915 function purgeCache( $options = [] ) {
916 // Refresh metadata cache
917 $this->purgeMetadataCache();
918
919 // Delete thumbnails
920 $this->purgeThumbnails( $options );
921
922 // Purge CDN cache for this file
923 DeferredUpdates::addUpdate(
924 new CdnCacheUpdate( [ $this->getUrl() ] ),
925 DeferredUpdates::PRESEND
926 );
927 }
928
933 function purgeOldThumbnails( $archiveName ) {
934 // Get a list of old thumbnails and URLs
935 $files = $this->getThumbnails( $archiveName );
936
937 // Purge any custom thumbnail caches
938 Hooks::run( 'LocalFilePurgeThumbnails', [ $this, $archiveName ] );
939
940 // Delete thumbnails
941 $dir = array_shift( $files );
942 $this->purgeThumbList( $dir, $files );
943
944 // Purge the CDN
945 $urls = [];
946 foreach ( $files as $file ) {
947 $urls[] = $this->getArchiveThumbUrl( $archiveName, $file );
948 }
949 DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
950 }
951
956 public function purgeThumbnails( $options = [] ) {
957 $files = $this->getThumbnails();
958 // Always purge all files from CDN regardless of handler filters
959 $urls = [];
960 foreach ( $files as $file ) {
961 $urls[] = $this->getThumbUrl( $file );
962 }
963 array_shift( $urls ); // don't purge directory
964
965 // Give media handler a chance to filter the file purge list
966 if ( !empty( $options['forThumbRefresh'] ) ) {
967 $handler = $this->getHandler();
968 if ( $handler ) {
970 }
971 }
972
973 // Purge any custom thumbnail caches
974 Hooks::run( 'LocalFilePurgeThumbnails', [ $this, false ] );
975
976 // Delete thumbnails
977 $dir = array_shift( $files );
978 $this->purgeThumbList( $dir, $files );
979
980 // Purge the CDN
981 DeferredUpdates::addUpdate( new CdnCacheUpdate( $urls ), DeferredUpdates::PRESEND );
982 }
983
989 public function prerenderThumbnails() {
991
992 $jobs = [];
993
995 rsort( $sizes );
996
997 foreach ( $sizes as $size ) {
998 if ( $this->isVectorized() || $this->getWidth() > $size ) {
999 $jobs[] = new ThumbnailRenderJob(
1000 $this->getTitle(),
1001 [ 'transformParams' => [ 'width' => $size ] ]
1002 );
1003 }
1004 }
1005
1006 if ( $jobs ) {
1007 JobQueueGroup::singleton()->lazyPush( $jobs );
1008 }
1009 }
1010
1016 protected function purgeThumbList( $dir, $files ) {
1017 $fileListDebug = strtr(
1018 var_export( $files, true ),
1019 [ "\n" => '' ]
1020 );
1021 wfDebug( __METHOD__ . ": $fileListDebug\n" );
1022
1023 $purgeList = [];
1024 foreach ( $files as $file ) {
1025 # Check that the base file name is part of the thumb name
1026 # This is a basic sanity check to avoid erasing unrelated directories
1027 if ( strpos( $file, $this->getName() ) !== false
1028 || strpos( $file, "-thumbnail" ) !== false // "short" thumb name
1029 ) {
1030 $purgeList[] = "{$dir}/{$file}";
1031 }
1032 }
1033
1034 # Delete the thumbnails
1035 $this->repo->quickPurgeBatch( $purgeList );
1036 # Clear out the thumbnail directory if empty
1037 $this->repo->quickCleanDir( $dir );
1038 }
1039
1050 function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
1051 $dbr = $this->repo->getReplicaDB();
1052 $tables = [ 'oldimage' ];
1053 $fields = OldLocalFile::selectFields();
1054 $conds = $opts = $join_conds = [];
1055 $eq = $inc ? '=' : '';
1056 $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
1057
1058 if ( $start ) {
1059 $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
1060 }
1061
1062 if ( $end ) {
1063 $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
1064 }
1065
1066 if ( $limit ) {
1067 $opts['LIMIT'] = $limit;
1068 }
1069
1070 // Search backwards for time > x queries
1071 $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
1072 $opts['ORDER BY'] = "oi_timestamp $order";
1073 $opts['USE INDEX'] = [ 'oldimage' => 'oi_name_timestamp' ];
1074
1075 // Avoid PHP 7.1 warning from passing $this by reference
1076 $localFile = $this;
1077 Hooks::run( 'LocalFile::getHistory', [ &$localFile, &$tables, &$fields,
1078 &$conds, &$opts, &$join_conds ] );
1079
1080 $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
1081 $r = [];
1082
1083 foreach ( $res as $row ) {
1084 $r[] = $this->repo->newFileFromRow( $row );
1085 }
1086
1087 if ( $order == 'ASC' ) {
1088 $r = array_reverse( $r ); // make sure it ends up descending
1089 }
1090
1091 return $r;
1092 }
1093
1103 public function nextHistoryLine() {
1104 # Polymorphic function name to distinguish foreign and local fetches
1105 $fname = static::class . '::' . __FUNCTION__;
1106
1107 $dbr = $this->repo->getReplicaDB();
1108
1109 if ( $this->historyLine == 0 ) { // called for the first time, return line from cur
1110 $this->historyRes = $dbr->select( 'image',
1111 [
1112 '*',
1113 "'' AS oi_archive_name",
1114 '0 as oi_deleted',
1115 'img_sha1'
1116 ],
1117 [ 'img_name' => $this->title->getDBkey() ],
1118 $fname
1119 );
1120
1121 if ( 0 == $dbr->numRows( $this->historyRes ) ) {
1122 $this->historyRes = null;
1123
1124 return false;
1125 }
1126 } elseif ( $this->historyLine == 1 ) {
1127 $this->historyRes = $dbr->select( 'oldimage', '*',
1128 [ 'oi_name' => $this->title->getDBkey() ],
1129 $fname,
1130 [ 'ORDER BY' => 'oi_timestamp DESC' ]
1131 );
1132 }
1133 $this->historyLine++;
1134
1135 return $dbr->fetchObject( $this->historyRes );
1136 }
1137
1141 public function resetHistory() {
1142 $this->historyLine = 0;
1143
1144 if ( !is_null( $this->historyRes ) ) {
1145 $this->historyRes = null;
1146 }
1147 }
1148
1179 function upload( $src, $comment, $pageText, $flags = 0, $props = false,
1180 $timestamp = false, $user = null, $tags = []
1181 ) {
1183
1184 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1185 return $this->readOnlyFatalStatus();
1186 }
1187
1188 $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1189 if ( !$props ) {
1190 if ( $this->repo->isVirtualUrl( $srcPath )
1191 || FileBackend::isStoragePath( $srcPath )
1192 ) {
1193 $props = $this->repo->getFileProps( $srcPath );
1194 } else {
1195 $mwProps = new MWFileProps( MimeMagic::singleton() );
1196 $props = $mwProps->getPropsFromPath( $srcPath, true );
1197 }
1198 }
1199
1200 $options = [];
1201 $handler = MediaHandler::getHandler( $props['mime'] );
1202 if ( $handler ) {
1203 $options['headers'] = $handler->getStreamHeaders( $props['metadata'] );
1204 } else {
1205 $options['headers'] = [];
1206 }
1207
1208 // Trim spaces on user supplied text
1209 $comment = trim( $comment );
1210
1211 // Truncate nicely or the DB will do it for us
1212 // non-nicely (dangling multi-byte chars, non-truncated version in cache).
1213 $comment = $wgContLang->truncate( $comment, 255 );
1214 $this->lock(); // begin
1215 $status = $this->publish( $src, $flags, $options );
1216
1217 if ( $status->successCount >= 2 ) {
1218 // There will be a copy+(one of move,copy,store).
1219 // The first succeeding does not commit us to updating the DB
1220 // since it simply copied the current version to a timestamped file name.
1221 // It is only *preferable* to avoid leaving such files orphaned.
1222 // Once the second operation goes through, then the current version was
1223 // updated and we must therefore update the DB too.
1224 $oldver = $status->value;
1225 if ( !$this->recordUpload2( $oldver, $comment, $pageText, $props, $timestamp, $user, $tags ) ) {
1226 $status->fatal( 'filenotfound', $srcPath );
1227 }
1228 }
1229
1230 $this->unlock(); // done
1231
1232 return $status;
1233 }
1234
1247 function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
1248 $watch = false, $timestamp = false, User $user = null ) {
1249 if ( !$user ) {
1251 $user = $wgUser;
1252 }
1253
1254 $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
1255
1256 if ( !$this->recordUpload2( $oldver, $desc, $pageText, false, $timestamp, $user ) ) {
1257 return false;
1258 }
1259
1260 if ( $watch ) {
1261 $user->addWatch( $this->getTitle() );
1262 }
1263
1264 return true;
1265 }
1266
1279 $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null, $tags = []
1280 ) {
1281 if ( is_null( $user ) ) {
1283 $user = $wgUser;
1284 }
1285
1286 $dbw = $this->repo->getMasterDB();
1287
1288 # Imports or such might force a certain timestamp; otherwise we generate
1289 # it and can fudge it slightly to keep (name,timestamp) unique on re-upload.
1290 if ( $timestamp === false ) {
1291 $timestamp = $dbw->timestamp();
1292 $allowTimeKludge = true;
1293 } else {
1294 $allowTimeKludge = false;
1295 }
1296
1297 $props = $props ?: $this->repo->getFileProps( $this->getVirtualUrl() );
1298 $props['description'] = $comment;
1299 $props['user'] = $user->getId();
1300 $props['user_text'] = $user->getName();
1301 $props['timestamp'] = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1302 $this->setProps( $props );
1303
1304 # Fail now if the file isn't there
1305 if ( !$this->fileExists ) {
1306 wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
1307
1308 return false;
1309 }
1310
1311 $dbw->startAtomic( __METHOD__ );
1312
1313 # Test to see if the row exists using INSERT IGNORE
1314 # This avoids race conditions by locking the row until the commit, and also
1315 # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
1316 $dbw->insert( 'image',
1317 [
1318 'img_name' => $this->getName(),
1319 'img_size' => $this->size,
1320 'img_width' => intval( $this->width ),
1321 'img_height' => intval( $this->height ),
1322 'img_bits' => $this->bits,
1323 'img_media_type' => $this->media_type,
1324 'img_major_mime' => $this->major_mime,
1325 'img_minor_mime' => $this->minor_mime,
1326 'img_timestamp' => $timestamp,
1327 'img_description' => $comment,
1328 'img_user' => $user->getId(),
1329 'img_user_text' => $user->getName(),
1330 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1331 'img_sha1' => $this->sha1
1332 ],
1333 __METHOD__,
1334 'IGNORE'
1335 );
1336
1337 $reupload = ( $dbw->affectedRows() == 0 );
1338 if ( $reupload ) {
1339 if ( $allowTimeKludge ) {
1340 # Use LOCK IN SHARE MODE to ignore any transaction snapshotting
1341 $ltimestamp = $dbw->selectField(
1342 'image',
1343 'img_timestamp',
1344 [ 'img_name' => $this->getName() ],
1345 __METHOD__,
1346 [ 'LOCK IN SHARE MODE' ]
1347 );
1348 $lUnixtime = $ltimestamp ? wfTimestamp( TS_UNIX, $ltimestamp ) : false;
1349 # Avoid a timestamp that is not newer than the last version
1350 # TODO: the image/oldimage tables should be like page/revision with an ID field
1351 if ( $lUnixtime && wfTimestamp( TS_UNIX, $timestamp ) <= $lUnixtime ) {
1352 sleep( 1 ); // fast enough re-uploads would go far in the future otherwise
1353 $timestamp = $dbw->timestamp( $lUnixtime + 1 );
1354 $this->timestamp = wfTimestamp( TS_MW, $timestamp ); // DB -> TS_MW
1355 }
1356 }
1357
1358 # (T36993) Note: $oldver can be empty here, if the previous
1359 # version of the file was broken. Allow registration of the new
1360 # version to continue anyway, because that's better than having
1361 # an image that's not fixable by user operations.
1362 # Collision, this is an update of a file
1363 # Insert previous contents into oldimage
1364 $dbw->insertSelect( 'oldimage', 'image',
1365 [
1366 'oi_name' => 'img_name',
1367 'oi_archive_name' => $dbw->addQuotes( $oldver ),
1368 'oi_size' => 'img_size',
1369 'oi_width' => 'img_width',
1370 'oi_height' => 'img_height',
1371 'oi_bits' => 'img_bits',
1372 'oi_timestamp' => 'img_timestamp',
1373 'oi_description' => 'img_description',
1374 'oi_user' => 'img_user',
1375 'oi_user_text' => 'img_user_text',
1376 'oi_metadata' => 'img_metadata',
1377 'oi_media_type' => 'img_media_type',
1378 'oi_major_mime' => 'img_major_mime',
1379 'oi_minor_mime' => 'img_minor_mime',
1380 'oi_sha1' => 'img_sha1'
1381 ],
1382 [ 'img_name' => $this->getName() ],
1383 __METHOD__
1384 );
1385
1386 # Update the current image row
1387 $dbw->update( 'image',
1388 [
1389 'img_size' => $this->size,
1390 'img_width' => intval( $this->width ),
1391 'img_height' => intval( $this->height ),
1392 'img_bits' => $this->bits,
1393 'img_media_type' => $this->media_type,
1394 'img_major_mime' => $this->major_mime,
1395 'img_minor_mime' => $this->minor_mime,
1396 'img_timestamp' => $timestamp,
1397 'img_description' => $comment,
1398 'img_user' => $user->getId(),
1399 'img_user_text' => $user->getName(),
1400 'img_metadata' => $dbw->encodeBlob( $this->metadata ),
1401 'img_sha1' => $this->sha1
1402 ],
1403 [ 'img_name' => $this->getName() ],
1404 __METHOD__
1405 );
1406 }
1407
1408 $descTitle = $this->getTitle();
1409 $descId = $descTitle->getArticleID();
1410 $wikiPage = new WikiFilePage( $descTitle );
1411 $wikiPage->setFile( $this );
1412
1413 // Add the log entry...
1414 $logEntry = new ManualLogEntry( 'upload', $reupload ? 'overwrite' : 'upload' );
1415 $logEntry->setTimestamp( $this->timestamp );
1416 $logEntry->setPerformer( $user );
1417 $logEntry->setComment( $comment );
1418 $logEntry->setTarget( $descTitle );
1419 // Allow people using the api to associate log entries with the upload.
1420 // Log has a timestamp, but sometimes different from upload timestamp.
1421 $logEntry->setParameters(
1422 [
1423 'img_sha1' => $this->sha1,
1424 'img_timestamp' => $timestamp,
1425 ]
1426 );
1427 // Note we keep $logId around since during new image
1428 // creation, page doesn't exist yet, so log_page = 0
1429 // but we want it to point to the page we're making,
1430 // so we later modify the log entry.
1431 // For a similar reason, we avoid making an RC entry
1432 // now and wait until the page exists.
1433 $logId = $logEntry->insert();
1434
1435 if ( $descTitle->exists() ) {
1436 // Use own context to get the action text in content language
1437 $formatter = LogFormatter::newFromEntry( $logEntry );
1438 $formatter->setContext( RequestContext::newExtraneousContext( $descTitle ) );
1439 $editSummary = $formatter->getPlainActionText();
1440
1441 $nullRevision = Revision::newNullRevision(
1442 $dbw,
1443 $descId,
1444 $editSummary,
1445 false,
1446 $user
1447 );
1448 if ( $nullRevision ) {
1449 $nullRevision->insertOn( $dbw );
1450 Hooks::run(
1451 'NewRevisionFromEditComplete',
1452 [ $wikiPage, $nullRevision, $nullRevision->getParentId(), $user ]
1453 );
1454 $wikiPage->updateRevisionOn( $dbw, $nullRevision );
1455 // Associate null revision id
1456 $logEntry->setAssociatedRevId( $nullRevision->getId() );
1457 }
1458
1459 $newPageContent = null;
1460 } else {
1461 // Make the description page and RC log entry post-commit
1462 $newPageContent = ContentHandler::makeContent( $pageText, $descTitle );
1463 }
1464
1465 # Defer purges, page creation, and link updates in case they error out.
1466 # The most important thing is that files and the DB registry stay synced.
1467 $dbw->endAtomic( __METHOD__ );
1468
1469 # Do some cache purges after final commit so that:
1470 # a) Changes are more likely to be seen post-purge
1471 # b) They won't cause rollback of the log publish/update above
1472 DeferredUpdates::addUpdate(
1473 new AutoCommitUpdate(
1474 $dbw,
1475 __METHOD__,
1476 function () use (
1477 $reupload, $wikiPage, $newPageContent, $comment, $user,
1478 $logEntry, $logId, $descId, $tags
1479 ) {
1480 # Update memcache after the commit
1481 $this->invalidateCache();
1482
1483 $updateLogPage = false;
1484 if ( $newPageContent ) {
1485 # New file page; create the description page.
1486 # There's already a log entry, so don't make a second RC entry
1487 # CDN and file cache for the description page are purged by doEditContent.
1488 $status = $wikiPage->doEditContent(
1489 $newPageContent,
1490 $comment,
1492 false,
1493 $user
1494 );
1495
1496 if ( isset( $status->value['revision'] ) ) {
1498 $rev = $status->value['revision'];
1499 // Associate new page revision id
1500 $logEntry->setAssociatedRevId( $rev->getId() );
1501 }
1502 // This relies on the resetArticleID() call in WikiPage::insertOn(),
1503 // which is triggered on $descTitle by doEditContent() above.
1504 if ( isset( $status->value['revision'] ) ) {
1506 $rev = $status->value['revision'];
1507 $updateLogPage = $rev->getPage();
1508 }
1509 } else {
1510 # Existing file page: invalidate description page cache
1511 $wikiPage->getTitle()->invalidateCache();
1512 $wikiPage->getTitle()->purgeSquid();
1513 # Allow the new file version to be patrolled from the page footer
1515 }
1516
1517 # Update associated rev id. This should be done by $logEntry->insert() earlier,
1518 # but setAssociatedRevId() wasn't called at that point yet...
1519 $logParams = $logEntry->getParameters();
1520 $logParams['associated_rev_id'] = $logEntry->getAssociatedRevId();
1521 $update = [ 'log_params' => LogEntryBase::makeParamBlob( $logParams ) ];
1522 if ( $updateLogPage ) {
1523 # Also log page, in case where we just created it above
1524 $update['log_page'] = $updateLogPage;
1525 }
1526 $this->getRepo()->getMasterDB()->update(
1527 'logging',
1528 $update,
1529 [ 'log_id' => $logId ],
1530 __METHOD__
1531 );
1532 $this->getRepo()->getMasterDB()->insert(
1533 'log_search',
1534 [
1535 'ls_field' => 'associated_rev_id',
1536 'ls_value' => $logEntry->getAssociatedRevId(),
1537 'ls_log_id' => $logId,
1538 ],
1539 __METHOD__
1540 );
1541
1542 # Add change tags, if any
1543 if ( $tags ) {
1544 $logEntry->setTags( $tags );
1545 }
1546
1547 # Uploads can be patrolled
1548 $logEntry->setIsPatrollable( true );
1549
1550 # Now that the log entry is up-to-date, make an RC entry.
1551 $logEntry->publish( $logId );
1552
1553 # Run hook for other updates (typically more cache purging)
1554 Hooks::run( 'FileUpload', [ $this, $reupload, !$newPageContent ] );
1555
1556 if ( $reupload ) {
1557 # Delete old thumbnails
1558 $this->purgeThumbnails();
1559 # Remove the old file from the CDN cache
1560 DeferredUpdates::addUpdate(
1561 new CdnCacheUpdate( [ $this->getUrl() ] ),
1562 DeferredUpdates::PRESEND
1563 );
1564 } else {
1565 # Update backlink pages pointing to this title if created
1566 LinksUpdate::queueRecursiveJobsForTable( $this->getTitle(), 'imagelinks' );
1567 }
1568
1569 $this->prerenderThumbnails();
1570 }
1571 ),
1572 DeferredUpdates::PRESEND
1573 );
1574
1575 if ( !$reupload ) {
1576 # This is a new file, so update the image count
1577 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
1578 }
1579
1580 # Invalidate cache for all pages using this file
1581 DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' ) );
1582
1583 return true;
1584 }
1585
1601 function publish( $src, $flags = 0, array $options = [] ) {
1602 return $this->publishTo( $src, $this->getRel(), $flags, $options );
1603 }
1604
1620 function publishTo( $src, $dstRel, $flags = 0, array $options = [] ) {
1621 $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1622
1623 $repo = $this->getRepo();
1624 if ( $repo->getReadOnlyReason() !== false ) {
1625 return $this->readOnlyFatalStatus();
1626 }
1627
1628 $this->lock(); // begin
1629
1630 $archiveName = wfTimestamp( TS_MW ) . '!' . $this->getName();
1631 $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
1632
1633 if ( $repo->hasSha1Storage() ) {
1634 $sha1 = $repo->isVirtualUrl( $srcPath )
1635 ? $repo->getFileSha1( $srcPath )
1636 : FSFile::getSha1Base36FromPath( $srcPath );
1638 $wrapperBackend = $repo->getBackend();
1639 $dst = $wrapperBackend->getPathForSHA1( $sha1 );
1640 $status = $repo->quickImport( $src, $dst );
1641 if ( $flags & File::DELETE_SOURCE ) {
1642 unlink( $srcPath );
1643 }
1644
1645 if ( $this->exists() ) {
1646 $status->value = $archiveName;
1647 }
1648 } else {
1650 $status = $repo->publish( $srcPath, $dstRel, $archiveRel, $flags, $options );
1651
1652 if ( $status->value == 'new' ) {
1653 $status->value = '';
1654 } else {
1655 $status->value = $archiveName;
1656 }
1657 }
1658
1659 $this->unlock(); // done
1660
1661 return $status;
1662 }
1663
1681 function move( $target ) {
1682 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1683 return $this->readOnlyFatalStatus();
1684 }
1685
1686 wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1687 $batch = new LocalFileMoveBatch( $this, $target );
1688
1689 $this->lock(); // begin
1690 $batch->addCurrent();
1691 $archiveNames = $batch->addOlds();
1692 $status = $batch->execute();
1693 $this->unlock(); // done
1694
1695 wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1696
1697 // Purge the source and target files...
1698 $oldTitleFile = wfLocalFile( $this->title );
1699 $newTitleFile = wfLocalFile( $target );
1700 // To avoid slow purges in the transaction, move them outside...
1701 DeferredUpdates::addUpdate(
1702 new AutoCommitUpdate(
1703 $this->getRepo()->getMasterDB(),
1704 __METHOD__,
1705 function () use ( $oldTitleFile, $newTitleFile, $archiveNames ) {
1706 $oldTitleFile->purgeEverything();
1707 foreach ( $archiveNames as $archiveName ) {
1708 $oldTitleFile->purgeOldThumbnails( $archiveName );
1709 }
1710 $newTitleFile->purgeEverything();
1711 }
1712 ),
1713 DeferredUpdates::PRESEND
1714 );
1715
1716 if ( $status->isOK() ) {
1717 // Now switch the object
1718 $this->title = $target;
1719 // Force regeneration of the name and hashpath
1720 unset( $this->name );
1721 unset( $this->hashPath );
1722 }
1723
1724 return $status;
1725 }
1726
1740 function delete( $reason, $suppress = false, $user = null ) {
1741 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1742 return $this->readOnlyFatalStatus();
1743 }
1744
1745 $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1746
1747 $this->lock(); // begin
1748 $batch->addCurrent();
1749 // Get old version relative paths
1750 $archiveNames = $batch->addOlds();
1751 $status = $batch->execute();
1752 $this->unlock(); // done
1753
1754 if ( $status->isOK() ) {
1755 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => -1 ] ) );
1756 }
1757
1758 // To avoid slow purges in the transaction, move them outside...
1759 DeferredUpdates::addUpdate(
1760 new AutoCommitUpdate(
1761 $this->getRepo()->getMasterDB(),
1762 __METHOD__,
1763 function () use ( $archiveNames ) {
1764 $this->purgeEverything();
1765 foreach ( $archiveNames as $archiveName ) {
1766 $this->purgeOldThumbnails( $archiveName );
1767 }
1768 }
1769 ),
1770 DeferredUpdates::PRESEND
1771 );
1772
1773 // Purge the CDN
1774 $purgeUrls = [];
1775 foreach ( $archiveNames as $archiveName ) {
1776 $purgeUrls[] = $this->getArchiveUrl( $archiveName );
1777 }
1778 DeferredUpdates::addUpdate( new CdnCacheUpdate( $purgeUrls ), DeferredUpdates::PRESEND );
1779
1780 return $status;
1781 }
1782
1798 function deleteOld( $archiveName, $reason, $suppress = false, $user = null ) {
1799 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1800 return $this->readOnlyFatalStatus();
1801 }
1802
1803 $batch = new LocalFileDeleteBatch( $this, $reason, $suppress, $user );
1804
1805 $this->lock(); // begin
1806 $batch->addOld( $archiveName );
1807 $status = $batch->execute();
1808 $this->unlock(); // done
1809
1810 $this->purgeOldThumbnails( $archiveName );
1811 if ( $status->isOK() ) {
1812 $this->purgeDescription();
1813 }
1814
1815 DeferredUpdates::addUpdate(
1816 new CdnCacheUpdate( [ $this->getArchiveUrl( $archiveName ) ] ),
1817 DeferredUpdates::PRESEND
1818 );
1819
1820 return $status;
1821 }
1822
1834 function restore( $versions = [], $unsuppress = false ) {
1835 if ( $this->getRepo()->getReadOnlyReason() !== false ) {
1836 return $this->readOnlyFatalStatus();
1837 }
1838
1839 $batch = new LocalFileRestoreBatch( $this, $unsuppress );
1840
1841 $this->lock(); // begin
1842 if ( !$versions ) {
1843 $batch->addAll();
1844 } else {
1845 $batch->addIds( $versions );
1846 }
1847 $status = $batch->execute();
1848 if ( $status->isGood() ) {
1849 $cleanupStatus = $batch->cleanup();
1850 $cleanupStatus->successCount = 0;
1851 $cleanupStatus->failCount = 0;
1852 $status->merge( $cleanupStatus );
1853 }
1854 $this->unlock(); // done
1855
1856 return $status;
1857 }
1858
1869 return $this->title->getLocalURL();
1870 }
1871
1880 function getDescriptionText( $lang = null ) {
1881 $revision = Revision::newFromTitle( $this->title, false, Revision::READ_NORMAL );
1882 if ( !$revision ) {
1883 return false;
1884 }
1885 $content = $revision->getContent();
1886 if ( !$content ) {
1887 return false;
1888 }
1889 $pout = $content->getParserOutput( $this->title, null, new ParserOptions( null, $lang ) );
1890
1891 return $pout->getText();
1892 }
1893
1899 function getDescription( $audience = self::FOR_PUBLIC, User $user = null ) {
1900 $this->load();
1901 if ( $audience == self::FOR_PUBLIC && $this->isDeleted( self::DELETED_COMMENT ) ) {
1902 return '';
1903 } elseif ( $audience == self::FOR_THIS_USER
1904 && !$this->userCan( self::DELETED_COMMENT, $user )
1905 ) {
1906 return '';
1907 } else {
1908 return $this->description;
1909 }
1910 }
1911
1915 function getTimestamp() {
1916 $this->load();
1917
1918 return $this->timestamp;
1919 }
1920
1924 public function getDescriptionTouched() {
1925 // The DB lookup might return false, e.g. if the file was just deleted, or the shared DB repo
1926 // itself gets it from elsewhere. To avoid repeating the DB lookups in such a case, we
1927 // need to differentiate between null (uninitialized) and false (failed to load).
1928 if ( $this->descriptionTouched === null ) {
1929 $cond = [
1930 'page_namespace' => $this->title->getNamespace(),
1931 'page_title' => $this->title->getDBkey()
1932 ];
1933 $touched = $this->repo->getReplicaDB()->selectField( 'page', 'page_touched', $cond, __METHOD__ );
1934 $this->descriptionTouched = $touched ? wfTimestamp( TS_MW, $touched ) : false;
1935 }
1936
1938 }
1939
1943 function getSha1() {
1944 $this->load();
1945 // Initialise now if necessary
1946 if ( $this->sha1 == '' && $this->fileExists ) {
1947 $this->lock(); // begin
1948
1949 $this->sha1 = $this->repo->getFileSha1( $this->getPath() );
1950 if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1951 $dbw = $this->repo->getMasterDB();
1952 $dbw->update( 'image',
1953 [ 'img_sha1' => $this->sha1 ],
1954 [ 'img_name' => $this->getName() ],
1955 __METHOD__ );
1956 $this->invalidateCache();
1957 }
1958
1959 $this->unlock(); // done
1960 }
1961
1962 return $this->sha1;
1963 }
1964
1968 function isCacheable() {
1969 $this->load();
1970
1971 // If extra data (metadata) was not loaded then it must have been large
1972 return $this->extraDataLoaded
1973 && strlen( serialize( $this->metadata ) ) <= self::CACHE_FIELD_MAX_LEN;
1974 }
1975
1980 public function acquireFileLock() {
1981 return $this->getRepo()->getBackend()->lockFiles(
1982 [ $this->getPath() ], LockManager::LOCK_EX, 10
1983 );
1984 }
1985
1990 public function releaseFileLock() {
1991 return $this->getRepo()->getBackend()->unlockFiles(
1992 [ $this->getPath() ], LockManager::LOCK_EX
1993 );
1994 }
1995
2005 public function lock() {
2006 if ( !$this->locked ) {
2007 $logger = LoggerFactory::getInstance( 'LocalFile' );
2008
2009 $dbw = $this->repo->getMasterDB();
2010 $makesTransaction = !$dbw->trxLevel();
2011 $dbw->startAtomic( self::ATOMIC_SECTION_LOCK );
2012 // T56736: use simple lock to handle when the file does not exist.
2013 // SELECT FOR UPDATE prevents changes, not other SELECTs with FOR UPDATE.
2014 // Also, that would cause contention on INSERT of similarly named rows.
2015 $status = $this->acquireFileLock(); // represents all versions of the file
2016 if ( !$status->isGood() ) {
2017 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2018 $logger->warning( "Failed to lock '{file}'", [ 'file' => $this->name ] );
2019
2020 throw new LocalFileLockError( $status );
2021 }
2022 // Release the lock *after* commit to avoid row-level contention.
2023 // Make sure it triggers on rollback() as well as commit() (T132921).
2024 $dbw->onTransactionResolution(
2025 function () use ( $logger ) {
2026 $status = $this->releaseFileLock();
2027 if ( !$status->isGood() ) {
2028 $logger->error( "Failed to unlock '{file}'", [ 'file' => $this->name ] );
2029 }
2030 },
2031 __METHOD__
2032 );
2033 // Callers might care if the SELECT snapshot is safely fresh
2034 $this->lockedOwnTrx = $makesTransaction;
2035 }
2036
2037 $this->locked++;
2038
2039 return $this->lockedOwnTrx;
2040 }
2041
2050 public function unlock() {
2051 if ( $this->locked ) {
2052 --$this->locked;
2053 if ( !$this->locked ) {
2054 $dbw = $this->repo->getMasterDB();
2055 $dbw->endAtomic( self::ATOMIC_SECTION_LOCK );
2056 $this->lockedOwnTrx = false;
2057 }
2058 }
2059 }
2060
2064 protected function readOnlyFatalStatus() {
2065 return $this->getRepo()->newFatal( 'filereadonlyerror', $this->getName(),
2066 $this->getRepo()->getName(), $this->getRepo()->getReadOnlyReason() );
2067 }
2068
2072 function __destruct() {
2073 $this->unlock();
2074 }
2075} // LocalFile class
2076
2077# ------------------------------------------------------------------------------
2078
2085 private $file;
2086
2088 private $reason;
2089
2091 private $srcRels = [];
2092
2094 private $archiveUrls = [];
2095
2098
2100 private $suppress;
2101
2103 private $status;
2104
2106 private $user;
2107
2114 function __construct( File $file, $reason = '', $suppress = false, $user = null ) {
2115 $this->file = $file;
2116 $this->reason = $reason;
2117 $this->suppress = $suppress;
2118 if ( $user ) {
2119 $this->user = $user;
2120 } else {
2122 $this->user = $wgUser;
2123 }
2124 $this->status = $file->repo->newGood();
2125 }
2126
2127 public function addCurrent() {
2128 $this->srcRels['.'] = $this->file->getRel();
2129 }
2130
2134 public function addOld( $oldName ) {
2135 $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
2136 $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
2137 }
2138
2143 public function addOlds() {
2144 $archiveNames = [];
2145
2146 $dbw = $this->file->repo->getMasterDB();
2147 $result = $dbw->select( 'oldimage',
2148 [ 'oi_archive_name' ],
2149 [ 'oi_name' => $this->file->getName() ],
2150 __METHOD__
2151 );
2152
2153 foreach ( $result as $row ) {
2154 $this->addOld( $row->oi_archive_name );
2155 $archiveNames[] = $row->oi_archive_name;
2156 }
2157
2158 return $archiveNames;
2159 }
2160
2164 protected function getOldRels() {
2165 if ( !isset( $this->srcRels['.'] ) ) {
2166 $oldRels =& $this->srcRels;
2167 $deleteCurrent = false;
2168 } else {
2169 $oldRels = $this->srcRels;
2170 unset( $oldRels['.'] );
2171 $deleteCurrent = true;
2172 }
2173
2174 return [ $oldRels, $deleteCurrent ];
2175 }
2176
2180 protected function getHashes() {
2181 $hashes = [];
2182 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2183
2184 if ( $deleteCurrent ) {
2185 $hashes['.'] = $this->file->getSha1();
2186 }
2187
2188 if ( count( $oldRels ) ) {
2189 $dbw = $this->file->repo->getMasterDB();
2190 $res = $dbw->select(
2191 'oldimage',
2192 [ 'oi_archive_name', 'oi_sha1' ],
2193 [ 'oi_archive_name' => array_keys( $oldRels ),
2194 'oi_name' => $this->file->getName() ], // performance
2195 __METHOD__
2196 );
2197
2198 foreach ( $res as $row ) {
2199 if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
2200 // Get the hash from the file
2201 $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
2202 $props = $this->file->repo->getFileProps( $oldUrl );
2203
2204 if ( $props['fileExists'] ) {
2205 // Upgrade the oldimage row
2206 $dbw->update( 'oldimage',
2207 [ 'oi_sha1' => $props['sha1'] ],
2208 [ 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ],
2209 __METHOD__ );
2210 $hashes[$row->oi_archive_name] = $props['sha1'];
2211 } else {
2212 $hashes[$row->oi_archive_name] = false;
2213 }
2214 } else {
2215 $hashes[$row->oi_archive_name] = $row->oi_sha1;
2216 }
2217 }
2218 }
2219
2220 $missing = array_diff_key( $this->srcRels, $hashes );
2221
2222 foreach ( $missing as $name => $rel ) {
2223 $this->status->error( 'filedelete-old-unregistered', $name );
2224 }
2225
2226 foreach ( $hashes as $name => $hash ) {
2227 if ( !$hash ) {
2228 $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
2229 unset( $hashes[$name] );
2230 }
2231 }
2232
2233 return $hashes;
2234 }
2235
2236 protected function doDBInserts() {
2237 $now = time();
2238 $dbw = $this->file->repo->getMasterDB();
2239 $encTimestamp = $dbw->addQuotes( $dbw->timestamp( $now ) );
2240 $encUserId = $dbw->addQuotes( $this->user->getId() );
2241 $encReason = $dbw->addQuotes( $this->reason );
2242 $encGroup = $dbw->addQuotes( 'deleted' );
2243 $ext = $this->file->getExtension();
2244 $dotExt = $ext === '' ? '' : ".$ext";
2245 $encExt = $dbw->addQuotes( $dotExt );
2246 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2247
2248 // Bitfields to further suppress the content
2249 if ( $this->suppress ) {
2250 $bitfield = Revision::SUPPRESSED_ALL;
2251 } else {
2252 $bitfield = 'oi_deleted';
2253 }
2254
2255 if ( $deleteCurrent ) {
2256 $dbw->insertSelect(
2257 'filearchive',
2258 'image',
2259 [
2260 'fa_storage_group' => $encGroup,
2261 'fa_storage_key' => $dbw->conditional(
2262 [ 'img_sha1' => '' ],
2263 $dbw->addQuotes( '' ),
2264 $dbw->buildConcat( [ "img_sha1", $encExt ] )
2265 ),
2266 'fa_deleted_user' => $encUserId,
2267 'fa_deleted_timestamp' => $encTimestamp,
2268 'fa_deleted_reason' => $encReason,
2269 'fa_deleted' => $this->suppress ? $bitfield : 0,
2270 'fa_name' => 'img_name',
2271 'fa_archive_name' => 'NULL',
2272 'fa_size' => 'img_size',
2273 'fa_width' => 'img_width',
2274 'fa_height' => 'img_height',
2275 'fa_metadata' => 'img_metadata',
2276 'fa_bits' => 'img_bits',
2277 'fa_media_type' => 'img_media_type',
2278 'fa_major_mime' => 'img_major_mime',
2279 'fa_minor_mime' => 'img_minor_mime',
2280 'fa_description' => 'img_description',
2281 'fa_user' => 'img_user',
2282 'fa_user_text' => 'img_user_text',
2283 'fa_timestamp' => 'img_timestamp',
2284 'fa_sha1' => 'img_sha1'
2285 ],
2286 [ 'img_name' => $this->file->getName() ],
2287 __METHOD__
2288 );
2289 }
2290
2291 if ( count( $oldRels ) ) {
2292 $res = $dbw->select(
2293 'oldimage',
2295 [
2296 'oi_name' => $this->file->getName(),
2297 'oi_archive_name' => array_keys( $oldRels )
2298 ],
2299 __METHOD__,
2300 [ 'FOR UPDATE' ]
2301 );
2302 $rowsInsert = [];
2303 foreach ( $res as $row ) {
2304 $rowsInsert[] = [
2305 // Deletion-specific fields
2306 'fa_storage_group' => 'deleted',
2307 'fa_storage_key' => ( $row->oi_sha1 === '' )
2308 ? ''
2309 : "{$row->oi_sha1}{$dotExt}",
2310 'fa_deleted_user' => $this->user->getId(),
2311 'fa_deleted_timestamp' => $dbw->timestamp( $now ),
2312 'fa_deleted_reason' => $this->reason,
2313 // Counterpart fields
2314 'fa_deleted' => $this->suppress ? $bitfield : $row->oi_deleted,
2315 'fa_name' => $row->oi_name,
2316 'fa_archive_name' => $row->oi_archive_name,
2317 'fa_size' => $row->oi_size,
2318 'fa_width' => $row->oi_width,
2319 'fa_height' => $row->oi_height,
2320 'fa_metadata' => $row->oi_metadata,
2321 'fa_bits' => $row->oi_bits,
2322 'fa_media_type' => $row->oi_media_type,
2323 'fa_major_mime' => $row->oi_major_mime,
2324 'fa_minor_mime' => $row->oi_minor_mime,
2325 'fa_description' => $row->oi_description,
2326 'fa_user' => $row->oi_user,
2327 'fa_user_text' => $row->oi_user_text,
2328 'fa_timestamp' => $row->oi_timestamp,
2329 'fa_sha1' => $row->oi_sha1
2330 ];
2331 }
2332
2333 $dbw->insert( 'filearchive', $rowsInsert, __METHOD__ );
2334 }
2335 }
2336
2337 function doDBDeletes() {
2338 $dbw = $this->file->repo->getMasterDB();
2339 list( $oldRels, $deleteCurrent ) = $this->getOldRels();
2340
2341 if ( count( $oldRels ) ) {
2342 $dbw->delete( 'oldimage',
2343 [
2344 'oi_name' => $this->file->getName(),
2345 'oi_archive_name' => array_keys( $oldRels )
2346 ], __METHOD__ );
2347 }
2348
2349 if ( $deleteCurrent ) {
2350 $dbw->delete( 'image', [ 'img_name' => $this->file->getName() ], __METHOD__ );
2351 }
2352 }
2353
2358 public function execute() {
2359 $repo = $this->file->getRepo();
2360 $this->file->lock();
2361
2362 // Prepare deletion batch
2363 $hashes = $this->getHashes();
2364 $this->deletionBatch = [];
2365 $ext = $this->file->getExtension();
2366 $dotExt = $ext === '' ? '' : ".$ext";
2367
2368 foreach ( $this->srcRels as $name => $srcRel ) {
2369 // Skip files that have no hash (e.g. missing DB record, or sha1 field and file source)
2370 if ( isset( $hashes[$name] ) ) {
2371 $hash = $hashes[$name];
2372 $key = $hash . $dotExt;
2373 $dstRel = $repo->getDeletedHashPath( $key ) . $key;
2374 $this->deletionBatch[$name] = [ $srcRel, $dstRel ];
2375 }
2376 }
2377
2378 if ( !$repo->hasSha1Storage() ) {
2379 // Removes non-existent file from the batch, so we don't get errors.
2380 // This also handles files in the 'deleted' zone deleted via revision deletion.
2381 $checkStatus = $this->removeNonexistentFiles( $this->deletionBatch );
2382 if ( !$checkStatus->isGood() ) {
2383 $this->status->merge( $checkStatus );
2384 return $this->status;
2385 }
2386 $this->deletionBatch = $checkStatus->value;
2387
2388 // Execute the file deletion batch
2389 $status = $this->file->repo->deleteBatch( $this->deletionBatch );
2390 if ( !$status->isGood() ) {
2391 $this->status->merge( $status );
2392 }
2393 }
2394
2395 if ( !$this->status->isOK() ) {
2396 // Critical file deletion error; abort
2397 $this->file->unlock();
2398
2399 return $this->status;
2400 }
2401
2402 // Copy the image/oldimage rows to filearchive
2403 $this->doDBInserts();
2404 // Delete image/oldimage rows
2405 $this->doDBDeletes();
2406
2407 // Commit and return
2408 $this->file->unlock();
2409
2410 return $this->status;
2411 }
2412
2418 protected function removeNonexistentFiles( $batch ) {
2419 $files = $newBatch = [];
2420
2421 foreach ( $batch as $batchItem ) {
2422 list( $src, ) = $batchItem;
2423 $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
2424 }
2425
2426 $result = $this->file->repo->fileExistsBatch( $files );
2427 if ( in_array( null, $result, true ) ) {
2428 return Status::newFatal( 'backend-fail-internal',
2429 $this->file->repo->getBackend()->getName() );
2430 }
2431
2432 foreach ( $batch as $batchItem ) {
2433 if ( $result[$batchItem[0]] ) {
2434 $newBatch[] = $batchItem;
2435 }
2436 }
2437
2438 return Status::newGood( $newBatch );
2439 }
2440}
2441
2442# ------------------------------------------------------------------------------
2443
2450 private $file;
2451
2454
2456 private $ids;
2457
2459 private $all;
2460
2462 private $unsuppress = false;
2463
2468 function __construct( File $file, $unsuppress = false ) {
2469 $this->file = $file;
2470 $this->cleanupBatch = $this->ids = [];
2471 $this->ids = [];
2472 $this->unsuppress = $unsuppress;
2473 }
2474
2479 public function addId( $fa_id ) {
2480 $this->ids[] = $fa_id;
2481 }
2482
2487 public function addIds( $ids ) {
2488 $this->ids = array_merge( $this->ids, $ids );
2489 }
2490
2494 public function addAll() {
2495 $this->all = true;
2496 }
2497
2506 public function execute() {
2509
2510 $repo = $this->file->getRepo();
2511 if ( !$this->all && !$this->ids ) {
2512 // Do nothing
2513 return $repo->newGood();
2514 }
2515
2516 $lockOwnsTrx = $this->file->lock();
2517
2518 $dbw = $this->file->repo->getMasterDB();
2519 $status = $this->file->repo->newGood();
2520
2521 $exists = (bool)$dbw->selectField( 'image', '1',
2522 [ 'img_name' => $this->file->getName() ],
2523 __METHOD__,
2524 // The lock() should already prevents changes, but this still may need
2525 // to bypass any transaction snapshot. However, if lock() started the
2526 // trx (which it probably did) then snapshot is post-lock and up-to-date.
2527 $lockOwnsTrx ? [] : [ 'LOCK IN SHARE MODE' ]
2528 );
2529
2530 // Fetch all or selected archived revisions for the file,
2531 // sorted from the most recent to the oldest.
2532 $conditions = [ 'fa_name' => $this->file->getName() ];
2533
2534 if ( !$this->all ) {
2535 $conditions['fa_id'] = $this->ids;
2536 }
2537
2538 $result = $dbw->select(
2539 'filearchive',
2541 $conditions,
2542 __METHOD__,
2543 [ 'ORDER BY' => 'fa_timestamp DESC' ]
2544 );
2545
2546 $idsPresent = [];
2547 $storeBatch = [];
2548 $insertBatch = [];
2549 $insertCurrent = false;
2550 $deleteIds = [];
2551 $first = true;
2552 $archiveNames = [];
2553
2554 foreach ( $result as $row ) {
2555 $idsPresent[] = $row->fa_id;
2556
2557 if ( $row->fa_name != $this->file->getName() ) {
2558 $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
2559 $status->failCount++;
2560 continue;
2561 }
2562
2563 if ( $row->fa_storage_key == '' ) {
2564 // Revision was missing pre-deletion
2565 $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
2566 $status->failCount++;
2567 continue;
2568 }
2569
2570 $deletedRel = $repo->getDeletedHashPath( $row->fa_storage_key ) .
2571 $row->fa_storage_key;
2572 $deletedUrl = $repo->getVirtualUrl() . '/deleted/' . $deletedRel;
2573
2574 if ( isset( $row->fa_sha1 ) ) {
2575 $sha1 = $row->fa_sha1;
2576 } else {
2577 // old row, populate from key
2578 $sha1 = LocalRepo::getHashFromKey( $row->fa_storage_key );
2579 }
2580
2581 # Fix leading zero
2582 if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
2583 $sha1 = substr( $sha1, 1 );
2584 }
2585
2586 if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
2587 || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
2588 || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
2589 || is_null( $row->fa_metadata )
2590 ) {
2591 // Refresh our metadata
2592 // Required for a new current revision; nice for older ones too. :)
2593 $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
2594 } else {
2595 $props = [
2596 'minor_mime' => $row->fa_minor_mime,
2597 'major_mime' => $row->fa_major_mime,
2598 'media_type' => $row->fa_media_type,
2599 'metadata' => $row->fa_metadata
2600 ];
2601 }
2602
2603 if ( $first && !$exists ) {
2604 // This revision will be published as the new current version
2605 $destRel = $this->file->getRel();
2606 $insertCurrent = [
2607 'img_name' => $row->fa_name,
2608 'img_size' => $row->fa_size,
2609 'img_width' => $row->fa_width,
2610 'img_height' => $row->fa_height,
2611 'img_metadata' => $props['metadata'],
2612 'img_bits' => $row->fa_bits,
2613 'img_media_type' => $props['media_type'],
2614 'img_major_mime' => $props['major_mime'],
2615 'img_minor_mime' => $props['minor_mime'],
2616 'img_description' => $row->fa_description,
2617 'img_user' => $row->fa_user,
2618 'img_user_text' => $row->fa_user_text,
2619 'img_timestamp' => $row->fa_timestamp,
2620 'img_sha1' => $sha1
2621 ];
2622
2623 // The live (current) version cannot be hidden!
2624 if ( !$this->unsuppress && $row->fa_deleted ) {
2625 $status->fatal( 'undeleterevdel' );
2626 $this->file->unlock();
2627 return $status;
2628 }
2629 } else {
2630 $archiveName = $row->fa_archive_name;
2631
2632 if ( $archiveName == '' ) {
2633 // This was originally a current version; we
2634 // have to devise a new archive name for it.
2635 // Format is <timestamp of archiving>!<name>
2636 $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
2637
2638 do {
2639 $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
2640 $timestamp++;
2641 } while ( isset( $archiveNames[$archiveName] ) );
2642 }
2643
2644 $archiveNames[$archiveName] = true;
2645 $destRel = $this->file->getArchiveRel( $archiveName );
2646 $insertBatch[] = [
2647 'oi_name' => $row->fa_name,
2648 'oi_archive_name' => $archiveName,
2649 'oi_size' => $row->fa_size,
2650 'oi_width' => $row->fa_width,
2651 'oi_height' => $row->fa_height,
2652 'oi_bits' => $row->fa_bits,
2653 'oi_description' => $row->fa_description,
2654 'oi_user' => $row->fa_user,
2655 'oi_user_text' => $row->fa_user_text,
2656 'oi_timestamp' => $row->fa_timestamp,
2657 'oi_metadata' => $props['metadata'],
2658 'oi_media_type' => $props['media_type'],
2659 'oi_major_mime' => $props['major_mime'],
2660 'oi_minor_mime' => $props['minor_mime'],
2661 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
2662 'oi_sha1' => $sha1 ];
2663 }
2664
2665 $deleteIds[] = $row->fa_id;
2666
2667 if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
2668 // private files can stay where they are
2669 $status->successCount++;
2670 } else {
2671 $storeBatch[] = [ $deletedUrl, 'public', $destRel ];
2672 $this->cleanupBatch[] = $row->fa_storage_key;
2673 }
2674
2675 $first = false;
2676 }
2677
2678 unset( $result );
2679
2680 // Add a warning to the status object for missing IDs
2681 $missingIds = array_diff( $this->ids, $idsPresent );
2682
2683 foreach ( $missingIds as $id ) {
2684 $status->error( 'undelete-missing-filearchive', $id );
2685 }
2686
2687 if ( !$repo->hasSha1Storage() ) {
2688 // Remove missing files from batch, so we don't get errors when undeleting them
2689 $checkStatus = $this->removeNonexistentFiles( $storeBatch );
2690 if ( !$checkStatus->isGood() ) {
2691 $status->merge( $checkStatus );
2692 return $status;
2693 }
2694 $storeBatch = $checkStatus->value;
2695
2696 // Run the store batch
2697 // Use the OVERWRITE_SAME flag to smooth over a common error
2698 $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
2699 $status->merge( $storeStatus );
2700
2701 if ( !$status->isGood() ) {
2702 // Even if some files could be copied, fail entirely as that is the
2703 // easiest thing to do without data loss
2704 $this->cleanupFailedBatch( $storeStatus, $storeBatch );
2705 $status->setOK( false );
2706 $this->file->unlock();
2707
2708 return $status;
2709 }
2710 }
2711
2712 // Run the DB updates
2713 // Because we have locked the image row, key conflicts should be rare.
2714 // If they do occur, we can roll back the transaction at this time with
2715 // no data loss, but leaving unregistered files scattered throughout the
2716 // public zone.
2717 // This is not ideal, which is why it's important to lock the image row.
2718 if ( $insertCurrent ) {
2719 $dbw->insert( 'image', $insertCurrent, __METHOD__ );
2720 }
2721
2722 if ( $insertBatch ) {
2723 $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
2724 }
2725
2726 if ( $deleteIds ) {
2727 $dbw->delete( 'filearchive',
2728 [ 'fa_id' => $deleteIds ],
2729 __METHOD__ );
2730 }
2731
2732 // If store batch is empty (all files are missing), deletion is to be considered successful
2733 if ( $status->successCount > 0 || !$storeBatch || $repo->hasSha1Storage() ) {
2734 if ( !$exists ) {
2735 wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
2736
2737 DeferredUpdates::addUpdate( SiteStatsUpdate::factory( [ 'images' => 1 ] ) );
2738
2739 $this->file->purgeEverything();
2740 } else {
2741 wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
2742 $this->file->purgeDescription();
2743 }
2744 }
2745
2746 $this->file->unlock();
2747
2748 return $status;
2749 }
2750
2756 protected function removeNonexistentFiles( $triplets ) {
2757 $files = $filteredTriplets = [];
2758 foreach ( $triplets as $file ) {
2759 $files[$file[0]] = $file[0];
2760 }
2761
2762 $result = $this->file->repo->fileExistsBatch( $files );
2763 if ( in_array( null, $result, true ) ) {
2764 return Status::newFatal( 'backend-fail-internal',
2765 $this->file->repo->getBackend()->getName() );
2766 }
2767
2768 foreach ( $triplets as $file ) {
2769 if ( $result[$file[0]] ) {
2770 $filteredTriplets[] = $file;
2771 }
2772 }
2773
2774 return Status::newGood( $filteredTriplets );
2775 }
2776
2782 protected function removeNonexistentFromCleanup( $batch ) {
2783 $files = $newBatch = [];
2784 $repo = $this->file->repo;
2785
2786 foreach ( $batch as $file ) {
2787 $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
2788 rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
2789 }
2790
2791 $result = $repo->fileExistsBatch( $files );
2792
2793 foreach ( $batch as $file ) {
2794 if ( $result[$file] ) {
2795 $newBatch[] = $file;
2796 }
2797 }
2798
2799 return $newBatch;
2800 }
2801
2807 public function cleanup() {
2808 if ( !$this->cleanupBatch ) {
2809 return $this->file->repo->newGood();
2810 }
2811
2812 $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
2813
2814 $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
2815
2816 return $status;
2817 }
2818
2826 protected function cleanupFailedBatch( $storeStatus, $storeBatch ) {
2827 $cleanupBatch = [];
2828
2829 foreach ( $storeStatus->success as $i => $success ) {
2830 // Check if this item of the batch was successfully copied
2831 if ( $success ) {
2832 // Item was successfully copied and needs to be removed again
2833 // Extract ($dstZone, $dstRel) from the batch
2834 $cleanupBatch[] = [ $storeBatch[$i][1], $storeBatch[$i][2] ];
2835 }
2836 }
2837 $this->file->repo->cleanupBatch( $cleanupBatch );
2838 }
2839}
2840
2841# ------------------------------------------------------------------------------
2842
2849 protected $file;
2850
2852 protected $target;
2853
2854 protected $cur;
2855
2856 protected $olds;
2857
2858 protected $oldCount;
2859
2860 protected $archive;
2861
2863 protected $db;
2864
2870 $this->file = $file;
2871 $this->target = $target;
2872 $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
2873 $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
2874 $this->oldName = $this->file->getName();
2875 $this->newName = $this->file->repo->getNameFromTitle( $this->target );
2876 $this->oldRel = $this->oldHash . $this->oldName;
2877 $this->newRel = $this->newHash . $this->newName;
2878 $this->db = $file->getRepo()->getMasterDB();
2879 }
2880
2884 public function addCurrent() {
2885 $this->cur = [ $this->oldRel, $this->newRel ];
2886 }
2887
2892 public function addOlds() {
2893 $archiveBase = 'archive';
2894 $this->olds = [];
2895 $this->oldCount = 0;
2896 $archiveNames = [];
2897
2898 $result = $this->db->select( 'oldimage',
2899 [ 'oi_archive_name', 'oi_deleted' ],
2900 [ 'oi_name' => $this->oldName ],
2901 __METHOD__,
2902 [ 'LOCK IN SHARE MODE' ] // ignore snapshot
2903 );
2904
2905 foreach ( $result as $row ) {
2906 $archiveNames[] = $row->oi_archive_name;
2907 $oldName = $row->oi_archive_name;
2908 $bits = explode( '!', $oldName, 2 );
2909
2910 if ( count( $bits ) != 2 ) {
2911 wfDebug( "Old file name missing !: '$oldName' \n" );
2912 continue;
2913 }
2914
2915 list( $timestamp, $filename ) = $bits;
2916
2917 if ( $this->oldName != $filename ) {
2918 wfDebug( "Old file name doesn't match: '$oldName' \n" );
2919 continue;
2920 }
2921
2922 $this->oldCount++;
2923
2924 // Do we want to add those to oldCount?
2925 if ( $row->oi_deleted & File::DELETED_FILE ) {
2926 continue;
2927 }
2928
2929 $this->olds[] = [
2930 "{$archiveBase}/{$this->oldHash}{$oldName}",
2931 "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
2932 ];
2933 }
2934
2935 return $archiveNames;
2936 }
2937
2942 public function execute() {
2943 $repo = $this->file->repo;
2944 $status = $repo->newGood();
2945 $destFile = wfLocalFile( $this->target );
2946
2947 $this->file->lock(); // begin
2948 $destFile->lock(); // quickly fail if destination is not available
2949
2950 $triplets = $this->getMoveTriplets();
2951 $checkStatus = $this->removeNonexistentFiles( $triplets );
2952 if ( !$checkStatus->isGood() ) {
2953 $destFile->unlock();
2954 $this->file->unlock();
2955 $status->merge( $checkStatus ); // couldn't talk to file backend
2956 return $status;
2957 }
2958 $triplets = $checkStatus->value;
2959
2960 // Verify the file versions metadata in the DB.
2961 $statusDb = $this->verifyDBUpdates();
2962 if ( !$statusDb->isGood() ) {
2963 $destFile->unlock();
2964 $this->file->unlock();
2965 $statusDb->setOK( false );
2966
2967 return $statusDb;
2968 }
2969
2970 if ( !$repo->hasSha1Storage() ) {
2971 // Copy the files into their new location.
2972 // If a prior process fataled copying or cleaning up files we tolerate any
2973 // of the existing files if they are identical to the ones being stored.
2974 $statusMove = $repo->storeBatch( $triplets, FileRepo::OVERWRITE_SAME );
2975 wfDebugLog( 'imagemove', "Moved files for {$this->file->getName()}: " .
2976 "{$statusMove->successCount} successes, {$statusMove->failCount} failures" );
2977 if ( !$statusMove->isGood() ) {
2978 // Delete any files copied over (while the destination is still locked)
2979 $this->cleanupTarget( $triplets );
2980 $destFile->unlock();
2981 $this->file->unlock();
2982 wfDebugLog( 'imagemove', "Error in moving files: "
2983 . $statusMove->getWikiText( false, false, 'en' ) );
2984 $statusMove->setOK( false );
2985
2986 return $statusMove;
2987 }
2988 $status->merge( $statusMove );
2989 }
2990
2991 // Rename the file versions metadata in the DB.
2992 $this->doDBUpdates();
2993
2994 wfDebugLog( 'imagemove', "Renamed {$this->file->getName()} in database: " .
2995 "{$statusDb->successCount} successes, {$statusDb->failCount} failures" );
2996
2997 $destFile->unlock();
2998 $this->file->unlock(); // done
2999
3000 // Everything went ok, remove the source files
3001 $this->cleanupSource( $triplets );
3002
3003 $status->merge( $statusDb );
3004
3005 return $status;
3006 }
3007
3014 protected function verifyDBUpdates() {
3015 $repo = $this->file->repo;
3016 $status = $repo->newGood();
3017 $dbw = $this->db;
3018
3019 $hasCurrent = $dbw->selectField(
3020 'image',
3021 '1',
3022 [ 'img_name' => $this->oldName ],
3023 __METHOD__,
3024 [ 'FOR UPDATE' ]
3025 );
3026 $oldRowCount = $dbw->selectField(
3027 'oldimage',
3028 'COUNT(*)',
3029 [ 'oi_name' => $this->oldName ],
3030 __METHOD__,
3031 [ 'FOR UPDATE' ]
3032 );
3033
3034 if ( $hasCurrent ) {
3035 $status->successCount++;
3036 } else {
3037 $status->failCount++;
3038 }
3039 $status->successCount += $oldRowCount;
3040 // T36934: oldCount is based on files that actually exist.
3041 // There may be more DB rows than such files, in which case $affected
3042 // can be greater than $total. We use max() to avoid negatives here.
3043 $status->failCount += max( 0, $this->oldCount - $oldRowCount );
3044 if ( $status->failCount ) {
3045 $status->error( 'imageinvalidfilename' );
3046 }
3047
3048 return $status;
3049 }
3050
3055 protected function doDBUpdates() {
3056 $dbw = $this->db;
3057
3058 // Update current image
3059 $dbw->update(
3060 'image',
3061 [ 'img_name' => $this->newName ],
3062 [ 'img_name' => $this->oldName ],
3063 __METHOD__
3064 );
3065 // Update old images
3066 $dbw->update(
3067 'oldimage',
3068 [
3069 'oi_name' => $this->newName,
3070 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name',
3071 $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
3072 ],
3073 [ 'oi_name' => $this->oldName ],
3074 __METHOD__
3075 );
3076 }
3077
3082 protected function getMoveTriplets() {
3083 $moves = array_merge( [ $this->cur ], $this->olds );
3084 $triplets = []; // The format is: (srcUrl, destZone, destUrl)
3085
3086 foreach ( $moves as $move ) {
3087 // $move: (oldRelativePath, newRelativePath)
3088 $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
3089 $triplets[] = [ $srcUrl, 'public', $move[1] ];
3090 wfDebugLog(
3091 'imagemove',
3092 "Generated move triplet for {$this->file->getName()}: {$srcUrl} :: public :: {$move[1]}"
3093 );
3094 }
3095
3096 return $triplets;
3097 }
3098
3104 protected function removeNonexistentFiles( $triplets ) {
3105 $files = [];
3106
3107 foreach ( $triplets as $file ) {
3108 $files[$file[0]] = $file[0];
3109 }
3110
3111 $result = $this->file->repo->fileExistsBatch( $files );
3112 if ( in_array( null, $result, true ) ) {
3113 return Status::newFatal( 'backend-fail-internal',
3114 $this->file->repo->getBackend()->getName() );
3115 }
3116
3117 $filteredTriplets = [];
3118 foreach ( $triplets as $file ) {
3119 if ( $result[$file[0]] ) {
3120 $filteredTriplets[] = $file;
3121 } else {
3122 wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
3123 }
3124 }
3125
3126 return Status::newGood( $filteredTriplets );
3127 }
3128
3134 protected function cleanupTarget( $triplets ) {
3135 // Create dest pairs from the triplets
3136 $pairs = [];
3137 foreach ( $triplets as $triplet ) {
3138 // $triplet: (old source virtual URL, dst zone, dest rel)
3139 $pairs[] = [ $triplet[1], $triplet[2] ];
3140 }
3141
3142 $this->file->repo->cleanupBatch( $pairs );
3143 }
3144
3150 protected function cleanupSource( $triplets ) {
3151 // Create source file names from the triplets
3152 $files = [];
3153 foreach ( $triplets as $triplet ) {
3154 $files[] = $triplet[0];
3155 }
3156
3157 $this->file->repo->cleanupBatch( $files );
3158 }
3159}
3160
3162 public function __construct( Status $status ) {
3163 parent::__construct(
3164 'actionfailed',
3165 $status->getMessage()
3166 );
3167 }
3168
3169 public function report() {
3170 global $wgOut;
3171 $wgOut->setStatusCode( 429 );
3172 parent::report();
3173 }
3174}
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
serialize()
$wgUpdateCompatibleMetadata
If to automatically update the img_metadata field if the metadata field is outdated but compatible wi...
$wgUploadThumbnailRenderMap
When defined, is an array of thumbnail widths to be rendered at upload time.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfReadOnly()
Check whether the wiki is in read-only mode.
wfLocalFile( $title)
Get an object referring to a locally registered file.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
$wgUser
Definition Setup.php:781
if(!defined( 'MEDIAWIKI')) $fname
This file is not a valid entry point, perform no further processing unless MEDIAWIKI is defined.
Definition Setup.php:36
$wgOut
Definition Setup.php:791
static selectFields()
Fields in the filearchive table.
static purgePatrolFooterCache( $articleID)
Purge the cache used to check if it is worth showing the patrol footer For example,...
Definition Article.php:1112
Deferrable Update for closure/callback updates that should use auto-commit mode.
Handles purging appropriate CDN URLs given a title (or titles)
static makeContent( $text, Title $title=null, $modelId=null, $format=null)
Convenience function for creating a Content object from a given textual representation.
An error page which can definitely be safely rendered using the OutputPage.
Class representing a non-directory file on the file system.
Definition FSFile.php:29
static getSha1Base36FromPath( $path)
Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case encoding,...
Definition FSFile.php:218
File backend exception for checked exceptions (e.g.
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
const OVERWRITE_SAME
Definition FileRepo.php:40
getFileSha1( $virtualUrl)
Get the sha1 (base 36) of a file with a given virtual URL/storage path.
hasSha1Storage()
Returns whether or not storage is SHA-1 based.
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...
const DELETE_SOURCE
Definition FileRepo.php:38
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Definition FileRepo.php:254
getReadOnlyReason()
Get an explanatory message if this repo is read-only.
Definition FileRepo.php:225
quickImport( $src, $dst, $options=null)
Import a file from the local file system into the repo.
Definition FileRepo.php:957
getBackend()
Get the file backend instance.
Definition FileRepo.php:215
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:51
string $url
The URL corresponding to one of the four basic zones.
Definition File.php:117
isVectorized()
Return true if the file is vectorized.
Definition File.php:555
MediaHandler $handler
Definition File.php:114
purgeDescription()
Purge the file description page, but don't go after pages using the file.
Definition File.php:1429
getPath()
Return the storage path to the file.
Definition File.php:417
getThumbPath( $suffix=false)
Get the path of the thumbnail directory, or a particular file if $suffix is specified.
Definition File.php:1611
getRel()
Get the path of the file relative to the public zone root.
Definition File.php:1512
assertRepoDefined()
Assert that $this->repo is set to a valid FileRepo instance.
Definition File.php:2248
getName()
Return the name of this file.
Definition File.php:297
const DELETE_SOURCE
Definition File.php:66
getThumbUrl( $suffix=false)
Get the URL of the thumbnail directory, or a particular file if $suffix is specified.
Definition File.php:1693
getVirtualUrl( $suffix=false)
Get the public zone virtual URL for a current version source file.
Definition File.php:1713
getRepo()
Returns the repository.
Definition File.php:1854
assertTitleDefined()
Assert that $this->title is set to a Title.
Definition File.php:2258
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
getTitle()
Return the associated title object.
Definition File.php:326
isMultipage()
Returns 'true' if this file is a type which supports multiple pages, e.g.
Definition File.php:1959
getThumbnails()
Get all thumbnail names previously generated for this file STUB Overridden by LocalFile.
Definition File.php:1410
string $name
The name of a file from its title object.
Definition File.php:123
FileRepo LocalRepo ForeignAPIRepo bool $repo
Some member variables can be lazy-initialised using __get().
Definition File.php:96
purgeEverything()
Purge metadata and all affected pages when the file is created, deleted, or majorly updated.
Definition File.php:1441
const DELETED_FILE
Definition File.php:53
static splitMime( $mime)
Split an internet media type into its two components; if not a two-part name, set the minor type to '...
Definition File.php:273
getUrl()
Return the URL of the file.
Definition File.php:348
Title string bool $title
Definition File.php:99
getArchiveUrl( $suffix=false)
Get the URL of the archive directory, or a particular file if $suffix is specified.
Definition File.php:1635
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
getHandler()
Get a MediaHandler instance for this file.
Definition File.php:1365
isDeleted( $field)
Is this file a "deleted" file in a private archive? STUB.
Definition File.php:1875
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
getHashPath()
Get the filename hash component of the directory including trailing slash, e.g.
Definition File.php:1497
Class to invalidate the HTML cache of all the pages linking to a given title.
static singleton( $wiki=false)
Internationalisation code.
Definition Language.php:35
static queueRecursiveJobsForTable(Title $title, $table)
Queue a RefreshLinks job for any table.
Helper class for file deletion.
addOlds()
Add the old versions of the image to the batch.
removeNonexistentFiles( $batch)
Removes non-existent files from a deletion batch.
array $deletionBatch
Items to be processed in the deletion batch.
__construct(File $file, $reason='', $suppress=false, $user=null)
bool $suppress
Whether to suppress all suppressable fields when deleting.
execute()
Run the transaction.
__construct(Status $status)
report()
Output a report about the exception and takes care of formatting.
Helper class for file movement.
cleanupTarget( $triplets)
Cleanup a partially moved array of triplets by deleting the target files.
addOlds()
Add the old versions of the image to the batch.
doDBUpdates()
Do the database updates and return a new Status indicating how many rows where updated.
getMoveTriplets()
Generate triplets for FileRepo::storeBatch().
__construct(File $file, Title $target)
execute()
Perform the move.
verifyDBUpdates()
Verify the database updates and return a new Status indicating how many rows would be updated.
removeNonexistentFiles( $triplets)
Removes non-existent files from move batch.
addCurrent()
Add the current image to the batch.
cleanupSource( $triplets)
Cleanup a fully moved array of triplets by deleting the source files.
Helper class for file undeletion.
execute()
Run the transaction, except the cleanup batch.
addIds( $ids)
Add a whole lot of files by ID.
addAll()
Add all revisions of the file.
bool $unsuppress
Whether to remove all settings for suppressed fields.
removeNonexistentFromCleanup( $batch)
Removes non-existent files from a cleanup batch.
array $ids
List of file IDs to restore.
addId( $fa_id)
Add a file by ID.
cleanup()
Delete unused files in the deleted zone.
removeNonexistentFiles( $triplets)
Removes non-existent files from a store batch.
cleanupFailedBatch( $storeStatus, $storeBatch)
Cleanup a failed batch.
bool $all
Add all revisions of the file.
array $cleanupBatch
List of file IDs to restore.
__construct(File $file, $unsuppress=false)
Class to represent a local file in the wiki's own database.
Definition LocalFile.php:45
exists()
canRender inherited
setProps( $info)
Set properties in this object to be equal to those given in the associative array $info.
string $major_mime
Major MIME type.
Definition LocalFile.php:96
maybeUpgradeRow()
Upgrade a row if it needs it.
static newFromKey( $sha1, $repo, $timestamp=false)
Create a LocalFile from a SHA-1 key Do not call this except from inside a repo class.
getMediaType()
Returns the type of the media in the file.
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.
move( $target)
getLinksTo inherited
lock()
Start an atomic DB section and lock the image for update or increments a reference counter if the loc...
loadFromRow( $row, $prefix='img_')
Load file metadata from a DB result row.
getCacheKey()
Get the memcached key for the main data for this file, or false if there is no access to the shared c...
getWidth( $page=1)
Return the width of the image.
__destruct()
Clean up any dangling locks.
const VERSION
Definition LocalFile.php:46
string $mime
MIME type, determined by MimeMagic::guessMimeType.
Definition LocalFile.php:66
isMissing()
splitMime inherited
deleteOld( $archiveName, $reason, $suppress=false, $user=null)
Delete an old version of the file.
getDescriptionUrl()
isMultipage inherited
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
getMutableCacheKeys(WANObjectCache $cache)
static selectFields()
Fields in the image table.
getDescription( $audience=self::FOR_PUBLIC, User $user=null)
loadFromDB( $flags=0)
Load file metadata from the DB.
load( $flags=0)
Load file metadata from cache or DB, unless already loaded.
bool $upgrading
Whether the row was scheduled to upgrade on load.
string $media_type
MEDIATYPE_xxx (bitmap, drawing, audio...)
Definition LocalFile.php:63
loadFromFile()
Load metadata from the file itself.
purgeCache( $options=[])
Delete all previously generated thumbnails, refresh metadata in memcached and purge the CDN.
getDescriptionTouched()
const LOAD_ALL
int $size
Size in bytes (loadFromXxx)
Definition LocalFile.php:69
string $minor_mime
Minor MIME type.
Definition LocalFile.php:99
int $user
User ID of uploader.
getDescriptionShortUrl()
Get short description URL for a file based on the page ID.
getThumbnails( $archiveName=false)
getTransformScript inherited
bool $upgraded
Whether the row was upgraded on load.
static newFromTitle( $title, $repo, $unused=null)
Create a LocalFile from a title Do not call this except from inside a repo class.
purgeMetadataCache()
Refresh metadata in memcached, but don't touch thumbnails or CDN.
loadFieldsWithTimestamp( $dbr, $fname)
string $timestamp
Upload timestamp.
int $height
Image height.
Definition LocalFile.php:57
const ATOMIC_SECTION_LOCK
purgeOldThumbnails( $archiveName)
Delete cached transformed files for an archived version only.
publishTo( $src, $dstRel, $flags=0, array $options=[])
Move or copy a file to a specified location.
int $historyRes
Result of the query for the file's history (nextHistoryLine)
Definition LocalFile.php:93
purgeThumbList( $dir, $files)
Delete a list of thumbnails visible at urls.
getUser( $type='text')
Returns ID or name of user who uploaded the file.
decodeRow( $row, $prefix='img_')
Decode a row from the database (either object or array) to an array with timestamps and MIME types de...
string $description
Description of current revision of the file.
const CACHE_FIELD_MAX_LEN
Definition LocalFile.php:48
unlock()
Decrement the lock reference count and end the atomic section if it reaches zero.
getLazyCacheFields( $prefix='img_')
getSize()
Returns the size of the image file, in bytes.
invalidateCache()
Purge the file object/metadata cache.
getMimeType()
Returns the MIME type of the file.
bool $extraDataLoaded
Whether or not lazy-loaded data has been loaded from the database.
Definition LocalFile.php:81
int $historyLine
Number of line to return by nextHistoryLine() (constructor)
Definition LocalFile.php:90
readOnlyFatalStatus()
string $sha1
SHA-1 base 36 content hash.
Definition LocalFile.php:75
getHeight( $page=1)
Return the height of the image.
bool $locked
True if the image row is locked.
getDescriptionText( $lang=null)
Get the HTML text of the description page This is not used by ImagePage for local files,...
prerenderThumbnails()
Prerenders a configurable set of thumbnails.
bool $lockedOwnTrx
True if the image row is locked with a lock initiated transaction.
resetHistory()
Reset the history pointer to the first element of the history.
unprefixRow( $row, $prefix='img_')
static newFromRow( $row, $repo)
Create a LocalFile from a title Do not call this except from inside a repo class.
publish( $src, $flags=0, array $options=[])
Move or copy a file to its public location.
string $user_text
User name of uploader.
string $metadata
Handler-specific metadata.
Definition LocalFile.php:72
restore( $versions=[], $unsuppress=false)
Restore all or specified deleted revisions to the given file.
getCacheFields( $prefix='img_')
int $bits
Returned by getimagesize (loadFromXxx)
Definition LocalFile.php:60
upload( $src, $comment, $pageText, $flags=0, $props=false, $timestamp=false, $user=null, $tags=[])
getHashPath inherited
bool $missing
True if file is not present in file system.
purgeThumbnails( $options=[])
Delete cached transformed files for the current version only.
loadExtraFromDB()
Load lazy file metadata from the DB.
recordUpload2( $oldver, $comment, $pageText, $props=false, $timestamp=false, $user=null, $tags=[])
Record a file upload in the upload log and the image table.
string $repoClass
Definition LocalFile.php:87
int $width
Image width.
Definition LocalFile.php:54
nextHistoryLine()
Returns the history of this file, line by line.
upgradeRow()
Fix assorted version-related problems with the image row by reloading it from the file.
int $deleted
Bitfield akin to rev_deleted.
Definition LocalFile.php:84
loadFromCache()
Try to load file metadata from memcached, falling back to the database.
string $descriptionTouched
TS_MW timestamp of the last change of the file description.
getMetadata()
Get handler-specific metadata.
__construct( $title, $repo)
Constructor.
bool $dataLoaded
Whether or not core data has been loaded from the database (loadFromXxx)
Definition LocalFile.php:78
bool $fileExists
Does the file exist on disk? (loadFromXxx)
Definition LocalFile.php:51
static getHashFromKey( $key)
Gets the SHA1 hash from a storage key.
static makeParamBlob( $params)
Create a blob from a parameter array.
Definition LogEntry.php:142
static newFromEntry(LogEntry $entry)
Constructs a new formatter suitable for given entry.
MediaWiki exception.
MimeMagic helper wrapper.
Class for creating log entries manually, to inject them into the database.
Definition LogEntry.php:396
setTimestamp( $timestamp)
Set the timestamp of when the logged action took place.
Definition LogEntry.php:513
const METADATA_COMPATIBLE
getStreamHeaders( $metadata)
Get useful response headers for GET/HEAD requests for a file with the given metadata.
static getHandler( $type)
Get a MediaHandler for a given MIME type from the instance cache.
filterThumbnailPurgeList(&$files, $options)
Remove files from the purge list.
getPageDimensions(File $image, $page)
Get an associative array of page dimensions Currently "width" and "height" are understood,...
isMetadataValid( $image, $metadata)
Check if the metadata string is valid for this handler.
PSR-3 logger instance factory.
static singleton()
Get an instance of this class.
Definition MimeMagic.php:33
static selectFields()
Fields in the oldimage table.
Set options of the Parser.
static singleton()
Get a RepoGroup instance.
Definition RepoGroup.php:59
static newExtraneousContext(Title $title, $request=[])
Create a new extraneous context.
static newNullRevision( $dbw, $pageId, $summary, $minor, $user=null)
Create a new null-revision for insertion into a page's history.
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:134
const SUPPRESSED_ALL
Definition Revision.php:95
static factory(array $deltas)
static getInitialPageText( $comment='', $license='', $copyStatus='', $source='', Config $config=null)
Get the initial image page text based on a comment and optional file status information.
isGood()
Returns whether the operation completed and didn't have any error or warnings.
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:40
Job for asynchronous rendering of thumbnails.
Represents a title within MediaWiki.
Definition Title.php:39
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:50
Multi-datacenter aware caching interface.
Special handling for file pages.
Relational database abstraction object.
Definition Database.php:45
$res
Definition database.txt:21
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
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:57
when a variable name is used in a it is silently declared as a new local masking the global
Definition design.txt:95
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
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:26
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
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
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.
const NS_FILE
Definition Defines.php:68
const EDIT_SUPPRESS_RC
Definition Defines.php:153
const EDIT_NEW
Definition Defines.php:150
the array() calling protocol came about after MediaWiki 1.4rc1.
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:2578
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:Array with elements of the form "language:title" in the order that they will be output. & $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':DEPRECATED! Use HtmlPageLinkRendererBegin instead. 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:1954
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1018
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup 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 please use GetContentModels hook to make them known to core 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:1143
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup 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:1102
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup 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:1100
it s the revision text itself In either if gzip is the revision text is gzipped $flags
Definition hooks.txt:2753
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 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:2604
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:304
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:108
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist and Watchlist you will want to construct new ChangesListBooleanFilter or ChangesListStringOptionsFilter objects When constructing you specify which group they belong to You can reuse existing or create your you must register them with $special registerFilterGroup 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:1049
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:1751
processing should stop and the error should be shown to the user * false
Definition hooks.txt:189
returning false will NOT prevent logging $e
Definition hooks.txt:2127
as see the revision history and available at free of to any person obtaining a copy of this software and associated documentation to deal in the Software without including without limitation the rights to publish
Definition LICENSE.txt:11
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:37
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:40
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[])
A SELECT wrapper which returns a single field from a single result row.
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
$batch
Definition linkcache.txt:23
$cache
Definition mcc.php:33
MediaWiki has optional support for a high distributed memory object caching system For general information on but for a larger site with heavy load
Definition memcached.txt:6
$source
title
width
if(!isset( $args[0])) $lang