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