MediaWiki REL1_40
UploadBase.php
Go to the documentation of this file.
1<?php
24use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
32use Wikimedia\AtEase\AtEase;
33
50abstract class UploadBase {
51 use ProtectedHookAccessorTrait;
52
54 protected $mTempPath;
56 protected $tempFileObj;
60 protected $mDestName;
64 protected $mSourceType;
66 protected $mTitle = false;
68 protected $mTitleError = 0;
70 protected $mFilteredName;
74 protected $mLocalFile;
76 protected $mStashFile;
78 protected $mFileSize;
80 protected $mFileProps;
84 protected $mJavaDetected;
86 protected $mSVGNSError;
87
88 protected static $safeXmlEncodings = [
89 'UTF-8',
90 'US-ASCII',
91 'ISO-8859-1',
92 'ISO-8859-2',
93 'UTF-16',
94 'UTF-32',
95 'WINDOWS-1250',
96 'WINDOWS-1251',
97 'WINDOWS-1252',
98 'WINDOWS-1253',
99 'WINDOWS-1254',
100 'WINDOWS-1255',
101 'WINDOWS-1256',
102 'WINDOWS-1257',
103 'WINDOWS-1258',
104 ];
105
106 public const SUCCESS = 0;
107 public const OK = 0;
108 public const EMPTY_FILE = 3;
109 public const MIN_LENGTH_PARTNAME = 4;
110 public const ILLEGAL_FILENAME = 5;
111 public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
112 public const FILETYPE_MISSING = 8;
113 public const FILETYPE_BADTYPE = 9;
114 public const VERIFICATION_ERROR = 10;
115 public const HOOK_ABORTED = 11;
116 public const FILE_TOO_LARGE = 12;
117 public const WINDOWS_NONASCII_FILENAME = 13;
118 public const FILENAME_TOO_LONG = 14;
119
124 public function getVerificationErrorCode( $error ) {
125 $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 return $code_to_status[$error] ?? 'unknown-error';
139 }
140
147 public static function isEnabled() {
148 $enableUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnableUploads );
149
150 return $enableUploads && wfIniGetBool( 'file_uploads' );
151 }
152
161 public static function isAllowed( Authority $performer ) {
162 foreach ( [ 'upload', 'edit' ] as $permission ) {
163 if ( !$performer->isAllowed( $permission ) ) {
164 return $permission;
165 }
166 }
167
168 return true;
169 }
170
177 public static function isThrottled( $user ) {
178 return $user->pingLimiter( 'upload' );
179 }
180
182 private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
183
191 public static function createFromRequest( &$request, $type = null ) {
192 $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
193
194 if ( !$type ) {
195 return null;
196 }
197
198 // Get the upload class
199 $type = ucfirst( $type );
200
201 // Give hooks the chance to handle this request
203 $className = null;
204 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
205 Hooks::runner()->onUploadCreateFromRequest( $type, $className );
206 if ( $className === null ) {
207 $className = 'UploadFrom' . $type;
208 wfDebug( __METHOD__ . ": class name: $className" );
209 if ( !in_array( $type, self::$uploadHandlers ) ) {
210 return null;
211 }
212 }
213
214 // Check whether this upload class is enabled
215 if ( !$className::isEnabled() ) {
216 return null;
217 }
218
219 // Check whether the request is valid
220 if ( !$className::isValidRequest( $request ) ) {
221 return null;
222 }
223
225 $handler = new $className;
226
227 $handler->initializeFromRequest( $request );
228
229 return $handler;
230 }
231
237 public static function isValidRequest( $request ) {
238 return false;
239 }
240
244 public function __construct() {
245 }
246
254 public function getSourceType() {
255 return null;
256 }
257
265 public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
266 $this->mDesiredDestName = $name;
267 if ( FileBackend::isStoragePath( $tempPath ) ) {
268 throw new MWException( __METHOD__ . " given storage path `$tempPath`." );
269 }
270
271 $this->setTempFile( $tempPath, $fileSize );
272 $this->mRemoveTempFile = $removeTempFile;
273 }
274
280 abstract public function initializeFromRequest( &$request );
281
286 protected function setTempFile( $tempPath, $fileSize = null ) {
287 $this->mTempPath = $tempPath ?? '';
288 $this->mFileSize = $fileSize ?: null;
289 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
290 $this->tempFileObj = new TempFSFile( $this->mTempPath );
291 if ( !$fileSize ) {
292 $this->mFileSize = filesize( $this->mTempPath );
293 }
294 } else {
295 $this->tempFileObj = null;
296 }
297 }
298
304 public function fetchFile() {
305 return Status::newGood();
306 }
307
312 public function isEmptyFile() {
313 return empty( $this->mFileSize );
314 }
315
320 public function getFileSize() {
321 return $this->mFileSize;
322 }
323
329 public function getTempFileSha1Base36() {
330 return FSFile::getSha1Base36FromPath( $this->mTempPath );
331 }
332
337 public function getRealPath( $srcPath ) {
338 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
339 if ( FileRepo::isVirtualUrl( $srcPath ) ) {
343 $tmpFile = $repo->getLocalCopy( $srcPath );
344 if ( $tmpFile ) {
345 $tmpFile->bind( $this ); // keep alive with $this
346 }
347 $path = $tmpFile ? $tmpFile->getPath() : false;
348 } else {
349 $path = $srcPath;
350 }
351
352 return $path;
353 }
354
372 public function verifyUpload() {
376 if ( $this->isEmptyFile() ) {
377 return [ 'status' => self::EMPTY_FILE ];
378 }
379
383 $maxSize = self::getMaxUploadSize( $this->getSourceType() );
384 if ( $this->mFileSize > $maxSize ) {
385 return [
386 'status' => self::FILE_TOO_LARGE,
387 'max' => $maxSize,
388 ];
389 }
390
396 $verification = $this->verifyFile();
397 if ( $verification !== true ) {
398 return [
399 'status' => self::VERIFICATION_ERROR,
400 'details' => $verification
401 ];
402 }
403
407 $result = $this->validateName();
408 if ( $result !== true ) {
409 return $result;
410 }
411
412 return [ 'status' => self::OK ];
413 }
414
421 public function validateName() {
422 $nt = $this->getTitle();
423 if ( $nt === null ) {
424 $result = [ 'status' => $this->mTitleError ];
425 if ( $this->mTitleError === self::ILLEGAL_FILENAME ) {
426 $result['filtered'] = $this->mFilteredName;
427 }
428 if ( $this->mTitleError === self::FILETYPE_BADTYPE ) {
429 $result['finalExt'] = $this->mFinalExtension;
430 if ( count( $this->mBlackListedExtensions ) ) {
431 $result['blacklistedExt'] = $this->mBlackListedExtensions;
432 }
433 }
434
435 return $result;
436 }
437 $this->mDestName = $this->getLocalFile()->getName();
438
439 return true;
440 }
441
451 protected function verifyMimeType( $mime ) {
452 $verifyMimeType = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeType );
453 if ( $verifyMimeType ) {
454 wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" );
455 $mimeTypeExclusions = MediaWikiServices::getInstance()->getMainConfig()
456 ->get( MainConfigNames::MimeTypeExclusions );
457 if ( self::checkFileExtension( $mime, $mimeTypeExclusions ) ) {
458 return [ 'filetype-badmime', $mime ];
459 }
460 }
461
462 return true;
463 }
464
470 protected function verifyFile() {
471 $config = MediaWikiServices::getInstance()->getMainConfig();
472 $verifyMimeType = $config->get( MainConfigNames::VerifyMimeType );
473 $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
474 $status = $this->verifyPartialFile();
475 if ( $status !== true ) {
476 return $status;
477 }
478
479 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
480 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
481 $mime = $this->mFileProps['mime'];
482
483 if ( $verifyMimeType ) {
484 # XXX: Missing extension will be caught by validateName() via getTitle()
485 if ( (string)$this->mFinalExtension !== '' &&
486 !self::verifyExtension( $mime, $this->mFinalExtension )
487 ) {
488 return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
489 }
490 }
491
492 # check for htmlish code and javascript
493 if ( !$disableUploadScriptChecks ) {
494 if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
495 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
496 if ( $svgStatus !== false ) {
497 return $svgStatus;
498 }
499 }
500 }
501
502 $handler = MediaHandler::getHandler( $mime );
503 if ( $handler ) {
504 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
505 if ( !$handlerStatus->isOK() ) {
506 $errors = $handlerStatus->getErrorsArray();
507
508 return reset( $errors );
509 }
510 }
511
512 $error = true;
513 $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
514 if ( $error !== true ) {
515 if ( !is_array( $error ) ) {
516 $error = [ $error ];
517 }
518 return $error;
519 }
520
521 wfDebug( __METHOD__ . ": all clear; passing." );
522
523 return true;
524 }
525
535 protected function verifyPartialFile() {
536 $config = MediaWikiServices::getInstance()->getMainConfig();
537 $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
538 # getTitle() sets some internal parameters like $this->mFinalExtension
539 $this->getTitle();
540
541 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
542 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
543
544 # check MIME type, if desired
545 $mime = $this->mFileProps['file-mime'];
546 $status = $this->verifyMimeType( $mime );
547 if ( $status !== true ) {
548 return $status;
549 }
550
551 # check for htmlish code and javascript
552 if ( !$disableUploadScriptChecks ) {
553 if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
554 return [ 'uploadscripted' ];
555 }
556 if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
557 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
558 if ( $svgStatus !== false ) {
559 return $svgStatus;
560 }
561 }
562 }
563
564 # Scan the uploaded file for viruses
565 $virus = self::detectVirus( $this->mTempPath );
566 if ( $virus ) {
567 return [ 'uploadvirus', $virus ];
568 }
569
570 return true;
571 }
572
578 public function zipEntryCallback( $entry ) {
579 $names = [ $entry['name'] ];
580
581 // If there is a null character, cut off the name at it, because JDK's
582 // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
583 // were constructed which had ".class\0" followed by a string chosen to
584 // make the hash collide with the truncated name, that file could be
585 // returned in response to a request for the .class file.
586 $nullPos = strpos( $entry['name'], "\000" );
587 if ( $nullPos !== false ) {
588 $names[] = substr( $entry['name'], 0, $nullPos );
589 }
590
591 // If there is a trailing slash in the file name, we have to strip it,
592 // because that's what ZIP_GetEntry() does.
593 if ( preg_grep( '!\.class/?$!', $names ) ) {
594 $this->mJavaDetected = true;
595 }
596 }
597
607 public function verifyPermissions( Authority $performer ) {
608 return $this->verifyTitlePermissions( $performer );
609 }
610
622 public function verifyTitlePermissions( Authority $performer ) {
627 $nt = $this->getTitle();
628 if ( $nt === null ) {
629 return true;
630 }
631
632 $status = PermissionStatus::newEmpty();
633 $performer->authorizeWrite( 'edit', $nt, $status );
634 $performer->authorizeWrite( 'upload', $nt, $status );
635 if ( !$status->isGood() ) {
636 return $status->toLegacyErrorArray();
637 }
638
639 $overwriteError = $this->checkOverwrite( $performer );
640 if ( $overwriteError !== true ) {
641 return [ $overwriteError ];
642 }
643
644 return true;
645 }
646
656 public function checkWarnings( $user = null ) {
657 if ( $user === null ) {
658 // TODO check uses and hard deprecate
659 $user = RequestContext::getMain()->getUser();
660 }
661
662 $warnings = [];
663
664 $localFile = $this->getLocalFile();
665 $localFile->load( File::READ_LATEST );
666 $filename = $localFile->getName();
667 $hash = $this->getTempFileSha1Base36();
668
669 $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
670 if ( $badFileName !== null ) {
671 $warnings['badfilename'] = $badFileName;
672 }
673
674 $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
675 if ( $unwantedFileExtensionDetails !== null ) {
676 $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
677 }
678
679 $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
680 if ( $fileSizeWarnings ) {
681 $warnings = array_merge( $warnings, $fileSizeWarnings );
682 }
683
684 $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
685 if ( $localFileExistsWarnings ) {
686 $warnings = array_merge( $warnings, $localFileExistsWarnings );
687 }
688
689 if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
690 $warnings['was-deleted'] = $filename;
691 }
692
693 // If a file with the same name exists locally then the local file has already been tested
694 // for duplication of content
695 $ignoreLocalDupes = isset( $warnings['exists'] );
696 $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
697 if ( $dupes ) {
698 $warnings['duplicate'] = $dupes;
699 }
700
701 $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
702 if ( $archivedDupes !== null ) {
703 $warnings['duplicate-archive'] = $archivedDupes;
704 }
705
706 return $warnings;
707 }
708
720 public static function makeWarningsSerializable( $warnings ) {
721 array_walk_recursive( $warnings, static function ( &$param, $key ) {
722 if ( $param instanceof File ) {
723 $param = [
724 'fileName' => $param->getName(),
725 'timestamp' => $param->getTimestamp()
726 ];
727 } elseif ( is_object( $param ) ) {
728 throw new InvalidArgumentException(
729 'UploadBase::makeWarningsSerializable: ' .
730 'Unexpected object of class ' . get_class( $param ) );
731 }
732 } );
733 return $warnings;
734 }
735
745 private function checkBadFileName( $filename, $desiredFileName ) {
746 $comparableName = str_replace( ' ', '_', $desiredFileName );
747 $comparableName = Title::capitalize( $comparableName, NS_FILE );
748
749 if ( $desiredFileName != $filename && $comparableName != $filename ) {
750 return $filename;
751 }
752
753 return null;
754 }
755
764 private function checkUnwantedFileExtensions( $fileExtension ) {
765 $checkFileExtensions = MediaWikiServices::getInstance()->getMainConfig()
766 ->get( MainConfigNames::CheckFileExtensions );
767 $fileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FileExtensions );
768 if ( $checkFileExtensions ) {
769 $extensions = array_unique( $fileExtensions );
770 if ( !self::checkFileExtension( $fileExtension, $extensions ) ) {
771 return [
772 $fileExtension,
773 Message::listParam( $extensions, 'comma' ),
774 count( $extensions )
775 ];
776 }
777 }
778
779 return null;
780 }
781
787 private function checkFileSize( $fileSize ) {
788 $uploadSizeWarning = MediaWikiServices::getInstance()->getMainConfig()
789 ->get( MainConfigNames::UploadSizeWarning );
790
791 $warnings = [];
792
793 if ( $uploadSizeWarning && ( $fileSize > $uploadSizeWarning ) ) {
794 $warnings['large-file'] = [
795 Message::sizeParam( $uploadSizeWarning ),
796 Message::sizeParam( $fileSize ),
797 ];
798 }
799
800 if ( $fileSize == 0 ) {
801 $warnings['empty-file'] = true;
802 }
803
804 return $warnings;
805 }
806
813 private function checkLocalFileExists( LocalFile $localFile, $hash ) {
814 $warnings = [];
815
816 $exists = self::getExistsWarning( $localFile );
817 if ( $exists !== false ) {
818 $warnings['exists'] = $exists;
819
820 // check if file is an exact duplicate of current file version
821 if ( $hash !== false && $hash === $localFile->getSha1() ) {
822 $warnings['no-change'] = $localFile;
823 }
824
825 // check if file is an exact duplicate of older versions of this file
826 $history = $localFile->getHistory();
827 foreach ( $history as $oldFile ) {
828 if ( $hash === $oldFile->getSha1() ) {
829 $warnings['duplicate-version'][] = $oldFile;
830 }
831 }
832 }
833
834 return $warnings;
835 }
836
837 private function checkLocalFileWasDeleted( LocalFile $localFile ) {
838 return $localFile->wasDeleted() && !$localFile->exists();
839 }
840
847 private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
848 if ( $hash === false ) {
849 return [];
850 }
851 $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
852 $title = $this->getTitle();
853 foreach ( $dupes as $key => $dupe ) {
854 if (
855 ( $dupe instanceof LocalFile ) &&
856 $ignoreLocalDupes &&
857 $title->equals( $dupe->getTitle() )
858 ) {
859 unset( $dupes[$key] );
860 }
861 }
862
863 return $dupes;
864 }
865
873 private function checkAgainstArchiveDupes( $hash, Authority $performer ) {
874 if ( $hash === false ) {
875 return null;
876 }
877 $archivedFile = new ArchivedFile( null, 0, '', $hash );
878 if ( $archivedFile->getID() > 0 ) {
879 if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
880 return $archivedFile->getName();
881 }
882 return '';
883 }
884
885 return null;
886 }
887
905 public function performUpload(
906 $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
907 ) {
908 $this->getLocalFile()->load( File::READ_LATEST );
909 $props = $this->mFileProps;
910
911 $error = null;
912 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
913 $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
914 if ( $error ) {
915 if ( !is_array( $error ) ) {
916 $error = [ $error ];
917 }
918 return Status::newFatal( ...$error );
919 }
920
921 $status = $this->getLocalFile()->upload(
922 $this->mTempPath,
923 $comment,
924 $pageText,
925 File::DELETE_SOURCE,
926 $props,
927 false,
928 $user,
929 $tags
930 );
931
932 if ( $status->isGood() ) {
933 if ( $watch ) {
934 MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
935 $user,
936 $this->getLocalFile()->getTitle(),
937 $watchlistExpiry
938 );
939 }
940 $this->getHookRunner()->onUploadComplete( $this );
941
942 $this->postProcessUpload();
943 }
944
945 return $status;
946 }
947
954 public function postProcessUpload() {
955 }
956
963 public function getTitle() {
964 if ( $this->mTitle !== false ) {
965 return $this->mTitle;
966 }
967 if ( !is_string( $this->mDesiredDestName ) ) {
968 $this->mTitleError = self::ILLEGAL_FILENAME;
969 $this->mTitle = null;
970
971 return $this->mTitle;
972 }
973 /* Assume that if a user specified File:Something.jpg, this is an error
974 * and that the namespace prefix needs to be stripped of.
975 */
976 $title = Title::newFromText( $this->mDesiredDestName );
977 if ( $title && $title->getNamespace() === NS_FILE ) {
978 $this->mFilteredName = $title->getDBkey();
979 } else {
980 $this->mFilteredName = $this->mDesiredDestName;
981 }
982
983 # oi_archive_name is max 255 bytes, which include a timestamp and an
984 # exclamation mark, so restrict file name to 240 bytes.
985 if ( strlen( $this->mFilteredName ) > 240 ) {
986 $this->mTitleError = self::FILENAME_TOO_LONG;
987 $this->mTitle = null;
988
989 return $this->mTitle;
990 }
991
997 $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
998 /* Normalize to title form before we do any further processing */
999 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1000 if ( $nt === null ) {
1001 $this->mTitleError = self::ILLEGAL_FILENAME;
1002 $this->mTitle = null;
1003
1004 return $this->mTitle;
1005 }
1006 $this->mFilteredName = $nt->getDBkey();
1007
1012 [ $partname, $ext ] = self::splitExtensions( $this->mFilteredName );
1013
1014 if ( $ext !== [] ) {
1015 $this->mFinalExtension = trim( end( $ext ) );
1016 } else {
1017 $this->mFinalExtension = '';
1018
1019 // No extension, try guessing one from the temporary file
1020 // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1021 // or incomplete test case in UploadBaseTest (T272328)
1022 if ( $this->mTempPath !== null ) {
1023 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1024 $mime = $magic->guessMimeType( $this->mTempPath );
1025 if ( $mime !== 'unknown/unknown' ) {
1026 # Get a space separated list of extensions
1027 $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1028 if ( $mimeExt !== null ) {
1029 # Set the extension to the canonical extension
1030 $this->mFinalExtension = $mimeExt;
1031
1032 # Fix up the other variables
1033 $this->mFilteredName .= ".{$this->mFinalExtension}";
1034 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1035 $ext = [ $this->mFinalExtension ];
1036 }
1037 }
1038 }
1039 }
1040
1041 // Don't allow users to override the list of prohibited file extensions (check file extension)
1042 $config = MediaWikiServices::getInstance()->getMainConfig();
1043 $checkFileExtensions = $config->get( MainConfigNames::CheckFileExtensions );
1044 $strictFileExtensions = $config->get( MainConfigNames::StrictFileExtensions );
1045 $fileExtensions = $config->get( MainConfigNames::FileExtensions );
1046 $prohibitedFileExtensions = $config->get( MainConfigNames::ProhibitedFileExtensions );
1047
1048 $blackListedExtensions = self::checkFileExtensionList( $ext, $prohibitedFileExtensions );
1049
1050 if ( $this->mFinalExtension == '' ) {
1051 $this->mTitleError = self::FILETYPE_MISSING;
1052 $this->mTitle = null;
1053
1054 return $this->mTitle;
1055 }
1056
1057 if ( $blackListedExtensions ||
1058 ( $checkFileExtensions && $strictFileExtensions &&
1059 !self::checkFileExtension( $this->mFinalExtension, $fileExtensions ) ) ) {
1060 $this->mBlackListedExtensions = $blackListedExtensions;
1061 $this->mTitleError = self::FILETYPE_BADTYPE;
1062 $this->mTitle = null;
1063
1064 return $this->mTitle;
1065 }
1066
1067 // Windows may be broken with special characters, see T3780
1068 if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1069 && !MediaWikiServices::getInstance()->getRepoGroup()
1070 ->getLocalRepo()->backendSupportsUnicodePaths()
1071 ) {
1072 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1073 $this->mTitle = null;
1074
1075 return $this->mTitle;
1076 }
1077
1078 # If there was more than one "extension", reassemble the base
1079 # filename to prevent bogus complaints about length
1080 if ( count( $ext ) > 1 ) {
1081 $iterations = count( $ext ) - 1;
1082 for ( $i = 0; $i < $iterations; $i++ ) {
1083 $partname .= '.' . $ext[$i];
1084 }
1085 }
1086
1087 if ( strlen( $partname ) < 1 ) {
1088 $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1089 $this->mTitle = null;
1090
1091 return $this->mTitle;
1092 }
1093
1094 $this->mTitle = $nt;
1095
1096 return $this->mTitle;
1097 }
1098
1105 public function getLocalFile() {
1106 if ( $this->mLocalFile === null ) {
1107 $nt = $this->getTitle();
1108 $this->mLocalFile = $nt === null
1109 ? null
1110 : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1111 }
1112
1113 return $this->mLocalFile;
1114 }
1115
1119 public function getStashFile() {
1120 return $this->mStashFile;
1121 }
1122
1135 public function tryStashFile( User $user, $isPartial = false ) {
1136 if ( !$isPartial ) {
1137 $error = $this->runUploadStashFileHook( $user );
1138 if ( $error ) {
1139 return Status::newFatal( ...$error );
1140 }
1141 }
1142 try {
1143 $file = $this->doStashFile( $user );
1144 return Status::newGood( $file );
1145 } catch ( UploadStashException $e ) {
1146 return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1147 }
1148 }
1149
1154 protected function runUploadStashFileHook( User $user ) {
1155 $props = $this->mFileProps;
1156 $error = null;
1157 $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1158 if ( $error && !is_array( $error ) ) {
1159 $error = [ $error ];
1160 }
1161 return $error;
1162 }
1163
1171 protected function doStashFile( User $user = null ) {
1172 $stash = MediaWikiServices::getInstance()->getRepoGroup()
1173 ->getLocalRepo()->getUploadStash( $user );
1174 $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
1175 $this->mStashFile = $file;
1176
1177 return $file;
1178 }
1179
1184 public function cleanupTempFile() {
1185 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1186 // Delete when all relevant TempFSFile handles go out of scope
1187 wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1188 $this->tempFileObj->autocollect();
1189 }
1190 }
1191
1192 public function getTempPath() {
1193 return $this->mTempPath;
1194 }
1195
1205 public static function splitExtensions( $filename ) {
1206 $bits = explode( '.', $filename );
1207 $basename = array_shift( $bits );
1208
1209 return [ $basename, $bits ];
1210 }
1211
1220 public static function checkFileExtension( $ext, $list ) {
1221 return in_array( strtolower( $ext ?? '' ), $list, true );
1222 }
1223
1232 public static function checkFileExtensionList( $ext, $list ) {
1233 return array_intersect( array_map( 'strtolower', $ext ), $list );
1234 }
1235
1243 public static function verifyExtension( $mime, $extension ) {
1244 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1245
1246 if ( !$mime || $mime === 'unknown' || $mime === 'unknown/unknown' ) {
1247 if ( !$magic->isRecognizableExtension( $extension ) ) {
1248 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1249 "unrecognized extension '$extension', can't verify" );
1250
1251 return true;
1252 }
1253
1254 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1255 "recognized extension '$extension', so probably invalid file" );
1256 return false;
1257 }
1258
1259 $match = $magic->isMatchingExtension( $extension, $mime );
1260
1261 if ( $match === null ) {
1262 if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1263 wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1264
1265 return false;
1266 }
1267
1268 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1269 return true;
1270 }
1271
1272 if ( $match ) {
1273 wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1274
1276 return true;
1277 }
1278
1279 wfDebug( __METHOD__
1280 . ": mime type $mime mismatches file extension $extension, rejecting file" );
1281
1282 return false;
1283 }
1284
1296 public static function detectScript( $file, $mime, $extension ) {
1297 # ugly hack: for text files, always look at the entire file.
1298 # For binary field, just check the first K.
1299
1300 if ( str_starts_with( $mime ?? '', 'text/' ) ) {
1301 $chunk = file_get_contents( $file );
1302 } else {
1303 $fp = fopen( $file, 'rb' );
1304 if ( !$fp ) {
1305 return false;
1306 }
1307 $chunk = fread( $fp, 1024 );
1308 fclose( $fp );
1309 }
1310
1311 $chunk = strtolower( $chunk );
1312
1313 if ( !$chunk ) {
1314 return false;
1315 }
1316
1317 # decode from UTF-16 if needed (could be used for obfuscation).
1318 if ( str_starts_with( $chunk, "\xfe\xff" ) ) {
1319 $enc = 'UTF-16BE';
1320 } elseif ( str_starts_with( $chunk, "\xff\xfe" ) ) {
1321 $enc = 'UTF-16LE';
1322 } else {
1323 $enc = null;
1324 }
1325
1326 if ( $enc !== null ) {
1327 $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1328 }
1329
1330 $chunk = trim( $chunk );
1331
1333 wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1334
1335 # check for HTML doctype
1336 if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1337 return true;
1338 }
1339
1340 // Some browsers will interpret obscure xml encodings as UTF-8, while
1341 // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1342 if ( $extension === 'svg' || str_starts_with( $mime ?? '', 'image/svg' ) ) {
1343 if ( self::checkXMLEncodingMissmatch( $file ) ) {
1344 return true;
1345 }
1346 }
1347
1348 // Quick check for HTML heuristics in old IE and Safari.
1349 //
1350 // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1351 // don't need them all here as it can cause many false positives.
1352 //
1353 // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1354 $tags = [
1355 '<body',
1356 '<head',
1357 '<html', # also in safari
1358 '<script', # also in safari
1359 ];
1360
1361 foreach ( $tags as $tag ) {
1362 if ( strpos( $chunk, $tag ) !== false ) {
1363 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1364
1365 return true;
1366 }
1367 }
1368
1369 /*
1370 * look for JavaScript
1371 */
1372
1373 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1374 $chunk = Sanitizer::decodeCharReferences( $chunk );
1375
1376 # look for script-types
1377 if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
1378 wfDebug( __METHOD__ . ": found script types" );
1379
1380 return true;
1381 }
1382
1383 # look for html-style script-urls
1384 if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1385 wfDebug( __METHOD__ . ": found html-style script urls" );
1386
1387 return true;
1388 }
1389
1390 # look for css-style script-urls
1391 if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1392 wfDebug( __METHOD__ . ": found css-style script urls" );
1393
1394 return true;
1395 }
1396
1397 wfDebug( __METHOD__ . ": no scripts found" );
1398
1399 return false;
1400 }
1401
1409 public static function checkXMLEncodingMissmatch( $file ) {
1410 $svgMetadataCutoff = MediaWikiServices::getInstance()->getMainConfig()
1411 ->get( MainConfigNames::SVGMetadataCutoff );
1412 $contents = file_get_contents( $file, false, null, 0, $svgMetadataCutoff );
1413 $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1414
1415 if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1416 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1417 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1418 ) {
1419 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1420
1421 return true;
1422 }
1423 } elseif ( preg_match( "!<\?xml\b!si", $contents ) ) {
1424 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1425 // bytes. There shouldn't be a legitimate reason for this to happen.
1426 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1427
1428 return true;
1429 } elseif ( str_starts_with( $contents, "\x4C\x6F\xA7\x94" ) ) {
1430 // EBCDIC encoded XML
1431 wfDebug( __METHOD__ . ": EBCDIC Encoded XML" );
1432
1433 return true;
1434 }
1435
1436 // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to
1437 // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings
1438 $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1439 foreach ( $attemptEncodings as $encoding ) {
1440 AtEase::suppressWarnings();
1441 $str = iconv( $encoding, 'UTF-8', $contents );
1442 AtEase::restoreWarnings();
1443 if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1444 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1445 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1446 ) {
1447 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1448
1449 return true;
1450 }
1451 } elseif ( $str != '' && preg_match( "!<\?xml\b!si", $str ) ) {
1452 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1453 // bytes. There shouldn't be a legitimate reason for this to happen.
1454 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1455
1456 return true;
1457 }
1458 }
1459
1460 return false;
1461 }
1462
1468 protected function detectScriptInSvg( $filename, $partial ) {
1469 $this->mSVGNSError = false;
1470 $check = new XmlTypeCheck(
1471 $filename,
1472 [ $this, 'checkSvgScriptCallback' ],
1473 true,
1474 [
1475 'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ],
1476 'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ],
1477 ]
1478 );
1479 if ( $check->wellFormed !== true ) {
1480 // Invalid xml (T60553)
1481 // But only when non-partial (T67724)
1482 return $partial ? false : [ 'uploadinvalidxml' ];
1483 }
1484
1485 if ( $check->filterMatch ) {
1486 if ( $this->mSVGNSError ) {
1487 return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1488 }
1489 return $check->filterMatchType;
1490 }
1491
1492 return false;
1493 }
1494
1501 public static function checkSvgPICallback( $target, $data ) {
1502 // Don't allow external stylesheets (T59550)
1503 if ( preg_match( '/xml-stylesheet/i', $target ) ) {
1504 return [ 'upload-scripted-pi-callback' ];
1505 }
1506
1507 return false;
1508 }
1509
1521 public static function checkSvgExternalDTD( $type, $publicId, $systemId ) {
1522 // This doesn't include the XHTML+MathML+SVG doctype since we don't
1523 // allow XHTML anyways.
1524 $allowedDTDs = [
1525 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1526 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1527 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1528 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1529 // https://phabricator.wikimedia.org/T168856
1530 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1531 ];
1532 if ( $type !== 'PUBLIC'
1533 || !in_array( $systemId, $allowedDTDs )
1534 || !str_starts_with( $publicId, "-//W3C//" )
1535 ) {
1536 return [ 'upload-scripted-dtd' ];
1537 }
1538 return false;
1539 }
1540
1548 public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1549 [ $namespace, $strippedElement ] = self::splitXmlNamespace( $element );
1550
1551 // We specifically don't include:
1552 // http://www.w3.org/1999/xhtml (T62771)
1553 static $validNamespaces = [
1554 '',
1555 'adobe:ns:meta/',
1556 'http://creativecommons.org/ns#',
1557 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1558 'http://ns.adobe.com/adobeillustrator/10.0/',
1559 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1560 'http://ns.adobe.com/extensibility/1.0/',
1561 'http://ns.adobe.com/flows/1.0/',
1562 'http://ns.adobe.com/illustrator/1.0/',
1563 'http://ns.adobe.com/imagereplacement/1.0/',
1564 'http://ns.adobe.com/pdf/1.3/',
1565 'http://ns.adobe.com/photoshop/1.0/',
1566 'http://ns.adobe.com/saveforweb/1.0/',
1567 'http://ns.adobe.com/variables/1.0/',
1568 'http://ns.adobe.com/xap/1.0/',
1569 'http://ns.adobe.com/xap/1.0/g/',
1570 'http://ns.adobe.com/xap/1.0/g/img/',
1571 'http://ns.adobe.com/xap/1.0/mm/',
1572 'http://ns.adobe.com/xap/1.0/rights/',
1573 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1574 'http://ns.adobe.com/xap/1.0/stype/font#',
1575 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1576 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1577 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1578 'http://ns.adobe.com/xap/1.0/t/pg/',
1579 'http://purl.org/dc/elements/1.1/',
1580 'http://purl.org/dc/elements/1.1',
1581 'http://schemas.microsoft.com/visio/2003/svgextensions/',
1582 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1583 'http://taptrix.com/inkpad/svg_extensions',
1584 'http://web.resource.org/cc/',
1585 'http://www.freesoftware.fsf.org/bkchem/cdml',
1586 'http://www.inkscape.org/namespaces/inkscape',
1587 'http://www.opengis.net/gml',
1588 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1589 'http://www.w3.org/2000/svg',
1590 'http://www.w3.org/tr/rec-rdf-syntax/',
1591 'http://www.w3.org/2000/01/rdf-schema#',
1592 'http://www.w3.org/2000/02/svg/testsuite/description/', // https://phabricator.wikimedia.org/T278044
1593 ];
1594
1595 // Inkscape mangles namespace definitions created by Adobe Illustrator.
1596 // This is nasty but harmless. (T144827)
1597 $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1598
1599 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1600 wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file." );
1602 $this->mSVGNSError = $namespace;
1603
1604 return true;
1605 }
1606
1607 /*
1608 * check for elements that can contain javascript
1609 */
1610 if ( $strippedElement === 'script' ) {
1611 wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file." );
1612
1613 return [ 'uploaded-script-svg', $strippedElement ];
1614 }
1615
1616 # e.g., <svg xmlns="http://www.w3.org/2000/svg">
1617 # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1618 if ( $strippedElement === 'handler' ) {
1619 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1620
1621 return [ 'uploaded-script-svg', $strippedElement ];
1622 }
1623
1624 # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1625 if ( $strippedElement === 'stylesheet' ) {
1626 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1627
1628 return [ 'uploaded-script-svg', $strippedElement ];
1629 }
1630
1631 # Block iframes, in case they pass the namespace check
1632 if ( $strippedElement === 'iframe' ) {
1633 wfDebug( __METHOD__ . ": iframe in uploaded file." );
1634
1635 return [ 'uploaded-script-svg', $strippedElement ];
1636 }
1637
1638 # Check <style> css
1639 if ( $strippedElement === 'style'
1640 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1641 ) {
1642 wfDebug( __METHOD__ . ": hostile css in style element." );
1643
1644 return [ 'uploaded-hostile-svg' ];
1645 }
1646
1647 foreach ( $attribs as $attrib => $value ) {
1648 // If attributeNamespace is '', it is relative to its element's namespace
1649 [ $attributeNamespace, $stripped ] = self::splitXmlNamespace( $attrib );
1650 $value = strtolower( $value );
1651
1652 if ( !(
1653 // Inkscape elements have valid attribs that start with on and are safe, fail all others
1654 $namespace === 'http://www.inkscape.org/namespaces/inkscape' &&
1655 $attributeNamespace === ''
1656 ) && str_starts_with( $stripped, 'on' )
1657 ) {
1658 wfDebug( __METHOD__
1659 . ": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1660
1661 return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1662 }
1663
1664 # Do not allow relative links, or unsafe url schemas.
1665 # For <a> tags, only data:, http: and https: and same-document
1666 # fragment links are allowed. For all other tags, only data:
1667 # and fragment are allowed.
1668 if ( $stripped === 'href'
1669 && $value !== ''
1670 && !str_starts_with( $value, 'data:' )
1671 && !str_starts_with( $value, '#' )
1672 ) {
1673 if ( !( $strippedElement === 'a'
1674 && preg_match( '!^https?://!i', $value ) )
1675 ) {
1676 wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1677 . "'$attrib'='$value' in uploaded file." );
1678
1679 return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1680 }
1681 }
1682
1683 # only allow data: targets that should be safe. This prevents vectors like,
1684 # image/svg, text/xml, application/xml, and text/html, which can contain scripts
1685 if ( $stripped === 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1686 // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
1687 // phpcs:ignore Generic.Files.LineLength
1688 $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1689
1690 if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1691 wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
1692 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1693 return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1694 }
1695 }
1696
1697 # Change href with animate from (http://html5sec.org/#137).
1698 if ( $stripped === 'attributename'
1699 && $strippedElement === 'animate'
1700 && $this->stripXmlNamespace( $value ) === 'href'
1701 ) {
1702 wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1703 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1704
1705 return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1706 }
1707
1708 # use set/animate to add event-handler attribute to parent
1709 if ( ( $strippedElement === 'set' || $strippedElement === 'animate' )
1710 && $stripped === 'attributename'
1711 && substr( $value, 0, 2 ) === 'on'
1712 ) {
1713 wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1714 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1715
1716 return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1717 }
1718
1719 # use set to add href attribute to parent element
1720 if ( $strippedElement === 'set'
1721 && $stripped === 'attributename'
1722 && str_contains( $value, 'href' )
1723 ) {
1724 wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file." );
1725
1726 return [ 'uploaded-setting-href-svg' ];
1727 }
1728
1729 # use set to add a remote / data / script target to an element
1730 if ( $strippedElement === 'set'
1731 && $stripped === 'to'
1732 && preg_match( '!(http|https|data|script):!sim', $value )
1733 ) {
1734 wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file." );
1735
1736 return [ 'uploaded-wrong-setting-svg', $value ];
1737 }
1738
1739 # use handler attribute with remote / data / script
1740 if ( $stripped === 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
1741 wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1742 . "'$attrib'='$value' in uploaded file." );
1743
1744 return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1745 }
1746
1747 # use CSS styles to bring in remote code
1748 if ( $stripped === 'style'
1749 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1750 ) {
1751 wfDebug( __METHOD__ . ": Found svg setting a style with "
1752 . "remote url '$attrib'='$value' in uploaded file." );
1753 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1754 }
1755
1756 # Several attributes can include css, css character escaping isn't allowed
1757 $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1758 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1759 if ( in_array( $stripped, $cssAttrs, true )
1760 && self::checkCssFragment( $value )
1761 ) {
1762 wfDebug( __METHOD__ . ": Found svg setting a style with "
1763 . "remote url '$attrib'='$value' in uploaded file." );
1764 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1765 }
1766
1767 # image filters can pull in url, which could be svg that executes scripts
1768 # Only allow url( "#foo" ). Do not allow url( http://example.com )
1769 if ( $strippedElement === 'image'
1770 && $stripped === 'filter'
1771 && preg_match( '!url\s*\‍(\s*["\']?[^#]!sim', $value )
1772 ) {
1773 wfDebug( __METHOD__ . ": Found image filter with url: "
1774 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1775
1776 return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1777 }
1778 }
1779
1780 return false; // No scripts detected
1781 }
1782
1789 private static function checkCssFragment( $value ) {
1790 # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1791 if ( stripos( $value, '@import' ) !== false ) {
1792 return true;
1793 }
1794
1795 # We allow @font-face to embed fonts with data: urls, so we snip the string
1796 # 'url' out so this case won't match when we check for urls below
1797 $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1798 $value = preg_replace( $pattern, '$1$2', $value );
1799
1800 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1801 # properties filter and accelerator don't seem to be useful for xss in SVG files.
1802 # Expression and -o-link don't seem to work either, but filtering them here in case.
1803 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1804 # but not local ones such as url("#..., url('#..., url(#....
1805 if ( preg_match( '!expression
1806 | -o-link\s*:
1807 | -o-link-source\s*:
1808 | -o-replace\s*:!imx', $value ) ) {
1809 return true;
1810 }
1811
1812 if ( preg_match_all(
1813 "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1814 $value,
1815 $matches
1816 ) !== 0
1817 ) {
1818 # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1819 foreach ( $matches[1] as $match ) {
1820 if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1821 return true;
1822 }
1823 }
1824 }
1825
1826 if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1827 return true;
1828 }
1829
1830 return false;
1831 }
1832
1838 private static function splitXmlNamespace( $element ) {
1839 // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1840 $parts = explode( ':', strtolower( $element ) );
1841 $name = array_pop( $parts );
1842 $ns = implode( ':', $parts );
1843
1844 return [ $ns, $name ];
1845 }
1846
1851 private function stripXmlNamespace( $name ) {
1852 // 'http://www.w3.org/2000/svg:script' -> 'script'
1853 $parts = explode( ':', strtolower( $name ) );
1854
1855 return array_pop( $parts );
1856 }
1857
1868 public static function detectVirus( $file ) {
1869 global $wgOut;
1870 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1871 $antivirus = $mainConfig->get( MainConfigNames::Antivirus );
1872 $antivirusSetup = $mainConfig->get( MainConfigNames::AntivirusSetup );
1873 $antivirusRequired = $mainConfig->get( MainConfigNames::AntivirusRequired );
1874 if ( !$antivirus ) {
1875 wfDebug( __METHOD__ . ": virus scanner disabled" );
1876
1877 return null;
1878 }
1879
1880 if ( !$antivirusSetup[$antivirus] ) {
1881 wfDebug( __METHOD__ . ": unknown virus scanner: {$antivirus}" );
1882 $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1883 [ 'virus-badscanner', $antivirus ] );
1884
1885 return wfMessage( 'virus-unknownscanner' )->text() . " {$antivirus}";
1886 }
1887
1888 # look up scanner configuration
1889 $command = $antivirusSetup[$antivirus]['command'];
1890 $exitCodeMap = $antivirusSetup[$antivirus]['codemap'];
1891 $msgPattern = $antivirusSetup[$antivirus]['messagepattern'] ?? null;
1892
1893 if ( !str_contains( $command, "%f" ) ) {
1894 # simple pattern: append file to scan
1895 $command .= " " . Shell::escape( $file );
1896 } else {
1897 # complex pattern: replace "%f" with file to scan
1898 $command = str_replace( "%f", Shell::escape( $file ), $command );
1899 }
1900
1901 wfDebug( __METHOD__ . ": running virus scan: $command " );
1902
1903 # execute virus scanner
1904 $exitCode = false;
1905
1906 # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1907 # that does not seem to be worth the pain.
1908 # Ask me (Duesentrieb) about it if it's ever needed.
1909 $output = wfShellExecWithStderr( $command, $exitCode );
1910
1911 # map exit code to AV_xxx constants.
1912 $mappedCode = $exitCode;
1913 if ( $exitCodeMap ) {
1914 if ( isset( $exitCodeMap[$exitCode] ) ) {
1915 $mappedCode = $exitCodeMap[$exitCode];
1916 } elseif ( isset( $exitCodeMap["*"] ) ) {
1917 $mappedCode = $exitCodeMap["*"];
1918 }
1919 }
1920
1921 /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
1922 * so we need the strict equalities === and thus can't use a switch here
1923 */
1924 if ( $mappedCode === AV_SCAN_FAILED ) {
1925 # scan failed (code was mapped to false by $exitCodeMap)
1926 wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1927
1928 $output = $antivirusRequired
1929 ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1930 : null;
1931 } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1932 # scan failed because filetype is unknown (probably immune)
1933 wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1934 $output = null;
1935 } elseif ( $mappedCode === AV_NO_VIRUS ) {
1936 # no virus found
1937 wfDebug( __METHOD__ . ": file passed virus scan." );
1938 $output = false;
1939 } else {
1940 $output = trim( $output );
1941
1942 if ( !$output ) {
1943 $output = true; # if there's no output, return true
1944 } elseif ( $msgPattern ) {
1945 $groups = [];
1946 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1947 $output = $groups[1];
1948 }
1949 }
1950
1951 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
1952 }
1953
1954 return $output;
1955 }
1956
1965 private function checkOverwrite( Authority $performer ) {
1966 // First check whether the local file can be overwritten
1967 $file = $this->getLocalFile();
1968 $file->load( File::READ_LATEST );
1969 if ( $file->exists() ) {
1970 if ( !self::userCanReUpload( $performer, $file ) ) {
1971 return [ 'fileexists-forbidden', $file->getName() ];
1972 }
1973
1974 return true;
1975 }
1976
1977 $services = MediaWikiServices::getInstance();
1978
1979 /* Check shared conflicts: if the local file does not exist, but
1980 * RepoGroup::findFile finds a file, it exists in a shared repository.
1981 */
1982 $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
1983 if ( $file && !$performer->isAllowed( 'reupload-shared' )
1984 ) {
1985 return [ 'fileexists-shared-forbidden', $file->getName() ];
1986 }
1987
1988 return true;
1989 }
1990
1998 public static function userCanReUpload( Authority $performer, File $img ) {
1999 if ( $performer->isAllowed( 'reupload' ) ) {
2000 return true; // non-conditional
2001 }
2002
2003 if ( !$performer->isAllowed( 'reupload-own' ) ) {
2004 return false;
2005 }
2006
2007 if ( !( $img instanceof LocalFile ) ) {
2008 return false;
2009 }
2010
2011 return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2012 }
2013
2025 public static function getExistsWarning( $file ) {
2026 if ( $file->exists() ) {
2027 return [ 'warning' => 'exists', 'file' => $file ];
2028 }
2029
2030 if ( $file->getTitle()->getArticleID() ) {
2031 return [ 'warning' => 'page-exists', 'file' => $file ];
2032 }
2033
2034 if ( !strpos( $file->getName(), '.' ) ) {
2035 $partname = $file->getName();
2036 $extension = '';
2037 } else {
2038 $n = strrpos( $file->getName(), '.' );
2039 $extension = substr( $file->getName(), $n + 1 );
2040 $partname = substr( $file->getName(), 0, $n );
2041 }
2042 $normalizedExtension = File::normalizeExtension( $extension );
2043 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2044
2045 if ( $normalizedExtension != $extension ) {
2046 // We're not using the normalized form of the extension.
2047 // Normal form is lowercase, using most common of alternate
2048 // extensions (eg 'jpg' rather than 'JPEG').
2049
2050 // Check for another file using the normalized form...
2051 $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2052 $file_lc = $localRepo->newFile( $nt_lc );
2053
2054 if ( $file_lc->exists() ) {
2055 return [
2056 'warning' => 'exists-normalized',
2057 'file' => $file,
2058 'normalizedFile' => $file_lc
2059 ];
2060 }
2061 }
2062
2063 // Check for files with the same name but a different extension
2064 $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2065 if ( count( $similarFiles ) ) {
2066 return [
2067 'warning' => 'exists-normalized',
2068 'file' => $file,
2069 'normalizedFile' => $similarFiles[0],
2070 ];
2071 }
2072
2073 if ( self::isThumbName( $file->getName() ) ) {
2074 # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2075 $nt_thb = Title::newFromText(
2076 substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2077 NS_FILE
2078 );
2079 $file_thb = $localRepo->newFile( $nt_thb );
2080 if ( $file_thb->exists() ) {
2081 return [
2082 'warning' => 'thumb',
2083 'file' => $file,
2084 'thumbFile' => $file_thb
2085 ];
2086 }
2087
2088 // File does not exist, but we just don't like the name
2089 return [
2090 'warning' => 'thumb-name',
2091 'file' => $file,
2092 'thumbFile' => $file_thb
2093 ];
2094 }
2095
2096 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2097 if ( str_starts_with( $partname, $prefix ) ) {
2098 return [
2099 'warning' => 'bad-prefix',
2100 'file' => $file,
2101 'prefix' => $prefix
2102 ];
2103 }
2104 }
2105
2106 return false;
2107 }
2108
2114 public static function isThumbName( $filename ) {
2115 $n = strrpos( $filename, '.' );
2116 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2117
2118 return (
2119 substr( $partname, 3, 3 ) === 'px-' ||
2120 substr( $partname, 2, 3 ) === 'px-'
2121 ) &&
2122 preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2123 }
2124
2130 public static function getFilenamePrefixBlacklist() {
2131 $list = [];
2132 $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2133 if ( !$message->isDisabled() ) {
2134 $lines = explode( "\n", $message->plain() );
2135 foreach ( $lines as $line ) {
2136 // Remove comment lines
2137 $comment = substr( trim( $line ), 0, 1 );
2138 if ( $comment === '#' || $comment == '' ) {
2139 continue;
2140 }
2141 // Remove additional comments after a prefix
2142 $comment = strpos( $line, '#' );
2143 if ( $comment > 0 ) {
2144 $line = substr( $line, 0, $comment - 1 );
2145 }
2146 $list[] = trim( $line );
2147 }
2148 }
2149
2150 return $list;
2151 }
2152
2164 public function getImageInfo( $result ) {
2165 $localFile = $this->getLocalFile();
2166 $stashFile = $this->getStashFile();
2167 // Calling a different API module depending on whether the file was stashed is less than optimal.
2168 // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
2169 if ( $stashFile ) {
2171 $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_fill_keys( $imParam, true ), $result );
2172 } else {
2174 $info = ApiQueryImageInfo::getInfo( $localFile, array_fill_keys( $imParam, true ), $result );
2175 }
2176
2177 return $info;
2178 }
2179
2184 public function convertVerifyErrorToStatus( $error ) {
2185 $code = $error['status'];
2186 unset( $code['status'] );
2187
2188 return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2189 }
2190
2198 public static function getMaxUploadSize( $forType = null ) {
2199 $maxUploadSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxUploadSize );
2200
2201 if ( is_array( $maxUploadSize ) ) {
2202 if ( $forType !== null && isset( $maxUploadSize[$forType] ) ) {
2203 return $maxUploadSize[$forType];
2204 }
2205 return $maxUploadSize['*'];
2206 }
2207 return intval( $maxUploadSize );
2208 }
2209
2217 public static function getMaxPhpUploadSize() {
2218 $phpMaxFileSize = wfShorthandToInteger(
2219 ini_get( 'upload_max_filesize' ),
2220 PHP_INT_MAX
2221 );
2222 $phpMaxPostSize = wfShorthandToInteger(
2223 ini_get( 'post_max_size' ),
2224 PHP_INT_MAX
2225 ) ?: PHP_INT_MAX;
2226 return min( $phpMaxFileSize, $phpMaxPostSize );
2227 }
2228
2240 public static function getSessionStatus( UserIdentity $user, $statusKey ) {
2241 $store = self::getUploadSessionStore();
2242 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2243
2244 return $store->get( $key );
2245 }
2246
2259 public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
2260 $store = self::getUploadSessionStore();
2261 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2262
2263 if ( $value === false ) {
2264 $store->delete( $key );
2265 } else {
2266 $store->set( $key, $value, $store::TTL_DAY );
2267 }
2268 }
2269
2276 private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
2277 return $store->makeKey(
2278 'uploadstatus',
2279 $user->isRegistered() ? $user->getId() : md5( $user->getName() ),
2280 $statusKey
2281 );
2282 }
2283
2287 private static function getUploadSessionStore() {
2288 return MediaWikiServices::getInstance()->getMainObjectStash();
2289 }
2290}
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.
Title null $mTitle
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode $wgOut
Definition Setup.php:527
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.
makeKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
set( $key, $value, $exptime=0, $flags=0)
Set an item.
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:286
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:68
getName()
Return the name of this file.
Definition File.php:334
wasDeleted()
Was this file ever deleted from the wiki?
Definition File.php:2085
Local file in the wiki's own database.
Definition LocalFile.php:61
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.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
A StatusValue for permission errors.
Executes shell commands.
Definition Shell.php:46
Represents a title within MediaWiki.
Definition Title.php:82
getDBkey()
Get the main part with underscores.
Definition Title.php:1090
static listParam(array $list, $type='text')
Definition Message.php:1278
static sizeParam( $size)
Definition Message.php:1245
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 blacklisted 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 a whitelist of xml encodings that are known not to be interpreted differently by the server's x...
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 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 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
internal since 1.36
Definition User.php:71
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)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
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