MediaWiki REL1_32
FileRepo.php
Go to the documentation of this file.
1<?php
11
39class FileRepo {
40 const DELETE_SOURCE = 1;
41 const OVERWRITE = 2;
42 const OVERWRITE_SAME = 4;
43 const SKIP_LOCKING = 8;
44
46
50
53
55 protected $hasSha1Storage = false;
56
58 protected $supportsSha1URLs = false;
59
61 protected $backend;
62
64 protected $zones = [];
65
67 protected $thumbScriptUrl;
68
72
76 protected $descBaseUrl;
77
81 protected $scriptDirUrl;
82
84 protected $articleUrl;
85
91 protected $initialCapital;
92
98 protected $pathDisclosureProtection = 'simple';
99
101 protected $url;
102
104 protected $thumbUrl;
105
107 protected $hashLevels;
108
111
117
119 protected $favicon;
120
122 protected $isPrivate;
123
125 protected $fileFactory = [ UnregisteredLocalFile::class, 'newFromTitle' ];
127 protected $oldFileFactory = false;
129 protected $fileFactoryKey = false;
131 protected $oldFileFactoryKey = false;
132
136 protected $thumbProxyUrl;
139
144 public function __construct( array $info = null ) {
145 // Verify required settings presence
146 if (
147 $info === null
148 || !array_key_exists( 'name', $info )
149 || !array_key_exists( 'backend', $info )
150 ) {
151 throw new MWException( __CLASS__ .
152 " requires an array of options having both 'name' and 'backend' keys.\n" );
153 }
154
155 // Required settings
156 $this->name = $info['name'];
157 if ( $info['backend'] instanceof FileBackend ) {
158 $this->backend = $info['backend']; // useful for testing
159 } else {
160 $this->backend = FileBackendGroup::singleton()->get( $info['backend'] );
161 }
162
163 // Optional settings that can have no value
164 $optionalSettings = [
165 'descBaseUrl', 'scriptDirUrl', 'articleUrl', 'fetchDescription',
166 'thumbScriptUrl', 'pathDisclosureProtection', 'descriptionCacheExpiry',
167 'favicon', 'thumbProxyUrl', 'thumbProxySecret',
168 ];
169 foreach ( $optionalSettings as $var ) {
170 if ( isset( $info[$var] ) ) {
171 $this->$var = $info[$var];
172 }
173 }
174
175 // Optional settings that have a default
176 $this->initialCapital = $info['initialCapital'] ?? MWNamespace::isCapitalized( NS_FILE );
177 $this->url = $info['url'] ?? false; // a subclass may set the URL (e.g. ForeignAPIRepo)
178 if ( isset( $info['thumbUrl'] ) ) {
179 $this->thumbUrl = $info['thumbUrl'];
180 } else {
181 $this->thumbUrl = $this->url ? "{$this->url}/thumb" : false;
182 }
183 $this->hashLevels = $info['hashLevels'] ?? 2;
184 $this->deletedHashLevels = $info['deletedHashLevels'] ?? $this->hashLevels;
185 $this->transformVia404 = !empty( $info['transformVia404'] );
186 $this->abbrvThreshold = $info['abbrvThreshold'] ?? 255;
187 $this->isPrivate = !empty( $info['isPrivate'] );
188 // Give defaults for the basic zones...
189 $this->zones = $info['zones'] ?? [];
190 foreach ( [ 'public', 'thumb', 'transcoded', 'temp', 'deleted' ] as $zone ) {
191 if ( !isset( $this->zones[$zone]['container'] ) ) {
192 $this->zones[$zone]['container'] = "{$this->name}-{$zone}";
193 }
194 if ( !isset( $this->zones[$zone]['directory'] ) ) {
195 $this->zones[$zone]['directory'] = '';
196 }
197 if ( !isset( $this->zones[$zone]['urlsByExt'] ) ) {
198 $this->zones[$zone]['urlsByExt'] = [];
199 }
200 }
201
202 $this->supportsSha1URLs = !empty( $info['supportsSha1URLs'] );
203 }
204
210 public function getBackend() {
211 return $this->backend;
212 }
213
220 public function getReadOnlyReason() {
221 return $this->backend->getReadOnlyReason();
222 }
223
231 protected function initZones( $doZones = [] ) {
232 $status = $this->newGood();
233 foreach ( (array)$doZones as $zone ) {
234 $root = $this->getZonePath( $zone );
235 if ( $root === null ) {
236 throw new MWException( "No '$zone' zone defined in the {$this->name} repo." );
237 }
238 }
239
240 return $status;
241 }
242
249 public static function isVirtualUrl( $url ) {
250 return substr( $url, 0, 9 ) == 'mwrepo://';
251 }
252
261 public function getVirtualUrl( $suffix = false ) {
262 $path = 'mwrepo://' . $this->name;
263 if ( $suffix !== false ) {
264 $path .= '/' . rawurlencode( $suffix );
265 }
266
267 return $path;
268 }
269
277 public function getZoneUrl( $zone, $ext = null ) {
278 if ( in_array( $zone, [ 'public', 'thumb', 'transcoded' ] ) ) {
279 // standard public zones
280 if ( $ext !== null && isset( $this->zones[$zone]['urlsByExt'][$ext] ) ) {
281 // custom URL for extension/zone
282 return $this->zones[$zone]['urlsByExt'][$ext];
283 } elseif ( isset( $this->zones[$zone]['url'] ) ) {
284 // custom URL for zone
285 return $this->zones[$zone]['url'];
286 }
287 }
288 switch ( $zone ) {
289 case 'public':
290 return $this->url;
291 case 'temp':
292 case 'deleted':
293 return false; // no public URL
294 case 'thumb':
295 return $this->thumbUrl;
296 case 'transcoded':
297 return "{$this->url}/transcoded";
298 default:
299 return false;
300 }
301 }
302
306 public function backendSupportsUnicodePaths() {
307 return (bool)( $this->getBackend()->getFeatures() & FileBackend::ATTR_UNICODE_PATHS );
308 }
309
318 public function resolveVirtualUrl( $url ) {
319 if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
320 throw new MWException( __METHOD__ . ': unknown protocol' );
321 }
322 $bits = explode( '/', substr( $url, 9 ), 3 );
323 if ( count( $bits ) != 3 ) {
324 throw new MWException( __METHOD__ . ": invalid mwrepo URL: $url" );
325 }
326 list( $repo, $zone, $rel ) = $bits;
327 if ( $repo !== $this->name ) {
328 throw new MWException( __METHOD__ . ": fetching from a foreign repo is not supported" );
329 }
330 $base = $this->getZonePath( $zone );
331 if ( !$base ) {
332 throw new MWException( __METHOD__ . ": invalid zone: $zone" );
333 }
334
335 return $base . '/' . rawurldecode( $rel );
336 }
337
344 protected function getZoneLocation( $zone ) {
345 if ( !isset( $this->zones[$zone] ) ) {
346 return [ null, null ]; // bogus
347 }
348
349 return [ $this->zones[$zone]['container'], $this->zones[$zone]['directory'] ];
350 }
351
358 public function getZonePath( $zone ) {
359 list( $container, $base ) = $this->getZoneLocation( $zone );
360 if ( $container === null || $base === null ) {
361 return null;
362 }
363 $backendName = $this->backend->getName();
364 if ( $base != '' ) { // may not be set
365 $base = "/{$base}";
366 }
367
368 return "mwstore://$backendName/{$container}{$base}";
369 }
370
382 public function newFile( $title, $time = false ) {
383 $title = File::normalizeTitle( $title );
384 if ( !$title ) {
385 return null;
386 }
387 if ( $time ) {
388 if ( $this->oldFileFactory ) {
389 return call_user_func( $this->oldFileFactory, $title, $this, $time );
390 } else {
391 return null;
392 }
393 } else {
394 return call_user_func( $this->fileFactory, $title, $this );
395 }
396 }
397
415 public function findFile( $title, $options = [] ) {
416 $title = File::normalizeTitle( $title );
417 if ( !$title ) {
418 return false;
419 }
420 if ( isset( $options['bypassCache'] ) ) {
421 $options['latest'] = $options['bypassCache']; // b/c
422 }
423 $time = $options['time'] ?? false;
424 $flags = !empty( $options['latest'] ) ? File::READ_LATEST : 0;
425 # First try the current version of the file to see if it precedes the timestamp
426 $img = $this->newFile( $title );
427 if ( !$img ) {
428 return false;
429 }
430 $img->load( $flags );
431 if ( $img->exists() && ( !$time || $img->getTimestamp() == $time ) ) {
432 return $img;
433 }
434 # Now try an old version of the file
435 if ( $time !== false ) {
436 $img = $this->newFile( $title, $time );
437 if ( $img ) {
438 $img->load( $flags );
439 if ( $img->exists() ) {
440 if ( !$img->isDeleted( File::DELETED_FILE ) ) {
441 return $img; // always OK
442 } elseif ( !empty( $options['private'] ) &&
443 $img->userCan( File::DELETED_FILE,
444 $options['private'] instanceof User ? $options['private'] : null
445 )
446 ) {
447 return $img;
448 }
449 }
450 }
451 }
452
453 # Now try redirects
454 if ( !empty( $options['ignoreRedirect'] ) ) {
455 return false;
456 }
457 $redir = $this->checkRedirect( $title );
458 if ( $redir && $title->getNamespace() == NS_FILE ) {
459 $img = $this->newFile( $redir );
460 if ( !$img ) {
461 return false;
462 }
463 $img->load( $flags );
464 if ( $img->exists() ) {
465 $img->redirectedFrom( $title->getDBkey() );
466
467 return $img;
468 }
469 }
470
471 return false;
472 }
473
491 public function findFiles( array $items, $flags = 0 ) {
492 $result = [];
493 foreach ( $items as $item ) {
494 if ( is_array( $item ) ) {
495 $title = $item['title'];
496 $options = $item;
497 unset( $options['title'] );
498 } else {
499 $title = $item;
500 $options = [];
501 }
502 $file = $this->findFile( $title, $options );
503 if ( $file ) {
504 $searchName = File::normalizeTitle( $title )->getDBkey(); // must be valid
505 if ( $flags & self::NAME_AND_TIME_ONLY ) {
506 $result[$searchName] = [
507 'title' => $file->getTitle()->getDBkey(),
508 'timestamp' => $file->getTimestamp()
509 ];
510 } else {
511 $result[$searchName] = $file;
512 }
513 }
514 }
515
516 return $result;
517 }
518
528 public function findFileFromKey( $sha1, $options = [] ) {
529 $time = $options['time'] ?? false;
530 # First try to find a matching current version of a file...
531 if ( !$this->fileFactoryKey ) {
532 return false; // find-by-sha1 not supported
533 }
534 $img = call_user_func( $this->fileFactoryKey, $sha1, $this, $time );
535 if ( $img && $img->exists() ) {
536 return $img;
537 }
538 # Now try to find a matching old version of a file...
539 if ( $time !== false && $this->oldFileFactoryKey ) { // find-by-sha1 supported?
540 $img = call_user_func( $this->oldFileFactoryKey, $sha1, $this, $time );
541 if ( $img && $img->exists() ) {
542 if ( !$img->isDeleted( File::DELETED_FILE ) ) {
543 return $img; // always OK
544 } elseif ( !empty( $options['private'] ) &&
545 $img->userCan( File::DELETED_FILE,
546 $options['private'] instanceof User ? $options['private'] : null
547 )
548 ) {
549 return $img;
550 }
551 }
552 }
553
554 return false;
555 }
556
565 public function findBySha1( $hash ) {
566 return [];
567 }
568
576 public function findBySha1s( array $hashes ) {
577 $result = [];
578 foreach ( $hashes as $hash ) {
579 $files = $this->findBySha1( $hash );
580 if ( count( $files ) ) {
581 $result[$hash] = $files;
582 }
583 }
584
585 return $result;
586 }
587
596 public function findFilesByPrefix( $prefix, $limit ) {
597 return [];
598 }
599
605 public function getThumbScriptUrl() {
607 }
608
614 public function getThumbProxyUrl() {
616 }
617
623 public function getThumbProxySecret() {
625 }
626
632 public function canTransformVia404() {
634 }
635
642 public function getNameFromTitle( Title $title ) {
643 if ( $this->initialCapital != MWNamespace::isCapitalized( NS_FILE ) ) {
644 $name = $title->getUserCaseDBKey();
645 if ( $this->initialCapital ) {
646 $name = MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $name );
647 }
648 } else {
649 $name = $title->getDBkey();
650 }
651
652 return $name;
653 }
654
660 public function getRootDirectory() {
661 return $this->getZonePath( 'public' );
662 }
663
671 public function getHashPath( $name ) {
672 return self::getHashPathForLevel( $name, $this->hashLevels );
673 }
674
682 public function getTempHashPath( $suffix ) {
683 $parts = explode( '!', $suffix, 2 ); // format is <timestamp>!<name> or just <name>
684 $name = $parts[1] ?? $suffix; // hash path is not based on timestamp
685 return self::getHashPathForLevel( $name, $this->hashLevels );
686 }
687
693 protected static function getHashPathForLevel( $name, $levels ) {
694 if ( $levels == 0 ) {
695 return '';
696 } else {
697 $hash = md5( $name );
698 $path = '';
699 for ( $i = 1; $i <= $levels; $i++ ) {
700 $path .= substr( $hash, 0, $i ) . '/';
701 }
702
703 return $path;
704 }
705 }
706
712 public function getHashLevels() {
713 return $this->hashLevels;
714 }
715
721 public function getName() {
722 return $this->name;
723 }
724
732 public function makeUrl( $query = '', $entry = 'index' ) {
733 if ( isset( $this->scriptDirUrl ) ) {
734 return wfAppendQuery( "{$this->scriptDirUrl}/{$entry}.php", $query );
735 }
736
737 return false;
738 }
739
752 public function getDescriptionUrl( $name ) {
753 $encName = wfUrlencode( $name );
754 if ( !is_null( $this->descBaseUrl ) ) {
755 # "http://example.com/wiki/File:"
756 return $this->descBaseUrl . $encName;
757 }
758 if ( !is_null( $this->articleUrl ) ) {
759 # "http://example.com/wiki/$1"
760 # We use "Image:" as the canonical namespace for
761 # compatibility across all MediaWiki versions.
762 return str_replace( '$1',
763 "Image:$encName", $this->articleUrl );
764 }
765 if ( !is_null( $this->scriptDirUrl ) ) {
766 # "http://example.com/w"
767 # We use "Image:" as the canonical namespace for
768 # compatibility across all MediaWiki versions,
769 # and just sort of hope index.php is right. ;)
770 return $this->makeUrl( "title=Image:$encName" );
771 }
772
773 return false;
774 }
775
786 public function getDescriptionRenderUrl( $name, $lang = null ) {
787 $query = 'action=render';
788 if ( !is_null( $lang ) ) {
789 $query .= '&uselang=' . urlencode( $lang );
790 }
791 if ( isset( $this->scriptDirUrl ) ) {
792 return $this->makeUrl(
793 'title=' .
794 wfUrlencode( 'Image:' . $name ) .
795 "&$query" );
796 } else {
797 $descUrl = $this->getDescriptionUrl( $name );
798 if ( $descUrl ) {
799 return wfAppendQuery( $descUrl, $query );
800 } else {
801 return false;
802 }
803 }
804 }
805
811 public function getDescriptionStylesheetUrl() {
812 if ( isset( $this->scriptDirUrl ) ) {
813 // Must match canonical query parameter order for optimum caching
814 // See Title::getCdnUrls
815 return $this->makeUrl( 'title=MediaWiki:Filepage.css&action=raw&ctype=text/css' );
816 }
817
818 return false;
819 }
820
834 public function store( $srcPath, $dstZone, $dstRel, $flags = 0 ) {
835 $this->assertWritableRepo(); // fail out if read-only
836
837 $status = $this->storeBatch( [ [ $srcPath, $dstZone, $dstRel ] ], $flags );
838 if ( $status->successCount == 0 ) {
839 $status->setOK( false );
840 }
841
842 return $status;
843 }
844
857 public function storeBatch( array $triplets, $flags = 0 ) {
858 $this->assertWritableRepo(); // fail out if read-only
859
860 if ( $flags & self::DELETE_SOURCE ) {
861 throw new InvalidArgumentException( "DELETE_SOURCE not supported in " . __METHOD__ );
862 }
863
864 $status = $this->newGood();
865 $backend = $this->backend; // convenience
866
867 $operations = [];
868 // Validate each triplet and get the store operation...
869 foreach ( $triplets as $triplet ) {
870 list( $srcPath, $dstZone, $dstRel ) = $triplet;
871 wfDebug( __METHOD__
872 . "( \$src='$srcPath', \$dstZone='$dstZone', \$dstRel='$dstRel' )\n"
873 );
874
875 // Resolve destination path
876 $root = $this->getZonePath( $dstZone );
877 if ( !$root ) {
878 throw new MWException( "Invalid zone: $dstZone" );
879 }
880 if ( !$this->validateFilename( $dstRel ) ) {
881 throw new MWException( 'Validation error in $dstRel' );
882 }
883 $dstPath = "$root/$dstRel";
884 $dstDir = dirname( $dstPath );
885 // Create destination directories for this triplet
886 if ( !$this->initDirectory( $dstDir )->isOK() ) {
887 return $this->newFatal( 'directorycreateerror', $dstDir );
888 }
889
890 // Resolve source to a storage path if virtual
891 $srcPath = $this->resolveToStoragePath( $srcPath );
892
893 // Get the appropriate file operation
894 if ( FileBackend::isStoragePath( $srcPath ) ) {
895 $opName = 'copy';
896 } else {
897 $opName = 'store';
898 }
899 $operations[] = [
900 'op' => $opName,
901 'src' => $srcPath,
902 'dst' => $dstPath,
903 'overwrite' => $flags & self::OVERWRITE,
904 'overwriteSame' => $flags & self::OVERWRITE_SAME,
905 ];
906 }
907
908 // Execute the store operation for each triplet
909 $opts = [ 'force' => true ];
910 if ( $flags & self::SKIP_LOCKING ) {
911 $opts['nonLocking'] = true;
912 }
913 $status->merge( $backend->doOperations( $operations, $opts ) );
914
915 return $status;
916 }
917
928 public function cleanupBatch( array $files, $flags = 0 ) {
929 $this->assertWritableRepo(); // fail out if read-only
930
931 $status = $this->newGood();
932
933 $operations = [];
934 foreach ( $files as $path ) {
935 if ( is_array( $path ) ) {
936 // This is a pair, extract it
937 list( $zone, $rel ) = $path;
938 $path = $this->getZonePath( $zone ) . "/$rel";
939 } else {
940 // Resolve source to a storage path if virtual
941 $path = $this->resolveToStoragePath( $path );
942 }
943 $operations[] = [ 'op' => 'delete', 'src' => $path ];
944 }
945 // Actually delete files from storage...
946 $opts = [ 'force' => true ];
947 if ( $flags & self::SKIP_LOCKING ) {
948 $opts['nonLocking'] = true;
949 }
950 $status->merge( $this->backend->doOperations( $operations, $opts ) );
951
952 return $status;
953 }
954
968 final public function quickImport( $src, $dst, $options = null ) {
969 return $this->quickImportBatch( [ [ $src, $dst, $options ] ] );
970 }
971
980 final public function quickPurge( $path ) {
981 return $this->quickPurgeBatch( [ $path ] );
982 }
983
991 public function quickCleanDir( $dir ) {
992 $status = $this->newGood();
993 $status->merge( $this->backend->clean(
994 [ 'dir' => $this->resolveToStoragePath( $dir ) ] ) );
995
996 return $status;
997 }
998
1011 public function quickImportBatch( array $triples ) {
1012 $status = $this->newGood();
1013 $operations = [];
1014 foreach ( $triples as $triple ) {
1015 list( $src, $dst ) = $triple;
1016 if ( $src instanceof FSFile ) {
1017 $op = 'store';
1018 } else {
1019 $src = $this->resolveToStoragePath( $src );
1020 $op = FileBackend::isStoragePath( $src ) ? 'copy' : 'store';
1021 }
1022 $dst = $this->resolveToStoragePath( $dst );
1023
1024 if ( !isset( $triple[2] ) ) {
1025 $headers = [];
1026 } elseif ( is_string( $triple[2] ) ) {
1027 // back-compat
1028 $headers = [ 'Content-Disposition' => $triple[2] ];
1029 } elseif ( is_array( $triple[2] ) && isset( $triple[2]['headers'] ) ) {
1030 $headers = $triple[2]['headers'];
1031 } else {
1032 $headers = [];
1033 }
1034
1035 $operations[] = [
1036 'op' => $op,
1037 'src' => $src,
1038 'dst' => $dst,
1039 'headers' => $headers
1040 ];
1041 $status->merge( $this->initDirectory( dirname( $dst ) ) );
1042 }
1043 $status->merge( $this->backend->doQuickOperations( $operations ) );
1044
1045 return $status;
1046 }
1047
1056 public function quickPurgeBatch( array $paths ) {
1057 $status = $this->newGood();
1058 $operations = [];
1059 foreach ( $paths as $path ) {
1060 $operations[] = [
1061 'op' => 'delete',
1062 'src' => $this->resolveToStoragePath( $path ),
1063 'ignoreMissingSource' => true
1064 ];
1065 }
1066 $status->merge( $this->backend->doQuickOperations( $operations ) );
1067
1068 return $status;
1069 }
1070
1081 public function storeTemp( $originalName, $srcPath ) {
1082 $this->assertWritableRepo(); // fail out if read-only
1083
1084 $date = MWTimestamp::getInstance()->format( 'YmdHis' );
1085 $hashPath = $this->getHashPath( $originalName );
1086 $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
1087 $virtualUrl = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
1088
1089 $result = $this->quickImport( $srcPath, $virtualUrl );
1090 $result->value = $virtualUrl;
1091
1092 return $result;
1093 }
1094
1101 public function freeTemp( $virtualUrl ) {
1102 $this->assertWritableRepo(); // fail out if read-only
1103
1104 $temp = $this->getVirtualUrl( 'temp' );
1105 if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
1106 wfDebug( __METHOD__ . ": Invalid temp virtual URL\n" );
1107
1108 return false;
1109 }
1110
1111 return $this->quickPurge( $virtualUrl )->isOK();
1112 }
1113
1123 public function concatenate( array $srcPaths, $dstPath, $flags = 0 ) {
1124 $this->assertWritableRepo(); // fail out if read-only
1125
1126 $status = $this->newGood();
1127
1128 $sources = [];
1129 foreach ( $srcPaths as $srcPath ) {
1130 // Resolve source to a storage path if virtual
1131 $source = $this->resolveToStoragePath( $srcPath );
1132 $sources[] = $source; // chunk to merge
1133 }
1134
1135 // Concatenate the chunks into one FS file
1136 $params = [ 'srcs' => $sources, 'dst' => $dstPath ];
1137 $status->merge( $this->backend->concatenate( $params ) );
1138 if ( !$status->isOK() ) {
1139 return $status;
1140 }
1141
1142 // Delete the sources if required
1143 if ( $flags & self::DELETE_SOURCE ) {
1144 $status->merge( $this->quickPurgeBatch( $srcPaths ) );
1145 }
1146
1147 // Make sure status is OK, despite any quickPurgeBatch() fatals
1148 $status->setResult( true );
1149
1150 return $status;
1151 }
1152
1172 public function publish(
1173 $src, $dstRel, $archiveRel, $flags = 0, array $options = []
1174 ) {
1175 $this->assertWritableRepo(); // fail out if read-only
1176
1177 $status = $this->publishBatch(
1178 [ [ $src, $dstRel, $archiveRel, $options ] ], $flags );
1179 if ( $status->successCount == 0 ) {
1180 $status->setOK( false );
1181 }
1182 $status->value = $status->value[0] ?? false;
1183
1184 return $status;
1185 }
1186
1197 public function publishBatch( array $ntuples, $flags = 0 ) {
1198 $this->assertWritableRepo(); // fail out if read-only
1199
1200 $backend = $this->backend; // convenience
1201 // Try creating directories
1202 $status = $this->initZones( 'public' );
1203 if ( !$status->isOK() ) {
1204 return $status;
1205 }
1206
1207 $status = $this->newGood( [] );
1208
1209 $operations = [];
1210 $sourceFSFilesToDelete = []; // cleanup for disk source files
1211 // Validate each triplet and get the store operation...
1212 foreach ( $ntuples as $ntuple ) {
1213 list( $src, $dstRel, $archiveRel ) = $ntuple;
1214 $srcPath = ( $src instanceof FSFile ) ? $src->getPath() : $src;
1215
1216 $options = $ntuple[3] ?? [];
1217 // Resolve source to a storage path if virtual
1218 $srcPath = $this->resolveToStoragePath( $srcPath );
1219 if ( !$this->validateFilename( $dstRel ) ) {
1220 throw new MWException( 'Validation error in $dstRel' );
1221 }
1222 if ( !$this->validateFilename( $archiveRel ) ) {
1223 throw new MWException( 'Validation error in $archiveRel' );
1224 }
1225
1226 $publicRoot = $this->getZonePath( 'public' );
1227 $dstPath = "$publicRoot/$dstRel";
1228 $archivePath = "$publicRoot/$archiveRel";
1229
1230 $dstDir = dirname( $dstPath );
1231 $archiveDir = dirname( $archivePath );
1232 // Abort immediately on directory creation errors since they're likely to be repetitive
1233 if ( !$this->initDirectory( $dstDir )->isOK() ) {
1234 return $this->newFatal( 'directorycreateerror', $dstDir );
1235 }
1236 if ( !$this->initDirectory( $archiveDir )->isOK() ) {
1237 return $this->newFatal( 'directorycreateerror', $archiveDir );
1238 }
1239
1240 // Set any desired headers to be use in GET/HEAD responses
1241 $headers = $options['headers'] ?? [];
1242
1243 // Archive destination file if it exists.
1244 // This will check if the archive file also exists and fail if does.
1245 // This is a sanity check to avoid data loss. On Windows and Linux,
1246 // copy() will overwrite, so the existence check is vulnerable to
1247 // race conditions unless a functioning LockManager is used.
1248 // LocalFile also uses SELECT FOR UPDATE for synchronization.
1249 $operations[] = [
1250 'op' => 'copy',
1251 'src' => $dstPath,
1252 'dst' => $archivePath,
1253 'ignoreMissingSource' => true
1254 ];
1255
1256 // Copy (or move) the source file to the destination
1257 if ( FileBackend::isStoragePath( $srcPath ) ) {
1258 if ( $flags & self::DELETE_SOURCE ) {
1259 $operations[] = [
1260 'op' => 'move',
1261 'src' => $srcPath,
1262 'dst' => $dstPath,
1263 'overwrite' => true, // replace current
1264 'headers' => $headers
1265 ];
1266 } else {
1267 $operations[] = [
1268 'op' => 'copy',
1269 'src' => $srcPath,
1270 'dst' => $dstPath,
1271 'overwrite' => true, // replace current
1272 'headers' => $headers
1273 ];
1274 }
1275 } else { // FS source path
1276 $operations[] = [
1277 'op' => 'store',
1278 'src' => $src, // prefer FSFile objects
1279 'dst' => $dstPath,
1280 'overwrite' => true, // replace current
1281 'headers' => $headers
1282 ];
1283 if ( $flags & self::DELETE_SOURCE ) {
1284 $sourceFSFilesToDelete[] = $srcPath;
1285 }
1286 }
1287 }
1288
1289 // Execute the operations for each triplet
1290 $status->merge( $backend->doOperations( $operations ) );
1291 // Find out which files were archived...
1292 foreach ( $ntuples as $i => $ntuple ) {
1293 list( , , $archiveRel ) = $ntuple;
1294 $archivePath = $this->getZonePath( 'public' ) . "/$archiveRel";
1295 if ( $this->fileExists( $archivePath ) ) {
1296 $status->value[$i] = 'archived';
1297 } else {
1298 $status->value[$i] = 'new';
1299 }
1300 }
1301 // Cleanup for disk source files...
1302 foreach ( $sourceFSFilesToDelete as $file ) {
1303 Wikimedia\suppressWarnings();
1304 unlink( $file ); // FS cleanup
1305 Wikimedia\restoreWarnings();
1306 }
1307
1308 return $status;
1309 }
1310
1318 protected function initDirectory( $dir ) {
1319 $path = $this->resolveToStoragePath( $dir );
1320 list( , $container, ) = FileBackend::splitStoragePath( $path );
1321
1322 $params = [ 'dir' => $path ];
1323 if ( $this->isPrivate
1324 || $container === $this->zones['deleted']['container']
1325 || $container === $this->zones['temp']['container']
1326 ) {
1327 # Take all available measures to prevent web accessibility of new deleted
1328 # directories, in case the user has not configured offline storage
1329 $params = [ 'noAccess' => true, 'noListing' => true ] + $params;
1330 }
1331
1332 $status = $this->newGood();
1333 $status->merge( $this->backend->prepare( $params ) );
1334
1335 return $status;
1336 }
1337
1344 public function cleanDir( $dir ) {
1345 $this->assertWritableRepo(); // fail out if read-only
1346
1347 $status = $this->newGood();
1348 $status->merge( $this->backend->clean(
1349 [ 'dir' => $this->resolveToStoragePath( $dir ) ] ) );
1350
1351 return $status;
1352 }
1353
1360 public function fileExists( $file ) {
1361 $result = $this->fileExistsBatch( [ $file ] );
1362
1363 return $result[0];
1364 }
1365
1372 public function fileExistsBatch( array $files ) {
1373 $paths = array_map( [ $this, 'resolveToStoragePath' ], $files );
1374 $this->backend->preloadFileStat( [ 'srcs' => $paths ] );
1375
1376 $result = [];
1377 foreach ( $files as $key => $file ) {
1378 $path = $this->resolveToStoragePath( $file );
1379 $result[$key] = $this->backend->fileExists( [ 'src' => $path ] );
1380 }
1381
1382 return $result;
1383 }
1384
1395 public function delete( $srcRel, $archiveRel ) {
1396 $this->assertWritableRepo(); // fail out if read-only
1397
1398 return $this->deleteBatch( [ [ $srcRel, $archiveRel ] ] );
1399 }
1400
1418 public function deleteBatch( array $sourceDestPairs ) {
1419 $this->assertWritableRepo(); // fail out if read-only
1420
1421 // Try creating directories
1422 $status = $this->initZones( [ 'public', 'deleted' ] );
1423 if ( !$status->isOK() ) {
1424 return $status;
1425 }
1426
1427 $status = $this->newGood();
1428
1429 $backend = $this->backend; // convenience
1430 $operations = [];
1431 // Validate filenames and create archive directories
1432 foreach ( $sourceDestPairs as $pair ) {
1433 list( $srcRel, $archiveRel ) = $pair;
1434 if ( !$this->validateFilename( $srcRel ) ) {
1435 throw new MWException( __METHOD__ . ':Validation error in $srcRel' );
1436 } elseif ( !$this->validateFilename( $archiveRel ) ) {
1437 throw new MWException( __METHOD__ . ':Validation error in $archiveRel' );
1438 }
1439
1440 $publicRoot = $this->getZonePath( 'public' );
1441 $srcPath = "{$publicRoot}/$srcRel";
1442
1443 $deletedRoot = $this->getZonePath( 'deleted' );
1444 $archivePath = "{$deletedRoot}/{$archiveRel}";
1445 $archiveDir = dirname( $archivePath ); // does not touch FS
1446
1447 // Create destination directories
1448 if ( !$this->initDirectory( $archiveDir )->isOK() ) {
1449 return $this->newFatal( 'directorycreateerror', $archiveDir );
1450 }
1451
1452 $operations[] = [
1453 'op' => 'move',
1454 'src' => $srcPath,
1455 'dst' => $archivePath,
1456 // We may have 2+ identical files being deleted,
1457 // all of which will map to the same destination file
1458 'overwriteSame' => true // also see T33792
1459 ];
1460 }
1461
1462 // Move the files by execute the operations for each pair.
1463 // We're now committed to returning an OK result, which will
1464 // lead to the files being moved in the DB also.
1465 $opts = [ 'force' => true ];
1466 $status->merge( $backend->doOperations( $operations, $opts ) );
1467
1468 return $status;
1469 }
1470
1477 public function cleanupDeletedBatch( array $storageKeys ) {
1478 $this->assertWritableRepo();
1479 }
1480
1489 public function getDeletedHashPath( $key ) {
1490 if ( strlen( $key ) < 31 ) {
1491 throw new MWException( "Invalid storage key '$key'." );
1492 }
1493 $path = '';
1494 for ( $i = 0; $i < $this->deletedHashLevels; $i++ ) {
1495 $path .= $key[$i] . '/';
1496 }
1497
1498 return $path;
1499 }
1500
1509 protected function resolveToStoragePath( $path ) {
1510 if ( $this->isVirtualUrl( $path ) ) {
1511 return $this->resolveVirtualUrl( $path );
1512 }
1513
1514 return $path;
1515 }
1516
1524 public function getLocalCopy( $virtualUrl ) {
1525 $path = $this->resolveToStoragePath( $virtualUrl );
1526
1527 return $this->backend->getLocalCopy( [ 'src' => $path ] );
1528 }
1529
1538 public function getLocalReference( $virtualUrl ) {
1539 $path = $this->resolveToStoragePath( $virtualUrl );
1540
1541 return $this->backend->getLocalReference( [ 'src' => $path ] );
1542 }
1543
1551 public function getFileProps( $virtualUrl ) {
1552 $fsFile = $this->getLocalReference( $virtualUrl );
1553 $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
1554 if ( $fsFile ) {
1555 $props = $mwProps->getPropsFromPath( $fsFile->getPath(), true );
1556 } else {
1557 $props = $mwProps->newPlaceholderProps();
1558 }
1559
1560 return $props;
1561 }
1562
1569 public function getFileTimestamp( $virtualUrl ) {
1570 $path = $this->resolveToStoragePath( $virtualUrl );
1571
1572 return $this->backend->getFileTimestamp( [ 'src' => $path ] );
1573 }
1574
1581 public function getFileSize( $virtualUrl ) {
1582 $path = $this->resolveToStoragePath( $virtualUrl );
1583
1584 return $this->backend->getFileSize( [ 'src' => $path ] );
1585 }
1586
1593 public function getFileSha1( $virtualUrl ) {
1594 $path = $this->resolveToStoragePath( $virtualUrl );
1595
1596 return $this->backend->getFileSha1Base36( [ 'src' => $path ] );
1597 }
1598
1608 public function streamFileWithStatus( $virtualUrl, $headers = [], $optHeaders = [] ) {
1609 $path = $this->resolveToStoragePath( $virtualUrl );
1610 $params = [ 'src' => $path, 'headers' => $headers, 'options' => $optHeaders ];
1611
1612 // T172851: HHVM does not flush the output properly, causing OOM
1613 ob_start( null, 1048576 );
1614 ob_implicit_flush( true );
1615
1616 $status = $this->newGood();
1617 $status->merge( $this->backend->streamFile( $params ) );
1618
1619 // T186565: Close the buffer, unless it has already been closed
1620 // in HTTPFileStreamer::resetOutputBuffers().
1621 if ( ob_get_status() ) {
1622 ob_end_flush();
1623 }
1624
1625 return $status;
1626 }
1627
1636 public function streamFile( $virtualUrl, $headers = [] ) {
1637 return $this->streamFileWithStatus( $virtualUrl, $headers )->isOK();
1638 }
1639
1648 public function enumFiles( $callback ) {
1649 $this->enumFilesInStorage( $callback );
1650 }
1651
1659 protected function enumFilesInStorage( $callback ) {
1660 $publicRoot = $this->getZonePath( 'public' );
1661 $numDirs = 1 << ( $this->hashLevels * 4 );
1662 // Use a priori assumptions about directory structure
1663 // to reduce the tree height of the scanning process.
1664 for ( $flatIndex = 0; $flatIndex < $numDirs; $flatIndex++ ) {
1665 $hexString = sprintf( "%0{$this->hashLevels}x", $flatIndex );
1666 $path = $publicRoot;
1667 for ( $hexPos = 0; $hexPos < $this->hashLevels; $hexPos++ ) {
1668 $path .= '/' . substr( $hexString, 0, $hexPos + 1 );
1669 }
1670 $iterator = $this->backend->getFileList( [ 'dir' => $path ] );
1671 foreach ( $iterator as $name ) {
1672 // Each item returned is a public file
1673 call_user_func( $callback, "{$path}/{$name}" );
1674 }
1675 }
1676 }
1677
1684 public function validateFilename( $filename ) {
1685 if ( strval( $filename ) == '' ) {
1686 return false;
1687 }
1688
1689 return FileBackend::isPathTraversalFree( $filename );
1690 }
1691
1698 switch ( $this->pathDisclosureProtection ) {
1699 case 'none':
1700 case 'simple': // b/c
1701 $callback = [ $this, 'passThrough' ];
1702 break;
1703 default: // 'paranoid'
1704 $callback = [ $this, 'paranoidClean' ];
1705 }
1706 return $callback;
1707 }
1708
1715 function paranoidClean( $param ) {
1716 return '[hidden]';
1717 }
1718
1725 function passThrough( $param ) {
1726 return $param;
1727 }
1728
1735 public function newFatal( $message /*, parameters...*/ ) {
1736 $status = Status::newFatal( ...func_get_args() );
1737 $status->cleanCallback = $this->getErrorCleanupFunction();
1738
1739 return $status;
1740 }
1741
1748 public function newGood( $value = null ) {
1749 $status = Status::newGood( $value );
1750 $status->cleanCallback = $this->getErrorCleanupFunction();
1751
1752 return $status;
1753 }
1754
1763 public function checkRedirect( Title $title ) {
1764 return false;
1765 }
1766
1774 public function invalidateImageRedirect( Title $title ) {
1775 }
1776
1782 public function getDisplayName() {
1783 global $wgSitename;
1784
1785 if ( $this->isLocal() ) {
1786 return $wgSitename;
1787 }
1788
1789 // 'shared-repo-name-wikimediacommons' is used when $wgUseInstantCommons = true
1790 return wfMessageFallback( 'shared-repo-name-' . $this->name, 'shared-repo' )->text();
1791 }
1792
1800 public function nameForThumb( $name ) {
1801 if ( strlen( $name ) > $this->abbrvThreshold ) {
1803 $name = ( $ext == '' ) ? 'thumbnail' : "thumbnail.$ext";
1804 }
1805
1806 return $name;
1807 }
1808
1814 public function isLocal() {
1815 return $this->getName() == 'local';
1816 }
1817
1826 public function getSharedCacheKey( /*...*/ ) {
1827 return false;
1828 }
1829
1837 public function getLocalCacheKey( /*...*/ ) {
1838 $args = func_get_args();
1839 array_unshift( $args, 'filerepo', $this->getName() );
1840
1841 return wfMemcKey( ...$args );
1842 }
1843
1852 public function getTempRepo() {
1853 return new TempFileRepo( [
1854 'name' => "{$this->name}-temp",
1855 'backend' => $this->backend,
1856 'zones' => [
1857 'public' => [
1858 // Same place storeTemp() uses in the base repo, though
1859 // the path hashing is mismatched, which is annoying.
1860 'container' => $this->zones['temp']['container'],
1861 'directory' => $this->zones['temp']['directory']
1862 ],
1863 'thumb' => [
1864 'container' => $this->zones['temp']['container'],
1865 'directory' => $this->zones['temp']['directory'] == ''
1866 ? 'thumb'
1867 : $this->zones['temp']['directory'] . '/thumb'
1868 ],
1869 'transcoded' => [
1870 'container' => $this->zones['temp']['container'],
1871 'directory' => $this->zones['temp']['directory'] == ''
1872 ? 'transcoded'
1873 : $this->zones['temp']['directory'] . '/transcoded'
1874 ]
1875 ],
1876 'hashLevels' => $this->hashLevels, // performance
1877 'isPrivate' => true // all in temp zone
1878 ] );
1879 }
1880
1887 public function getUploadStash( User $user = null ) {
1888 return new UploadStash( $this, $user );
1889 }
1890
1898 protected function assertWritableRepo() {
1899 }
1900
1907 public function getInfo() {
1908 $ret = [
1909 'name' => $this->getName(),
1910 'displayname' => $this->getDisplayName(),
1911 'rootUrl' => $this->getZoneUrl( 'public' ),
1912 'local' => $this->isLocal(),
1913 ];
1914
1915 $optionalSettings = [
1916 'url', 'thumbUrl', 'initialCapital', 'descBaseUrl', 'scriptDirUrl', 'articleUrl',
1917 'fetchDescription', 'descriptionCacheExpiry', 'favicon'
1918 ];
1919 foreach ( $optionalSettings as $k ) {
1920 if ( isset( $this->$k ) ) {
1921 $ret[$k] = $this->$k;
1922 }
1923 }
1924
1925 return $ret;
1926 }
1927
1932 public function hasSha1Storage() {
1933 return $this->hasSha1Storage;
1934 }
1935
1940 public function supportsSha1URLs() {
1942 }
1943}
$wgSitename
Name of the site.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfMessageFallback(... $keys)
This function accepts multiple message keys and returns a message instance for the first message whic...
wfMemcKey(... $args)
Make a cache key for the local wiki.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
if( $line===false) $args
Definition cdb.php:64
Class representing a non-directory file on the file system.
Definition FSFile.php:29
Base class for all file backend classes (including multi-write backends).
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
const ATTR_UNICODE_PATHS
static splitStoragePath( $storagePath)
Split a storage path into a backend name, a container name, and a relative file path.
static extensionFromPath( $path, $case='lowercase')
Get the final extension from a storage or FS path.
doOperations(array $ops, array $opts=[])
This is the main entry point into the backend for write operations.
static isPathTraversalFree( $path)
Check if a relative path has no directory traversals.
Base class for file repositories.
Definition FileRepo.php:39
string $pathDisclosureProtection
May be 'paranoid' to remove all parameters from error messages, 'none' to leave the paths in unchange...
Definition FileRepo.php:98
getTempHashPath( $suffix)
Get a relative path including trailing slash, e.g.
Definition FileRepo.php:682
int $hashLevels
The number of directory levels for hash-based division of files.
Definition FileRepo.php:107
getTempRepo()
Get a temporary private FileRepo associated with this repo.
cleanupDeletedBatch(array $storageKeys)
Delete files in the deleted directory if they are not referenced in the filearchive table.
getLocalCacheKey()
Get a key for this repo in the local cache domain.
const OVERWRITE_SAME
Definition FileRepo.php:42
resolveVirtualUrl( $url)
Get the backend storage path corresponding to a virtual URL.
Definition FileRepo.php:318
nameForThumb( $name)
Get the portion of the file that contains the origin file name.
publishBatch(array $ntuples, $flags=0)
Publish a batch of files.
findFiles(array $items, $flags=0)
Find many files at once.
Definition FileRepo.php:491
getThumbProxyUrl()
Get the URL thumb.php requests are being proxied to.
Definition FileRepo.php:614
getZoneLocation( $zone)
The the storage container and base path of a zone.
Definition FileRepo.php:344
string $favicon
The URL of the repo's favicon, if any.
Definition FileRepo.php:119
fileExists( $file)
Checks existence of a file.
getFileSha1( $virtualUrl)
Get the sha1 (base 36) of a file with a given virtual URL/storage path.
bool $supportsSha1URLs
Definition FileRepo.php:58
quickImportBatch(array $triples)
Import a batch of files from the local file system into the repo.
assertWritableRepo()
Throw an exception if this repo is read-only by design.
array $oldFileFactoryKey
callable|bool Override these in the base class
Definition FileRepo.php:131
getRootDirectory()
Get the public zone root storage directory of the repository.
Definition FileRepo.php:660
supportsSha1URLs()
Returns whether or not repo supports having originals SHA-1s in the thumb URLs.
newGood( $value=null)
Create a new good result.
findFilesByPrefix( $prefix, $limit)
Return an array of files where the name starts with $prefix.
Definition FileRepo.php:596
getHashLevels()
Get the number of hash directory levels.
Definition FileRepo.php:712
string $thumbProxySecret
Secret key to pass as an X-Swift-Secret header to the proxied thumb service.
Definition FileRepo.php:138
streamFileWithStatus( $virtualUrl, $headers=[], $optHeaders=[])
Attempt to stream a file with the given virtual URL/storage path.
getName()
Get the name of this repository, as specified by $info['name]' to the constructor.
Definition FileRepo.php:721
store( $srcPath, $dstZone, $dstRel, $flags=0)
Store a file to a given destination.
Definition FileRepo.php:834
findFile( $title, $options=[])
Find an instance of the named file created at the specified time Returns false if the file does not e...
Definition FileRepo.php:415
getVirtualUrl( $suffix=false)
Get a URL referring to this repository, with the private mwrepo protocol.
Definition FileRepo.php:261
const NAME_AND_TIME_ONLY
Definition FileRepo.php:45
quickPurge( $path)
Purge a file from the repo.
Definition FileRepo.php:980
streamFile( $virtualUrl, $headers=[])
Attempt to stream a file with the given virtual URL/storage path.
quickPurgeBatch(array $paths)
Purge a batch of files from the repo.
passThrough( $param)
Path disclosure protection function.
static getHashPathForLevel( $name, $levels)
Definition FileRepo.php:693
array $zones
Map of zones to config.
Definition FileRepo.php:64
getUploadStash(User $user=null)
Get an UploadStash associated with this repo.
getDisplayName()
Get the human-readable name of the repo.
array $oldFileFactory
callable|bool Override these in the base class
Definition FileRepo.php:127
getSharedCacheKey()
Get a key on the primary cache for this repository.
storeBatch(array $triplets, $flags=0)
Store a batch of files.
Definition FileRepo.php:857
enumFiles( $callback)
Call a callback function for every public regular file in the repository.
hasSha1Storage()
Returns whether or not storage is SHA-1 based.
cleanupBatch(array $files, $flags=0)
Deletes a batch of files.
Definition FileRepo.php:928
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...
initDirectory( $dir)
Creates a directory with the appropriate zone permissions.
int $abbrvThreshold
File names over this size will use the short form of thumbnail names.
Definition FileRepo.php:116
makeUrl( $query='', $entry='index')
Make an url to this repo.
Definition FileRepo.php:732
findBySha1s(array $hashes)
Get an array of arrays or iterators of file objects for files that have the given SHA-1 content hashe...
Definition FileRepo.php:576
string $thumbProxyUrl
URL of where to proxy thumb.php requests to.
Definition FileRepo.php:136
concatenate(array $srcPaths, $dstPath, $flags=0)
Concatenate a list of temporary files into a target file location.
FileBackend $backend
Definition FileRepo.php:61
fileExistsBatch(array $files)
Checks existence of an array of files.
int $descriptionCacheExpiry
Definition FileRepo.php:52
paranoidClean( $param)
Path disclosure protection function.
const SKIP_LOCKING
Definition FileRepo.php:43
initZones( $doZones=[])
Check if a single zone or list of zones is defined for usage.
Definition FileRepo.php:231
string $thumbUrl
The base thumbnail URL.
Definition FileRepo.php:104
getFileProps( $virtualUrl)
Get properties of a file with a given virtual URL/storage path.
const OVERWRITE
Definition FileRepo.php:41
isLocal()
Returns true if this the local file repository.
getZonePath( $zone)
Get the storage path corresponding to one of the zones.
Definition FileRepo.php:358
array $fileFactoryKey
callable|bool Override these in the base class
Definition FileRepo.php:129
getDescriptionUrl( $name)
Get the URL of an image description page.
Definition FileRepo.php:752
cleanDir( $dir)
Deletes a directory if empty.
getDeletedHashPath( $key)
Get a relative path for a deletion archive key, e.g.
resolveToStoragePath( $path)
If a path is a virtual URL, resolve it to a storage path.
bool $hasSha1Storage
Definition FileRepo.php:55
const DELETE_SOURCE
Definition FileRepo.php:40
getFileSize( $virtualUrl)
Get the size of a file with a given virtual URL/storage path.
getThumbProxySecret()
Get the secret key for the proxied thumb service.
Definition FileRepo.php:623
bool $fetchDescription
Whether to fetch commons image description pages and display them on the local wiki.
Definition FileRepo.php:49
string false $url
Public zone URL.
Definition FileRepo.php:101
array $fileFactory
callable Override these in the base class
Definition FileRepo.php:125
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Definition FileRepo.php:249
getDescriptionStylesheetUrl()
Get the URL of the stylesheet to apply to description pages.
Definition FileRepo.php:811
bool $transformVia404
Whether to skip media file transformation on parse and rely on a 404 handler instead.
Definition FileRepo.php:71
getFileTimestamp( $virtualUrl)
Get the timestamp of a file with a given virtual URL/storage path.
checkRedirect(Title $title)
Checks if there is a redirect named as $title.
bool $isPrivate
Whether all zones should be private (e.g.
Definition FileRepo.php:122
string $scriptDirUrl
URL of the MediaWiki installation, equivalent to $wgScriptPath, e.g.
Definition FileRepo.php:81
string $descBaseUrl
URL of image description pages, e.g.
Definition FileRepo.php:76
getZoneUrl( $zone, $ext=null)
Get the URL corresponding to one of the four basic zones.
Definition FileRepo.php:277
newFatal( $message)
Create a new fatal error.
getReadOnlyReason()
Get an explanatory message if this repo is read-only.
Definition FileRepo.php:220
newFile( $title, $time=false)
Create a new File object from the local repository.
Definition FileRepo.php:382
storeTemp( $originalName, $srcPath)
Pick a random name in the temp zone and store a file to it.
quickCleanDir( $dir)
Deletes a directory if empty.
Definition FileRepo.php:991
canTransformVia404()
Returns true if the repository can transform files via a 404 handler.
Definition FileRepo.php:632
enumFilesInStorage( $callback)
Call a callback function for every public file in the repository.
validateFilename( $filename)
Determine if a relative path is valid, i.e.
findFileFromKey( $sha1, $options=[])
Find an instance of the file with this key, created at the specified time Returns false if the file d...
Definition FileRepo.php:528
int $deletedHashLevels
The number of directory levels for hash-based division of deleted files.
Definition FileRepo.php:110
string $thumbScriptUrl
URL of thumb.php.
Definition FileRepo.php:67
backendSupportsUnicodePaths()
Definition FileRepo.php:306
__construct(array $info=null)
Definition FileRepo.php:144
bool $initialCapital
Equivalent to $wgCapitalLinks (or $wgCapitalLinkOverrides[NS_FILE], determines whether filenames impl...
Definition FileRepo.php:91
getLocalCopy( $virtualUrl)
Get a local FS copy of a file with a given virtual URL/storage path.
deleteBatch(array $sourceDestPairs)
Move a group of files to the deletion archive.
getErrorCleanupFunction()
Get a callback function to use for cleaning error message parameters.
getHashPath( $name)
Get a relative path including trailing slash, e.g.
Definition FileRepo.php:671
getNameFromTitle(Title $title)
Get the name of a file from its title object.
Definition FileRepo.php:642
invalidateImageRedirect(Title $title)
Invalidates image redirect cache related to that image Doesn't do anything for repositories that don'...
quickImport( $src, $dst, $options=null)
Import a file from the local file system into the repo.
Definition FileRepo.php:968
string $articleUrl
Equivalent to $wgArticlePath, e.g.
Definition FileRepo.php:84
getDescriptionRenderUrl( $name, $lang=null)
Get the URL of the content-only fragment of the description page.
Definition FileRepo.php:786
freeTemp( $virtualUrl)
Remove a temporary file or mark it for garbage collection.
findBySha1( $hash)
Get an array or iterator of file objects for files that have a given SHA-1 content hash.
Definition FileRepo.php:565
getBackend()
Get the file backend instance.
Definition FileRepo.php:210
getInfo()
Return information about the repository.
getThumbScriptUrl()
Get the URL of thumb.php.
Definition FileRepo.php:605
getLocalReference( $virtualUrl)
Get a local FS file with a given virtual URL/storage path.
static normalizeTitle( $title, $exception=false)
Given a string or Title object return either a valid Title object with namespace NS_FILE or null.
Definition File.php:184
const DELETED_FILE
Definition File.php:53
MediaWiki exception.
MimeMagic helper wrapper.
MediaWikiServices is the service locator for the application scope of MediaWiki.
FileRepo for temporary files created via FileRepo::getTempRepo()
Represents a title within MediaWiki.
Definition Title.php:39
UploadStash is intended to accomplish a few things:
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:47
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
namespace being checked & $result
Definition hooks.txt:2385
see documentation in includes Linker php for Linker::makeImageLink & $time
Definition hooks.txt:1841
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2050
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return true
Definition hooks.txt:2055
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2054
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, 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. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy: boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1071
> value, names are case insensitive). Two headers get special handling:If-Modified-Since(value must be a valid HTTP date) and Range(must be of the form "bytes=(\d*-\d*)") will be honored when streaming the file. '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 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name '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 since 1.16! Use $magicWords in a file listed in $wgExtensionMessagesFiles instead. Use this to define synonyms of magic words depending of the language & $magicExtensions:associative array of magic words synonyms $lang:language code(string) 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetSpecialPageAliases':DEPRECATED! Use $specialPageAliases in a file listed in $wgExtensionMessagesFiles instead. Use to define aliases of special pages names depending of the language & $specialPageAliases:associative array of magic words synonyms $lang:language code(string) 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) name
Definition hooks.txt:1889
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition hooks.txt:1656
const NS_FILE
Definition Defines.php:70
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
$source
A helper class for throttling authentication attempts.
if(!is_readable( $file)) $ext
Definition router.php:55
$params
if(!isset( $args[0])) $lang