MediaWiki REL1_41
UploadBase.php
Go to the documentation of this file.
1<?php
25use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
37use Wikimedia\AtEase\AtEase;
38
55abstract class UploadBase {
56 use ProtectedHookAccessorTrait;
57
59 protected $mTempPath;
61 protected $tempFileObj;
65 protected $mDestName;
69 protected $mSourceType;
71 protected $mTitle = false;
73 protected $mTitleError = 0;
75 protected $mFilteredName;
79 protected $mLocalFile;
81 protected $mStashFile;
83 protected $mFileSize;
85 protected $mFileProps;
89 protected $mJavaDetected;
91 protected $mSVGNSError;
92
93 protected static $safeXmlEncodings = [
94 'UTF-8',
95 'US-ASCII',
96 'ISO-8859-1',
97 'ISO-8859-2',
98 'UTF-16',
99 'UTF-32',
100 'WINDOWS-1250',
101 'WINDOWS-1251',
102 'WINDOWS-1252',
103 'WINDOWS-1253',
104 'WINDOWS-1254',
105 'WINDOWS-1255',
106 'WINDOWS-1256',
107 'WINDOWS-1257',
108 'WINDOWS-1258',
109 ];
110
111 public const SUCCESS = 0;
112 public const OK = 0;
113 public const EMPTY_FILE = 3;
114 public const MIN_LENGTH_PARTNAME = 4;
115 public const ILLEGAL_FILENAME = 5;
116 public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
117 public const FILETYPE_MISSING = 8;
118 public const FILETYPE_BADTYPE = 9;
119 public const VERIFICATION_ERROR = 10;
120 public const HOOK_ABORTED = 11;
121 public const FILE_TOO_LARGE = 12;
122 public const WINDOWS_NONASCII_FILENAME = 13;
123 public const FILENAME_TOO_LONG = 14;
124
125 private const CODE_TO_STATUS = [
126 self::EMPTY_FILE => 'empty-file',
127 self::FILE_TOO_LARGE => 'file-too-large',
128 self::FILETYPE_MISSING => 'filetype-missing',
129 self::FILETYPE_BADTYPE => 'filetype-banned',
130 self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
131 self::ILLEGAL_FILENAME => 'illegal-filename',
132 self::OVERWRITE_EXISTING_FILE => 'overwrite',
133 self::VERIFICATION_ERROR => 'verification-error',
134 self::HOOK_ABORTED => 'hookaborted',
135 self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
136 self::FILENAME_TOO_LONG => 'filename-toolong',
137 ];
138
143 public function getVerificationErrorCode( $error ) {
144 return self::CODE_TO_STATUS[$error] ?? 'unknown-error';
145 }
146
153 public static function isEnabled() {
154 $enableUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnableUploads );
155
156 return $enableUploads && wfIniGetBool( 'file_uploads' );
157 }
158
167 public static function isAllowed( Authority $performer ) {
168 foreach ( [ 'upload', 'edit' ] as $permission ) {
169 if ( !$performer->isAllowed( $permission ) ) {
170 return $permission;
171 }
172 }
173
174 return true;
175 }
176
186 public static function isThrottled( $user ) {
187 wfDeprecated( __METHOD__, '1.41' );
188 return $user->pingLimiter( 'upload' );
189 }
190
192 private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
193
201 public static function createFromRequest( &$request, $type = null ) {
202 $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
203
204 if ( !$type ) {
205 return null;
206 }
207
208 // Get the upload class
209 $type = ucfirst( $type );
210
211 // Give hooks the chance to handle this request
213 $className = null;
214 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
215 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
216 ->onUploadCreateFromRequest( $type, $className );
217 if ( $className === null ) {
218 $className = 'UploadFrom' . $type;
219 wfDebug( __METHOD__ . ": class name: $className" );
220 if ( !in_array( $type, self::$uploadHandlers ) ) {
221 return null;
222 }
223 }
224
225 if ( !$className::isEnabled() || !$className::isValidRequest( $request ) ) {
226 return null;
227 }
228
230 $handler = new $className;
231
232 $handler->initializeFromRequest( $request );
233
234 return $handler;
235 }
236
242 public static function isValidRequest( $request ) {
243 return false;
244 }
245
249 public function __construct() {
250 }
251
259 public function getSourceType() {
260 return null;
261 }
262
270 public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
271 $this->mDesiredDestName = $name;
272 if ( FileBackend::isStoragePath( $tempPath ) ) {
273 throw new MWException( __METHOD__ . " given storage path `$tempPath`." );
274 }
275
276 $this->setTempFile( $tempPath, $fileSize );
277 $this->mRemoveTempFile = $removeTempFile;
278 }
279
285 abstract public function initializeFromRequest( &$request );
286
291 protected function setTempFile( $tempPath, $fileSize = null ) {
292 $this->mTempPath = $tempPath ?? '';
293 $this->mFileSize = $fileSize ?: null;
294 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
295 $this->tempFileObj = new TempFSFile( $this->mTempPath );
296 if ( !$fileSize ) {
297 $this->mFileSize = filesize( $this->mTempPath );
298 }
299 } else {
300 $this->tempFileObj = null;
301 }
302 }
303
309 public function fetchFile() {
310 return Status::newGood();
311 }
312
317 public function isEmptyFile() {
318 return !$this->mFileSize;
319 }
320
325 public function getFileSize() {
326 return $this->mFileSize;
327 }
328
334 public function getTempFileSha1Base36() {
335 return FSFile::getSha1Base36FromPath( $this->mTempPath );
336 }
337
342 public function getRealPath( $srcPath ) {
343 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
344 if ( FileRepo::isVirtualUrl( $srcPath ) ) {
348 $tmpFile = $repo->getLocalCopy( $srcPath );
349 if ( $tmpFile ) {
350 $tmpFile->bind( $this ); // keep alive with $this
351 }
352 $path = $tmpFile ? $tmpFile->getPath() : false;
353 } else {
354 $path = $srcPath;
355 }
356
357 return $path;
358 }
359
377 public function verifyUpload() {
381 if ( $this->isEmptyFile() ) {
382 return [ 'status' => self::EMPTY_FILE ];
383 }
384
388 $maxSize = self::getMaxUploadSize( $this->getSourceType() );
389 if ( $this->mFileSize > $maxSize ) {
390 return [
391 'status' => self::FILE_TOO_LARGE,
392 'max' => $maxSize,
393 ];
394 }
395
401 $verification = $this->verifyFile();
402 if ( $verification !== true ) {
403 return [
404 'status' => self::VERIFICATION_ERROR,
405 'details' => $verification
406 ];
407 }
408
412 $result = $this->validateName();
413 if ( $result !== true ) {
414 return $result;
415 }
416
417 return [ 'status' => self::OK ];
418 }
419
426 public function validateName() {
427 $nt = $this->getTitle();
428 if ( $nt === null ) {
429 $result = [ 'status' => $this->mTitleError ];
430 if ( $this->mTitleError === self::ILLEGAL_FILENAME ) {
431 $result['filtered'] = $this->mFilteredName;
432 }
433 if ( $this->mTitleError === self::FILETYPE_BADTYPE ) {
434 $result['finalExt'] = $this->mFinalExtension;
435 if ( count( $this->mBlackListedExtensions ) ) {
436 $result['blacklistedExt'] = $this->mBlackListedExtensions;
437 }
438 }
439
440 return $result;
441 }
442 $this->mDestName = $this->getLocalFile()->getName();
443
444 return true;
445 }
446
455 protected function verifyMimeType( $mime ) {
456 $verifyMimeType = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeType );
457 if ( $verifyMimeType ) {
458 wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" );
459 $mimeTypeExclusions = MediaWikiServices::getInstance()->getMainConfig()
460 ->get( MainConfigNames::MimeTypeExclusions );
461 if ( self::checkFileExtension( $mime, $mimeTypeExclusions ) ) {
462 return [ 'filetype-badmime', $mime ];
463 }
464 }
465
466 return true;
467 }
468
474 protected function verifyFile() {
475 $config = MediaWikiServices::getInstance()->getMainConfig();
476 $verifyMimeType = $config->get( MainConfigNames::VerifyMimeType );
477 $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
478 $status = $this->verifyPartialFile();
479 if ( $status !== true ) {
480 return $status;
481 }
482
483 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
484 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
485 $mime = $this->mFileProps['mime'];
486
487 if ( $verifyMimeType ) {
488 # XXX: Missing extension will be caught by validateName() via getTitle()
489 if ( (string)$this->mFinalExtension !== '' &&
490 !self::verifyExtension( $mime, $this->mFinalExtension )
491 ) {
492 return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
493 }
494 }
495
496 # check for htmlish code and javascript
497 if ( !$disableUploadScriptChecks ) {
498 if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
499 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
500 if ( $svgStatus !== false ) {
501 return $svgStatus;
502 }
503 }
504 }
505
506 $handler = MediaHandler::getHandler( $mime );
507 if ( $handler ) {
508 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
509 if ( !$handlerStatus->isOK() ) {
510 $errors = $handlerStatus->getErrorsArray();
511
512 return reset( $errors );
513 }
514 }
515
516 $error = true;
517 $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
518 if ( $error !== true ) {
519 if ( !is_array( $error ) ) {
520 $error = [ $error ];
521 }
522 return $error;
523 }
524
525 wfDebug( __METHOD__ . ": all clear; passing." );
526
527 return true;
528 }
529
539 protected function verifyPartialFile() {
540 $config = MediaWikiServices::getInstance()->getMainConfig();
541 $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
542 # getTitle() sets some internal parameters like $this->mFinalExtension
543 $this->getTitle();
544
545 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
546 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
547
548 # check MIME type, if desired
549 $mime = $this->mFileProps['file-mime'];
550 $status = $this->verifyMimeType( $mime );
551 if ( $status !== true ) {
552 return $status;
553 }
554
555 # check for htmlish code and javascript
556 if ( !$disableUploadScriptChecks ) {
557 if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
558 return [ 'uploadscripted' ];
559 }
560 if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
561 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
562 if ( $svgStatus !== false ) {
563 return $svgStatus;
564 }
565 }
566 }
567
568 # Scan the uploaded file for viruses
569 $virus = self::detectVirus( $this->mTempPath );
570 if ( $virus ) {
571 return [ 'uploadvirus', $virus ];
572 }
573
574 return true;
575 }
576
582 public function zipEntryCallback( $entry ) {
583 $names = [ $entry['name'] ];
584
585 // If there is a null character, cut off the name at it, because JDK's
586 // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
587 // were constructed which had ".class\0" followed by a string chosen to
588 // make the hash collide with the truncated name, that file could be
589 // returned in response to a request for the .class file.
590 $nullPos = strpos( $entry['name'], "\000" );
591 if ( $nullPos !== false ) {
592 $names[] = substr( $entry['name'], 0, $nullPos );
593 }
594
595 // If there is a trailing slash in the file name, we have to strip it,
596 // because that's what ZIP_GetEntry() does.
597 if ( preg_grep( '!\.class/?$!', $names ) ) {
598 $this->mJavaDetected = true;
599 }
600 }
601
611 public function verifyPermissions( Authority $performer ) {
612 return $this->verifyTitlePermissions( $performer );
613 }
614
626 public function verifyTitlePermissions( Authority $performer ) {
631 $nt = $this->getTitle();
632 if ( $nt === null ) {
633 return true;
634 }
635
636 $status = PermissionStatus::newEmpty();
637 $performer->authorizeWrite( 'edit', $nt, $status );
638 $performer->authorizeWrite( 'upload', $nt, $status );
639 if ( !$status->isGood() ) {
640 return $status->toLegacyErrorArray();
641 }
642
643 $overwriteError = $this->checkOverwrite( $performer );
644 if ( $overwriteError !== true ) {
645 return [ $overwriteError ];
646 }
647
648 return true;
649 }
650
660 public function checkWarnings( $user = null ) {
661 if ( $user === null ) {
662 // TODO check uses and hard deprecate
663 $user = RequestContext::getMain()->getUser();
664 }
665
666 $warnings = [];
667
668 $localFile = $this->getLocalFile();
669 $localFile->load( File::READ_LATEST );
670 $filename = $localFile->getName();
671 $hash = $this->getTempFileSha1Base36();
672
673 $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
674 if ( $badFileName !== null ) {
675 $warnings['badfilename'] = $badFileName;
676 }
677
678 $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
679 if ( $unwantedFileExtensionDetails !== null ) {
680 $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
681 }
682
683 $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
684 if ( $fileSizeWarnings ) {
685 $warnings = array_merge( $warnings, $fileSizeWarnings );
686 }
687
688 $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
689 if ( $localFileExistsWarnings ) {
690 $warnings = array_merge( $warnings, $localFileExistsWarnings );
691 }
692
693 if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
694 $warnings['was-deleted'] = $filename;
695 }
696
697 // If a file with the same name exists locally then the local file has already been tested
698 // for duplication of content
699 $ignoreLocalDupes = isset( $warnings['exists'] );
700 $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
701 if ( $dupes ) {
702 $warnings['duplicate'] = $dupes;
703 }
704
705 $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
706 if ( $archivedDupes !== null ) {
707 $warnings['duplicate-archive'] = $archivedDupes;
708 }
709
710 return $warnings;
711 }
712
724 public static function makeWarningsSerializable( $warnings ) {
725 array_walk_recursive( $warnings, static function ( &$param, $key ) {
726 if ( $param instanceof File ) {
727 $param = [
728 'fileName' => $param->getName(),
729 'timestamp' => $param->getTimestamp()
730 ];
731 } elseif ( is_object( $param ) ) {
732 throw new InvalidArgumentException(
733 'UploadBase::makeWarningsSerializable: ' .
734 'Unexpected object of class ' . get_class( $param ) );
735 }
736 } );
737 return $warnings;
738 }
739
749 private function checkBadFileName( $filename, $desiredFileName ) {
750 $comparableName = str_replace( ' ', '_', $desiredFileName );
751 $comparableName = Title::capitalize( $comparableName, NS_FILE );
752
753 if ( $desiredFileName != $filename && $comparableName != $filename ) {
754 return $filename;
755 }
756
757 return null;
758 }
759
768 private function checkUnwantedFileExtensions( $fileExtension ) {
769 $checkFileExtensions = MediaWikiServices::getInstance()->getMainConfig()
770 ->get( MainConfigNames::CheckFileExtensions );
771 $fileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FileExtensions );
772 if ( $checkFileExtensions ) {
773 $extensions = array_unique( $fileExtensions );
774 if ( !self::checkFileExtension( $fileExtension, $extensions ) ) {
775 return [
776 $fileExtension,
777 Message::listParam( $extensions, 'comma' ),
778 count( $extensions )
779 ];
780 }
781 }
782
783 return null;
784 }
785
791 private function checkFileSize( $fileSize ) {
792 $uploadSizeWarning = MediaWikiServices::getInstance()->getMainConfig()
793 ->get( MainConfigNames::UploadSizeWarning );
794
795 $warnings = [];
796
797 if ( $uploadSizeWarning && ( $fileSize > $uploadSizeWarning ) ) {
798 $warnings['large-file'] = [
799 Message::sizeParam( $uploadSizeWarning ),
800 Message::sizeParam( $fileSize ),
801 ];
802 }
803
804 if ( $fileSize == 0 ) {
805 $warnings['empty-file'] = true;
806 }
807
808 return $warnings;
809 }
810
817 private function checkLocalFileExists( LocalFile $localFile, $hash ) {
818 $warnings = [];
819
820 $exists = self::getExistsWarning( $localFile );
821 if ( $exists !== false ) {
822 $warnings['exists'] = $exists;
823
824 // check if file is an exact duplicate of current file version
825 if ( $hash !== false && $hash === $localFile->getSha1() ) {
826 $warnings['no-change'] = $localFile;
827 }
828
829 // check if file is an exact duplicate of older versions of this file
830 $history = $localFile->getHistory();
831 foreach ( $history as $oldFile ) {
832 if ( $hash === $oldFile->getSha1() ) {
833 $warnings['duplicate-version'][] = $oldFile;
834 }
835 }
836 }
837
838 return $warnings;
839 }
840
841 private function checkLocalFileWasDeleted( LocalFile $localFile ) {
842 return $localFile->wasDeleted() && !$localFile->exists();
843 }
844
851 private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
852 if ( $hash === false ) {
853 return [];
854 }
855 $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
856 $title = $this->getTitle();
857 foreach ( $dupes as $key => $dupe ) {
858 if (
859 ( $dupe instanceof LocalFile ) &&
860 $ignoreLocalDupes &&
861 $title->equals( $dupe->getTitle() )
862 ) {
863 unset( $dupes[$key] );
864 }
865 }
866
867 return $dupes;
868 }
869
877 private function checkAgainstArchiveDupes( $hash, Authority $performer ) {
878 if ( $hash === false ) {
879 return null;
880 }
881 $archivedFile = new ArchivedFile( null, 0, '', $hash );
882 if ( $archivedFile->getID() > 0 ) {
883 if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
884 return $archivedFile->getName();
885 }
886 return '';
887 }
888
889 return null;
890 }
891
909 public function performUpload(
910 $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
911 ) {
912 $this->getLocalFile()->load( File::READ_LATEST );
913 $props = $this->mFileProps;
914
915 $error = null;
916 $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
917 if ( $error ) {
918 if ( !is_array( $error ) ) {
919 $error = [ $error ];
920 }
921 return Status::newFatal( ...$error );
922 }
923
924 $status = $this->getLocalFile()->upload(
925 $this->mTempPath,
926 $comment,
927 $pageText !== false ? $pageText : '',
928 File::DELETE_SOURCE,
929 $props,
930 false,
931 $user,
932 $tags
933 );
934
935 if ( $status->isGood() ) {
936 if ( $watch ) {
937 MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
938 $user,
939 $this->getLocalFile()->getTitle(),
940 $watchlistExpiry
941 );
942 }
943 $this->getHookRunner()->onUploadComplete( $this );
944
945 $this->postProcessUpload();
946 }
947
948 return $status;
949 }
950
957 public function postProcessUpload() {
958 }
959
966 public function getTitle() {
967 if ( $this->mTitle !== false ) {
968 return $this->mTitle;
969 }
970 if ( !is_string( $this->mDesiredDestName ) ) {
971 $this->mTitleError = self::ILLEGAL_FILENAME;
972 $this->mTitle = null;
973
974 return $this->mTitle;
975 }
976 /* Assume that if a user specified File:Something.jpg, this is an error
977 * and that the namespace prefix needs to be stripped of.
978 */
979 $title = Title::newFromText( $this->mDesiredDestName );
980 if ( $title && $title->getNamespace() === NS_FILE ) {
981 $this->mFilteredName = $title->getDBkey();
982 } else {
983 $this->mFilteredName = $this->mDesiredDestName;
984 }
985
986 # oi_archive_name is max 255 bytes, which include a timestamp and an
987 # exclamation mark, so restrict file name to 240 bytes.
988 if ( strlen( $this->mFilteredName ) > 240 ) {
989 $this->mTitleError = self::FILENAME_TOO_LONG;
990 $this->mTitle = null;
991
992 return $this->mTitle;
993 }
994
1000 $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
1001 /* Normalize to title form before we do any further processing */
1002 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1003 if ( $nt === null ) {
1004 $this->mTitleError = self::ILLEGAL_FILENAME;
1005 $this->mTitle = null;
1006
1007 return $this->mTitle;
1008 }
1009 $this->mFilteredName = $nt->getDBkey();
1010
1015 [ $partname, $ext ] = self::splitExtensions( $this->mFilteredName );
1016
1017 if ( $ext !== [] ) {
1018 $this->mFinalExtension = trim( end( $ext ) );
1019 } else {
1020 $this->mFinalExtension = '';
1021
1022 // No extension, try guessing one from the temporary file
1023 // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1024 // or incomplete test case in UploadBaseTest (T272328)
1025 if ( $this->mTempPath !== null ) {
1026 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1027 $mime = $magic->guessMimeType( $this->mTempPath );
1028 if ( $mime !== 'unknown/unknown' ) {
1029 # Get a space separated list of extensions
1030 $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1031 if ( $mimeExt !== null ) {
1032 # Set the extension to the canonical extension
1033 $this->mFinalExtension = $mimeExt;
1034
1035 # Fix up the other variables
1036 $this->mFilteredName .= ".{$this->mFinalExtension}";
1037 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1038 $ext = [ $this->mFinalExtension ];
1039 }
1040 }
1041 }
1042 }
1043
1044 // Don't allow users to override the list of prohibited file extensions (check file extension)
1045 $config = MediaWikiServices::getInstance()->getMainConfig();
1046 $checkFileExtensions = $config->get( MainConfigNames::CheckFileExtensions );
1047 $strictFileExtensions = $config->get( MainConfigNames::StrictFileExtensions );
1048 $fileExtensions = $config->get( MainConfigNames::FileExtensions );
1049 $prohibitedFileExtensions = $config->get( MainConfigNames::ProhibitedFileExtensions );
1050
1051 $badList = self::checkFileExtensionList( $ext, $prohibitedFileExtensions );
1052
1053 if ( $this->mFinalExtension == '' ) {
1054 $this->mTitleError = self::FILETYPE_MISSING;
1055 $this->mTitle = null;
1056
1057 return $this->mTitle;
1058 }
1059
1060 if ( $badList ||
1061 ( $checkFileExtensions && $strictFileExtensions &&
1062 !self::checkFileExtension( $this->mFinalExtension, $fileExtensions ) )
1063 ) {
1064 $this->mBlackListedExtensions = $badList;
1065 $this->mTitleError = self::FILETYPE_BADTYPE;
1066 $this->mTitle = null;
1067
1068 return $this->mTitle;
1069 }
1070
1071 // Windows may be broken with special characters, see T3780
1072 if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1073 && !MediaWikiServices::getInstance()->getRepoGroup()
1074 ->getLocalRepo()->backendSupportsUnicodePaths()
1075 ) {
1076 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1077 $this->mTitle = null;
1078
1079 return $this->mTitle;
1080 }
1081
1082 # If there was more than one file "extension", reassemble the base
1083 # filename to prevent bogus complaints about length
1084 if ( count( $ext ) > 1 ) {
1085 $iterations = count( $ext ) - 1;
1086 for ( $i = 0; $i < $iterations; $i++ ) {
1087 $partname .= '.' . $ext[$i];
1088 }
1089 }
1090
1091 if ( strlen( $partname ) < 1 ) {
1092 $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1093 $this->mTitle = null;
1094
1095 return $this->mTitle;
1096 }
1097
1098 $this->mTitle = $nt;
1099
1100 return $this->mTitle;
1101 }
1102
1109 public function getLocalFile() {
1110 if ( $this->mLocalFile === null ) {
1111 $nt = $this->getTitle();
1112 $this->mLocalFile = $nt === null
1113 ? null
1114 : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1115 }
1116
1117 return $this->mLocalFile;
1118 }
1119
1123 public function getStashFile() {
1124 return $this->mStashFile;
1125 }
1126
1139 public function tryStashFile( User $user, $isPartial = false ) {
1140 if ( !$isPartial ) {
1141 $error = $this->runUploadStashFileHook( $user );
1142 if ( $error ) {
1143 return Status::newFatal( ...$error );
1144 }
1145 }
1146 try {
1147 $file = $this->doStashFile( $user );
1148 return Status::newGood( $file );
1149 } catch ( UploadStashException $e ) {
1150 return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1151 }
1152 }
1153
1158 protected function runUploadStashFileHook( User $user ) {
1159 $props = $this->mFileProps;
1160 $error = null;
1161 $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1162 if ( $error && !is_array( $error ) ) {
1163 $error = [ $error ];
1164 }
1165 return $error;
1166 }
1167
1175 protected function doStashFile( User $user = null ) {
1176 $stash = MediaWikiServices::getInstance()->getRepoGroup()
1177 ->getLocalRepo()->getUploadStash( $user );
1178 $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
1179 $this->mStashFile = $file;
1180
1181 return $file;
1182 }
1183
1188 public function cleanupTempFile() {
1189 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1190 // Delete when all relevant TempFSFile handles go out of scope
1191 wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1192 $this->tempFileObj->autocollect();
1193 }
1194 }
1195
1199 public function getTempPath() {
1200 return $this->mTempPath;
1201 }
1202
1212 public static function splitExtensions( $filename ) {
1213 $bits = explode( '.', $filename );
1214 $basename = array_shift( $bits );
1215
1216 return [ $basename, $bits ];
1217 }
1218
1226 public static function checkFileExtension( $ext, $list ) {
1227 return in_array( strtolower( $ext ?? '' ), $list, true );
1228 }
1229
1238 public static function checkFileExtensionList( $ext, $list ) {
1239 return array_intersect( array_map( 'strtolower', $ext ), $list );
1240 }
1241
1249 public static function verifyExtension( $mime, $extension ) {
1250 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1251
1252 if ( !$mime || $mime === 'unknown' || $mime === 'unknown/unknown' ) {
1253 if ( !$magic->isRecognizableExtension( $extension ) ) {
1254 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1255 "unrecognized extension '$extension', can't verify" );
1256
1257 return true;
1258 }
1259
1260 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1261 "recognized extension '$extension', so probably invalid file" );
1262 return false;
1263 }
1264
1265 $match = $magic->isMatchingExtension( $extension, $mime );
1266
1267 if ( $match === null ) {
1268 if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1269 wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1270
1271 return false;
1272 }
1273
1274 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1275 return true;
1276 }
1277
1278 if ( $match ) {
1279 wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1280
1282 return true;
1283 }
1284
1285 wfDebug( __METHOD__
1286 . ": mime type $mime mismatches file extension $extension, rejecting file" );
1287
1288 return false;
1289 }
1290
1302 public static function detectScript( $file, $mime, $extension ) {
1303 # ugly hack: for text files, always look at the entire file.
1304 # For binary field, just check the first K.
1305
1306 if ( str_starts_with( $mime ?? '', 'text/' ) ) {
1307 $chunk = file_get_contents( $file );
1308 } else {
1309 $fp = fopen( $file, 'rb' );
1310 if ( !$fp ) {
1311 return false;
1312 }
1313 $chunk = fread( $fp, 1024 );
1314 fclose( $fp );
1315 }
1316
1317 $chunk = strtolower( $chunk );
1318
1319 if ( !$chunk ) {
1320 return false;
1321 }
1322
1323 # decode from UTF-16 if needed (could be used for obfuscation).
1324 if ( str_starts_with( $chunk, "\xfe\xff" ) ) {
1325 $enc = 'UTF-16BE';
1326 } elseif ( str_starts_with( $chunk, "\xff\xfe" ) ) {
1327 $enc = 'UTF-16LE';
1328 } else {
1329 $enc = null;
1330 }
1331
1332 if ( $enc !== null ) {
1333 $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1334 }
1335
1336 $chunk = trim( $chunk );
1337
1339 wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1340
1341 # check for HTML doctype
1342 if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1343 return true;
1344 }
1345
1346 // Some browsers will interpret obscure xml encodings as UTF-8, while
1347 // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1348 if ( $extension === 'svg' || str_starts_with( $mime ?? '', 'image/svg' ) ) {
1349 if ( self::checkXMLEncodingMissmatch( $file ) ) {
1350 return true;
1351 }
1352 }
1353
1354 // Quick check for HTML heuristics in old IE and Safari.
1355 //
1356 // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1357 // don't need them all here as it can cause many false positives.
1358 //
1359 // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1360 $tags = [
1361 '<body',
1362 '<head',
1363 '<html', # also in safari
1364 '<script', # also in safari
1365 ];
1366
1367 foreach ( $tags as $tag ) {
1368 if ( strpos( $chunk, $tag ) !== false ) {
1369 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1370
1371 return true;
1372 }
1373 }
1374
1375 /*
1376 * look for JavaScript
1377 */
1378
1379 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1380 $chunk = Sanitizer::decodeCharReferences( $chunk );
1381
1382 # look for script-types
1383 if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!im', $chunk ) ) {
1384 wfDebug( __METHOD__ . ": found script types" );
1385
1386 return true;
1387 }
1388
1389 # look for html-style script-urls
1390 if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) {
1391 wfDebug( __METHOD__ . ": found html-style script urls" );
1392
1393 return true;
1394 }
1395
1396 # look for css-style script-urls
1397 if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) {
1398 wfDebug( __METHOD__ . ": found css-style script urls" );
1399
1400 return true;
1401 }
1402
1403 wfDebug( __METHOD__ . ": no scripts found" );
1404
1405 return false;
1406 }
1407
1415 public static function checkXMLEncodingMissmatch( $file ) {
1416 $svgMetadataCutoff = MediaWikiServices::getInstance()->getMainConfig()
1417 ->get( MainConfigNames::SVGMetadataCutoff );
1418 $contents = file_get_contents( $file, false, null, 0, $svgMetadataCutoff );
1419 $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1420
1421 if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1422 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1423 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1424 ) {
1425 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1426
1427 return true;
1428 }
1429 } elseif ( preg_match( "!<\?xml\b!i", $contents ) ) {
1430 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1431 // bytes. There shouldn't be a legitimate reason for this to happen.
1432 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1433
1434 return true;
1435 } elseif ( str_starts_with( $contents, "\x4C\x6F\xA7\x94" ) ) {
1436 // EBCDIC encoded XML
1437 wfDebug( __METHOD__ . ": EBCDIC Encoded XML" );
1438
1439 return true;
1440 }
1441
1442 // It's possible the file is encoded with multibyte encoding, so re-encode attempt to
1443 // detect the encoding in case it specifies an encoding not allowed in self::$safeXmlEncodings
1444 $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1445 foreach ( $attemptEncodings as $encoding ) {
1446 AtEase::suppressWarnings();
1447 $str = iconv( $encoding, 'UTF-8', $contents );
1448 AtEase::restoreWarnings();
1449 if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1450 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1451 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1452 ) {
1453 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1454
1455 return true;
1456 }
1457 } elseif ( $str != '' && preg_match( "!<\?xml\b!i", $str ) ) {
1458 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1459 // bytes. There shouldn't be a legitimate reason for this to happen.
1460 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1461
1462 return true;
1463 }
1464 }
1465
1466 return false;
1467 }
1468
1474 protected function detectScriptInSvg( $filename, $partial ) {
1475 $this->mSVGNSError = false;
1476 $check = new XmlTypeCheck(
1477 $filename,
1478 [ $this, 'checkSvgScriptCallback' ],
1479 true,
1480 [
1481 'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ],
1482 'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ],
1483 ]
1484 );
1485 if ( $check->wellFormed !== true ) {
1486 // Invalid xml (T60553)
1487 // But only when non-partial (T67724)
1488 return $partial ? false : [ 'uploadinvalidxml' ];
1489 }
1490
1491 if ( $check->filterMatch ) {
1492 if ( $this->mSVGNSError ) {
1493 return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1494 }
1495 return $check->filterMatchType;
1496 }
1497
1498 return false;
1499 }
1500
1508 public static function checkSvgPICallback( $target, $data ) {
1509 // Don't allow external stylesheets (T59550)
1510 if ( preg_match( '/xml-stylesheet/i', $target ) ) {
1511 return [ 'upload-scripted-pi-callback' ];
1512 }
1513
1514 return false;
1515 }
1516
1529 public static function checkSvgExternalDTD( $type, $publicId, $systemId ) {
1530 // This doesn't include the XHTML+MathML+SVG doctype since we don't
1531 // allow XHTML anyway.
1532 static $allowedDTDs = [
1533 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1534 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1535 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1536 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1537 // https://phabricator.wikimedia.org/T168856
1538 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1539 ];
1540 if ( $type !== 'PUBLIC'
1541 || !in_array( $systemId, $allowedDTDs )
1542 || !str_starts_with( $publicId, "-//W3C//" )
1543 ) {
1544 return [ 'upload-scripted-dtd' ];
1545 }
1546 return false;
1547 }
1548
1556 public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1557 [ $namespace, $strippedElement ] = self::splitXmlNamespace( $element );
1558
1559 // We specifically don't include:
1560 // http://www.w3.org/1999/xhtml (T62771)
1561 static $validNamespaces = [
1562 '',
1563 'adobe:ns:meta/',
1564 'http://creativecommons.org/ns#',
1565 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1566 'http://ns.adobe.com/adobeillustrator/10.0/',
1567 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1568 'http://ns.adobe.com/extensibility/1.0/',
1569 'http://ns.adobe.com/flows/1.0/',
1570 'http://ns.adobe.com/illustrator/1.0/',
1571 'http://ns.adobe.com/imagereplacement/1.0/',
1572 'http://ns.adobe.com/pdf/1.3/',
1573 'http://ns.adobe.com/photoshop/1.0/',
1574 'http://ns.adobe.com/saveforweb/1.0/',
1575 'http://ns.adobe.com/variables/1.0/',
1576 'http://ns.adobe.com/xap/1.0/',
1577 'http://ns.adobe.com/xap/1.0/g/',
1578 'http://ns.adobe.com/xap/1.0/g/img/',
1579 'http://ns.adobe.com/xap/1.0/mm/',
1580 'http://ns.adobe.com/xap/1.0/rights/',
1581 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1582 'http://ns.adobe.com/xap/1.0/stype/font#',
1583 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1584 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1585 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1586 'http://ns.adobe.com/xap/1.0/t/pg/',
1587 'http://purl.org/dc/elements/1.1/',
1588 'http://purl.org/dc/elements/1.1',
1589 'http://schemas.microsoft.com/visio/2003/svgextensions/',
1590 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1591 'http://taptrix.com/inkpad/svg_extensions',
1592 'http://web.resource.org/cc/',
1593 'http://www.freesoftware.fsf.org/bkchem/cdml',
1594 'http://www.inkscape.org/namespaces/inkscape',
1595 'http://www.opengis.net/gml',
1596 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1597 'http://www.w3.org/2000/svg',
1598 'http://www.w3.org/tr/rec-rdf-syntax/',
1599 'http://www.w3.org/2000/01/rdf-schema#',
1600 'http://www.w3.org/2000/02/svg/testsuite/description/', // https://phabricator.wikimedia.org/T278044
1601 ];
1602
1603 // Inkscape mangles namespace definitions created by Adobe Illustrator.
1604 // This is nasty but harmless. (T144827)
1605 $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1606
1607 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1608 wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file." );
1610 $this->mSVGNSError = $namespace;
1611
1612 return true;
1613 }
1614
1615 // check for elements that can contain javascript
1616 if ( $strippedElement === 'script' ) {
1617 wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file." );
1618
1619 return [ 'uploaded-script-svg', $strippedElement ];
1620 }
1621
1622 // e.g., <svg xmlns="http://www.w3.org/2000/svg">
1623 // <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1624 if ( $strippedElement === 'handler' ) {
1625 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1626
1627 return [ 'uploaded-script-svg', $strippedElement ];
1628 }
1629
1630 // SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1631 if ( $strippedElement === 'stylesheet' ) {
1632 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1633
1634 return [ 'uploaded-script-svg', $strippedElement ];
1635 }
1636
1637 // Block iframes, in case they pass the namespace check
1638 if ( $strippedElement === 'iframe' ) {
1639 wfDebug( __METHOD__ . ": iframe in uploaded file." );
1640
1641 return [ 'uploaded-script-svg', $strippedElement ];
1642 }
1643
1644 // Check <style> css
1645 if ( $strippedElement === 'style'
1646 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1647 ) {
1648 wfDebug( __METHOD__ . ": hostile css in style element." );
1649
1650 return [ 'uploaded-hostile-svg' ];
1651 }
1652
1653 static $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1654 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1655
1656 foreach ( $attribs as $attrib => $value ) {
1657 // If attributeNamespace is '', it is relative to its element's namespace
1658 [ $attributeNamespace, $stripped ] = self::splitXmlNamespace( $attrib );
1659 $value = strtolower( $value );
1660
1661 if ( !(
1662 // Inkscape element's have valid attribs that start with on and are safe, fail all others
1663 $namespace === 'http://www.inkscape.org/namespaces/inkscape' &&
1664 $attributeNamespace === ''
1665 ) && str_starts_with( $stripped, 'on' )
1666 ) {
1667 wfDebug( __METHOD__
1668 . ": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1669
1670 return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1671 }
1672
1673 // Do not allow relative links, or unsafe url schemas.
1674 // For <a> tags, only data:, http: and https: and same-document
1675 // fragment links are allowed.
1676 // For all other tags, only 'data:' and fragments (#) are allowed.
1677 if (
1678 $stripped === 'href'
1679 && $value !== ''
1680 && !str_starts_with( $value, 'data:' )
1681 && !str_starts_with( $value, '#' )
1682 && !( $strippedElement === 'a' && preg_match( '!^https?://!i', $value ) )
1683 ) {
1684 wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1685 . "'$attrib'='$value' in uploaded file." );
1686
1687 return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1688 }
1689
1690 // Only allow 'data:\' targets that should be safe.
1691 // This prevents vectors like image/svg, text/xml, application/xml, and text/html, which can contain scripts
1692 if ( $stripped === 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1693 // RFC2397 parameters.
1694 // This is only slightly slower than (;[\w;]+)*.
1695 // phpcs:ignore Generic.Files.LineLength
1696 $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1697
1698 if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1699 wfDebug( __METHOD__ . ": Found href to allow listed data: uri "
1700 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1701 return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1702 }
1703 }
1704
1705 // Change href with animate from (http://html5sec.org/#137).
1706 if ( $stripped === 'attributename'
1707 && $strippedElement === 'animate'
1708 && $this->stripXmlNamespace( $value ) === 'href'
1709 ) {
1710 wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1711 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1712
1713 return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1714 }
1715
1716 // Use set/animate to add event-handler attribute to parent.
1717 if ( ( $strippedElement === 'set' || $strippedElement === 'animate' )
1718 && $stripped === 'attributename'
1719 && str_starts_with( $value, 'on' )
1720 ) {
1721 wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1722 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1723
1724 return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1725 }
1726
1727 // use set to add href attribute to parent element.
1728 if ( $strippedElement === 'set'
1729 && $stripped === 'attributename'
1730 && str_contains( $value, 'href' )
1731 ) {
1732 wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file." );
1733
1734 return [ 'uploaded-setting-href-svg' ];
1735 }
1736
1737 // use set to add a remote / data / script target to an element.
1738 if ( $strippedElement === 'set'
1739 && $stripped === 'to'
1740 && preg_match( '!(http|https|data|script):!im', $value )
1741 ) {
1742 wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file." );
1743
1744 return [ 'uploaded-wrong-setting-svg', $value ];
1745 }
1746
1747 // use handler attribute with remote / data / script.
1748 if ( $stripped === 'handler' && preg_match( '!(http|https|data|script):!im', $value ) ) {
1749 wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1750 . "'$attrib'='$value' in uploaded file." );
1751
1752 return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1753 }
1754
1755 // use CSS styles to bring in remote code.
1756 if ( $stripped === 'style'
1757 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1758 ) {
1759 wfDebug( __METHOD__ . ": Found svg setting a style with "
1760 . "remote url '$attrib'='$value' in uploaded file." );
1761 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1762 }
1763
1764 // Several attributes can include css, css character escaping isn't allowed.
1765 if ( in_array( $stripped, $cssAttrs, true )
1766 && self::checkCssFragment( $value )
1767 ) {
1768 wfDebug( __METHOD__ . ": Found svg setting a style with "
1769 . "remote url '$attrib'='$value' in uploaded file." );
1770 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1771 }
1772
1773 // image filters can pull in url, which could be svg that executes scripts.
1774 // Only allow url( "#foo" ).
1775 // Do not allow url( http://example.com )
1776 if ( $strippedElement === 'image'
1777 && $stripped === 'filter'
1778 && preg_match( '!url\s*\‍(\s*["\']?[^#]!im', $value )
1779 ) {
1780 wfDebug( __METHOD__ . ": Found image filter with url: "
1781 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1782
1783 return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1784 }
1785 }
1786
1787 return false; // No scripts detected
1788 }
1789
1796 private static function checkCssFragment( $value ) {
1797 # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1798 if ( stripos( $value, '@import' ) !== false ) {
1799 return true;
1800 }
1801
1802 # We allow @font-face to embed fonts with data: urls, so we snip the string
1803 # 'url' out so that this case won't match when we check for urls below
1804 $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1805 $value = preg_replace( $pattern, '$1$2', $value );
1806
1807 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1808 # properties filter and accelerator don't seem to be useful for xss in SVG files.
1809 # Expression and -o-link don't seem to work either, but filtering them here in case.
1810 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1811 # but not local ones such as url("#..., url('#..., url(#....
1812 if ( preg_match( '!expression
1813 | -o-link\s*:
1814 | -o-link-source\s*:
1815 | -o-replace\s*:!imx', $value ) ) {
1816 return true;
1817 }
1818
1819 if ( preg_match_all(
1820 "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1821 $value,
1822 $matches
1823 ) !== 0
1824 ) {
1825 # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1826 foreach ( $matches[1] as $match ) {
1827 if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1828 return true;
1829 }
1830 }
1831 }
1832
1833 if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1834 return true;
1835 }
1836
1837 return false;
1838 }
1839
1845 private static function splitXmlNamespace( $element ) {
1846 // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1847 $parts = explode( ':', strtolower( $element ) );
1848 $name = array_pop( $parts );
1849 $ns = implode( ':', $parts );
1850
1851 return [ $ns, $name ];
1852 }
1853
1858 private function stripXmlNamespace( $element ) {
1859 // 'http://www.w3.org/2000/svg:script' -> 'script'
1860 return self::splitXmlNamespace( $element )[1];
1861 }
1862
1873 public static function detectVirus( $file ) {
1874 global $wgOut;
1875 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1876 $antivirus = $mainConfig->get( MainConfigNames::Antivirus );
1877 $antivirusSetup = $mainConfig->get( MainConfigNames::AntivirusSetup );
1878 $antivirusRequired = $mainConfig->get( MainConfigNames::AntivirusRequired );
1879 if ( !$antivirus ) {
1880 wfDebug( __METHOD__ . ": virus scanner disabled" );
1881
1882 return null;
1883 }
1884
1885 if ( !$antivirusSetup[$antivirus] ) {
1886 wfDebug( __METHOD__ . ": unknown virus scanner: {$antivirus}" );
1887 $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1888 [ 'virus-badscanner', $antivirus ] );
1889
1890 return wfMessage( 'virus-unknownscanner' )->text() . " {$antivirus}";
1891 }
1892
1893 # look up scanner configuration
1894 $command = $antivirusSetup[$antivirus]['command'];
1895 $exitCodeMap = $antivirusSetup[$antivirus]['codemap'];
1896 $msgPattern = $antivirusSetup[$antivirus]['messagepattern'] ?? null;
1897
1898 if ( !str_contains( $command, "%f" ) ) {
1899 # simple pattern: append file to scan
1900 $command .= " " . Shell::escape( $file );
1901 } else {
1902 # complex pattern: replace "%f" with file to scan
1903 $command = str_replace( "%f", Shell::escape( $file ), $command );
1904 }
1905
1906 wfDebug( __METHOD__ . ": running virus scan: $command " );
1907
1908 # execute virus scanner
1909 $exitCode = false;
1910
1911 # NOTE: there's a 50-line workaround to make stderr redirection work on windows, too.
1912 # that does not seem to be worth the pain.
1913 # Ask me (Duesentrieb) about it if it's ever needed.
1914 $output = wfShellExecWithStderr( $command, $exitCode );
1915
1916 # map exit code to AV_xxx constants.
1917 $mappedCode = $exitCode;
1918 if ( $exitCodeMap ) {
1919 if ( isset( $exitCodeMap[$exitCode] ) ) {
1920 $mappedCode = $exitCodeMap[$exitCode];
1921 } elseif ( isset( $exitCodeMap["*"] ) ) {
1922 $mappedCode = $exitCodeMap["*"];
1923 }
1924 }
1925
1926 # NB: AV_NO_VIRUS is 0, but AV_SCAN_FAILED is false,
1927 # so we need the strict equalities === and thus can't use a switch here
1928 if ( $mappedCode === AV_SCAN_FAILED ) {
1929 # scan failed (code was mapped to false by $exitCodeMap)
1930 wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1931
1932 $output = $antivirusRequired
1933 ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1934 : null;
1935 } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1936 # scan failed because filetype is unknown (probably immune)
1937 wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1938 $output = null;
1939 } elseif ( $mappedCode === AV_NO_VIRUS ) {
1940 # no virus found
1941 wfDebug( __METHOD__ . ": file passed virus scan." );
1942 $output = false;
1943 } else {
1944 $output = trim( $output );
1945
1946 if ( !$output ) {
1947 $output = true; # if there's no output, return true
1948 } elseif ( $msgPattern ) {
1949 $groups = [];
1950 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1951 $output = $groups[1];
1952 }
1953 }
1954
1955 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
1956 }
1957
1958 return $output;
1959 }
1960
1969 private function checkOverwrite( Authority $performer ) {
1970 // First check whether the local file can be overwritten
1971 $file = $this->getLocalFile();
1972 $file->load( File::READ_LATEST );
1973 if ( $file->exists() ) {
1974 if ( !self::userCanReUpload( $performer, $file ) ) {
1975 return [ 'fileexists-forbidden', $file->getName() ];
1976 }
1977
1978 return true;
1979 }
1980
1981 $services = MediaWikiServices::getInstance();
1982
1983 /* Check shared conflicts: if the local file does not exist, but
1984 * RepoGroup::findFile finds a file, it exists in a shared repository.
1985 */
1986 $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
1987 if ( $file && !$performer->isAllowed( 'reupload-shared' ) ) {
1988 return [ 'fileexists-shared-forbidden', $file->getName() ];
1989 }
1990
1991 return true;
1992 }
1993
2001 public static function userCanReUpload( Authority $performer, File $img ) {
2002 if ( $performer->isAllowed( 'reupload' ) ) {
2003 return true; // non-conditional
2004 }
2005
2006 if ( !$performer->isAllowed( 'reupload-own' ) ) {
2007 return false;
2008 }
2009
2010 if ( !( $img instanceof LocalFile ) ) {
2011 return false;
2012 }
2013
2014 return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2015 }
2016
2028 public static function getExistsWarning( $file ) {
2029 if ( $file->exists() ) {
2030 return [ 'warning' => 'exists', 'file' => $file ];
2031 }
2032
2033 if ( $file->getTitle()->getArticleID() ) {
2034 return [ 'warning' => 'page-exists', 'file' => $file ];
2035 }
2036
2037 if ( !strpos( $file->getName(), '.' ) ) {
2038 $partname = $file->getName();
2039 $extension = '';
2040 } else {
2041 $n = strrpos( $file->getName(), '.' );
2042 $extension = substr( $file->getName(), $n + 1 );
2043 $partname = substr( $file->getName(), 0, $n );
2044 }
2045 $normalizedExtension = File::normalizeExtension( $extension );
2046 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2047
2048 if ( $normalizedExtension != $extension ) {
2049 // We're not using the normalized form of the extension.
2050 // Normal form is lowercase, using most common of alternate
2051 // extensions (e.g. 'jpg' rather than 'JPEG').
2052
2053 // Check for another file using the normalized form...
2054 $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2055 $file_lc = $localRepo->newFile( $nt_lc );
2056
2057 if ( $file_lc->exists() ) {
2058 return [
2059 'warning' => 'exists-normalized',
2060 'file' => $file,
2061 'normalizedFile' => $file_lc
2062 ];
2063 }
2064 }
2065
2066 // Check for files with the same name but a different extension
2067 $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2068 if ( count( $similarFiles ) ) {
2069 return [
2070 'warning' => 'exists-normalized',
2071 'file' => $file,
2072 'normalizedFile' => $similarFiles[0],
2073 ];
2074 }
2075
2076 if ( self::isThumbName( $file->getName() ) ) {
2077 // Check for filenames like 50px- or 180px-, these are mostly thumbnails
2078 $nt_thb = Title::newFromText(
2079 substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2080 NS_FILE
2081 );
2082 $file_thb = $localRepo->newFile( $nt_thb );
2083 if ( $file_thb->exists() ) {
2084 return [
2085 'warning' => 'thumb',
2086 'file' => $file,
2087 'thumbFile' => $file_thb
2088 ];
2089 }
2090
2091 // The file does not exist, but we just don't like the name
2092 return [
2093 'warning' => 'thumb-name',
2094 'file' => $file,
2095 'thumbFile' => $file_thb
2096 ];
2097 }
2098
2099 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2100 if ( str_starts_with( $partname, $prefix ) ) {
2101 return [
2102 'warning' => 'bad-prefix',
2103 'file' => $file,
2104 'prefix' => $prefix
2105 ];
2106 }
2107 }
2108
2109 return false;
2110 }
2111
2117 public static function isThumbName( $filename ) {
2118 $n = strrpos( $filename, '.' );
2119 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2120
2121 return (
2122 substr( $partname, 3, 3 ) === 'px-' ||
2123 substr( $partname, 2, 3 ) === 'px-'
2124 ) && preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2125 }
2126
2132 public static function getFilenamePrefixBlacklist() {
2133 $list = [];
2134 $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2135 if ( !$message->isDisabled() ) {
2136 $lines = explode( "\n", $message->plain() );
2137 foreach ( $lines as $line ) {
2138 // Remove comment lines
2139 $comment = substr( trim( $line ), 0, 1 );
2140 if ( $comment === '#' || $comment == '' ) {
2141 continue;
2142 }
2143 // Remove additional comments after a prefix
2144 $comment = strpos( $line, '#' );
2145 if ( $comment > 0 ) {
2146 $line = substr( $line, 0, $comment - 1 );
2147 }
2148 $list[] = trim( $line );
2149 }
2150 }
2151
2152 return $list;
2153 }
2154
2166 public function getImageInfo( $result ) {
2167 $stashFile = $this->getStashFile();
2168 // Calling a different API module depending on whether the file was stashed is less than optimal.
2169 // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
2170 if ( $stashFile ) {
2172 $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_fill_keys( $imParam, true ), $result );
2173 } else {
2174 $localFile = $this->getLocalFile();
2176 $info = ApiQueryImageInfo::getInfo( $localFile, array_fill_keys( $imParam, true ), $result );
2177 }
2178
2179 return $info;
2180 }
2181
2186 public function convertVerifyErrorToStatus( $error ) {
2187 $code = $error['status'];
2188 unset( $code['status'] );
2189
2190 return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2191 }
2192
2200 public static function getMaxUploadSize( $forType = null ) {
2201 $maxUploadSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxUploadSize );
2202
2203 if ( is_array( $maxUploadSize ) ) {
2204 if ( $forType !== null && isset( $maxUploadSize[$forType] ) ) {
2205 return $maxUploadSize[$forType];
2206 }
2207 return $maxUploadSize['*'];
2208 }
2209 return intval( $maxUploadSize );
2210 }
2211
2219 public static function getMaxPhpUploadSize() {
2220 $phpMaxFileSize = wfShorthandToInteger(
2221 ini_get( 'upload_max_filesize' ),
2222 PHP_INT_MAX
2223 );
2224 $phpMaxPostSize = wfShorthandToInteger(
2225 ini_get( 'post_max_size' ),
2226 PHP_INT_MAX
2227 ) ?: PHP_INT_MAX;
2228 return min( $phpMaxFileSize, $phpMaxPostSize );
2229 }
2230
2242 public static function getSessionStatus( UserIdentity $user, $statusKey ) {
2243 $store = self::getUploadSessionStore();
2244 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2245
2246 return $store->get( $key );
2247 }
2248
2261 public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
2262 $store = self::getUploadSessionStore();
2263 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2264
2265 if ( $value === false ) {
2266 $store->delete( $key );
2267 } else {
2268 $store->set( $key, $value, $store::TTL_DAY );
2269 }
2270 }
2271
2278 private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
2279 return $store->makeKey(
2280 'uploadstatus',
2281 $user->isRegistered() ? $user->getId() : md5( $user->getName() ),
2282 $statusKey
2283 );
2284 }
2285
2289 private static function getUploadSessionStore() {
2290 return MediaWikiServices::getInstance()->getMainObjectStash();
2291 }
2292}
const AV_SCAN_FAILED
Definition Defines.php:99
const NS_FILE
Definition Defines.php:70
const AV_SCAN_ABORTED
Definition Defines.php:98
const AV_NO_VIRUS
Definition Defines.php:96
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') &&! $wgCommandLineMode $wgOut
Definition Setup.php:535
static getPropertyNames( $filter=[])
Returns all possible parameters to iiprop.
static getInfo( $file, $prop, $result, $thumbParams=null, $opts=false)
Get result information for an image revision.
static getPropertyNames( $filter=null)
Returns all possible parameters to siiprop.
Deleted file in the 'filearchive' table.
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
get( $key, $flags=0)
Get an item.
delete( $key, $flags=0)
Delete an item if it exists.
set( $key, $value, $exptime=0, $flags=0)
Set an item.
makeKey( $keygroup,... $components)
Make a cache key from the given components, in the default keyspace.
static getSha1Base36FromPath( $path)
Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case encoding,...
Definition FSFile.php:225
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
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:70
getName()
Return the name of this file.
Definition File.php:336
wasDeleted()
Was this file ever deleted from the wiki?
Definition File.php:2086
Local file in the wiki's own database.
Definition LocalFile.php:64
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.
MediaWiki exception.
MimeMagic helper wrapper.
static getHandler( $type)
Get a MediaHandler for a given MIME type from the instance cache.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
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 stripping il...
Executes shell commands.
Definition Shell.php:46
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:58
Represents a title within MediaWiki.
Definition Title.php:76
getDBkey()
Get the main part with underscores.
Definition Title.php:1049
internal since 1.36
Definition User.php:98
static listParam(array $list, $type='text')
Definition Message.php:1286
static sizeParam( $size)
Definition Message.php:1253
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.
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.
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.
getImageInfo( $result)
Gets image info about the file just uploaded.
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
static getMaxPhpUploadSize()
Get the PHP maximum uploaded file size, based on ini settings.
static $safeXmlEncodings
verifyMimeType( $mime)
Verify the MIME type.
initializeFromRequest(&$request)
Initialize from a WebRequest.
string false $mSVGNSError
This interface represents the authority associated the current execution context, such as a web reque...
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)
$mime
Definition router.php:60
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42
if(!is_readable( $file)) $ext
Definition router.php:48
if(!file_exists( $CREDITS)) $lines