MediaWiki REL1_39
UploadBase.php
Go to the documentation of this file.
1<?php
24use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
31use Wikimedia\AtEase\AtEase;
32
49abstract class UploadBase {
50 use ProtectedHookAccessorTrait;
51
53 protected $mTempPath;
55 protected $tempFileObj;
59 protected $mDestName;
63 protected $mSourceType;
65 protected $mTitle = false;
67 protected $mTitleError = 0;
69 protected $mFilteredName;
73 protected $mLocalFile;
75 protected $mStashFile;
77 protected $mFileSize;
79 protected $mFileProps;
83 protected $mJavaDetected;
85 protected $mSVGNSError;
86
87 protected static $safeXmlEncodings = [
88 'UTF-8',
89 'US-ASCII',
90 'ISO-8859-1',
91 'ISO-8859-2',
92 'UTF-16',
93 'UTF-32',
94 'WINDOWS-1250',
95 'WINDOWS-1251',
96 'WINDOWS-1252',
97 'WINDOWS-1253',
98 'WINDOWS-1254',
99 'WINDOWS-1255',
100 'WINDOWS-1256',
101 'WINDOWS-1257',
102 'WINDOWS-1258',
103 ];
104
105 public const SUCCESS = 0;
106 public const OK = 0;
107 public const EMPTY_FILE = 3;
108 public const MIN_LENGTH_PARTNAME = 4;
109 public const ILLEGAL_FILENAME = 5;
110 public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
111 public const FILETYPE_MISSING = 8;
112 public const FILETYPE_BADTYPE = 9;
113 public const VERIFICATION_ERROR = 10;
114 public const HOOK_ABORTED = 11;
115 public const FILE_TOO_LARGE = 12;
116 public const WINDOWS_NONASCII_FILENAME = 13;
117 public const FILENAME_TOO_LONG = 14;
118
123 public function getVerificationErrorCode( $error ) {
124 $code_to_status = [
125 self::EMPTY_FILE => 'empty-file',
126 self::FILE_TOO_LARGE => 'file-too-large',
127 self::FILETYPE_MISSING => 'filetype-missing',
128 self::FILETYPE_BADTYPE => 'filetype-banned',
129 self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
130 self::ILLEGAL_FILENAME => 'illegal-filename',
131 self::OVERWRITE_EXISTING_FILE => 'overwrite',
132 self::VERIFICATION_ERROR => 'verification-error',
133 self::HOOK_ABORTED => 'hookaborted',
134 self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
135 self::FILENAME_TOO_LONG => 'filename-toolong',
136 ];
137 return $code_to_status[$error] ?? 'unknown-error';
138 }
139
146 public static function isEnabled() {
147 $enableUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnableUploads );
148
149 return $enableUploads && wfIniGetBool( 'file_uploads' );
150 }
151
160 public static function isAllowed( Authority $performer ) {
161 foreach ( [ 'upload', 'edit' ] as $permission ) {
162 if ( !$performer->isAllowed( $permission ) ) {
163 return $permission;
164 }
165 }
166
167 return true;
168 }
169
176 public static function isThrottled( $user ) {
177 return $user->pingLimiter( 'upload' );
178 }
179
181 private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
182
190 public static function createFromRequest( &$request, $type = null ) {
191 $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
192
193 if ( !$type ) {
194 return null;
195 }
196
197 // Get the upload class
198 $type = ucfirst( $type );
199
200 // Give hooks the chance to handle this request
202 $className = null;
203 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
204 Hooks::runner()->onUploadCreateFromRequest( $type, $className );
205 if ( $className === null ) {
206 $className = 'UploadFrom' . $type;
207 wfDebug( __METHOD__ . ": class name: $className" );
208 if ( !in_array( $type, self::$uploadHandlers ) ) {
209 return null;
210 }
211 }
212
213 // Check whether this upload class is enabled
214 if ( !$className::isEnabled() ) {
215 return null;
216 }
217
218 // Check whether the request is valid
219 if ( !$className::isValidRequest( $request ) ) {
220 return null;
221 }
222
224 $handler = new $className;
225
226 $handler->initializeFromRequest( $request );
227
228 return $handler;
229 }
230
236 public static function isValidRequest( $request ) {
237 return false;
238 }
239
243 public function __construct() {
244 }
245
253 public function getSourceType() {
254 return null;
255 }
256
264 public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
265 $this->mDesiredDestName = $name;
266 if ( FileBackend::isStoragePath( $tempPath ) ) {
267 throw new MWException( __METHOD__ . " given storage path `$tempPath`." );
268 }
269
270 $this->setTempFile( $tempPath, $fileSize );
271 $this->mRemoveTempFile = $removeTempFile;
272 }
273
279 abstract public function initializeFromRequest( &$request );
280
285 protected function setTempFile( $tempPath, $fileSize = null ) {
286 $this->mTempPath = $tempPath ?? '';
287 $this->mFileSize = $fileSize ?: null;
288 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
289 $this->tempFileObj = new TempFSFile( $this->mTempPath );
290 if ( !$fileSize ) {
291 $this->mFileSize = filesize( $this->mTempPath );
292 }
293 } else {
294 $this->tempFileObj = null;
295 }
296 }
297
303 public function fetchFile() {
304 return Status::newGood();
305 }
306
311 public function isEmptyFile() {
312 return empty( $this->mFileSize );
313 }
314
319 public function getFileSize() {
320 return $this->mFileSize;
321 }
322
328 public function getTempFileSha1Base36() {
329 return FSFile::getSha1Base36FromPath( $this->mTempPath );
330 }
331
336 public function getRealPath( $srcPath ) {
337 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
338 if ( FileRepo::isVirtualUrl( $srcPath ) ) {
342 $tmpFile = $repo->getLocalCopy( $srcPath );
343 if ( $tmpFile ) {
344 $tmpFile->bind( $this ); // keep alive with $this
345 }
346 $path = $tmpFile ? $tmpFile->getPath() : false;
347 } else {
348 $path = $srcPath;
349 }
350
351 return $path;
352 }
353
371 public function verifyUpload() {
375 if ( $this->isEmptyFile() ) {
376 return [ 'status' => self::EMPTY_FILE ];
377 }
378
382 $maxSize = self::getMaxUploadSize( $this->getSourceType() );
383 if ( $this->mFileSize > $maxSize ) {
384 return [
385 'status' => self::FILE_TOO_LARGE,
386 'max' => $maxSize,
387 ];
388 }
389
395 $verification = $this->verifyFile();
396 if ( $verification !== true ) {
397 return [
398 'status' => self::VERIFICATION_ERROR,
399 'details' => $verification
400 ];
401 }
402
406 $result = $this->validateName();
407 if ( $result !== true ) {
408 return $result;
409 }
410
411 return [ 'status' => self::OK ];
412 }
413
420 public function validateName() {
421 $nt = $this->getTitle();
422 if ( $nt === null ) {
423 $result = [ 'status' => $this->mTitleError ];
424 if ( $this->mTitleError == self::ILLEGAL_FILENAME ) {
425 $result['filtered'] = $this->mFilteredName;
426 }
427 if ( $this->mTitleError == self::FILETYPE_BADTYPE ) {
428 $result['finalExt'] = $this->mFinalExtension;
429 if ( count( $this->mBlackListedExtensions ) ) {
430 $result['blacklistedExt'] = $this->mBlackListedExtensions;
431 }
432 }
433
434 return $result;
435 }
436 $this->mDestName = $this->getLocalFile()->getName();
437
438 return true;
439 }
440
450 protected function verifyMimeType( $mime ) {
451 $verifyMimeType = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeType );
452 $verifyMimeTypeIE = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeTypeIE );
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 if ( $verifyMimeTypeIE ) {
462 # Check what Internet Explorer would detect
463 $fp = fopen( $this->mTempPath, 'rb' );
464 if ( $fp ) {
465 $chunk = fread( $fp, 256 );
466 fclose( $fp );
467
468 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
469 $extMime = $magic->getMimeTypeFromExtensionOrNull( (string)$this->mFinalExtension ) ?? '';
470 $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
471 foreach ( $ieTypes as $ieType ) {
472 if ( self::checkFileExtension( $ieType, $mimeTypeExclusions ) ) {
473 return [ 'filetype-bad-ie-mime', $ieType ];
474 }
475 }
476 }
477 }
478 }
479
480 return true;
481 }
482
488 protected function verifyFile() {
489 $config = MediaWikiServices::getInstance()->getMainConfig();
490 $verifyMimeType = $config->get( MainConfigNames::VerifyMimeType );
491 $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
492 $status = $this->verifyPartialFile();
493 if ( $status !== true ) {
494 return $status;
495 }
496
497 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
498 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
499 $mime = $this->mFileProps['mime'];
500
501 if ( $verifyMimeType ) {
502 # XXX: Missing extension will be caught by validateName() via getTitle()
503 if ( (string)$this->mFinalExtension !== '' &&
504 !$this->verifyExtension( $mime, $this->mFinalExtension )
505 ) {
506 return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
507 }
508 }
509
510 # check for htmlish code and javascript
511 if ( !$disableUploadScriptChecks ) {
512 if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
513 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
514 if ( $svgStatus !== false ) {
515 return $svgStatus;
516 }
517 }
518 }
519
520 $handler = MediaHandler::getHandler( $mime );
521 if ( $handler ) {
522 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
523 if ( !$handlerStatus->isOK() ) {
524 $errors = $handlerStatus->getErrorsArray();
525
526 return reset( $errors );
527 }
528 }
529
530 $error = true;
531 $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
532 if ( $error !== true ) {
533 if ( !is_array( $error ) ) {
534 $error = [ $error ];
535 }
536 return $error;
537 }
538
539 wfDebug( __METHOD__ . ": all clear; passing." );
540
541 return true;
542 }
543
553 protected function verifyPartialFile() {
554 $config = MediaWikiServices::getInstance()->getMainConfig();
555 $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
556 # getTitle() sets some internal parameters like $this->mFinalExtension
557 $this->getTitle();
558
559 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
560 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
561
562 # check MIME type, if desired
563 $mime = $this->mFileProps['file-mime'];
564 $status = $this->verifyMimeType( $mime );
565 if ( $status !== true ) {
566 return $status;
567 }
568
569 # check for htmlish code and javascript
570 if ( !$disableUploadScriptChecks ) {
571 if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
572 return [ 'uploadscripted' ];
573 }
574 if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
575 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
576 if ( $svgStatus !== false ) {
577 return $svgStatus;
578 }
579 }
580 }
581
582 # Scan the uploaded file for viruses
583 $virus = $this->detectVirus( $this->mTempPath );
584 if ( $virus ) {
585 return [ 'uploadvirus', $virus ];
586 }
587
588 return true;
589 }
590
596 public function zipEntryCallback( $entry ) {
597 $names = [ $entry['name'] ];
598
599 // If there is a null character, cut off the name at it, because JDK's
600 // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
601 // were constructed which had ".class\0" followed by a string chosen to
602 // make the hash collide with the truncated name, that file could be
603 // returned in response to a request for the .class file.
604 $nullPos = strpos( $entry['name'], "\000" );
605 if ( $nullPos !== false ) {
606 $names[] = substr( $entry['name'], 0, $nullPos );
607 }
608
609 // If there is a trailing slash in the file name, we have to strip it,
610 // because that's what ZIP_GetEntry() does.
611 if ( preg_grep( '!\.class/?$!', $names ) ) {
612 $this->mJavaDetected = true;
613 }
614 }
615
625 public function verifyPermissions( Authority $performer ) {
626 return $this->verifyTitlePermissions( $performer );
627 }
628
640 public function verifyTitlePermissions( Authority $performer ) {
645 $nt = $this->getTitle();
646 if ( $nt === null ) {
647 return true;
648 }
649
650 $status = PermissionStatus::newEmpty();
651 $performer->authorizeWrite( 'edit', $nt, $status );
652 $performer->authorizeWrite( 'upload', $nt, $status );
653 if ( !$status->isGood() ) {
654 return $status->toLegacyErrorArray();
655 }
656
657 $overwriteError = $this->checkOverwrite( $performer );
658 if ( $overwriteError !== true ) {
659 return [ $overwriteError ];
660 }
661
662 return true;
663 }
664
674 public function checkWarnings( $user = null ) {
675 if ( $user === null ) {
676 // TODO check uses and hard deprecate
677 $user = RequestContext::getMain()->getUser();
678 }
679
680 $warnings = [];
681
682 $localFile = $this->getLocalFile();
683 $localFile->load( File::READ_LATEST );
684 $filename = $localFile->getName();
685 $hash = $this->getTempFileSha1Base36();
686
687 $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
688 if ( $badFileName !== null ) {
689 $warnings['badfilename'] = $badFileName;
690 }
691
692 $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
693 if ( $unwantedFileExtensionDetails !== null ) {
694 $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
695 }
696
697 $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
698 if ( $fileSizeWarnings ) {
699 $warnings = array_merge( $warnings, $fileSizeWarnings );
700 }
701
702 $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
703 if ( $localFileExistsWarnings ) {
704 $warnings = array_merge( $warnings, $localFileExistsWarnings );
705 }
706
707 if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
708 $warnings['was-deleted'] = $filename;
709 }
710
711 // If a file with the same name exists locally then the local file has already been tested
712 // for duplication of content
713 $ignoreLocalDupes = isset( $warnings['exists'] );
714 $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
715 if ( $dupes ) {
716 $warnings['duplicate'] = $dupes;
717 }
718
719 $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
720 if ( $archivedDupes !== null ) {
721 $warnings['duplicate-archive'] = $archivedDupes;
722 }
723
724 return $warnings;
725 }
726
738 public static function makeWarningsSerializable( $warnings ) {
739 array_walk_recursive( $warnings, static function ( &$param, $key ) {
740 if ( $param instanceof File ) {
741 $param = [
742 'fileName' => $param->getName(),
743 'timestamp' => $param->getTimestamp()
744 ];
745 } elseif ( is_object( $param ) ) {
746 throw new InvalidArgumentException(
747 'UploadBase::makeWarningsSerializable: ' .
748 'Unexpected object of class ' . get_class( $param ) );
749 }
750 } );
751 return $warnings;
752 }
753
763 private function checkBadFileName( $filename, $desiredFileName ) {
764 $comparableName = str_replace( ' ', '_', $desiredFileName );
765 $comparableName = Title::capitalize( $comparableName, NS_FILE );
766
767 if ( $desiredFileName != $filename && $comparableName != $filename ) {
768 return $filename;
769 }
770
771 return null;
772 }
773
782 private function checkUnwantedFileExtensions( $fileExtension ) {
783 global $wgLang;
784 $checkFileExtensions = MediaWikiServices::getInstance()->getMainConfig()
785 ->get( MainConfigNames::CheckFileExtensions );
786 $fileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FileExtensions );
787 if ( $checkFileExtensions ) {
788 $extensions = array_unique( $fileExtensions );
789 if ( !$this->checkFileExtension( $fileExtension, $extensions ) ) {
790 return [
791 $fileExtension,
792 $wgLang->commaList( $extensions ),
793 count( $extensions )
794 ];
795 }
796 }
797
798 return null;
799 }
800
806 private function checkFileSize( $fileSize ) {
807 $uploadSizeWarning = MediaWikiServices::getInstance()->getMainConfig()
808 ->get( MainConfigNames::UploadSizeWarning );
809
810 $warnings = [];
811
812 if ( $uploadSizeWarning && ( $fileSize > $uploadSizeWarning ) ) {
813 $warnings['large-file'] = [
814 Message::sizeParam( $uploadSizeWarning ),
815 Message::sizeParam( $fileSize ),
816 ];
817 }
818
819 if ( $fileSize == 0 ) {
820 $warnings['empty-file'] = true;
821 }
822
823 return $warnings;
824 }
825
832 private function checkLocalFileExists( LocalFile $localFile, $hash ) {
833 $warnings = [];
834
835 $exists = self::getExistsWarning( $localFile );
836 if ( $exists !== false ) {
837 $warnings['exists'] = $exists;
838
839 // check if file is an exact duplicate of current file version
840 if ( $hash !== false && $hash === $localFile->getSha1() ) {
841 $warnings['no-change'] = $localFile;
842 }
843
844 // check if file is an exact duplicate of older versions of this file
845 $history = $localFile->getHistory();
846 foreach ( $history as $oldFile ) {
847 if ( $hash === $oldFile->getSha1() ) {
848 $warnings['duplicate-version'][] = $oldFile;
849 }
850 }
851 }
852
853 return $warnings;
854 }
855
856 private function checkLocalFileWasDeleted( LocalFile $localFile ) {
857 return $localFile->wasDeleted() && !$localFile->exists();
858 }
859
866 private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
867 if ( $hash === false ) {
868 return [];
869 }
870 $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
871 $title = $this->getTitle();
872 foreach ( $dupes as $key => $dupe ) {
873 if (
874 ( $dupe instanceof LocalFile ) &&
875 $ignoreLocalDupes &&
876 $title->equals( $dupe->getTitle() )
877 ) {
878 unset( $dupes[$key] );
879 }
880 }
881
882 return $dupes;
883 }
884
892 private function checkAgainstArchiveDupes( $hash, Authority $performer ) {
893 if ( $hash === false ) {
894 return null;
895 }
896 $archivedFile = new ArchivedFile( null, 0, '', $hash );
897 if ( $archivedFile->getID() > 0 ) {
898 if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
899 return $archivedFile->getName();
900 } else {
901 return '';
902 }
903 }
904
905 return null;
906 }
907
925 public function performUpload(
926 $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
927 ) {
928 $this->getLocalFile()->load( File::READ_LATEST );
929 $props = $this->mFileProps;
930
931 $error = null;
932 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
933 $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
934 if ( $error ) {
935 if ( !is_array( $error ) ) {
936 $error = [ $error ];
937 }
938 return Status::newFatal( ...$error );
939 }
940
941 $status = $this->getLocalFile()->upload(
942 $this->mTempPath,
943 $comment,
944 $pageText,
945 File::DELETE_SOURCE,
946 $props,
947 false,
948 $user,
949 $tags
950 );
951
952 if ( $status->isGood() ) {
953 if ( $watch ) {
954 MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
955 $user,
956 $this->getLocalFile()->getTitle(),
957 $watchlistExpiry
958 );
959 }
960 $this->getHookRunner()->onUploadComplete( $this );
961
962 $this->postProcessUpload();
963 }
964
965 return $status;
966 }
967
974 public function postProcessUpload() {
975 }
976
983 public function getTitle() {
984 if ( $this->mTitle !== false ) {
985 return $this->mTitle;
986 }
987 if ( !is_string( $this->mDesiredDestName ) ) {
988 $this->mTitleError = self::ILLEGAL_FILENAME;
989 $this->mTitle = null;
990
991 return $this->mTitle;
992 }
993 /* Assume that if a user specified File:Something.jpg, this is an error
994 * and that the namespace prefix needs to be stripped of.
995 */
996 $title = Title::newFromText( $this->mDesiredDestName );
997 if ( $title && $title->getNamespace() === NS_FILE ) {
998 $this->mFilteredName = $title->getDBkey();
999 } else {
1000 $this->mFilteredName = $this->mDesiredDestName;
1001 }
1002
1003 # oi_archive_name is max 255 bytes, which include a timestamp and an
1004 # exclamation mark, so restrict file name to 240 bytes.
1005 if ( strlen( $this->mFilteredName ) > 240 ) {
1006 $this->mTitleError = self::FILENAME_TOO_LONG;
1007 $this->mTitle = null;
1008
1009 return $this->mTitle;
1010 }
1011
1017 $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
1018 /* Normalize to title form before we do any further processing */
1019 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1020 if ( $nt === null ) {
1021 $this->mTitleError = self::ILLEGAL_FILENAME;
1022 $this->mTitle = null;
1023
1024 return $this->mTitle;
1025 }
1026 $this->mFilteredName = $nt->getDBkey();
1027
1032 list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
1033
1034 if ( $ext !== [] ) {
1035 $this->mFinalExtension = trim( end( $ext ) );
1036 } else {
1037 $this->mFinalExtension = '';
1038
1039 // No extension, try guessing one from the temporary file
1040 // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1041 // or incomplete test case in UploadBaseTest (T272328)
1042 if ( $this->mTempPath !== null ) {
1043 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1044 $mime = $magic->guessMimeType( $this->mTempPath );
1045 if ( $mime !== 'unknown/unknown' ) {
1046 # Get a space separated list of extensions
1047 $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1048 if ( $mimeExt !== null ) {
1049 # Set the extension to the canonical extension
1050 $this->mFinalExtension = $mimeExt;
1051
1052 # Fix up the other variables
1053 $this->mFilteredName .= ".{$this->mFinalExtension}";
1054 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1055 $ext = [ $this->mFinalExtension ];
1056 }
1057 }
1058 }
1059 }
1060
1061 // Don't allow users to override the list of prohibited file extensions (check file extension)
1062 $config = MediaWikiServices::getInstance()->getMainConfig();
1063 $checkFileExtensions = $config->get( MainConfigNames::CheckFileExtensions );
1064 $strictFileExtensions = $config->get( MainConfigNames::StrictFileExtensions );
1065 $fileExtensions = $config->get( MainConfigNames::FileExtensions );
1066 $prohibitedFileExtensions = $config->get( MainConfigNames::ProhibitedFileExtensions );
1067
1068 $blackListedExtensions = self::checkFileExtensionList( $ext, $prohibitedFileExtensions );
1069
1070 if ( $this->mFinalExtension == '' ) {
1071 $this->mTitleError = self::FILETYPE_MISSING;
1072 $this->mTitle = null;
1073
1074 return $this->mTitle;
1075 } elseif ( $blackListedExtensions ||
1076 ( $checkFileExtensions && $strictFileExtensions &&
1077 !$this->checkFileExtension( $this->mFinalExtension, $fileExtensions ) )
1078 ) {
1079 $this->mBlackListedExtensions = $blackListedExtensions;
1080 $this->mTitleError = self::FILETYPE_BADTYPE;
1081 $this->mTitle = null;
1082
1083 return $this->mTitle;
1084 }
1085
1086 // Windows may be broken with special characters, see T3780
1087 if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1088 && !MediaWikiServices::getInstance()->getRepoGroup()
1089 ->getLocalRepo()->backendSupportsUnicodePaths()
1090 ) {
1091 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1092 $this->mTitle = null;
1093
1094 return $this->mTitle;
1095 }
1096
1097 # If there was more than one "extension", reassemble the base
1098 # filename to prevent bogus complaints about length
1099 if ( count( $ext ) > 1 ) {
1100 $iterations = count( $ext ) - 1;
1101 for ( $i = 0; $i < $iterations; $i++ ) {
1102 $partname .= '.' . $ext[$i];
1103 }
1104 }
1105
1106 if ( strlen( $partname ) < 1 ) {
1107 $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1108 $this->mTitle = null;
1109
1110 return $this->mTitle;
1111 }
1112
1113 $this->mTitle = $nt;
1114
1115 return $this->mTitle;
1116 }
1117
1124 public function getLocalFile() {
1125 if ( $this->mLocalFile === null ) {
1126 $nt = $this->getTitle();
1127 $this->mLocalFile = $nt === null
1128 ? null
1129 : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1130 }
1131
1132 return $this->mLocalFile;
1133 }
1134
1138 public function getStashFile() {
1139 return $this->mStashFile;
1140 }
1141
1154 public function tryStashFile( User $user, $isPartial = false ) {
1155 if ( !$isPartial ) {
1156 $error = $this->runUploadStashFileHook( $user );
1157 if ( $error ) {
1158 return Status::newFatal( ...$error );
1159 }
1160 }
1161 try {
1162 $file = $this->doStashFile( $user );
1163 return Status::newGood( $file );
1164 } catch ( UploadStashException $e ) {
1165 return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1166 }
1167 }
1168
1173 protected function runUploadStashFileHook( User $user ) {
1174 $props = $this->mFileProps;
1175 $error = null;
1176 $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1177 if ( $error && !is_array( $error ) ) {
1178 $error = [ $error ];
1179 }
1180 return $error;
1181 }
1182
1190 protected function doStashFile( User $user = null ) {
1191 $stash = MediaWikiServices::getInstance()->getRepoGroup()
1192 ->getLocalRepo()->getUploadStash( $user );
1193 $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
1194 $this->mStashFile = $file;
1195
1196 return $file;
1197 }
1198
1203 public function cleanupTempFile() {
1204 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1205 // Delete when all relevant TempFSFile handles go out of scope
1206 wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1207 $this->tempFileObj->autocollect();
1208 }
1209 }
1210
1211 public function getTempPath() {
1212 return $this->mTempPath;
1213 }
1214
1224 public static function splitExtensions( $filename ) {
1225 $bits = explode( '.', $filename );
1226 $basename = array_shift( $bits );
1227
1228 return [ $basename, $bits ];
1229 }
1230
1239 public static function checkFileExtension( $ext, $list ) {
1240 return in_array( strtolower( $ext ), $list );
1241 }
1242
1251 public static function checkFileExtensionList( $ext, $list ) {
1252 return array_intersect( array_map( 'strtolower', $ext ), $list );
1253 }
1254
1262 public static function verifyExtension( $mime, $extension ) {
1263 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1264
1265 if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) {
1266 if ( !$magic->isRecognizableExtension( $extension ) ) {
1267 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1268 "unrecognized extension '$extension', can't verify" );
1269
1270 return true;
1271 } else {
1272 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1273 "recognized extension '$extension', so probably invalid file" );
1274
1275 return false;
1276 }
1277 }
1278
1279 $match = $magic->isMatchingExtension( $extension, $mime );
1280
1281 if ( $match === null ) {
1282 if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1283 wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1284
1285 return false;
1286 } else {
1287 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1288
1289 return true;
1290 }
1291 } elseif ( $match ) {
1292 wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1293
1295 return true;
1296 } else {
1297 wfDebug( __METHOD__
1298 . ": mime type $mime mismatches file extension $extension, rejecting file" );
1299
1300 return false;
1301 }
1302 }
1303
1315 public static function detectScript( $file, $mime, $extension ) {
1316 # ugly hack: for text files, always look at the entire file.
1317 # For binary field, just check the first K.
1318
1319 $isText = strpos( $mime, 'text/' ) === 0;
1320 if ( $isText ) {
1321 $chunk = file_get_contents( $file );
1322 } else {
1323 $fp = fopen( $file, 'rb' );
1324 if ( !$fp ) {
1325 return false;
1326 }
1327 $chunk = fread( $fp, 1024 );
1328 fclose( $fp );
1329 }
1330
1331 $chunk = strtolower( $chunk );
1332
1333 if ( !$chunk ) {
1334 return false;
1335 }
1336
1337 # decode from UTF-16 if needed (could be used for obfuscation).
1338 if ( substr( $chunk, 0, 2 ) == "\xfe\xff" ) {
1339 $enc = 'UTF-16BE';
1340 } elseif ( substr( $chunk, 0, 2 ) == "\xff\xfe" ) {
1341 $enc = 'UTF-16LE';
1342 } else {
1343 $enc = null;
1344 }
1345
1346 if ( $enc !== null ) {
1347 AtEase::suppressWarnings();
1348 $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1349 AtEase::restoreWarnings();
1350 }
1351
1352 $chunk = trim( $chunk );
1353
1355 wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1356
1357 # check for HTML doctype
1358 if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1359 return true;
1360 }
1361
1362 // Some browsers will interpret obscure xml encodings as UTF-8, while
1363 // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1364 if ( $extension == 'svg' || strpos( $mime, 'image/svg' ) === 0 ) {
1365 if ( self::checkXMLEncodingMissmatch( $file ) ) {
1366 return true;
1367 }
1368 }
1369
1370 // Quick check for HTML heuristics in old IE and Safari.
1371 //
1372 // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1373 // don't need them all here as it can cause many false positives.
1374 //
1375 // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1376 $tags = [
1377 '<body',
1378 '<head',
1379 '<html', # also in safari
1380 '<script', # also in safari
1381 ];
1382
1383 foreach ( $tags as $tag ) {
1384 if ( strpos( $chunk, $tag ) !== false ) {
1385 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1386
1387 return true;
1388 }
1389 }
1390
1391 /*
1392 * look for JavaScript
1393 */
1394
1395 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1396 $chunk = Sanitizer::decodeCharReferences( $chunk );
1397
1398 # look for script-types
1399 if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
1400 wfDebug( __METHOD__ . ": found script types" );
1401
1402 return true;
1403 }
1404
1405 # look for html-style script-urls
1406 if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1407 wfDebug( __METHOD__ . ": found html-style script urls" );
1408
1409 return true;
1410 }
1411
1412 # look for css-style script-urls
1413 if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1414 wfDebug( __METHOD__ . ": found css-style script urls" );
1415
1416 return true;
1417 }
1418
1419 wfDebug( __METHOD__ . ": no scripts found" );
1420
1421 return false;
1422 }
1423
1431 public static function checkXMLEncodingMissmatch( $file ) {
1432 $svgMetadataCutoff = MediaWikiServices::getInstance()->getMainConfig()
1433 ->get( MainConfigNames::SVGMetadataCutoff );
1434 $contents = file_get_contents( $file, false, null, 0, $svgMetadataCutoff );
1435 $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1436
1437 if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1438 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1439 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1440 ) {
1441 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1442
1443 return true;
1444 }
1445 } elseif ( preg_match( "!<\?xml\b!si", $contents ) ) {
1446 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1447 // bytes. There shouldn't be a legitimate reason for this to happen.
1448 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1449
1450 return true;
1451 } elseif ( substr( $contents, 0, 4 ) == "\x4C\x6F\xA7\x94" ) {
1452 // EBCDIC encoded XML
1453 wfDebug( __METHOD__ . ": EBCDIC Encoded XML" );
1454
1455 return true;
1456 }
1457
1458 // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to
1459 // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings
1460 $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1461 foreach ( $attemptEncodings as $encoding ) {
1462 AtEase::suppressWarnings();
1463 $str = iconv( $encoding, 'UTF-8', $contents );
1464 AtEase::restoreWarnings();
1465 if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1466 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1467 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1468 ) {
1469 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1470
1471 return true;
1472 }
1473 } elseif ( $str != '' && preg_match( "!<\?xml\b!si", $str ) ) {
1474 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1475 // bytes. There shouldn't be a legitimate reason for this to happen.
1476 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1477
1478 return true;
1479 }
1480 }
1481
1482 return false;
1483 }
1484
1490 protected function detectScriptInSvg( $filename, $partial ) {
1491 $this->mSVGNSError = false;
1492 $check = new XmlTypeCheck(
1493 $filename,
1494 [ $this, 'checkSvgScriptCallback' ],
1495 true,
1496 [
1497 'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ],
1498 'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ],
1499 ]
1500 );
1501 if ( $check->wellFormed !== true ) {
1502 // Invalid xml (T60553)
1503 // But only when non-partial (T67724)
1504 return $partial ? false : [ 'uploadinvalidxml' ];
1505 } elseif ( $check->filterMatch ) {
1506 if ( $this->mSVGNSError ) {
1507 return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1508 }
1509
1510 return $check->filterMatchType;
1511 }
1512
1513 return false;
1514 }
1515
1522 public static function checkSvgPICallback( $target, $data ) {
1523 // Don't allow external stylesheets (T59550)
1524 if ( preg_match( '/xml-stylesheet/i', $target ) ) {
1525 return [ 'upload-scripted-pi-callback' ];
1526 }
1527
1528 return false;
1529 }
1530
1542 public static function checkSvgExternalDTD( $type, $publicId, $systemId ) {
1543 // This doesn't include the XHTML+MathML+SVG doctype since we don't
1544 // allow XHTML anyways.
1545 $allowedDTDs = [
1546 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1547 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1548 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1549 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1550 // https://phabricator.wikimedia.org/T168856
1551 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1552 ];
1553 if ( $type !== 'PUBLIC'
1554 || !in_array( $systemId, $allowedDTDs )
1555 || strpos( $publicId, "-//W3C//" ) !== 0
1556 ) {
1557 return [ 'upload-scripted-dtd' ];
1558 }
1559 return false;
1560 }
1561
1569 public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1570 list( $namespace, $strippedElement ) = $this->splitXmlNamespace( $element );
1571
1572 // We specifically don't include:
1573 // http://www.w3.org/1999/xhtml (T62771)
1574 static $validNamespaces = [
1575 '',
1576 'adobe:ns:meta/',
1577 'http://creativecommons.org/ns#',
1578 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1579 'http://ns.adobe.com/adobeillustrator/10.0/',
1580 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1581 'http://ns.adobe.com/extensibility/1.0/',
1582 'http://ns.adobe.com/flows/1.0/',
1583 'http://ns.adobe.com/illustrator/1.0/',
1584 'http://ns.adobe.com/imagereplacement/1.0/',
1585 'http://ns.adobe.com/pdf/1.3/',
1586 'http://ns.adobe.com/photoshop/1.0/',
1587 'http://ns.adobe.com/saveforweb/1.0/',
1588 'http://ns.adobe.com/variables/1.0/',
1589 'http://ns.adobe.com/xap/1.0/',
1590 'http://ns.adobe.com/xap/1.0/g/',
1591 'http://ns.adobe.com/xap/1.0/g/img/',
1592 'http://ns.adobe.com/xap/1.0/mm/',
1593 'http://ns.adobe.com/xap/1.0/rights/',
1594 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1595 'http://ns.adobe.com/xap/1.0/stype/font#',
1596 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1597 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1598 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1599 'http://ns.adobe.com/xap/1.0/t/pg/',
1600 'http://purl.org/dc/elements/1.1/',
1601 'http://purl.org/dc/elements/1.1',
1602 'http://schemas.microsoft.com/visio/2003/svgextensions/',
1603 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1604 'http://taptrix.com/inkpad/svg_extensions',
1605 'http://web.resource.org/cc/',
1606 'http://www.freesoftware.fsf.org/bkchem/cdml',
1607 'http://www.inkscape.org/namespaces/inkscape',
1608 'http://www.opengis.net/gml',
1609 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1610 'http://www.w3.org/2000/svg',
1611 'http://www.w3.org/tr/rec-rdf-syntax/',
1612 'http://www.w3.org/2000/01/rdf-schema#',
1613 ];
1614
1615 // Inkscape mangles namespace definitions created by Adobe Illustrator.
1616 // This is nasty but harmless. (T144827)
1617 $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1618
1619 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1620 wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file." );
1622 $this->mSVGNSError = $namespace;
1623
1624 return true;
1625 }
1626
1627 /*
1628 * check for elements that can contain javascript
1629 */
1630 if ( $strippedElement == 'script' ) {
1631 wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file." );
1632
1633 return [ 'uploaded-script-svg', $strippedElement ];
1634 }
1635
1636 # e.g., <svg xmlns="http://www.w3.org/2000/svg">
1637 # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1638 if ( $strippedElement == 'handler' ) {
1639 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1640
1641 return [ 'uploaded-script-svg', $strippedElement ];
1642 }
1643
1644 # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1645 if ( $strippedElement == 'stylesheet' ) {
1646 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1647
1648 return [ 'uploaded-script-svg', $strippedElement ];
1649 }
1650
1651 # Block iframes, in case they pass the namespace check
1652 if ( $strippedElement == 'iframe' ) {
1653 wfDebug( __METHOD__ . ": iframe in uploaded file." );
1654
1655 return [ 'uploaded-script-svg', $strippedElement ];
1656 }
1657
1658 # Check <style> css
1659 if ( $strippedElement == 'style'
1660 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1661 ) {
1662 wfDebug( __METHOD__ . ": hostile css in style element." );
1663 return [ 'uploaded-hostile-svg' ];
1664 }
1665
1666 foreach ( $attribs as $attrib => $value ) {
1667 $stripped = $this->stripXmlNamespace( $attrib );
1668 $value = strtolower( $value );
1669
1670 if ( substr( $stripped, 0, 2 ) == 'on' ) {
1671 wfDebug( __METHOD__
1672 . ": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1673
1674 return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1675 }
1676
1677 # Do not allow relative links, or unsafe url schemas.
1678 # For <a> tags, only data:, http: and https: and same-document
1679 # fragment links are allowed. For all other tags, only data:
1680 # and fragment are allowed.
1681 if ( $stripped == 'href'
1682 && $value !== ''
1683 && strpos( $value, 'data:' ) !== 0
1684 && strpos( $value, '#' ) !== 0
1685 ) {
1686 if ( !( $strippedElement === 'a'
1687 && preg_match( '!^https?://!i', $value ) )
1688 ) {
1689 wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1690 . "'$attrib'='$value' in uploaded file." );
1691
1692 return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1693 }
1694 }
1695
1696 # only allow data: targets that should be safe. This prevents vectors like,
1697 # image/svg, text/xml, application/xml, and text/html, which can contain scripts
1698 if ( $stripped == 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1699 // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
1700 // phpcs:ignore Generic.Files.LineLength
1701 $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1702
1703 if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1704 wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
1705 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1706 return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1707 }
1708 }
1709
1710 # Change href with animate from (http://html5sec.org/#137).
1711 if ( $stripped === 'attributename'
1712 && $strippedElement === 'animate'
1713 && $this->stripXmlNamespace( $value ) == 'href'
1714 ) {
1715 wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1716 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1717
1718 return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1719 }
1720
1721 # use set/animate to add event-handler attribute to parent
1722 if ( ( $strippedElement == 'set' || $strippedElement == 'animate' )
1723 && $stripped == 'attributename'
1724 && substr( $value, 0, 2 ) == 'on'
1725 ) {
1726 wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1727 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1728
1729 return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1730 }
1731
1732 # use set to add href attribute to parent element
1733 if ( $strippedElement == 'set'
1734 && $stripped == 'attributename'
1735 && strpos( $value, 'href' ) !== false
1736 ) {
1737 wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file." );
1738
1739 return [ 'uploaded-setting-href-svg' ];
1740 }
1741
1742 # use set to add a remote / data / script target to an element
1743 if ( $strippedElement == 'set'
1744 && $stripped == 'to'
1745 && preg_match( '!(http|https|data|script):!sim', $value )
1746 ) {
1747 wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file." );
1748
1749 return [ 'uploaded-wrong-setting-svg', $value ];
1750 }
1751
1752 # use handler attribute with remote / data / script
1753 if ( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
1754 wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1755 . "'$attrib'='$value' in uploaded file." );
1756
1757 return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1758 }
1759
1760 # use CSS styles to bring in remote code
1761 if ( $stripped == 'style'
1762 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1763 ) {
1764 wfDebug( __METHOD__ . ": Found svg setting a style with "
1765 . "remote url '$attrib'='$value' in uploaded file." );
1766 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1767 }
1768
1769 # Several attributes can include css, css character escaping isn't allowed
1770 $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1771 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1772 if ( in_array( $stripped, $cssAttrs )
1773 && self::checkCssFragment( $value )
1774 ) {
1775 wfDebug( __METHOD__ . ": Found svg setting a style with "
1776 . "remote url '$attrib'='$value' in uploaded file." );
1777 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1778 }
1779
1780 # image filters can pull in url, which could be svg that executes scripts
1781 # Only allow url( "#foo" ). Do not allow url( http://example.com )
1782 if ( $strippedElement == 'image'
1783 && $stripped == 'filter'
1784 && preg_match( '!url\s*\‍(\s*["\']?[^#]!sim', $value )
1785 ) {
1786 wfDebug( __METHOD__ . ": Found image filter with url: "
1787 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1788
1789 return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1790 }
1791 }
1792
1793 return false; // No scripts detected
1794 }
1795
1802 private static function checkCssFragment( $value ) {
1803 # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1804 if ( stripos( $value, '@import' ) !== false ) {
1805 return true;
1806 }
1807
1808 # We allow @font-face to embed fonts with data: urls, so we snip the string
1809 # 'url' out so this case won't match when we check for urls below
1810 $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1811 $value = preg_replace( $pattern, '$1$2', $value );
1812
1813 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1814 # properties filter and accelerator don't seem to be useful for xss in SVG files.
1815 # Expression and -o-link don't seem to work either, but filtering them here in case.
1816 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1817 # but not local ones such as url("#..., url('#..., url(#....
1818 if ( preg_match( '!expression
1819 | -o-link\s*:
1820 | -o-link-source\s*:
1821 | -o-replace\s*:!imx', $value ) ) {
1822 return true;
1823 }
1824
1825 if ( preg_match_all(
1826 "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1827 $value,
1828 $matches
1829 ) !== 0
1830 ) {
1831 # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1832 foreach ( $matches[1] as $match ) {
1833 if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1834 return true;
1835 }
1836 }
1837 }
1838
1839 if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1840 return true;
1841 }
1842
1843 return false;
1844 }
1845
1851 private static function splitXmlNamespace( $element ) {
1852 // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1853 $parts = explode( ':', strtolower( $element ) );
1854 $name = array_pop( $parts );
1855 $ns = implode( ':', $parts );
1856
1857 return [ $ns, $name ];
1858 }
1859
1864 private function stripXmlNamespace( $name ) {
1865 // 'http://www.w3.org/2000/svg:script' -> 'script'
1866 $parts = explode( ':', strtolower( $name ) );
1867
1868 return array_pop( $parts );
1869 }
1870
1881 public static function detectVirus( $file ) {
1882 global $wgOut;
1883 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1884 $antivirus = $mainConfig->get( MainConfigNames::Antivirus );
1885 $antivirusSetup = $mainConfig->get( MainConfigNames::AntivirusSetup );
1886 $antivirusRequired = $mainConfig->get( MainConfigNames::AntivirusRequired );
1887 if ( !$antivirus ) {
1888 wfDebug( __METHOD__ . ": virus scanner disabled" );
1889
1890 return null;
1891 }
1892
1893 if ( !$antivirusSetup[$antivirus] ) {
1894 wfDebug( __METHOD__ . ": unknown virus scanner: {$antivirus}" );
1895 $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1896 [ 'virus-badscanner', $antivirus ] );
1897
1898 return wfMessage( 'virus-unknownscanner' )->text() . " {$antivirus}";
1899 }
1900
1901 # look up scanner configuration
1902 $command = $antivirusSetup[$antivirus]['command'];
1903 $exitCodeMap = $antivirusSetup[$antivirus]['codemap'];
1904 $msgPattern = $antivirusSetup[$antivirus]['messagepattern'] ?? null;
1905
1906 if ( strpos( $command, "%f" ) === false ) {
1907 # simple pattern: append file to scan
1908 $command .= " " . Shell::escape( $file );
1909 } else {
1910 # complex pattern: replace "%f" with file to scan
1911 $command = str_replace( "%f", Shell::escape( $file ), $command );
1912 }
1913
1914 wfDebug( __METHOD__ . ": running virus scan: $command " );
1915
1916 # execute virus scanner
1917 $exitCode = false;
1918
1919 # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1920 # that does not seem to be worth the pain.
1921 # Ask me (Duesentrieb) about it if it's ever needed.
1922 $output = wfShellExecWithStderr( $command, $exitCode );
1923
1924 # map exit code to AV_xxx constants.
1925 $mappedCode = $exitCode;
1926 if ( $exitCodeMap ) {
1927 if ( isset( $exitCodeMap[$exitCode] ) ) {
1928 $mappedCode = $exitCodeMap[$exitCode];
1929 } elseif ( isset( $exitCodeMap["*"] ) ) {
1930 $mappedCode = $exitCodeMap["*"];
1931 }
1932 }
1933
1934 /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
1935 * so we need the strict equalities === and thus can't use a switch here
1936 */
1937 if ( $mappedCode === AV_SCAN_FAILED ) {
1938 # scan failed (code was mapped to false by $exitCodeMap)
1939 wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1940
1941 $output = $antivirusRequired
1942 ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1943 : null;
1944 } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1945 # scan failed because filetype is unknown (probably immune)
1946 wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1947 $output = null;
1948 } elseif ( $mappedCode === AV_NO_VIRUS ) {
1949 # no virus found
1950 wfDebug( __METHOD__ . ": file passed virus scan." );
1951 $output = false;
1952 } else {
1953 $output = trim( $output );
1954
1955 if ( !$output ) {
1956 $output = true; # if there's no output, return true
1957 } elseif ( $msgPattern ) {
1958 $groups = [];
1959 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1960 $output = $groups[1];
1961 }
1962 }
1963
1964 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
1965 }
1966
1967 return $output;
1968 }
1969
1978 private function checkOverwrite( Authority $performer ) {
1979 // First check whether the local file can be overwritten
1980 $file = $this->getLocalFile();
1981 $file->load( File::READ_LATEST );
1982 if ( $file->exists() ) {
1983 if ( !self::userCanReUpload( $performer, $file ) ) {
1984 return [ 'fileexists-forbidden', $file->getName() ];
1985 } else {
1986 return true;
1987 }
1988 }
1989
1990 $services = MediaWikiServices::getInstance();
1991
1992 /* Check shared conflicts: if the local file does not exist, but
1993 * RepoGroup::findFile finds a file, it exists in a shared repository.
1994 */
1995 $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
1996 if ( $file && !$performer->isAllowed( 'reupload-shared' )
1997 ) {
1998 return [ 'fileexists-shared-forbidden', $file->getName() ];
1999 }
2000
2001 return true;
2002 }
2003
2011 public static function userCanReUpload( Authority $performer, File $img ) {
2012 if ( $performer->isAllowed( 'reupload' ) ) {
2013 return true; // non-conditional
2014 } elseif ( !$performer->isAllowed( 'reupload-own' ) ) {
2015 return false;
2016 }
2017
2018 if ( !( $img instanceof LocalFile ) ) {
2019 return false;
2020 }
2021
2022 return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2023 }
2024
2036 public static function getExistsWarning( $file ) {
2037 if ( $file->exists() ) {
2038 return [ 'warning' => 'exists', 'file' => $file ];
2039 }
2040
2041 if ( $file->getTitle()->getArticleID() ) {
2042 return [ 'warning' => 'page-exists', 'file' => $file ];
2043 }
2044
2045 if ( !strpos( $file->getName(), '.' ) ) {
2046 $partname = $file->getName();
2047 $extension = '';
2048 } else {
2049 $n = strrpos( $file->getName(), '.' );
2050 $extension = substr( $file->getName(), $n + 1 );
2051 $partname = substr( $file->getName(), 0, $n );
2052 }
2053 $normalizedExtension = File::normalizeExtension( $extension );
2054 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2055
2056 if ( $normalizedExtension != $extension ) {
2057 // We're not using the normalized form of the extension.
2058 // Normal form is lowercase, using most common of alternate
2059 // extensions (eg 'jpg' rather than 'JPEG').
2060
2061 // Check for another file using the normalized form...
2062 $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2063 $file_lc = $localRepo->newFile( $nt_lc );
2064
2065 if ( $file_lc->exists() ) {
2066 return [
2067 'warning' => 'exists-normalized',
2068 'file' => $file,
2069 'normalizedFile' => $file_lc
2070 ];
2071 }
2072 }
2073
2074 // Check for files with the same name but a different extension
2075 $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2076 if ( count( $similarFiles ) ) {
2077 return [
2078 'warning' => 'exists-normalized',
2079 'file' => $file,
2080 'normalizedFile' => $similarFiles[0],
2081 ];
2082 }
2083
2084 if ( self::isThumbName( $file->getName() ) ) {
2085 # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2086 $nt_thb = Title::newFromText(
2087 substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2088 NS_FILE
2089 );
2090 $file_thb = $localRepo->newFile( $nt_thb );
2091 if ( $file_thb->exists() ) {
2092 return [
2093 'warning' => 'thumb',
2094 'file' => $file,
2095 'thumbFile' => $file_thb
2096 ];
2097 } else {
2098 // File does not exist, but we just don't like the name
2099 return [
2100 'warning' => 'thumb-name',
2101 'file' => $file,
2102 'thumbFile' => $file_thb
2103 ];
2104 }
2105 }
2106
2107 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2108 if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
2109 return [
2110 'warning' => 'bad-prefix',
2111 'file' => $file,
2112 'prefix' => $prefix
2113 ];
2114 }
2115 }
2116
2117 return false;
2118 }
2119
2125 public static function isThumbName( $filename ) {
2126 $n = strrpos( $filename, '.' );
2127 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2128
2129 return (
2130 substr( $partname, 3, 3 ) == 'px-' ||
2131 substr( $partname, 2, 3 ) == 'px-'
2132 ) &&
2133 preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2134 }
2135
2141 public static function getFilenamePrefixBlacklist() {
2142 $list = [];
2143 $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2144 if ( !$message->isDisabled() ) {
2145 $lines = explode( "\n", $message->plain() );
2146 foreach ( $lines as $line ) {
2147 // Remove comment lines
2148 $comment = substr( trim( $line ), 0, 1 );
2149 if ( $comment == '#' || $comment == '' ) {
2150 continue;
2151 }
2152 // Remove additional comments after a prefix
2153 $comment = strpos( $line, '#' );
2154 if ( $comment > 0 ) {
2155 $line = substr( $line, 0, $comment - 1 );
2156 }
2157 $list[] = trim( $line );
2158 }
2159 }
2160
2161 return $list;
2162 }
2163
2175 public function getImageInfo( $result ) {
2176 $localFile = $this->getLocalFile();
2177 $stashFile = $this->getStashFile();
2178 // Calling a different API module depending on whether the file was stashed is less than optimal.
2179 // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
2180 if ( $stashFile ) {
2182 $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_fill_keys( $imParam, true ), $result );
2183 } else {
2185 $info = ApiQueryImageInfo::getInfo( $localFile, array_fill_keys( $imParam, true ), $result );
2186 }
2187
2188 return $info;
2189 }
2190
2195 public function convertVerifyErrorToStatus( $error ) {
2196 $code = $error['status'];
2197 unset( $code['status'] );
2198
2199 return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2200 }
2201
2209 public static function getMaxUploadSize( $forType = null ) {
2210 $maxUploadSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxUploadSize );
2211
2212 if ( is_array( $maxUploadSize ) ) {
2213 if ( $forType !== null && isset( $maxUploadSize[$forType] ) ) {
2214 return $maxUploadSize[$forType];
2215 } else {
2216 return $maxUploadSize['*'];
2217 }
2218 } else {
2219 return intval( $maxUploadSize );
2220 }
2221 }
2222
2230 public static function getMaxPhpUploadSize() {
2231 $phpMaxFileSize = wfShorthandToInteger(
2232 ini_get( 'upload_max_filesize' ),
2233 PHP_INT_MAX
2234 );
2235 $phpMaxPostSize = wfShorthandToInteger(
2236 ini_get( 'post_max_size' ),
2237 PHP_INT_MAX
2238 ) ?: PHP_INT_MAX;
2239 return min( $phpMaxFileSize, $phpMaxPostSize );
2240 }
2241
2253 public static function getSessionStatus( UserIdentity $user, $statusKey ) {
2254 $store = self::getUploadSessionStore();
2255 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2256
2257 return $store->get( $key );
2258 }
2259
2272 public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
2273 $store = self::getUploadSessionStore();
2274 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2275
2276 if ( $value === false ) {
2277 $store->delete( $key );
2278 } else {
2279 $store->set( $key, $value, $store::TTL_DAY );
2280 }
2281 }
2282
2289 private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
2290 return $store->makeKey(
2291 'uploadstatus',
2292 $user->isRegistered() ? $user->getId() : md5( $user->getName() ),
2293 $statusKey
2294 );
2295 }
2296
2300 private static function getUploadSessionStore() {
2301 return MediaWikiServices::getInstance()->getMainObjectStash();
2302 }
2303}
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:497
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode $wgLang
Definition Setup.php:497
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:285
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:67
getName()
Return the name of this file.
Definition File.php:333
wasDeleted()
Was this file ever deleted from the wiki?
Definition File.php:2089
Local file in the wiki's own database.
Definition LocalFile.php:60
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
static sizeParam( $size)
Definition Message.php:1244
This class is used to hold the location and do limited manipulation of files stored temporarily (this...
Represents a title within MediaWiki.
Definition Title.php:49
getDBkey()
Get the main part with underscores.
Definition Title.php:1057
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition Title.php:370
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:70
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)
$line
Definition mcc.php:119
$command
Definition mcc.php:125
$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