MediaWiki master
UploadBase.php
Go to the documentation of this file.
1<?php
26use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
40use Wikimedia\AtEase\AtEase;
43
60abstract class UploadBase {
61 use ProtectedHookAccessorTrait;
62
64 protected $mTempPath;
66 protected $tempFileObj;
70 protected $mDestName;
74 protected $mSourceType;
76 protected $mTitle = false;
78 protected $mTitleError = 0;
80 protected $mFilteredName;
84 protected $mLocalFile;
86 protected $mStashFile;
88 protected $mFileSize;
90 protected $mFileProps;
94 protected $mJavaDetected;
96 protected $mSVGNSError;
97
98 protected static $safeXmlEncodings = [
99 'UTF-8',
100 'US-ASCII',
101 'ISO-8859-1',
102 'ISO-8859-2',
103 'UTF-16',
104 'UTF-32',
105 'WINDOWS-1250',
106 'WINDOWS-1251',
107 'WINDOWS-1252',
108 'WINDOWS-1253',
109 'WINDOWS-1254',
110 'WINDOWS-1255',
111 'WINDOWS-1256',
112 'WINDOWS-1257',
113 'WINDOWS-1258',
114 ];
115
116 public const SUCCESS = 0;
117 public const OK = 0;
118 public const EMPTY_FILE = 3;
119 public const MIN_LENGTH_PARTNAME = 4;
120 public const ILLEGAL_FILENAME = 5;
121 public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
122 public const FILETYPE_MISSING = 8;
123 public const FILETYPE_BADTYPE = 9;
124 public const VERIFICATION_ERROR = 10;
125 public const HOOK_ABORTED = 11;
126 public const FILE_TOO_LARGE = 12;
127 public const WINDOWS_NONASCII_FILENAME = 13;
128 public const FILENAME_TOO_LONG = 14;
129
130 private const CODE_TO_STATUS = [
131 self::EMPTY_FILE => 'empty-file',
132 self::FILE_TOO_LARGE => 'file-too-large',
133 self::FILETYPE_MISSING => 'filetype-missing',
134 self::FILETYPE_BADTYPE => 'filetype-banned',
135 self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
136 self::ILLEGAL_FILENAME => 'illegal-filename',
137 self::OVERWRITE_EXISTING_FILE => 'overwrite',
138 self::VERIFICATION_ERROR => 'verification-error',
139 self::HOOK_ABORTED => 'hookaborted',
140 self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
141 self::FILENAME_TOO_LONG => 'filename-toolong',
142 ];
143
148 public function getVerificationErrorCode( $error ) {
149 return self::CODE_TO_STATUS[$error] ?? 'unknown-error';
150 }
151
158 public static function isEnabled() {
159 $enableUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnableUploads );
160
161 return $enableUploads && wfIniGetBool( 'file_uploads' );
162 }
163
172 public static function isAllowed( Authority $performer ) {
173 foreach ( [ 'upload', 'edit' ] as $permission ) {
174 if ( !$performer->isAllowed( $permission ) ) {
175 return $permission;
176 }
177 }
178
179 return true;
180 }
181
191 public static function isThrottled( $user ) {
192 wfDeprecated( __METHOD__, '1.41' );
193 return $user->pingLimiter( 'upload' );
194 }
195
197 private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
198
206 public static function createFromRequest( &$request, $type = null ) {
207 $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
208
209 if ( !$type ) {
210 return null;
211 }
212
213 // Get the upload class
214 $type = ucfirst( $type );
215
216 // Give hooks the chance to handle this request
218 $className = null;
219 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
220 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
221 ->onUploadCreateFromRequest( $type, $className );
222 if ( $className === null ) {
223 $className = 'UploadFrom' . $type;
224 wfDebug( __METHOD__ . ": class name: $className" );
225 if ( !in_array( $type, self::$uploadHandlers ) ) {
226 return null;
227 }
228 }
229
230 if ( !$className::isEnabled() || !$className::isValidRequest( $request ) ) {
231 return null;
232 }
233
235 $handler = new $className;
236
237 $handler->initializeFromRequest( $request );
238
239 return $handler;
240 }
241
247 public static function isValidRequest( $request ) {
248 return false;
249 }
250
255 public function getDesiredDestName() {
256 return $this->mDesiredDestName;
257 }
258
262 public function __construct() {
263 }
264
272 public function getSourceType() {
273 return null;
274 }
275
282 public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
283 $this->mDesiredDestName = $name;
284 if ( FileBackend::isStoragePath( $tempPath ) ) {
285 throw new InvalidArgumentException( __METHOD__ . " given storage path `$tempPath`." );
286 }
287
288 $this->setTempFile( $tempPath, $fileSize );
289 $this->mRemoveTempFile = $removeTempFile;
290 }
291
297 abstract public function initializeFromRequest( &$request );
298
303 protected function setTempFile( $tempPath, $fileSize = null ) {
304 $this->mTempPath = $tempPath ?? '';
305 $this->mFileSize = $fileSize ?: null;
306 $this->mFileProps = null;
307 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
308 $this->tempFileObj = new TempFSFile( $this->mTempPath );
309 if ( !$fileSize ) {
310 $this->mFileSize = filesize( $this->mTempPath );
311 }
312 } else {
313 $this->tempFileObj = null;
314 }
315 }
316
322 public function fetchFile() {
323 return Status::newGood();
324 }
325
331 public function canFetchFile() {
332 return Status::newGood();
333 }
334
339 public function isEmptyFile() {
340 return !$this->mFileSize;
341 }
342
347 public function getFileSize() {
348 return $this->mFileSize;
349 }
350
356 public function getTempFileSha1Base36() {
357 // Use cached version if we already have it.
358 if ( $this->mFileProps && is_string( $this->mFileProps['sha1'] ) ) {
359 return $this->mFileProps['sha1'];
360 }
361 return FSFile::getSha1Base36FromPath( $this->mTempPath );
362 }
363
368 public function getRealPath( $srcPath ) {
369 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
370 if ( FileRepo::isVirtualUrl( $srcPath ) ) {
374 $tmpFile = $repo->getLocalCopy( $srcPath );
375 if ( $tmpFile ) {
376 $tmpFile->bind( $this ); // keep alive with $this
377 }
378 $path = $tmpFile ? $tmpFile->getPath() : false;
379 } else {
380 $path = $srcPath;
381 }
382
383 return $path;
384 }
385
403 public function verifyUpload() {
407 if ( $this->isEmptyFile() ) {
408 return [ 'status' => self::EMPTY_FILE ];
409 }
410
414 $maxSize = self::getMaxUploadSize( $this->getSourceType() );
415 if ( $this->mFileSize > $maxSize ) {
416 return [
417 'status' => self::FILE_TOO_LARGE,
418 'max' => $maxSize,
419 ];
420 }
421
427 $verification = $this->verifyFile();
428 if ( $verification !== true ) {
429 return [
430 'status' => self::VERIFICATION_ERROR,
431 'details' => $verification
432 ];
433 }
434
438 $result = $this->validateName();
439 if ( $result !== true ) {
440 return $result;
441 }
442
443 return [ 'status' => self::OK ];
444 }
445
452 public function validateName() {
453 $nt = $this->getTitle();
454 if ( $nt === null ) {
455 $result = [ 'status' => $this->mTitleError ];
456 if ( $this->mTitleError === self::ILLEGAL_FILENAME ) {
457 $result['filtered'] = $this->mFilteredName;
458 }
459 if ( $this->mTitleError === self::FILETYPE_BADTYPE ) {
460 $result['finalExt'] = $this->mFinalExtension;
461 if ( count( $this->mBlackListedExtensions ) ) {
462 $result['blacklistedExt'] = $this->mBlackListedExtensions;
463 }
464 }
465
466 return $result;
467 }
468 $this->mDestName = $this->getLocalFile()->getName();
469
470 return true;
471 }
472
481 protected function verifyMimeType( $mime ) {
482 $verifyMimeType = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeType );
483 if ( $verifyMimeType ) {
484 wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" );
485 $mimeTypeExclusions = MediaWikiServices::getInstance()->getMainConfig()
486 ->get( MainConfigNames::MimeTypeExclusions );
487 if ( self::checkFileExtension( $mime, $mimeTypeExclusions ) ) {
488 return [ 'filetype-badmime', $mime ];
489 }
490 }
491
492 return true;
493 }
494
500 protected function verifyFile() {
501 $config = MediaWikiServices::getInstance()->getMainConfig();
502 $verifyMimeType = $config->get( MainConfigNames::VerifyMimeType );
503 $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
504 $status = $this->verifyPartialFile();
505 if ( $status !== true ) {
506 return $status;
507 }
508
509 // Calculating props calculates the sha1 which is expensive.
510 // reuse props if we already have them
511 if ( !is_array( $this->mFileProps ) ) {
512 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
513 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
514 }
515 $mime = $this->mFileProps['mime'];
516
517 if ( $verifyMimeType ) {
518 # XXX: Missing extension will be caught by validateName() via getTitle()
519 if ( (string)$this->mFinalExtension !== '' &&
520 !self::verifyExtension( $mime, $this->mFinalExtension )
521 ) {
522 return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
523 }
524 }
525
526 # check for htmlish code and javascript
527 if ( !$disableUploadScriptChecks ) {
528 if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
529 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
530 if ( $svgStatus !== false ) {
531 return $svgStatus;
532 }
533 }
534 }
535
536 $handler = MediaHandler::getHandler( $mime );
537 if ( $handler ) {
538 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
539 if ( !$handlerStatus->isOK() ) {
540 $errors = $handlerStatus->getErrorsArray();
541
542 return reset( $errors );
543 }
544 }
545
546 $error = true;
547 $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
548 if ( $error !== true ) {
549 if ( !is_array( $error ) ) {
550 $error = [ $error ];
551 }
552 return $error;
553 }
554
555 wfDebug( __METHOD__ . ": all clear; passing." );
556
557 return true;
558 }
559
569 protected function verifyPartialFile() {
570 $config = MediaWikiServices::getInstance()->getMainConfig();
571 $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
572 # getTitle() sets some internal parameters like $this->mFinalExtension
573 $this->getTitle();
574
575 // Calculating props calculates the sha1 which is expensive.
576 // reuse props if we already have them (e.g. During stashed upload)
577 if ( !is_array( $this->mFileProps ) ) {
578 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
579 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
580 }
581
582 # check MIME type, if desired
583 $mime = $this->mFileProps['file-mime'];
584 $status = $this->verifyMimeType( $mime );
585 if ( $status !== true ) {
586 return $status;
587 }
588
589 # check for htmlish code and javascript
590 if ( !$disableUploadScriptChecks ) {
591 if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
592 return [ 'uploadscripted' ];
593 }
594 if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
595 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
596 if ( $svgStatus !== false ) {
597 return $svgStatus;
598 }
599 }
600 }
601
602 # Scan the uploaded file for viruses
603 $virus = self::detectVirus( $this->mTempPath );
604 if ( $virus ) {
605 return [ 'uploadvirus', $virus ];
606 }
607
608 return true;
609 }
610
616 public function zipEntryCallback( $entry ) {
617 $names = [ $entry['name'] ];
618
619 // If there is a null character, cut off the name at it, because JDK's
620 // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
621 // were constructed which had ".class\0" followed by a string chosen to
622 // make the hash collide with the truncated name, that file could be
623 // returned in response to a request for the .class file.
624 $nullPos = strpos( $entry['name'], "\000" );
625 if ( $nullPos !== false ) {
626 $names[] = substr( $entry['name'], 0, $nullPos );
627 }
628
629 // If there is a trailing slash in the file name, we have to strip it,
630 // because that's what ZIP_GetEntry() does.
631 if ( preg_grep( '!\.class/?$!', $names ) ) {
632 $this->mJavaDetected = true;
633 }
634 }
635
645 public function verifyPermissions( Authority $performer ) {
646 return $this->verifyTitlePermissions( $performer );
647 }
648
660 public function verifyTitlePermissions( Authority $performer ) {
665 $nt = $this->getTitle();
666 if ( $nt === null ) {
667 return true;
668 }
669
670 $status = PermissionStatus::newEmpty();
671 $performer->authorizeWrite( 'edit', $nt, $status );
672 $performer->authorizeWrite( 'upload', $nt, $status );
673 if ( !$status->isGood() ) {
674 return $status->toLegacyErrorArray();
675 }
676
677 $overwriteError = $this->checkOverwrite( $performer );
678 if ( $overwriteError !== true ) {
679 return [ $overwriteError ];
680 }
681
682 return true;
683 }
684
694 public function checkWarnings( $user = null ) {
695 if ( $user === null ) {
696 // TODO check uses and hard deprecate
697 $user = RequestContext::getMain()->getUser();
698 }
699
700 $warnings = [];
701
702 $localFile = $this->getLocalFile();
703 $localFile->load( IDBAccessObject::READ_LATEST );
704 $filename = $localFile->getName();
705 $hash = $this->getTempFileSha1Base36();
706
707 $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
708 if ( $badFileName !== null ) {
709 $warnings['badfilename'] = $badFileName;
710 }
711
712 $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
713 if ( $unwantedFileExtensionDetails !== null ) {
714 $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
715 }
716
717 $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
718 if ( $fileSizeWarnings ) {
719 $warnings = array_merge( $warnings, $fileSizeWarnings );
720 }
721
722 $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
723 if ( $localFileExistsWarnings ) {
724 $warnings = array_merge( $warnings, $localFileExistsWarnings );
725 }
726
727 if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
728 $warnings['was-deleted'] = $filename;
729 }
730
731 // If a file with the same name exists locally then the local file has already been tested
732 // for duplication of content
733 $ignoreLocalDupes = isset( $warnings['exists'] );
734 $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
735 if ( $dupes ) {
736 $warnings['duplicate'] = $dupes;
737 }
738
739 $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
740 if ( $archivedDupes !== null ) {
741 $warnings['duplicate-archive'] = $archivedDupes;
742 }
743
744 return $warnings;
745 }
746
758 public static function makeWarningsSerializable( $warnings ) {
759 array_walk_recursive( $warnings, static function ( &$param, $key ) {
760 if ( $param instanceof File ) {
761 $param = [
762 'fileName' => $param->getName(),
763 'timestamp' => $param->getTimestamp()
764 ];
765 } elseif ( is_object( $param ) ) {
766 throw new InvalidArgumentException(
767 'UploadBase::makeWarningsSerializable: ' .
768 'Unexpected object of class ' . get_class( $param ) );
769 }
770 } );
771 return $warnings;
772 }
773
781 public static function unserializeWarnings( $warnings ) {
782 foreach ( $warnings as $key => $value ) {
783 if ( is_array( $value ) ) {
784 if ( isset( $value['fileName'] ) && isset( $value['timestamp'] ) ) {
785 $warnings[$key] = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
786 $value['fileName'],
787 [ 'time' => $value['timestamp'] ]
788 );
789 } else {
790 $warnings[$key] = self::unserializeWarnings( $value );
791 }
792 }
793 }
794 return $warnings;
795 }
796
806 private function checkBadFileName( $filename, $desiredFileName ) {
807 $comparableName = str_replace( ' ', '_', $desiredFileName );
808 $comparableName = Title::capitalize( $comparableName, NS_FILE );
809
810 if ( $desiredFileName != $filename && $comparableName != $filename ) {
811 return $filename;
812 }
813
814 return null;
815 }
816
825 private function checkUnwantedFileExtensions( $fileExtension ) {
826 $checkFileExtensions = MediaWikiServices::getInstance()->getMainConfig()
827 ->get( MainConfigNames::CheckFileExtensions );
828 $fileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FileExtensions );
829 if ( $checkFileExtensions ) {
830 $extensions = array_unique( $fileExtensions );
831 if ( !self::checkFileExtension( $fileExtension, $extensions ) ) {
832 return [
833 $fileExtension,
834 Message::listParam( $extensions, 'comma' ),
835 count( $extensions )
836 ];
837 }
838 }
839
840 return null;
841 }
842
848 private function checkFileSize( $fileSize ) {
849 $uploadSizeWarning = MediaWikiServices::getInstance()->getMainConfig()
850 ->get( MainConfigNames::UploadSizeWarning );
851
852 $warnings = [];
853
854 if ( $uploadSizeWarning && ( $fileSize > $uploadSizeWarning ) ) {
855 $warnings['large-file'] = [
856 Message::sizeParam( $uploadSizeWarning ),
857 Message::sizeParam( $fileSize ),
858 ];
859 }
860
861 if ( $fileSize == 0 ) {
862 $warnings['empty-file'] = true;
863 }
864
865 return $warnings;
866 }
867
874 private function checkLocalFileExists( LocalFile $localFile, $hash ) {
875 $warnings = [];
876
877 $exists = self::getExistsWarning( $localFile );
878 if ( $exists !== false ) {
879 $warnings['exists'] = $exists;
880
881 // check if file is an exact duplicate of current file version
882 if ( $hash !== false && $hash === $localFile->getSha1() ) {
883 $warnings['no-change'] = $localFile;
884 }
885
886 // check if file is an exact duplicate of older versions of this file
887 $history = $localFile->getHistory();
888 foreach ( $history as $oldFile ) {
889 if ( $hash === $oldFile->getSha1() ) {
890 $warnings['duplicate-version'][] = $oldFile;
891 }
892 }
893 }
894
895 return $warnings;
896 }
897
898 private function checkLocalFileWasDeleted( LocalFile $localFile ) {
899 return $localFile->wasDeleted() && !$localFile->exists();
900 }
901
908 private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
909 if ( $hash === false ) {
910 return [];
911 }
912 $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
913 $title = $this->getTitle();
914 foreach ( $dupes as $key => $dupe ) {
915 if (
916 ( $dupe instanceof LocalFile ) &&
917 $ignoreLocalDupes &&
918 $title->equals( $dupe->getTitle() )
919 ) {
920 unset( $dupes[$key] );
921 }
922 }
923
924 return $dupes;
925 }
926
934 private function checkAgainstArchiveDupes( $hash, Authority $performer ) {
935 if ( $hash === false ) {
936 return null;
937 }
938 $archivedFile = new ArchivedFile( null, 0, '', $hash );
939 if ( $archivedFile->getID() > 0 ) {
940 if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
941 return $archivedFile->getName();
942 }
943 return '';
944 }
945
946 return null;
947 }
948
966 public function performUpload(
967 $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
968 ) {
969 $this->getLocalFile()->load( IDBAccessObject::READ_LATEST );
970 $props = $this->mFileProps;
971
972 $error = null;
973 $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
974 if ( $error ) {
975 if ( !is_array( $error ) ) {
976 $error = [ $error ];
977 }
978 return Status::newFatal( ...$error );
979 }
980
981 $status = $this->getLocalFile()->upload(
982 $this->mTempPath,
983 $comment,
984 $pageText !== false ? $pageText : '',
985 File::DELETE_SOURCE,
986 $props,
987 false,
988 $user,
989 $tags
990 );
991
992 if ( $status->isGood() ) {
993 if ( $watch ) {
994 MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
995 $user,
996 $this->getLocalFile()->getTitle(),
997 $watchlistExpiry
998 );
999 }
1000 $this->getHookRunner()->onUploadComplete( $this );
1001
1002 $this->postProcessUpload();
1003 }
1004
1005 return $status;
1006 }
1007
1014 public function postProcessUpload() {
1015 }
1016
1023 public function getTitle() {
1024 if ( $this->mTitle !== false ) {
1025 return $this->mTitle;
1026 }
1027 if ( !is_string( $this->mDesiredDestName ) ) {
1028 $this->mTitleError = self::ILLEGAL_FILENAME;
1029 $this->mTitle = null;
1030
1031 return $this->mTitle;
1032 }
1033 /* Assume that if a user specified File:Something.jpg, this is an error
1034 * and that the namespace prefix needs to be stripped of.
1035 */
1036 $title = Title::newFromText( $this->mDesiredDestName );
1037 if ( $title && $title->getNamespace() === NS_FILE ) {
1038 $this->mFilteredName = $title->getDBkey();
1039 } else {
1040 $this->mFilteredName = $this->mDesiredDestName;
1041 }
1042
1043 # oi_archive_name is max 255 bytes, which include a timestamp and an
1044 # exclamation mark, so restrict file name to 240 bytes.
1045 if ( strlen( $this->mFilteredName ) > 240 ) {
1046 $this->mTitleError = self::FILENAME_TOO_LONG;
1047 $this->mTitle = null;
1048
1049 return $this->mTitle;
1050 }
1051
1057 $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
1058 /* Normalize to title form before we do any further processing */
1059 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1060 if ( $nt === null ) {
1061 $this->mTitleError = self::ILLEGAL_FILENAME;
1062 $this->mTitle = null;
1063
1064 return $this->mTitle;
1065 }
1066 $this->mFilteredName = $nt->getDBkey();
1067
1072 [ $partname, $ext ] = self::splitExtensions( $this->mFilteredName );
1073
1074 if ( $ext !== [] ) {
1075 $this->mFinalExtension = trim( end( $ext ) );
1076 } else {
1077 $this->mFinalExtension = '';
1078
1079 // No extension, try guessing one from the temporary file
1080 // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1081 // or incomplete test case in UploadBaseTest (T272328)
1082 if ( $this->mTempPath !== null ) {
1083 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1084 $mime = $magic->guessMimeType( $this->mTempPath );
1085 if ( $mime !== 'unknown/unknown' ) {
1086 # Get a space separated list of extensions
1087 $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1088 if ( $mimeExt !== null ) {
1089 # Set the extension to the canonical extension
1090 $this->mFinalExtension = $mimeExt;
1091
1092 # Fix up the other variables
1093 $this->mFilteredName .= ".{$this->mFinalExtension}";
1094 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1095 $ext = [ $this->mFinalExtension ];
1096 }
1097 }
1098 }
1099 }
1100
1101 // Don't allow users to override the list of prohibited file extensions (check file extension)
1102 $config = MediaWikiServices::getInstance()->getMainConfig();
1103 $checkFileExtensions = $config->get( MainConfigNames::CheckFileExtensions );
1104 $strictFileExtensions = $config->get( MainConfigNames::StrictFileExtensions );
1105 $fileExtensions = $config->get( MainConfigNames::FileExtensions );
1106 $prohibitedFileExtensions = $config->get( MainConfigNames::ProhibitedFileExtensions );
1107
1108 $badList = self::checkFileExtensionList( $ext, $prohibitedFileExtensions );
1109
1110 if ( $this->mFinalExtension == '' ) {
1111 $this->mTitleError = self::FILETYPE_MISSING;
1112 $this->mTitle = null;
1113
1114 return $this->mTitle;
1115 }
1116
1117 if ( $badList ||
1118 ( $checkFileExtensions && $strictFileExtensions &&
1119 !self::checkFileExtension( $this->mFinalExtension, $fileExtensions ) )
1120 ) {
1121 $this->mBlackListedExtensions = $badList;
1122 $this->mTitleError = self::FILETYPE_BADTYPE;
1123 $this->mTitle = null;
1124
1125 return $this->mTitle;
1126 }
1127
1128 // Windows may be broken with special characters, see T3780
1129 if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1130 && !MediaWikiServices::getInstance()->getRepoGroup()
1131 ->getLocalRepo()->backendSupportsUnicodePaths()
1132 ) {
1133 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1134 $this->mTitle = null;
1135
1136 return $this->mTitle;
1137 }
1138
1139 # If there was more than one file "extension", reassemble the base
1140 # filename to prevent bogus complaints about length
1141 if ( count( $ext ) > 1 ) {
1142 $iterations = count( $ext ) - 1;
1143 for ( $i = 0; $i < $iterations; $i++ ) {
1144 $partname .= '.' . $ext[$i];
1145 }
1146 }
1147
1148 if ( strlen( $partname ) < 1 ) {
1149 $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1150 $this->mTitle = null;
1151
1152 return $this->mTitle;
1153 }
1154
1155 $this->mTitle = $nt;
1156
1157 return $this->mTitle;
1158 }
1159
1166 public function getLocalFile() {
1167 if ( $this->mLocalFile === null ) {
1168 $nt = $this->getTitle();
1169 $this->mLocalFile = $nt === null
1170 ? null
1171 : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1172 }
1173
1174 return $this->mLocalFile;
1175 }
1176
1180 public function getStashFile() {
1181 return $this->mStashFile;
1182 }
1183
1196 public function tryStashFile( User $user, $isPartial = false ) {
1197 if ( !$isPartial ) {
1198 $error = $this->runUploadStashFileHook( $user );
1199 if ( $error ) {
1200 return Status::newFatal( ...$error );
1201 }
1202 }
1203 try {
1204 $file = $this->doStashFile( $user );
1205 return Status::newGood( $file );
1206 } catch ( UploadStashException $e ) {
1207 return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1208 }
1209 }
1210
1215 protected function runUploadStashFileHook( User $user ) {
1216 $props = $this->mFileProps;
1217 $error = null;
1218 $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1219 if ( $error && !is_array( $error ) ) {
1220 $error = [ $error ];
1221 }
1222 return $error;
1223 }
1224
1232 protected function doStashFile( User $user = null ) {
1233 $stash = MediaWikiServices::getInstance()->getRepoGroup()
1234 ->getLocalRepo()->getUploadStash( $user );
1235 $file = $stash->stashFile( $this->mTempPath, $this->getSourceType(), $this->mFileProps );
1236 $this->mStashFile = $file;
1237
1238 return $file;
1239 }
1240
1245 public function cleanupTempFile() {
1246 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1247 // Delete when all relevant TempFSFile handles go out of scope
1248 wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1249 $this->tempFileObj->autocollect();
1250 }
1251 }
1252
1256 public function getTempPath() {
1257 return $this->mTempPath;
1258 }
1259
1269 public static function splitExtensions( $filename ) {
1270 $bits = explode( '.', $filename );
1271 $basename = array_shift( $bits );
1272
1273 return [ $basename, $bits ];
1274 }
1275
1283 public static function checkFileExtension( $ext, $list ) {
1284 return in_array( strtolower( $ext ?? '' ), $list, true );
1285 }
1286
1295 public static function checkFileExtensionList( $ext, $list ) {
1296 return array_intersect( array_map( 'strtolower', $ext ), $list );
1297 }
1298
1306 public static function verifyExtension( $mime, $extension ) {
1307 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1308
1309 if ( !$mime || $mime === 'unknown' || $mime === 'unknown/unknown' ) {
1310 if ( !$magic->isRecognizableExtension( $extension ) ) {
1311 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1312 "unrecognized extension '$extension', can't verify" );
1313
1314 return true;
1315 }
1316
1317 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1318 "recognized extension '$extension', so probably invalid file" );
1319 return false;
1320 }
1321
1322 $match = $magic->isMatchingExtension( $extension, $mime );
1323
1324 if ( $match === null ) {
1325 if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1326 wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1327
1328 return false;
1329 }
1330
1331 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1332 return true;
1333 }
1334
1335 if ( $match ) {
1336 wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1337
1339 return true;
1340 }
1341
1342 wfDebug( __METHOD__
1343 . ": mime type $mime mismatches file extension $extension, rejecting file" );
1344
1345 return false;
1346 }
1347
1359 public static function detectScript( $file, $mime, $extension ) {
1360 # ugly hack: for text files, always look at the entire file.
1361 # For binary field, just check the first K.
1362
1363 if ( str_starts_with( $mime ?? '', 'text/' ) ) {
1364 $chunk = file_get_contents( $file );
1365 } else {
1366 $fp = fopen( $file, 'rb' );
1367 if ( !$fp ) {
1368 return false;
1369 }
1370 $chunk = fread( $fp, 1024 );
1371 fclose( $fp );
1372 }
1373
1374 $chunk = strtolower( $chunk );
1375
1376 if ( !$chunk ) {
1377 return false;
1378 }
1379
1380 # decode from UTF-16 if needed (could be used for obfuscation).
1381 if ( str_starts_with( $chunk, "\xfe\xff" ) ) {
1382 $enc = 'UTF-16BE';
1383 } elseif ( str_starts_with( $chunk, "\xff\xfe" ) ) {
1384 $enc = 'UTF-16LE';
1385 } else {
1386 $enc = null;
1387 }
1388
1389 if ( $enc !== null ) {
1390 $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1391 }
1392
1393 $chunk = trim( $chunk );
1394
1396 wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1397
1398 # check for HTML doctype
1399 if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1400 return true;
1401 }
1402
1403 // Some browsers will interpret obscure xml encodings as UTF-8, while
1404 // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1405 if ( $extension === 'svg' || str_starts_with( $mime ?? '', 'image/svg' ) ) {
1406 if ( self::checkXMLEncodingMissmatch( $file ) ) {
1407 return true;
1408 }
1409 }
1410
1411 // Quick check for HTML heuristics in old IE and Safari.
1412 //
1413 // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1414 // don't need them all here as it can cause many false positives.
1415 //
1416 // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1417 $tags = [
1418 '<body',
1419 '<head',
1420 '<html', # also in safari
1421 '<script', # also in safari
1422 ];
1423
1424 foreach ( $tags as $tag ) {
1425 if ( strpos( $chunk, $tag ) !== false ) {
1426 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1427
1428 return true;
1429 }
1430 }
1431
1432 /*
1433 * look for JavaScript
1434 */
1435
1436 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1437 $chunk = Sanitizer::decodeCharReferences( $chunk );
1438
1439 # look for script-types
1440 if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!im', $chunk ) ) {
1441 wfDebug( __METHOD__ . ": found script types" );
1442
1443 return true;
1444 }
1445
1446 # look for html-style script-urls
1447 if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) {
1448 wfDebug( __METHOD__ . ": found html-style script urls" );
1449
1450 return true;
1451 }
1452
1453 # look for css-style script-urls
1454 if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) {
1455 wfDebug( __METHOD__ . ": found css-style script urls" );
1456
1457 return true;
1458 }
1459
1460 wfDebug( __METHOD__ . ": no scripts found" );
1461
1462 return false;
1463 }
1464
1472 public static function checkXMLEncodingMissmatch( $file ) {
1473 // https://mimesniff.spec.whatwg.org/#resource-header says browsers
1474 // should read the first 1445 bytes. Do 4096 bytes for good measure.
1475 // XML Spec says XML declaration if present must be first thing in file
1476 // other than BOM
1477 $contents = file_get_contents( $file, false, null, 0, 4096 );
1478 $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1479
1480 if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1481 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1482 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1483 ) {
1484 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1485
1486 return true;
1487 }
1488 } elseif ( preg_match( "!<\?xml\b!i", $contents ) ) {
1489 // Start of XML declaration without an end in the first 4096 bytes
1490 // bytes. There shouldn't be a legitimate reason for this to happen.
1491 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1492
1493 return true;
1494 } elseif ( str_starts_with( $contents, "\x4C\x6F\xA7\x94" ) ) {
1495 // EBCDIC encoded XML
1496 wfDebug( __METHOD__ . ": EBCDIC Encoded XML" );
1497
1498 return true;
1499 }
1500
1501 // It's possible the file is encoded with multibyte encoding, so re-encode attempt to
1502 // detect the encoding in case it specifies an encoding not allowed in self::$safeXmlEncodings
1503 $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1504 foreach ( $attemptEncodings as $encoding ) {
1505 AtEase::suppressWarnings();
1506 $str = iconv( $encoding, 'UTF-8', $contents );
1507 AtEase::restoreWarnings();
1508 if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1509 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1510 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1511 ) {
1512 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1513
1514 return true;
1515 }
1516 } elseif ( $str != '' && preg_match( "!<\?xml\b!i", $str ) ) {
1517 // Start of XML declaration without an end in the first 4096 bytes
1518 // bytes. There shouldn't be a legitimate reason for this to happen.
1519 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1520
1521 return true;
1522 }
1523 }
1524
1525 return false;
1526 }
1527
1533 protected function detectScriptInSvg( $filename, $partial ) {
1534 $this->mSVGNSError = false;
1535 $check = new XmlTypeCheck(
1536 $filename,
1537 [ $this, 'checkSvgScriptCallback' ],
1538 true,
1539 [
1540 'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ],
1541 'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ],
1542 ]
1543 );
1544 if ( $check->wellFormed !== true ) {
1545 // Invalid xml (T60553)
1546 // But only when non-partial (T67724)
1547 return $partial ? false : [ 'uploadinvalidxml' ];
1548 }
1549
1550 if ( $check->filterMatch ) {
1551 if ( $this->mSVGNSError ) {
1552 return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1553 }
1554 return $check->filterMatchType;
1555 }
1556
1557 return false;
1558 }
1559
1567 public static function checkSvgPICallback( $target, $data ) {
1568 // Don't allow external stylesheets (T59550)
1569 if ( preg_match( '/xml-stylesheet/i', $target ) ) {
1570 return [ 'upload-scripted-pi-callback' ];
1571 }
1572
1573 return false;
1574 }
1575
1588 public static function checkSvgExternalDTD( $type, $publicId, $systemId ) {
1589 // This doesn't include the XHTML+MathML+SVG doctype since we don't
1590 // allow XHTML anyway.
1591 static $allowedDTDs = [
1592 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1593 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1594 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1595 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1596 // https://phabricator.wikimedia.org/T168856
1597 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1598 ];
1599 if ( $type !== 'PUBLIC'
1600 || !in_array( $systemId, $allowedDTDs )
1601 || !str_starts_with( $publicId, "-//W3C//" )
1602 ) {
1603 return [ 'upload-scripted-dtd' ];
1604 }
1605 return false;
1606 }
1607
1615 public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1616 [ $namespace, $strippedElement ] = self::splitXmlNamespace( $element );
1617
1618 // We specifically don't include:
1619 // http://www.w3.org/1999/xhtml (T62771)
1620 static $validNamespaces = [
1621 '',
1622 'adobe:ns:meta/',
1623 'http://creativecommons.org/ns#',
1624 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1625 'http://ns.adobe.com/adobeillustrator/10.0/',
1626 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1627 'http://ns.adobe.com/extensibility/1.0/',
1628 'http://ns.adobe.com/flows/1.0/',
1629 'http://ns.adobe.com/illustrator/1.0/',
1630 'http://ns.adobe.com/imagereplacement/1.0/',
1631 'http://ns.adobe.com/pdf/1.3/',
1632 'http://ns.adobe.com/photoshop/1.0/',
1633 'http://ns.adobe.com/saveforweb/1.0/',
1634 'http://ns.adobe.com/variables/1.0/',
1635 'http://ns.adobe.com/xap/1.0/',
1636 'http://ns.adobe.com/xap/1.0/g/',
1637 'http://ns.adobe.com/xap/1.0/g/img/',
1638 'http://ns.adobe.com/xap/1.0/mm/',
1639 'http://ns.adobe.com/xap/1.0/rights/',
1640 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1641 'http://ns.adobe.com/xap/1.0/stype/font#',
1642 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1643 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1644 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1645 'http://ns.adobe.com/xap/1.0/t/pg/',
1646 'http://purl.org/dc/elements/1.1/',
1647 'http://purl.org/dc/elements/1.1',
1648 'http://schemas.microsoft.com/visio/2003/svgextensions/',
1649 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1650 'http://taptrix.com/inkpad/svg_extensions',
1651 'http://web.resource.org/cc/',
1652 'http://www.freesoftware.fsf.org/bkchem/cdml',
1653 'http://www.inkscape.org/namespaces/inkscape',
1654 'http://www.opengis.net/gml',
1655 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1656 'http://www.w3.org/2000/svg',
1657 'http://www.w3.org/tr/rec-rdf-syntax/',
1658 'http://www.w3.org/2000/01/rdf-schema#',
1659 'http://www.w3.org/2000/02/svg/testsuite/description/', // https://phabricator.wikimedia.org/T278044
1660 ];
1661
1662 // Inkscape mangles namespace definitions created by Adobe Illustrator.
1663 // This is nasty but harmless. (T144827)
1664 $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1665
1666 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1667 wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file." );
1669 $this->mSVGNSError = $namespace;
1670
1671 return true;
1672 }
1673
1674 // check for elements that can contain javascript
1675 if ( $strippedElement === 'script' ) {
1676 wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file." );
1677
1678 return [ 'uploaded-script-svg', $strippedElement ];
1679 }
1680
1681 // e.g., <svg xmlns="http://www.w3.org/2000/svg">
1682 // <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1683 if ( $strippedElement === 'handler' ) {
1684 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1685
1686 return [ 'uploaded-script-svg', $strippedElement ];
1687 }
1688
1689 // SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1690 if ( $strippedElement === 'stylesheet' ) {
1691 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1692
1693 return [ 'uploaded-script-svg', $strippedElement ];
1694 }
1695
1696 // Block iframes, in case they pass the namespace check
1697 if ( $strippedElement === 'iframe' ) {
1698 wfDebug( __METHOD__ . ": iframe in uploaded file." );
1699
1700 return [ 'uploaded-script-svg', $strippedElement ];
1701 }
1702
1703 // Check <style> css
1704 if ( $strippedElement === 'style'
1705 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1706 ) {
1707 wfDebug( __METHOD__ . ": hostile css in style element." );
1708
1709 return [ 'uploaded-hostile-svg' ];
1710 }
1711
1712 static $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1713 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1714
1715 foreach ( $attribs as $attrib => $value ) {
1716 // If attributeNamespace is '', it is relative to its element's namespace
1717 [ $attributeNamespace, $stripped ] = self::splitXmlNamespace( $attrib );
1718 $value = strtolower( $value );
1719
1720 if ( !(
1721 // Inkscape element's have valid attribs that start with on and are safe, fail all others
1722 $namespace === 'http://www.inkscape.org/namespaces/inkscape' &&
1723 $attributeNamespace === ''
1724 ) && str_starts_with( $stripped, 'on' )
1725 ) {
1726 wfDebug( __METHOD__
1727 . ": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1728
1729 return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1730 }
1731
1732 // Do not allow relative links, or unsafe url schemas.
1733 // For <a> tags, only data:, http: and https: and same-document
1734 // fragment links are allowed.
1735 // For all other tags, only 'data:' and fragments (#) are allowed.
1736 if (
1737 $stripped === 'href'
1738 && $value !== ''
1739 && !str_starts_with( $value, 'data:' )
1740 && !str_starts_with( $value, '#' )
1741 && !( $strippedElement === 'a' && preg_match( '!^https?://!i', $value ) )
1742 ) {
1743 wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1744 . "'$attrib'='$value' in uploaded file." );
1745
1746 return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1747 }
1748
1749 // Only allow 'data:\' targets that should be safe.
1750 // This prevents vectors like image/svg, text/xml, application/xml, and text/html, which can contain scripts
1751 if ( $stripped === 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1752 // RFC2397 parameters.
1753 // This is only slightly slower than (;[\w;]+)*.
1754 // phpcs:ignore Generic.Files.LineLength
1755 $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1756
1757 if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1758 wfDebug( __METHOD__ . ": Found href to allow listed data: uri "
1759 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1760 return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1761 }
1762 }
1763
1764 // Change href with animate from (http://html5sec.org/#137).
1765 if ( $stripped === 'attributename'
1766 && $strippedElement === 'animate'
1767 && $this->stripXmlNamespace( $value ) === 'href'
1768 ) {
1769 wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1770 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1771
1772 return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1773 }
1774
1775 // Use set/animate to add event-handler attribute to parent.
1776 if ( ( $strippedElement === 'set' || $strippedElement === 'animate' )
1777 && $stripped === 'attributename'
1778 && str_starts_with( $value, 'on' )
1779 ) {
1780 wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1781 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1782
1783 return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1784 }
1785
1786 // use set to add href attribute to parent element.
1787 if ( $strippedElement === 'set'
1788 && $stripped === 'attributename'
1789 && str_contains( $value, 'href' )
1790 ) {
1791 wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file." );
1792
1793 return [ 'uploaded-setting-href-svg' ];
1794 }
1795
1796 // use set to add a remote / data / script target to an element.
1797 if ( $strippedElement === 'set'
1798 && $stripped === 'to'
1799 && preg_match( '!(http|https|data|script):!im', $value )
1800 ) {
1801 wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file." );
1802
1803 return [ 'uploaded-wrong-setting-svg', $value ];
1804 }
1805
1806 // use handler attribute with remote / data / script.
1807 if ( $stripped === 'handler' && preg_match( '!(http|https|data|script):!im', $value ) ) {
1808 wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1809 . "'$attrib'='$value' in uploaded file." );
1810
1811 return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1812 }
1813
1814 // use CSS styles to bring in remote code.
1815 if ( $stripped === 'style'
1816 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1817 ) {
1818 wfDebug( __METHOD__ . ": Found svg setting a style with "
1819 . "remote url '$attrib'='$value' in uploaded file." );
1820 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1821 }
1822
1823 // Several attributes can include css, css character escaping isn't allowed.
1824 if ( in_array( $stripped, $cssAttrs, true )
1825 && self::checkCssFragment( $value )
1826 ) {
1827 wfDebug( __METHOD__ . ": Found svg setting a style with "
1828 . "remote url '$attrib'='$value' in uploaded file." );
1829 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1830 }
1831
1832 // image filters can pull in url, which could be svg that executes scripts.
1833 // Only allow url( "#foo" ).
1834 // Do not allow url( http://example.com )
1835 if ( $strippedElement === 'image'
1836 && $stripped === 'filter'
1837 && preg_match( '!url\s*\‍(\s*["\']?[^#]!im', $value )
1838 ) {
1839 wfDebug( __METHOD__ . ": Found image filter with url: "
1840 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1841
1842 return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1843 }
1844 }
1845
1846 return false; // No scripts detected
1847 }
1848
1855 private static function checkCssFragment( $value ) {
1856 # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1857 if ( stripos( $value, '@import' ) !== false ) {
1858 return true;
1859 }
1860
1861 # We allow @font-face to embed fonts with data: urls, so we snip the string
1862 # 'url' out so that this case won't match when we check for urls below
1863 $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1864 $value = preg_replace( $pattern, '$1$2', $value );
1865
1866 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1867 # properties filter and accelerator don't seem to be useful for xss in SVG files.
1868 # Expression and -o-link don't seem to work either, but filtering them here in case.
1869 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1870 # but not local ones such as url("#..., url('#..., url(#....
1871 if ( preg_match( '!expression
1872 | -o-link\s*:
1873 | -o-link-source\s*:
1874 | -o-replace\s*:!imx', $value ) ) {
1875 return true;
1876 }
1877
1878 if ( preg_match_all(
1879 "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1880 $value,
1881 $matches
1882 ) !== 0
1883 ) {
1884 # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1885 foreach ( $matches[1] as $match ) {
1886 if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1887 return true;
1888 }
1889 }
1890 }
1891
1892 if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1893 return true;
1894 }
1895
1896 return false;
1897 }
1898
1904 private static function splitXmlNamespace( $element ) {
1905 // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1906 $parts = explode( ':', strtolower( $element ) );
1907 $name = array_pop( $parts );
1908 $ns = implode( ':', $parts );
1909
1910 return [ $ns, $name ];
1911 }
1912
1917 private function stripXmlNamespace( $element ) {
1918 // 'http://www.w3.org/2000/svg:script' -> 'script'
1919 return self::splitXmlNamespace( $element )[1];
1920 }
1921
1932 public static function detectVirus( $file ) {
1933 global $wgOut;
1934 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1935 $antivirus = $mainConfig->get( MainConfigNames::Antivirus );
1936 $antivirusSetup = $mainConfig->get( MainConfigNames::AntivirusSetup );
1937 $antivirusRequired = $mainConfig->get( MainConfigNames::AntivirusRequired );
1938 if ( !$antivirus ) {
1939 wfDebug( __METHOD__ . ": virus scanner disabled" );
1940
1941 return null;
1942 }
1943
1944 if ( !$antivirusSetup[$antivirus] ) {
1945 wfDebug( __METHOD__ . ": unknown virus scanner: {$antivirus}" );
1946 $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1947 [ 'virus-badscanner', $antivirus ] );
1948
1949 return wfMessage( 'virus-unknownscanner' )->text() . " {$antivirus}";
1950 }
1951
1952 # look up scanner configuration
1953 $command = $antivirusSetup[$antivirus]['command'];
1954 $exitCodeMap = $antivirusSetup[$antivirus]['codemap'];
1955 $msgPattern = $antivirusSetup[$antivirus]['messagepattern'] ?? null;
1956
1957 if ( !str_contains( $command, "%f" ) ) {
1958 # simple pattern: append file to scan
1959 $command .= " " . Shell::escape( $file );
1960 } else {
1961 # complex pattern: replace "%f" with file to scan
1962 $command = str_replace( "%f", Shell::escape( $file ), $command );
1963 }
1964
1965 wfDebug( __METHOD__ . ": running virus scan: $command " );
1966
1967 # execute virus scanner
1968 $exitCode = false;
1969
1970 # NOTE: there's a 50-line workaround to make stderr redirection work on windows, too.
1971 # that does not seem to be worth the pain.
1972 # Ask me (Duesentrieb) about it if it's ever needed.
1973 $output = wfShellExecWithStderr( $command, $exitCode );
1974
1975 # map exit code to AV_xxx constants.
1976 $mappedCode = $exitCode;
1977 if ( $exitCodeMap ) {
1978 if ( isset( $exitCodeMap[$exitCode] ) ) {
1979 $mappedCode = $exitCodeMap[$exitCode];
1980 } elseif ( isset( $exitCodeMap["*"] ) ) {
1981 $mappedCode = $exitCodeMap["*"];
1982 }
1983 }
1984
1985 # NB: AV_NO_VIRUS is 0, but AV_SCAN_FAILED is false,
1986 # so we need the strict equalities === and thus can't use a switch here
1987 if ( $mappedCode === AV_SCAN_FAILED ) {
1988 # scan failed (code was mapped to false by $exitCodeMap)
1989 wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1990
1991 $output = $antivirusRequired
1992 ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1993 : null;
1994 } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1995 # scan failed because filetype is unknown (probably immune)
1996 wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1997 $output = null;
1998 } elseif ( $mappedCode === AV_NO_VIRUS ) {
1999 # no virus found
2000 wfDebug( __METHOD__ . ": file passed virus scan." );
2001 $output = false;
2002 } else {
2003 $output = trim( $output );
2004
2005 if ( !$output ) {
2006 $output = true; # if there's no output, return true
2007 } elseif ( $msgPattern ) {
2008 $groups = [];
2009 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
2010 $output = $groups[1];
2011 }
2012 }
2013
2014 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
2015 }
2016
2017 return $output;
2018 }
2019
2028 private function checkOverwrite( Authority $performer ) {
2029 // First check whether the local file can be overwritten
2030 $file = $this->getLocalFile();
2031 $file->load( IDBAccessObject::READ_LATEST );
2032 if ( $file->exists() ) {
2033 if ( !self::userCanReUpload( $performer, $file ) ) {
2034 return [ 'fileexists-forbidden', $file->getName() ];
2035 }
2036
2037 return true;
2038 }
2039
2040 $services = MediaWikiServices::getInstance();
2041
2042 /* Check shared conflicts: if the local file does not exist, but
2043 * RepoGroup::findFile finds a file, it exists in a shared repository.
2044 */
2045 $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
2046 if ( $file && !$performer->isAllowed( 'reupload-shared' ) ) {
2047 return [ 'fileexists-shared-forbidden', $file->getName() ];
2048 }
2049
2050 return true;
2051 }
2052
2060 public static function userCanReUpload( Authority $performer, File $img ) {
2061 if ( $performer->isAllowed( 'reupload' ) ) {
2062 return true; // non-conditional
2063 }
2064
2065 if ( !$performer->isAllowed( 'reupload-own' ) ) {
2066 return false;
2067 }
2068
2069 if ( !( $img instanceof LocalFile ) ) {
2070 return false;
2071 }
2072
2073 return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2074 }
2075
2087 public static function getExistsWarning( $file ) {
2088 if ( $file->exists() ) {
2089 return [ 'warning' => 'exists', 'file' => $file ];
2090 }
2091
2092 if ( $file->getTitle()->getArticleID() ) {
2093 return [ 'warning' => 'page-exists', 'file' => $file ];
2094 }
2095
2096 $n = strrpos( $file->getName(), '.' );
2097 if ( $n > 0 ) {
2098 $partname = substr( $file->getName(), 0, $n );
2099 $extension = substr( $file->getName(), $n + 1 );
2100 } else {
2101 $partname = $file->getName();
2102 $extension = '';
2103 }
2104 $normalizedExtension = File::normalizeExtension( $extension );
2105 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2106
2107 if ( $normalizedExtension != $extension ) {
2108 // We're not using the normalized form of the extension.
2109 // Normal form is lowercase, using most common of alternate
2110 // extensions (e.g. 'jpg' rather than 'JPEG').
2111
2112 // Check for another file using the normalized form...
2113 $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2114 $file_lc = $localRepo->newFile( $nt_lc );
2115
2116 if ( $file_lc->exists() ) {
2117 return [
2118 'warning' => 'exists-normalized',
2119 'file' => $file,
2120 'normalizedFile' => $file_lc
2121 ];
2122 }
2123 }
2124
2125 // Check for files with the same name but a different extension
2126 $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2127 if ( count( $similarFiles ) ) {
2128 return [
2129 'warning' => 'exists-normalized',
2130 'file' => $file,
2131 'normalizedFile' => $similarFiles[0],
2132 ];
2133 }
2134
2135 if ( self::isThumbName( $file->getName() ) ) {
2136 // Check for filenames like 50px- or 180px-, these are mostly thumbnails
2137 $nt_thb = Title::newFromText(
2138 substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2139 NS_FILE
2140 );
2141 $file_thb = $localRepo->newFile( $nt_thb );
2142 if ( $file_thb->exists() ) {
2143 return [
2144 'warning' => 'thumb',
2145 'file' => $file,
2146 'thumbFile' => $file_thb
2147 ];
2148 }
2149
2150 // The file does not exist, but we just don't like the name
2151 return [
2152 'warning' => 'thumb-name',
2153 'file' => $file,
2154 'thumbFile' => $file_thb
2155 ];
2156 }
2157
2158 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2159 if ( str_starts_with( $partname, $prefix ) ) {
2160 return [
2161 'warning' => 'bad-prefix',
2162 'file' => $file,
2163 'prefix' => $prefix
2164 ];
2165 }
2166 }
2167
2168 return false;
2169 }
2170
2176 public static function isThumbName( $filename ) {
2177 $n = strrpos( $filename, '.' );
2178 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2179
2180 return (
2181 substr( $partname, 3, 3 ) === 'px-' ||
2182 substr( $partname, 2, 3 ) === 'px-'
2183 ) && preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2184 }
2185
2191 public static function getFilenamePrefixBlacklist() {
2192 $list = [];
2193 $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2194 if ( !$message->isDisabled() ) {
2195 $lines = explode( "\n", $message->plain() );
2196 foreach ( $lines as $line ) {
2197 // Remove comment lines
2198 $comment = substr( trim( $line ), 0, 1 );
2199 if ( $comment === '#' || $comment == '' ) {
2200 continue;
2201 }
2202 // Remove additional comments after a prefix
2203 $comment = strpos( $line, '#' );
2204 if ( $comment > 0 ) {
2205 $line = substr( $line, 0, $comment - 1 );
2206 }
2207 $list[] = trim( $line );
2208 }
2209 }
2210
2211 return $list;
2212 }
2213
2223 public function getImageInfo( $result = null ) {
2224 $apiUpload = ApiUpload::getDummyInstance();
2225 return $apiUpload->getUploadImageInfo( $this );
2226 }
2227
2232 public function convertVerifyErrorToStatus( $error ) {
2233 $code = $error['status'];
2234 unset( $code['status'] );
2235
2236 return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2237 }
2238
2246 public static function getMaxUploadSize( $forType = null ) {
2247 $maxUploadSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxUploadSize );
2248
2249 if ( is_array( $maxUploadSize ) ) {
2250 if ( $forType !== null && isset( $maxUploadSize[$forType] ) ) {
2251 return $maxUploadSize[$forType];
2252 }
2253 return $maxUploadSize['*'];
2254 }
2255 return intval( $maxUploadSize );
2256 }
2257
2265 public static function getMaxPhpUploadSize() {
2266 $phpMaxFileSize = wfShorthandToInteger(
2267 ini_get( 'upload_max_filesize' ),
2268 PHP_INT_MAX
2269 );
2270 $phpMaxPostSize = wfShorthandToInteger(
2271 ini_get( 'post_max_size' ),
2272 PHP_INT_MAX
2273 ) ?: PHP_INT_MAX;
2274 return min( $phpMaxFileSize, $phpMaxPostSize );
2275 }
2276
2288 public static function getSessionStatus( UserIdentity $user, $statusKey ) {
2289 $store = self::getUploadSessionStore();
2290 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2291
2292 return $store->get( $key );
2293 }
2294
2307 public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
2308 $store = self::getUploadSessionStore();
2309 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2310 $logger = LoggerFactory::getInstance( 'upload' );
2311
2312 if ( is_array( $value ) && ( $value['result'] ?? '' ) === 'Failure' ) {
2313 $logger->info( 'Upload session {key} for {user} set to failure {status} at {stage}',
2314 [
2315 'result' => $value['result'] ?? '',
2316 'stage' => $value['stage'] ?? 'unknown',
2317 'user' => $user->getName(),
2318 'status' => (string)( $value['status'] ?? '-' ),
2319 'filekey' => $value['filekey'] ?? '',
2320 'key' => $statusKey
2321 ]
2322 );
2323 } elseif ( is_array( $value ) ) {
2324 $logger->debug( 'Upload session {key} for {user} changed {status} at {stage}',
2325 [
2326 'result' => $value['result'] ?? '',
2327 'stage' => $value['stage'] ?? 'unknown',
2328 'user' => $user->getName(),
2329 'status' => (string)( $value['status'] ?? '-' ),
2330 'filekey' => $value['filekey'] ?? '',
2331 'key' => $statusKey
2332 ]
2333 );
2334 } else {
2335 $logger->debug( "Upload session {key} deleted for {user}",
2336 [
2337 'value' => $value,
2338 'key' => $statusKey,
2339 'user' => $user->getName()
2340 ]
2341 );
2342 }
2343
2344 if ( $value === false ) {
2345 $store->delete( $key );
2346 } else {
2347 $store->set( $key, $value, $store::TTL_DAY );
2348 }
2349 }
2350
2357 private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
2358 return $store->makeKey(
2359 'uploadstatus',
2360 $user->isRegistered() ? $user->getId() : md5( $user->getName() ),
2361 $statusKey
2362 );
2363 }
2364
2368 private static function getUploadSessionStore() {
2369 return MediaWikiServices::getInstance()->getMainObjectStash();
2370 }
2371}
const AV_SCAN_FAILED
Definition Defines.php:100
const NS_FILE
Definition Defines.php:71
const AV_SCAN_ABORTED
Definition Defines.php:99
const AV_NO_VIRUS
Definition Defines.php:97
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
wfShorthandToInteger(?string $string='', int $default=-1)
Converts shorthand byte notation to integer form.
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with '-'.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Title null $mTitle
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgOut
Definition Setup.php:538
static getDummyInstance()
Deleted file in the 'filearchive' table.
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Definition FileRepo.php:288
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:74
getName()
Return the name of this file.
Definition File.php:342
wasDeleted()
Was this file ever deleted from the wiki?
Definition File.php:2093
Local file in the wiki's own database.
Definition LocalFile.php:69
exists()
canRender inherited
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
load( $flags=0)
Load file metadata from cache or DB, unless already loaded.
MimeMagic helper wrapper.
Group all the pieces relevant to the context of a request into one instance.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:158
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
A StatusValue for permission errors.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Executes shell commands.
Definition Shell.php:46
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Represents a title within MediaWiki.
Definition Title.php:79
getDBkey()
Get the main part with underscores.
Definition Title.php:1036
internal since 1.36
Definition User.php:93
This class is used to hold the location and do limited manipulation of files stored temporarily (this...
UploadBase and subclasses are the backend of MediaWiki's file uploads.
getSourceType()
Returns the upload type.
getDesiredDestName()
Get the desired destination name.
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
int $mTitleError
static setSessionStatus(UserIdentity $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling).
const EMPTY_FILE
UploadStashFile null $mStashFile
static verifyExtension( $mime, $extension)
Checks if the MIME type of the uploaded file matches the file extension.
postProcessUpload()
Perform extra steps after a successful upload.
checkSvgScriptCallback( $element, $attribs, $data=null)
verifyPermissions(Authority $performer)
Alias for verifyTitlePermissions.
getLocalFile()
Return the local file and initializes if necessary.
const SUCCESS
bool null $mJavaDetected
string null $mFilteredName
getRealPath( $srcPath)
static createFromRequest(&$request, $type=null)
Create a form of UploadBase depending on wpSourceType and initializes it.
runUploadStashFileHook(User $user)
zipEntryCallback( $entry)
Callback for ZipDirectoryReader to detect Java class files.
static checkSvgPICallback( $target, $data)
Callback to filter SVG Processing Instructions.
static isValidRequest( $request)
Check whether a request if valid for this handler.
convertVerifyErrorToStatus( $error)
string null $mFinalExtension
verifyPartialFile()
A verification routine suitable for partial files.
static detectScript( $file, $mime, $extension)
Heuristic for detecting files that could contain JavaScript instructions or things that may look like...
verifyFile()
Verifies that it's ok to include the uploaded file.
array null $mFileProps
static isEnabled()
Returns true if uploads are enabled.
static isThumbName( $filename)
Helper function that checks whether the filename looks like a thumbnail.
getVerificationErrorCode( $error)
performUpload( $comment, $pageText, $watch, $user, $tags=[], ?string $watchlistExpiry=null)
Really perform the upload.
string null $mDesiredDestName
verifyTitlePermissions(Authority $performer)
Check whether the user can edit, upload and create the image.
static getFilenamePrefixBlacklist()
Get a list of disallowed filename prefixes from [[MediaWiki:Filename-prefix-blacklist]].
const OVERWRITE_EXISTING_FILE
setTempFile( $tempPath, $fileSize=null)
static getSessionStatus(UserIdentity $user, $statusKey)
Get the current status of a chunked upload (used for polling).
static checkXMLEncodingMissmatch( $file)
Check an allowed list of xml encodings that are known not to be interpreted differently by the server...
doStashFile(User $user=null)
Implementation for stashFile() and tryStashFile().
string null $mDestName
string[] $mBlackListedExtensions
static isAllowed(Authority $performer)
Returns true if the user can use this upload module or else a string identifying the missing permissi...
cleanupTempFile()
If we've modified the upload file, then we need to manually remove it on exit to clean up.
getImageInfo( $result=null)
Gets image info about the file just uploaded.
validateName()
Verify that the name is valid and, if necessary, that we can overwrite.
string null $mSourceType
int null $mFileSize
isEmptyFile()
Return true if the file is empty.
static checkFileExtension( $ext, $list)
Perform case-insensitive match against a list of file extensions.
tryStashFile(User $user, $isPartial=false)
Like stashFile(), but respects extensions' wishes to prevent the stashing.
getTitle()
Returns the title of the file to be uploaded.
initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile=false)
static getMaxUploadSize( $forType=null)
Get MediaWiki's maximum uploaded file size for a given type of upload, based on $wgMaxUploadSize.
bool null $mRemoveTempFile
static checkSvgExternalDTD( $type, $publicId, $systemId)
Verify that DTD URLs referenced are only the standard DTDs.
getTempFileSha1Base36()
Get the base 36 SHA1 of the file.
detectScriptInSvg( $filename, $partial)
static splitExtensions( $filename)
Split a file into a base name and all dot-delimited 'extensions' on the end.
fetchFile()
Fetch the file.
checkWarnings( $user=null)
Check for non fatal problems with the file.
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
getFileSize()
Return the file size.
verifyUpload()
Verify whether the upload is sensible.
const ILLEGAL_FILENAME
const MIN_LENGTH_PARTNAME
static checkFileExtensionList( $ext, $list)
Perform case-insensitive match against a list of file extensions.
static detectVirus( $file)
Generic wrapper function for a virus scanner program.
string null $mTempPath
Local file system path to the file to upload (or a local copy)
TempFSFile null $tempFileObj
Wrapper to handle deleting the temp file.
LocalFile null $mLocalFile
canFetchFile()
Perform checks to see if the file can be fetched.
static getMaxPhpUploadSize()
Get the PHP maximum uploaded file size, based on ini settings.
static $safeXmlEncodings
verifyMimeType( $mime)
Verify the MIME type.
static unserializeWarnings( $warnings)
Convert the serialized warnings array created by makeWarningsSerializable() back to the output of che...
initializeFromRequest(&$request)
Initialize from a WebRequest.
string false $mSVGNSError
Base class for all file backend classes (including multi-write backends).
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:88
delete( $key, $flags=0)
Delete an item if it exists.
set( $key, $value, $exptime=0, $flags=0)
Set an item.
get( $key, $flags=0)
Get an item.
makeKey( $keygroup,... $components)
Make a cache key from the given components, in the default keyspace.
XML syntax and type checker.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
authorizeWrite(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize write access.
isAllowed(string $permission, PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
getId( $wikiId=self::LOCAL)
if(!file_exists( $CREDITS)) $lines