MediaWiki REL1_37
UploadBase.php
Go to the documentation of this file.
1<?php
24use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
30
47abstract class UploadBase {
48 use ProtectedHookAccessorTrait;
49
51 protected $mTempPath;
53 protected $tempFileObj;
57 protected $mDestName;
61 protected $mSourceType;
63 protected $mTitle = false;
65 protected $mTitleError = 0;
67 protected $mFilteredName;
71 protected $mLocalFile;
73 protected $mStashFile;
75 protected $mFileSize;
77 protected $mFileProps;
81 protected $mJavaDetected;
83 protected $mSVGNSError;
84
85 protected static $safeXmlEncodings = [
86 'UTF-8',
87 'ISO-8859-1',
88 'ISO-8859-2',
89 'UTF-16',
90 'UTF-32',
91 'WINDOWS-1250',
92 'WINDOWS-1251',
93 'WINDOWS-1252',
94 'WINDOWS-1253',
95 'WINDOWS-1254',
96 'WINDOWS-1255',
97 'WINDOWS-1256',
98 'WINDOWS-1257',
99 'WINDOWS-1258',
100 ];
101
102 public const SUCCESS = 0;
103 public const OK = 0;
104 public const EMPTY_FILE = 3;
105 public const MIN_LENGTH_PARTNAME = 4;
106 public const ILLEGAL_FILENAME = 5;
107 public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
108 public const FILETYPE_MISSING = 8;
109 public const FILETYPE_BADTYPE = 9;
110 public const VERIFICATION_ERROR = 10;
111 public const HOOK_ABORTED = 11;
112 public const FILE_TOO_LARGE = 12;
113 public const WINDOWS_NONASCII_FILENAME = 13;
114 public const FILENAME_TOO_LONG = 14;
115
120 public function getVerificationErrorCode( $error ) {
121 $code_to_status = [
122 self::EMPTY_FILE => 'empty-file',
123 self::FILE_TOO_LARGE => 'file-too-large',
124 self::FILETYPE_MISSING => 'filetype-missing',
125 self::FILETYPE_BADTYPE => 'filetype-banned',
126 self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
127 self::ILLEGAL_FILENAME => 'illegal-filename',
128 self::OVERWRITE_EXISTING_FILE => 'overwrite',
129 self::VERIFICATION_ERROR => 'verification-error',
130 self::HOOK_ABORTED => 'hookaborted',
131 self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
132 self::FILENAME_TOO_LONG => 'filename-toolong',
133 ];
134 return $code_to_status[$error] ?? 'unknown-error';
135 }
136
143 public static function isEnabled() {
144 global $wgEnableUploads;
145
146 return $wgEnableUploads && wfIniGetBool( 'file_uploads' );
147 }
148
157 public static function isAllowed( Authority $performer ) {
158 foreach ( [ 'upload', 'edit' ] as $permission ) {
159 if ( !$performer->isAllowed( $permission ) ) {
160 return $permission;
161 }
162 }
163
164 return true;
165 }
166
173 public static function isThrottled( $user ) {
174 return $user->pingLimiter( 'upload' );
175 }
176
178 private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
179
187 public static function createFromRequest( &$request, $type = null ) {
188 $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
189
190 if ( !$type ) {
191 return null;
192 }
193
194 // Get the upload class
195 $type = ucfirst( $type );
196
197 // Give hooks the chance to handle this request
199 $className = null;
200 Hooks::runner()->onUploadCreateFromRequest( $type, $className );
201 if ( $className === null ) {
202 $className = 'UploadFrom' . $type;
203 wfDebug( __METHOD__ . ": class name: $className" );
204 if ( !in_array( $type, self::$uploadHandlers ) ) {
205 return null;
206 }
207 }
208
209 // Check whether this upload class is enabled
210 if ( !$className::isEnabled() ) {
211 return null;
212 }
213
214 // Check whether the request is valid
215 if ( !$className::isValidRequest( $request ) ) {
216 return null;
217 }
218
220 $handler = new $className;
221
222 $handler->initializeFromRequest( $request );
223
224 return $handler;
225 }
226
232 public static function isValidRequest( $request ) {
233 return false;
234 }
235
239 public function __construct() {
240 }
241
249 public function getSourceType() {
250 return null;
251 }
252
260 public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
261 $this->mDesiredDestName = $name;
262 if ( FileBackend::isStoragePath( $tempPath ) ) {
263 throw new MWException( __METHOD__ . " given storage path `$tempPath`." );
264 }
265
266 $this->setTempFile( $tempPath, $fileSize );
267 $this->mRemoveTempFile = $removeTempFile;
268 }
269
275 abstract public function initializeFromRequest( &$request );
276
281 protected function setTempFile( $tempPath, $fileSize = null ) {
282 $this->mTempPath = $tempPath ?? '';
283 $this->mFileSize = $fileSize ?: null;
284 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
285 $this->tempFileObj = new TempFSFile( $this->mTempPath );
286 if ( !$fileSize ) {
287 $this->mFileSize = filesize( $this->mTempPath );
288 }
289 } else {
290 $this->tempFileObj = null;
291 }
292 }
293
299 public function fetchFile() {
300 return Status::newGood();
301 }
302
307 public function isEmptyFile() {
308 return empty( $this->mFileSize );
309 }
310
315 public function getFileSize() {
316 return $this->mFileSize;
317 }
318
324 public function getTempFileSha1Base36() {
325 return FSFile::getSha1Base36FromPath( $this->mTempPath );
326 }
327
332 public function getRealPath( $srcPath ) {
333 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
334 if ( FileRepo::isVirtualUrl( $srcPath ) ) {
338 $tmpFile = $repo->getLocalCopy( $srcPath );
339 if ( $tmpFile ) {
340 $tmpFile->bind( $this ); // keep alive with $this
341 }
342 $path = $tmpFile ? $tmpFile->getPath() : false;
343 } else {
344 $path = $srcPath;
345 }
346
347 return $path;
348 }
349
367 public function verifyUpload() {
371 if ( $this->isEmptyFile() ) {
372 return [ 'status' => self::EMPTY_FILE ];
373 }
374
378 $maxSize = self::getMaxUploadSize( $this->getSourceType() );
379 if ( $this->mFileSize > $maxSize ) {
380 return [
381 'status' => self::FILE_TOO_LARGE,
382 'max' => $maxSize,
383 ];
384 }
385
391 $verification = $this->verifyFile();
392 if ( $verification !== true ) {
393 return [
394 'status' => self::VERIFICATION_ERROR,
395 'details' => $verification
396 ];
397 }
398
402 $result = $this->validateName();
403 if ( $result !== true ) {
404 return $result;
405 }
406
407 return [ 'status' => self::OK ];
408 }
409
416 public function validateName() {
417 $nt = $this->getTitle();
418 if ( $nt === null ) {
419 $result = [ 'status' => $this->mTitleError ];
420 if ( $this->mTitleError == self::ILLEGAL_FILENAME ) {
421 $result['filtered'] = $this->mFilteredName;
422 }
423 if ( $this->mTitleError == self::FILETYPE_BADTYPE ) {
424 $result['finalExt'] = $this->mFinalExtension;
425 if ( count( $this->mBlackListedExtensions ) ) {
426 $result['blacklistedExt'] = $this->mBlackListedExtensions;
427 }
428 }
429
430 return $result;
431 }
432 $this->mDestName = $this->getLocalFile()->getName();
433
434 return true;
435 }
436
446 protected function verifyMimeType( $mime ) {
448 if ( $wgVerifyMimeType ) {
449 wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" );
451 if ( self::checkFileExtension( $mime, $wgMimeTypeExclusions ) ) {
452 return [ 'filetype-badmime', $mime ];
453 }
454
455 if ( $wgVerifyMimeTypeIE ) {
456 # Check what Internet Explorer would detect
457 $fp = fopen( $this->mTempPath, 'rb' );
458 if ( $fp ) {
459 $chunk = fread( $fp, 256 );
460 fclose( $fp );
461
462 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
463 $extMime = $magic->getMimeTypeFromExtensionOrNull( (string)$this->mFinalExtension ) ?? '';
464 $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
465 foreach ( $ieTypes as $ieType ) {
466 if ( self::checkFileExtension( $ieType, $wgMimeTypeExclusions ) ) {
467 return [ 'filetype-bad-ie-mime', $ieType ];
468 }
469 }
470 }
471 }
472 }
473
474 return true;
475 }
476
482 protected function verifyFile() {
484
485 $status = $this->verifyPartialFile();
486 if ( $status !== true ) {
487 return $status;
488 }
489
490 $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
491 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
492 $mime = $this->mFileProps['mime'];
493
494 if ( $wgVerifyMimeType ) {
495 # XXX: Missing extension will be caught by validateName() via getTitle()
496 if ( (string)$this->mFinalExtension !== '' &&
497 !$this->verifyExtension( $mime, $this->mFinalExtension )
498 ) {
499 return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
500 }
501 }
502
503 # check for htmlish code and javascript
505 if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
506 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
507 if ( $svgStatus !== false ) {
508 return $svgStatus;
509 }
510 }
511 }
512
513 $handler = MediaHandler::getHandler( $mime );
514 if ( $handler ) {
515 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
516 if ( !$handlerStatus->isOK() ) {
517 $errors = $handlerStatus->getErrorsArray();
518
519 return reset( $errors );
520 }
521 }
522
523 $error = true;
524 $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
525 if ( $error !== true ) {
526 if ( !is_array( $error ) ) {
527 $error = [ $error ];
528 }
529 return $error;
530 }
531
532 wfDebug( __METHOD__ . ": all clear; passing." );
533
534 return true;
535 }
536
545 protected function verifyPartialFile() {
547
548 # getTitle() sets some internal parameters like $this->mFinalExtension
549 $this->getTitle();
550
551 $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
552 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
553
554 # check MIME type, if desired
555 $mime = $this->mFileProps['file-mime'];
556 $status = $this->verifyMimeType( $mime );
557 if ( $status !== true ) {
558 return $status;
559 }
560
561 # check for htmlish code and javascript
563 if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
564 return [ 'uploadscripted' ];
565 }
566 if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
567 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
568 if ( $svgStatus !== false ) {
569 return $svgStatus;
570 }
571 }
572 }
573
574 # Check for Java applets, which if uploaded can bypass cross-site
575 # restrictions.
576 if ( !$wgAllowJavaUploads ) {
577 $this->mJavaDetected = false;
578 $zipStatus = ZipDirectoryReader::read( $this->mTempPath,
579 [ $this, 'zipEntryCallback' ] );
580 if ( !$zipStatus->isOK() ) {
581 $errors = $zipStatus->getErrorsArray();
582 $error = reset( $errors );
583 if ( $error[0] !== 'zip-wrong-format' ) {
584 return $error;
585 }
586 }
587 if ( $this->mJavaDetected ) {
588 return [ 'uploadjava' ];
589 }
590 }
591
592 # Scan the uploaded file for viruses
593 $virus = $this->detectVirus( $this->mTempPath );
594 if ( $virus ) {
595 return [ 'uploadvirus', $virus ];
596 }
597
598 return true;
599 }
600
606 public function zipEntryCallback( $entry ) {
607 $names = [ $entry['name'] ];
608
609 // If there is a null character, cut off the name at it, because JDK's
610 // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
611 // were constructed which had ".class\0" followed by a string chosen to
612 // make the hash collide with the truncated name, that file could be
613 // returned in response to a request for the .class file.
614 $nullPos = strpos( $entry['name'], "\000" );
615 if ( $nullPos !== false ) {
616 $names[] = substr( $entry['name'], 0, $nullPos );
617 }
618
619 // If there is a trailing slash in the file name, we have to strip it,
620 // because that's what ZIP_GetEntry() does.
621 if ( preg_grep( '!\.class/?$!', $names ) ) {
622 $this->mJavaDetected = true;
623 }
624 }
625
635 public function verifyPermissions( Authority $performer ) {
636 return $this->verifyTitlePermissions( $performer );
637 }
638
650 public function verifyTitlePermissions( Authority $performer ) {
655 $nt = $this->getTitle();
656 if ( $nt === null ) {
657 return true;
658 }
659
660 $status = PermissionStatus::newEmpty();
661 $performer->authorizeWrite( 'edit', $nt, $status );
662 $performer->authorizeWrite( 'upload', $nt, $status );
663 if ( !$status->isGood() ) {
664 return $status->toLegacyErrorArray();
665 }
666
667 $overwriteError = $this->checkOverwrite( $performer );
668 if ( $overwriteError !== true ) {
669 return [ $overwriteError ];
670 }
671
672 return true;
673 }
674
684 public function checkWarnings( $user = null ) {
685 if ( $user === null ) {
686 // TODO check uses and hard deprecate
687 $user = RequestContext::getMain()->getUser();
688 }
689
690 $warnings = [];
691
692 $localFile = $this->getLocalFile();
693 $localFile->load( File::READ_LATEST );
694 $filename = $localFile->getName();
695 $hash = $this->getTempFileSha1Base36();
696
697 $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
698 if ( $badFileName !== null ) {
699 $warnings['badfilename'] = $badFileName;
700 }
701
702 $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
703 if ( $unwantedFileExtensionDetails !== null ) {
704 $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
705 }
706
707 $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
708 if ( $fileSizeWarnings ) {
709 $warnings = array_merge( $warnings, $fileSizeWarnings );
710 }
711
712 $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
713 if ( $localFileExistsWarnings ) {
714 $warnings = array_merge( $warnings, $localFileExistsWarnings );
715 }
716
717 if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
718 $warnings['was-deleted'] = $filename;
719 }
720
721 // If a file with the same name exists locally then the local file has already been tested
722 // for duplication of content
723 $ignoreLocalDupes = isset( $warnings['exists'] );
724 $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
725 if ( $dupes ) {
726 $warnings['duplicate'] = $dupes;
727 }
728
729 $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
730 if ( $archivedDupes !== null ) {
731 $warnings['duplicate-archive'] = $archivedDupes;
732 }
733
734 return $warnings;
735 }
736
748 public static function makeWarningsSerializable( $warnings ) {
749 array_walk_recursive( $warnings, static function ( &$param, $key ) {
750 if ( $param instanceof File ) {
751 $param = [
752 'fileName' => $param->getName(),
753 'timestamp' => $param->getTimestamp()
754 ];
755 } elseif ( is_object( $param ) ) {
756 throw new InvalidArgumentException(
757 'UploadBase::makeWarningsSerializable: ' .
758 'Unexpected object of class ' . get_class( $param ) );
759 }
760 } );
761 return $warnings;
762 }
763
773 private function checkBadFileName( $filename, $desiredFileName ) {
774 $comparableName = str_replace( ' ', '_', $desiredFileName );
775 $comparableName = Title::capitalize( $comparableName, NS_FILE );
776
777 if ( $desiredFileName != $filename && $comparableName != $filename ) {
778 return $filename;
779 }
780
781 return null;
782 }
783
792 private function checkUnwantedFileExtensions( $fileExtension ) {
794
796 $extensions = array_unique( $wgFileExtensions );
797 if ( !$this->checkFileExtension( $fileExtension, $extensions ) ) {
798 return [
799 $fileExtension,
800 $wgLang->commaList( $extensions ),
801 count( $extensions )
802 ];
803 }
804 }
805
806 return null;
807 }
808
814 private function checkFileSize( $fileSize ) {
816
817 $warnings = [];
818
819 if ( $wgUploadSizeWarning && ( $fileSize > $wgUploadSizeWarning ) ) {
820 $warnings['large-file'] = [
822 Message::sizeParam( $fileSize ),
823 ];
824 }
825
826 if ( $fileSize == 0 ) {
827 $warnings['empty-file'] = true;
828 }
829
830 return $warnings;
831 }
832
839 private function checkLocalFileExists( LocalFile $localFile, $hash ) {
840 $warnings = [];
841
842 $exists = self::getExistsWarning( $localFile );
843 if ( $exists !== false ) {
844 $warnings['exists'] = $exists;
845
846 // check if file is an exact duplicate of current file version
847 if ( $hash !== false && $hash === $localFile->getSha1() ) {
848 $warnings['no-change'] = $localFile;
849 }
850
851 // check if file is an exact duplicate of older versions of this file
852 $history = $localFile->getHistory();
853 foreach ( $history as $oldFile ) {
854 if ( $hash === $oldFile->getSha1() ) {
855 $warnings['duplicate-version'][] = $oldFile;
856 }
857 }
858 }
859
860 return $warnings;
861 }
862
863 private function checkLocalFileWasDeleted( LocalFile $localFile ) {
864 return $localFile->wasDeleted() && !$localFile->exists();
865 }
866
873 private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
874 if ( $hash === false ) {
875 return [];
876 }
877 $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
878 $title = $this->getTitle();
879 foreach ( $dupes as $key => $dupe ) {
880 if (
881 ( $dupe instanceof LocalFile ) &&
882 $ignoreLocalDupes &&
883 $title->equals( $dupe->getTitle() )
884 ) {
885 unset( $dupes[$key] );
886 }
887 }
888
889 return $dupes;
890 }
891
899 private function checkAgainstArchiveDupes( $hash, Authority $performer ) {
900 if ( $hash === false ) {
901 return null;
902 }
903 $archivedFile = new ArchivedFile( null, 0, '', $hash );
904 if ( $archivedFile->getID() > 0 ) {
905 if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
906 return $archivedFile->getName();
907 } else {
908 return '';
909 }
910 }
911
912 return null;
913 }
914
932 public function performUpload(
933 $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
934 ) {
935 $this->getLocalFile()->load( File::READ_LATEST );
936 $props = $this->mFileProps;
937
938 $error = null;
939 $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
940 if ( $error ) {
941 if ( !is_array( $error ) ) {
942 $error = [ $error ];
943 }
944 return Status::newFatal( ...$error );
945 }
946
947 $status = $this->getLocalFile()->upload(
948 $this->mTempPath,
949 $comment,
950 $pageText,
951 File::DELETE_SOURCE,
952 $props,
953 false,
954 $user,
955 $tags
956 );
957
958 if ( $status->isGood() ) {
959 if ( $watch ) {
960 MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
961 $user,
962 $this->getLocalFile()->getTitle(),
963 $watchlistExpiry
964 );
965 }
966 $this->getHookRunner()->onUploadComplete( $this );
967
968 $this->postProcessUpload();
969 }
970
971 return $status;
972 }
973
980 public function postProcessUpload() {
981 }
982
989 public function getTitle() {
990 if ( $this->mTitle !== false ) {
991 return $this->mTitle;
992 }
993 if ( !is_string( $this->mDesiredDestName ) ) {
994 $this->mTitleError = self::ILLEGAL_FILENAME;
995 $this->mTitle = null;
996
997 return $this->mTitle;
998 }
999 /* Assume that if a user specified File:Something.jpg, this is an error
1000 * and that the namespace prefix needs to be stripped of.
1001 */
1002 $title = Title::newFromText( $this->mDesiredDestName );
1003 if ( $title && $title->getNamespace() === NS_FILE ) {
1004 $this->mFilteredName = $title->getDBkey();
1005 } else {
1006 $this->mFilteredName = $this->mDesiredDestName;
1007 }
1008
1009 # oi_archive_name is max 255 bytes, which include a timestamp and an
1010 # exclamation mark, so restrict file name to 240 bytes.
1011 if ( strlen( $this->mFilteredName ) > 240 ) {
1012 $this->mTitleError = self::FILENAME_TOO_LONG;
1013 $this->mTitle = null;
1014
1015 return $this->mTitle;
1016 }
1017
1023 $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
1024 /* Normalize to title form before we do any further processing */
1025 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1026 if ( $nt === null ) {
1027 $this->mTitleError = self::ILLEGAL_FILENAME;
1028 $this->mTitle = null;
1029
1030 return $this->mTitle;
1031 }
1032 $this->mFilteredName = $nt->getDBkey();
1033
1038 list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
1039
1040 if ( $ext !== [] ) {
1041 $this->mFinalExtension = trim( end( $ext ) );
1042 } else {
1043 $this->mFinalExtension = '';
1044
1045 // No extension, try guessing one from the temporary file
1046 // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1047 // or incomplete test case in UploadBaseTest (T272328)
1048 if ( $this->mTempPath !== null ) {
1049 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
1050 $mime = $magic->guessMimeType( $this->mTempPath );
1051 if ( $mime !== 'unknown/unknown' ) {
1052 # Get a space separated list of extensions
1053 $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1054 if ( $mimeExt !== null ) {
1055 # Set the extension to the canonical extension
1056 $this->mFinalExtension = $mimeExt;
1057
1058 # Fix up the other variables
1059 $this->mFilteredName .= ".{$this->mFinalExtension}";
1060 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1061 $ext = [ $this->mFinalExtension ];
1062 }
1063 }
1064 }
1065 }
1066
1067 // Don't allow users to override the list of prohibited file extensions (check file extension)
1070
1071 $blackListedExtensions = self::checkFileExtensionList( $ext, $wgProhibitedFileExtensions );
1072
1073 if ( $this->mFinalExtension == '' ) {
1074 $this->mTitleError = self::FILETYPE_MISSING;
1075 $this->mTitle = null;
1076
1077 return $this->mTitle;
1078 } elseif ( $blackListedExtensions ||
1080 !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) )
1081 ) {
1082 $this->mBlackListedExtensions = $blackListedExtensions;
1083 $this->mTitleError = self::FILETYPE_BADTYPE;
1084 $this->mTitle = null;
1085
1086 return $this->mTitle;
1087 }
1088
1089 // Windows may be broken with special characters, see T3780
1090 if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1091 && !MediaWikiServices::getInstance()->getRepoGroup()
1092 ->getLocalRepo()->backendSupportsUnicodePaths()
1093 ) {
1094 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1095 $this->mTitle = null;
1096
1097 return $this->mTitle;
1098 }
1099
1100 # If there was more than one "extension", reassemble the base
1101 # filename to prevent bogus complaints about length
1102 if ( count( $ext ) > 1 ) {
1103 $iterations = count( $ext ) - 1;
1104 for ( $i = 0; $i < $iterations; $i++ ) {
1105 $partname .= '.' . $ext[$i];
1106 }
1107 }
1108
1109 if ( strlen( $partname ) < 1 ) {
1110 $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1111 $this->mTitle = null;
1112
1113 return $this->mTitle;
1114 }
1115
1116 $this->mTitle = $nt;
1117
1118 return $this->mTitle;
1119 }
1120
1127 public function getLocalFile() {
1128 if ( $this->mLocalFile === null ) {
1129 $nt = $this->getTitle();
1130 $this->mLocalFile = $nt === null
1131 ? null
1132 : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1133 }
1134
1135 return $this->mLocalFile;
1136 }
1137
1141 public function getStashFile() {
1142 return $this->mStashFile;
1143 }
1144
1157 public function tryStashFile( User $user, $isPartial = false ) {
1158 if ( !$isPartial ) {
1159 $error = $this->runUploadStashFileHook( $user );
1160 if ( $error ) {
1161 return Status::newFatal( ...$error );
1162 }
1163 }
1164 try {
1165 $file = $this->doStashFile( $user );
1166 return Status::newGood( $file );
1167 } catch ( UploadStashException $e ) {
1168 return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1169 }
1170 }
1171
1176 protected function runUploadStashFileHook( User $user ) {
1177 $props = $this->mFileProps;
1178 $error = null;
1179 $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1180 if ( $error && !is_array( $error ) ) {
1181 $error = [ $error ];
1182 }
1183 return $error;
1184 }
1185
1193 protected function doStashFile( User $user = null ) {
1194 $stash = MediaWikiServices::getInstance()->getRepoGroup()
1195 ->getLocalRepo()->getUploadStash( $user );
1196 $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
1197 $this->mStashFile = $file;
1198
1199 return $file;
1200 }
1201
1206 public function cleanupTempFile() {
1207 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1208 // Delete when all relevant TempFSFile handles go out of scope
1209 wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1210 $this->tempFileObj->autocollect();
1211 }
1212 }
1213
1214 public function getTempPath() {
1215 return $this->mTempPath;
1216 }
1217
1227 public static function splitExtensions( $filename ) {
1228 $bits = explode( '.', $filename );
1229 $basename = array_shift( $bits );
1230
1231 return [ $basename, $bits ];
1232 }
1233
1242 public static function checkFileExtension( $ext, $list ) {
1243 return in_array( strtolower( $ext ), $list );
1244 }
1245
1254 public static function checkFileExtensionList( $ext, $list ) {
1255 return array_intersect( array_map( 'strtolower', $ext ), $list );
1256 }
1257
1265 public static function verifyExtension( $mime, $extension ) {
1266 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
1267
1268 if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) {
1269 if ( !$magic->isRecognizableExtension( $extension ) ) {
1270 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1271 "unrecognized extension '$extension', can't verify" );
1272
1273 return true;
1274 } else {
1275 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1276 "recognized extension '$extension', so probably invalid file" );
1277
1278 return false;
1279 }
1280 }
1281
1282 $match = $magic->isMatchingExtension( $extension, $mime );
1283
1284 if ( $match === null ) {
1285 if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1286 wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1287
1288 return false;
1289 } else {
1290 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1291
1292 return true;
1293 }
1294 } elseif ( $match ) {
1295 wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1296
1298 return true;
1299 } else {
1300 wfDebug( __METHOD__
1301 . ": mime type $mime mismatches file extension $extension, rejecting file" );
1302
1303 return false;
1304 }
1305 }
1306
1318 public static function detectScript( $file, $mime, $extension ) {
1319 # ugly hack: for text files, always look at the entire file.
1320 # For binary field, just check the first K.
1321
1322 $isText = strpos( $mime, 'text/' ) === 0;
1323 if ( $isText ) {
1324 $chunk = file_get_contents( $file );
1325 } else {
1326 $fp = fopen( $file, 'rb' );
1327 if ( !$fp ) {
1328 return false;
1329 }
1330 $chunk = fread( $fp, 1024 );
1331 fclose( $fp );
1332 }
1333
1334 $chunk = strtolower( $chunk );
1335
1336 if ( !$chunk ) {
1337 return false;
1338 }
1339
1340 # decode from UTF-16 if needed (could be used for obfuscation).
1341 if ( substr( $chunk, 0, 2 ) == "\xfe\xff" ) {
1342 $enc = 'UTF-16BE';
1343 } elseif ( substr( $chunk, 0, 2 ) == "\xff\xfe" ) {
1344 $enc = 'UTF-16LE';
1345 } else {
1346 $enc = null;
1347 }
1348
1349 if ( $enc !== null ) {
1350 $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1351 }
1352
1353 $chunk = trim( $chunk );
1354
1356 wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1357
1358 # check for HTML doctype
1359 if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1360 return true;
1361 }
1362
1363 // Some browsers will interpret obscure xml encodings as UTF-8, while
1364 // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1365 if ( $extension == 'svg' || strpos( $mime, 'image/svg' ) === 0 ) {
1366 if ( self::checkXMLEncodingMissmatch( $file ) ) {
1367 return true;
1368 }
1369 }
1370
1371 // Quick check for HTML heuristics in old IE and Safari.
1372 //
1373 // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1374 // don't need them all here as it can cause many false positives.
1375 //
1376 // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1377 $tags = [
1378 '<body',
1379 '<head',
1380 '<html', # also in safari
1381 '<script', # also in safari
1382 ];
1383
1384 foreach ( $tags as $tag ) {
1385 if ( strpos( $chunk, $tag ) !== false ) {
1386 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1387
1388 return true;
1389 }
1390 }
1391
1392 /*
1393 * look for JavaScript
1394 */
1395
1396 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1397 $chunk = Sanitizer::decodeCharReferences( $chunk );
1398
1399 # look for script-types
1400 if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
1401 wfDebug( __METHOD__ . ": found script types" );
1402
1403 return true;
1404 }
1405
1406 # look for html-style script-urls
1407 if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1408 wfDebug( __METHOD__ . ": found html-style script urls" );
1409
1410 return true;
1411 }
1412
1413 # look for css-style script-urls
1414 if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1415 wfDebug( __METHOD__ . ": found css-style script urls" );
1416
1417 return true;
1418 }
1419
1420 wfDebug( __METHOD__ . ": no scripts found" );
1421
1422 return false;
1423 }
1424
1432 public static function checkXMLEncodingMissmatch( $file ) {
1433 global $wgSVGMetadataCutoff;
1434 $contents = file_get_contents( $file, false, null, 0, $wgSVGMetadataCutoff );
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 Wikimedia\suppressWarnings();
1463 $str = iconv( $encoding, 'UTF-8', $contents );
1464 Wikimedia\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 ) {
1883
1884 if ( !$wgAntivirus ) {
1885 wfDebug( __METHOD__ . ": virus scanner disabled" );
1886
1887 return null;
1888 }
1889
1891 wfDebug( __METHOD__ . ": unknown virus scanner: $wgAntivirus" );
1892 $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1893 [ 'virus-badscanner', $wgAntivirus ] );
1894
1895 return wfMessage( 'virus-unknownscanner' )->text() . " $wgAntivirus";
1896 }
1897
1898 # look up scanner configuration
1900 $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]['codemap'];
1901 $msgPattern = $wgAntivirusSetup[$wgAntivirus]['messagepattern'] ?? null;
1902
1903 if ( strpos( $command, "%f" ) === false ) {
1904 # simple pattern: append file to scan
1905 $command .= " " . Shell::escape( $file );
1906 } else {
1907 # complex pattern: replace "%f" with file to scan
1908 $command = str_replace( "%f", Shell::escape( $file ), $command );
1909 }
1910
1911 wfDebug( __METHOD__ . ": running virus scan: $command " );
1912
1913 # execute virus scanner
1914 $exitCode = false;
1915
1916 # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1917 # that does not seem to be worth the pain.
1918 # Ask me (Duesentrieb) about it if it's ever needed.
1919 $output = wfShellExecWithStderr( $command, $exitCode );
1920
1921 # map exit code to AV_xxx constants.
1922 $mappedCode = $exitCode;
1923 if ( $exitCodeMap ) {
1924 if ( isset( $exitCodeMap[$exitCode] ) ) {
1925 $mappedCode = $exitCodeMap[$exitCode];
1926 } elseif ( isset( $exitCodeMap["*"] ) ) {
1927 $mappedCode = $exitCodeMap["*"];
1928 }
1929 }
1930
1931 /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
1932 * so we need the strict equalities === and thus can't use a switch here
1933 */
1934 if ( $mappedCode === AV_SCAN_FAILED ) {
1935 # scan failed (code was mapped to false by $exitCodeMap)
1936 wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1937
1938 $output = $wgAntivirusRequired
1939 ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1940 : null;
1941 } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1942 # scan failed because filetype is unknown (probably imune)
1943 wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1944 $output = null;
1945 } elseif ( $mappedCode === AV_NO_VIRUS ) {
1946 # no virus found
1947 wfDebug( __METHOD__ . ": file passed virus scan." );
1948 $output = false;
1949 } else {
1950 $output = trim( $output );
1951
1952 if ( !$output ) {
1953 $output = true; # if there's no output, return true
1954 } elseif ( $msgPattern ) {
1955 $groups = [];
1956 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1957 $output = $groups[1];
1958 }
1959 }
1960
1961 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
1962 }
1963
1964 return $output;
1965 }
1966
1975 private function checkOverwrite( Authority $performer ) {
1976 // First check whether the local file can be overwritten
1977 $file = $this->getLocalFile();
1978 $file->load( File::READ_LATEST );
1979 if ( $file->exists() ) {
1980 if ( !self::userCanReUpload( $performer, $file ) ) {
1981 return [ 'fileexists-forbidden', $file->getName() ];
1982 } else {
1983 return true;
1984 }
1985 }
1986
1987 $services = MediaWikiServices::getInstance();
1988
1989 /* Check shared conflicts: if the local file does not exist, but
1990 * RepoGroup::findFile finds a file, it exists in a shared repository.
1991 */
1992 $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
1993 if ( $file && !$performer->isAllowed( 'reupload-shared' )
1994 ) {
1995 return [ 'fileexists-shared-forbidden', $file->getName() ];
1996 }
1997
1998 return true;
1999 }
2000
2008 public static function userCanReUpload( Authority $performer, File $img ) {
2009 if ( $performer->isAllowed( 'reupload' ) ) {
2010 return true; // non-conditional
2011 } elseif ( !$performer->isAllowed( 'reupload-own' ) ) {
2012 return false;
2013 }
2014
2015 if ( !( $img instanceof LocalFile ) ) {
2016 return false;
2017 }
2018
2019 return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2020 }
2021
2033 public static function getExistsWarning( $file ) {
2034 if ( $file->exists() ) {
2035 return [ 'warning' => 'exists', 'file' => $file ];
2036 }
2037
2038 if ( $file->getTitle()->getArticleID() ) {
2039 return [ 'warning' => 'page-exists', 'file' => $file ];
2040 }
2041
2042 if ( !strpos( $file->getName(), '.' ) ) {
2043 $partname = $file->getName();
2044 $extension = '';
2045 } else {
2046 $n = strrpos( $file->getName(), '.' );
2047 $extension = substr( $file->getName(), $n + 1 );
2048 $partname = substr( $file->getName(), 0, $n );
2049 }
2050 $normalizedExtension = File::normalizeExtension( $extension );
2051 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2052
2053 if ( $normalizedExtension != $extension ) {
2054 // We're not using the normalized form of the extension.
2055 // Normal form is lowercase, using most common of alternate
2056 // extensions (eg 'jpg' rather than 'JPEG').
2057
2058 // Check for another file using the normalized form...
2059 $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2060 $file_lc = $localRepo->newFile( $nt_lc );
2061
2062 if ( $file_lc->exists() ) {
2063 return [
2064 'warning' => 'exists-normalized',
2065 'file' => $file,
2066 'normalizedFile' => $file_lc
2067 ];
2068 }
2069 }
2070
2071 // Check for files with the same name but a different extension
2072 $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2073 if ( count( $similarFiles ) ) {
2074 return [
2075 'warning' => 'exists-normalized',
2076 'file' => $file,
2077 'normalizedFile' => $similarFiles[0],
2078 ];
2079 }
2080
2081 if ( self::isThumbName( $file->getName() ) ) {
2082 # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2083 $nt_thb = Title::newFromText(
2084 substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2085 NS_FILE
2086 );
2087 $file_thb = $localRepo->newFile( $nt_thb );
2088 if ( $file_thb->exists() ) {
2089 return [
2090 'warning' => 'thumb',
2091 'file' => $file,
2092 'thumbFile' => $file_thb
2093 ];
2094 } else {
2095 // File does not exist, but we just don't like the name
2096 return [
2097 'warning' => 'thumb-name',
2098 'file' => $file,
2099 'thumbFile' => $file_thb
2100 ];
2101 }
2102 }
2103
2104 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2105 if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
2106 return [
2107 'warning' => 'bad-prefix',
2108 'file' => $file,
2109 'prefix' => $prefix
2110 ];
2111 }
2112 }
2113
2114 return false;
2115 }
2116
2122 public static function isThumbName( $filename ) {
2123 $n = strrpos( $filename, '.' );
2124 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2125
2126 return (
2127 substr( $partname, 3, 3 ) == 'px-' ||
2128 substr( $partname, 2, 3 ) == 'px-'
2129 ) &&
2130 preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2131 }
2132
2138 public static function getFilenamePrefixBlacklist() {
2139 $list = [];
2140 $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2141 if ( !$message->isDisabled() ) {
2142 $lines = explode( "\n", $message->plain() );
2143 foreach ( $lines as $line ) {
2144 // Remove comment lines
2145 $comment = substr( trim( $line ), 0, 1 );
2146 if ( $comment == '#' || $comment == '' ) {
2147 continue;
2148 }
2149 // Remove additional comments after a prefix
2150 $comment = strpos( $line, '#' );
2151 if ( $comment > 0 ) {
2152 $line = substr( $line, 0, $comment - 1 );
2153 }
2154 $list[] = trim( $line );
2155 }
2156 }
2157
2158 return $list;
2159 }
2160
2172 public function getImageInfo( $result ) {
2173 $localFile = $this->getLocalFile();
2174 $stashFile = $this->getStashFile();
2175 // Calling a different API module depending on whether the file was stashed is less than optimal.
2176 // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
2177 if ( $stashFile ) {
2179 $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_fill_keys( $imParam, true ), $result );
2180 } else {
2182 $info = ApiQueryImageInfo::getInfo( $localFile, array_fill_keys( $imParam, true ), $result );
2183 }
2184
2185 return $info;
2186 }
2187
2192 public function convertVerifyErrorToStatus( $error ) {
2193 $code = $error['status'];
2194 unset( $code['status'] );
2195
2196 return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2197 }
2198
2206 public static function getMaxUploadSize( $forType = null ) {
2207 global $wgMaxUploadSize;
2208
2209 if ( is_array( $wgMaxUploadSize ) ) {
2210 if ( $forType !== null && isset( $wgMaxUploadSize[$forType] ) ) {
2211 return $wgMaxUploadSize[$forType];
2212 } else {
2213 return $wgMaxUploadSize['*'];
2214 }
2215 } else {
2216 return intval( $wgMaxUploadSize );
2217 }
2218 }
2219
2227 public static function getMaxPhpUploadSize() {
2228 $phpMaxFileSize = wfShorthandToInteger(
2229 ini_get( 'upload_max_filesize' ),
2230 PHP_INT_MAX
2231 );
2232 $phpMaxPostSize = wfShorthandToInteger(
2233 ini_get( 'post_max_size' ),
2234 PHP_INT_MAX
2235 ) ?: PHP_INT_MAX;
2236 return min( $phpMaxFileSize, $phpMaxPostSize );
2237 }
2238
2248 public static function getSessionStatus( UserIdentity $user, $statusKey ) {
2249 $store = self::getUploadSessionStore();
2250 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2251
2252 return $store->get( $key );
2253 }
2254
2267 public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
2268 $store = self::getUploadSessionStore();
2269 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2270
2271 if ( $value === false ) {
2272 $store->delete( $key );
2273 } else {
2274 $store->set( $key, $value, $store::TTL_DAY );
2275 }
2276 }
2277
2284 private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
2285 return $store->makeKey(
2286 'uploadstatus',
2287 $user->getId() ?: md5( $user->getName() ),
2288 $statusKey
2289 );
2290 }
2291
2295 private static function getUploadSessionStore() {
2296 return ObjectCache::getInstance( 'db-replicated' );
2297 }
2298}
$wgProhibitedFileExtensions
Files with these extensions will never be allowed as uploads.
$wgAntivirus
Internal name of virus scanner.
$wgFileExtensions
This is the list of preferred extensions for uploading files.
$wgCheckFileExtensions
This is a flag to determine whether or not to check file extensions on upload.
$wgAntivirusRequired
Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected.
$wgUploadSizeWarning
Warn if uploaded files are larger than this (in bytes), or false to disable.
$wgDisableUploadScriptChecks
Setting this to true will disable the upload system's checks for HTML/JavaScript.
$wgVerifyMimeType
Determines if the MIME type of uploaded files should be checked.
$wgAntivirusSetup
Configuration for different virus scanners.
$wgVerifyMimeTypeIE
Determines whether extra checks for IE type detection should be applied.
$wgEnableUploads
Allow users to upload files.
$wgAllowJavaUploads
Allow Java archive uploads.
$wgStrictFileExtensions
If this is turned off, users may override the warning for files not covered by $wgFileExtensions.
$wgMimeTypeExclusions
Files with these MIME types will never be allowed as uploads if $wgVerifyMimeType is enabled.
$wgMaxUploadSize
Max size for uploads, in bytes.
$wgSVGMetadataCutoff
Don't read SVG metadata beyond this point.
const AV_SCAN_FAILED
Definition Defines.php:98
const NS_FILE
Definition Defines.php:70
const AV_SCAN_ABORTED
Definition Defines.php:97
const AV_NO_VIRUS
Definition Defines.php:95
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
$wgOut
Definition Setup.php:836
$wgLang
Definition Setup.php:831
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.
Class representing a row of the 'filearchive' table.
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:86
makeKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
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:290
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:66
wasDeleted()
Was this file ever deleted from the wiki?
Definition File.php:2075
Class to represent a local file in the wiki's own database.
Definition LocalFile.php:63
exists()
canRender inherited
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
MediaWiki exception.
MimeMagic helper wrapper.
static getHandler( $type)
Get a MediaHandler for a given MIME type from the instance cache.
MediaWikiServices is the service locator for the application scope of MediaWiki.
A StatusValue for permission errors.
Executes shell commands.
Definition Shell.php:45
static sizeParam( $size)
Definition Message.php:1178
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:48
getDBkey()
Get the main part with underscores.
Definition Title.php:1069
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition Title.php:383
UploadBase and subclasses are the backend of MediaWiki's file uploads.
static string[] $uploadHandlers
Upload handlers.
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.
checkLocalFileExists(LocalFile $localFile, $hash)
getLocalFile()
Return the local file and initializes if necessary.
const SUCCESS
stripXmlNamespace( $name)
bool null $mJavaDetected
string null $mFilteredName
checkBadFileName( $filename, $desiredFileName)
Check whether the resulting filename is different from the desired one, but ignore things like ucfirs...
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.
static getUploadSessionStore()
getVerificationErrorCode( $error)
performUpload( $comment, $pageText, $watch, $user, $tags=[], ?string $watchlistExpiry=null)
Really perform the upload.
string null $mDesiredDestName
static checkCssFragment( $value)
Check a block of CSS or CSS fragment for anything that looks like it is bringing in remote code.
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...
string null $mSVGNSError
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.
checkFileSize( $fileSize)
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.
checkAgainstArchiveDupes( $hash, Authority $performer)
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.
static splitXmlNamespace( $element)
Divide the element name passed by the xml parser to the callback into URI and prifix.
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 getUploadSessionKey(BagOStuff $store, UserIdentity $user, $statusKey)
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
checkLocalFileWasDeleted(LocalFile $localFile)
getFileSize()
Return the file size.
verifyUpload()
Verify whether the upload is sane.
const ILLEGAL_FILENAME
checkOverwrite(Authority $performer)
Check if there's an overwrite conflict and, if so, if restrictions forbid this user from performing t...
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.
checkUnwantedFileExtensions( $fileExtension)
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.
checkAgainstExistingDupes( $hash, $ignoreLocalDupes)
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
static read( $fileName, $callback, $options=[])
Read a ZIP file and call a function for each file discovered in it.
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
A helper class for throttling authentication attempts.
$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