MediaWiki REL1_38
UploadBase.php
Go to the documentation of this file.
1<?php
24use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
30use Wikimedia\AtEase\AtEase;
31
48abstract class UploadBase {
49 use ProtectedHookAccessorTrait;
50
52 protected $mTempPath;
54 protected $tempFileObj;
58 protected $mDestName;
62 protected $mSourceType;
64 protected $mTitle = false;
66 protected $mTitleError = 0;
68 protected $mFilteredName;
72 protected $mLocalFile;
74 protected $mStashFile;
76 protected $mFileSize;
78 protected $mFileProps;
82 protected $mJavaDetected;
84 protected $mSVGNSError;
85
86 protected static $safeXmlEncodings = [
87 'UTF-8',
88 'ISO-8859-1',
89 'ISO-8859-2',
90 'UTF-16',
91 'UTF-32',
92 'WINDOWS-1250',
93 'WINDOWS-1251',
94 'WINDOWS-1252',
95 'WINDOWS-1253',
96 'WINDOWS-1254',
97 'WINDOWS-1255',
98 'WINDOWS-1256',
99 'WINDOWS-1257',
100 'WINDOWS-1258',
101 ];
102
103 public const SUCCESS = 0;
104 public const OK = 0;
105 public const EMPTY_FILE = 3;
106 public const MIN_LENGTH_PARTNAME = 4;
107 public const ILLEGAL_FILENAME = 5;
108 public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
109 public const FILETYPE_MISSING = 8;
110 public const FILETYPE_BADTYPE = 9;
111 public const VERIFICATION_ERROR = 10;
112 public const HOOK_ABORTED = 11;
113 public const FILE_TOO_LARGE = 12;
114 public const WINDOWS_NONASCII_FILENAME = 13;
115 public const FILENAME_TOO_LONG = 14;
116
121 public function getVerificationErrorCode( $error ) {
122 $code_to_status = [
123 self::EMPTY_FILE => 'empty-file',
124 self::FILE_TOO_LARGE => 'file-too-large',
125 self::FILETYPE_MISSING => 'filetype-missing',
126 self::FILETYPE_BADTYPE => 'filetype-banned',
127 self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
128 self::ILLEGAL_FILENAME => 'illegal-filename',
129 self::OVERWRITE_EXISTING_FILE => 'overwrite',
130 self::VERIFICATION_ERROR => 'verification-error',
131 self::HOOK_ABORTED => 'hookaborted',
132 self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
133 self::FILENAME_TOO_LONG => 'filename-toolong',
134 ];
135 return $code_to_status[$error] ?? 'unknown-error';
136 }
137
144 public static function isEnabled() {
145 $enableUploads = MediaWikiServices::getInstance()->getMainConfig()->get( 'EnableUploads' );
146
147 return $enableUploads && wfIniGetBool( 'file_uploads' );
148 }
149
158 public static function isAllowed( Authority $performer ) {
159 foreach ( [ 'upload', 'edit' ] as $permission ) {
160 if ( !$performer->isAllowed( $permission ) ) {
161 return $permission;
162 }
163 }
164
165 return true;
166 }
167
174 public static function isThrottled( $user ) {
175 return $user->pingLimiter( 'upload' );
176 }
177
179 private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
180
188 public static function createFromRequest( &$request, $type = null ) {
189 $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
190
191 if ( !$type ) {
192 return null;
193 }
194
195 // Get the upload class
196 $type = ucfirst( $type );
197
198 // Give hooks the chance to handle this request
200 $className = null;
201 Hooks::runner()->onUploadCreateFromRequest( $type, $className );
202 if ( $className === null ) {
203 $className = 'UploadFrom' . $type;
204 wfDebug( __METHOD__ . ": class name: $className" );
205 if ( !in_array( $type, self::$uploadHandlers ) ) {
206 return null;
207 }
208 }
209
210 // Check whether this upload class is enabled
211 if ( !$className::isEnabled() ) {
212 return null;
213 }
214
215 // Check whether the request is valid
216 if ( !$className::isValidRequest( $request ) ) {
217 return null;
218 }
219
221 $handler = new $className;
222
223 $handler->initializeFromRequest( $request );
224
225 return $handler;
226 }
227
233 public static function isValidRequest( $request ) {
234 return false;
235 }
236
240 public function __construct() {
241 }
242
250 public function getSourceType() {
251 return null;
252 }
253
261 public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
262 $this->mDesiredDestName = $name;
263 if ( FileBackend::isStoragePath( $tempPath ) ) {
264 throw new MWException( __METHOD__ . " given storage path `$tempPath`." );
265 }
266
267 $this->setTempFile( $tempPath, $fileSize );
268 $this->mRemoveTempFile = $removeTempFile;
269 }
270
276 abstract public function initializeFromRequest( &$request );
277
282 protected function setTempFile( $tempPath, $fileSize = null ) {
283 $this->mTempPath = $tempPath ?? '';
284 $this->mFileSize = $fileSize ?: null;
285 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
286 $this->tempFileObj = new TempFSFile( $this->mTempPath );
287 if ( !$fileSize ) {
288 $this->mFileSize = filesize( $this->mTempPath );
289 }
290 } else {
291 $this->tempFileObj = null;
292 }
293 }
294
300 public function fetchFile() {
301 return Status::newGood();
302 }
303
308 public function isEmptyFile() {
309 return empty( $this->mFileSize );
310 }
311
316 public function getFileSize() {
317 return $this->mFileSize;
318 }
319
325 public function getTempFileSha1Base36() {
326 return FSFile::getSha1Base36FromPath( $this->mTempPath );
327 }
328
333 public function getRealPath( $srcPath ) {
334 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
335 if ( FileRepo::isVirtualUrl( $srcPath ) ) {
339 $tmpFile = $repo->getLocalCopy( $srcPath );
340 if ( $tmpFile ) {
341 $tmpFile->bind( $this ); // keep alive with $this
342 }
343 $path = $tmpFile ? $tmpFile->getPath() : false;
344 } else {
345 $path = $srcPath;
346 }
347
348 return $path;
349 }
350
368 public function verifyUpload() {
372 if ( $this->isEmptyFile() ) {
373 return [ 'status' => self::EMPTY_FILE ];
374 }
375
379 $maxSize = self::getMaxUploadSize( $this->getSourceType() );
380 if ( $this->mFileSize > $maxSize ) {
381 return [
382 'status' => self::FILE_TOO_LARGE,
383 'max' => $maxSize,
384 ];
385 }
386
392 $verification = $this->verifyFile();
393 if ( $verification !== true ) {
394 return [
395 'status' => self::VERIFICATION_ERROR,
396 'details' => $verification
397 ];
398 }
399
403 $result = $this->validateName();
404 if ( $result !== true ) {
405 return $result;
406 }
407
408 return [ 'status' => self::OK ];
409 }
410
417 public function validateName() {
418 $nt = $this->getTitle();
419 if ( $nt === null ) {
420 $result = [ 'status' => $this->mTitleError ];
421 if ( $this->mTitleError == self::ILLEGAL_FILENAME ) {
422 $result['filtered'] = $this->mFilteredName;
423 }
424 if ( $this->mTitleError == self::FILETYPE_BADTYPE ) {
425 $result['finalExt'] = $this->mFinalExtension;
426 if ( count( $this->mBlackListedExtensions ) ) {
427 $result['blacklistedExt'] = $this->mBlackListedExtensions;
428 }
429 }
430
431 return $result;
432 }
433 $this->mDestName = $this->getLocalFile()->getName();
434
435 return true;
436 }
437
447 protected function verifyMimeType( $mime ) {
448 $verifyMimeType = MediaWikiServices::getInstance()->getMainConfig()->get( 'VerifyMimeType' );
449 $verifyMimeTypeIE = MediaWikiServices::getInstance()->getMainConfig()->get( 'VerifyMimeTypeIE' );
450 if ( $verifyMimeType ) {
451 wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" );
452 $mimeTypeExclusions = MediaWikiServices::getInstance()->getMainConfig()->get( 'MimeTypeExclusions' );
453 if ( self::checkFileExtension( $mime, $mimeTypeExclusions ) ) {
454 return [ 'filetype-badmime', $mime ];
455 }
456
457 if ( $verifyMimeTypeIE ) {
458 # Check what Internet Explorer would detect
459 $fp = fopen( $this->mTempPath, 'rb' );
460 if ( $fp ) {
461 $chunk = fread( $fp, 256 );
462 fclose( $fp );
463
464 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
465 $extMime = $magic->getMimeTypeFromExtensionOrNull( (string)$this->mFinalExtension ) ?? '';
466 $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
467 foreach ( $ieTypes as $ieType ) {
468 if ( self::checkFileExtension( $ieType, $mimeTypeExclusions ) ) {
469 return [ 'filetype-bad-ie-mime', $ieType ];
470 }
471 }
472 }
473 }
474 }
475
476 return true;
477 }
478
484 protected function verifyFile() {
485 $config = MediaWikiServices::getInstance()->getMainConfig();
486 $verifyMimeType = $config->get( 'VerifyMimeType' );
487 $disableUploadScriptChecks = $config->get( 'DisableUploadScriptChecks' );
488 $status = $this->verifyPartialFile();
489 if ( $status !== true ) {
490 return $status;
491 }
492
493 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
494 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
495 $mime = $this->mFileProps['mime'];
496
497 if ( $verifyMimeType ) {
498 # XXX: Missing extension will be caught by validateName() via getTitle()
499 if ( (string)$this->mFinalExtension !== '' &&
500 !$this->verifyExtension( $mime, $this->mFinalExtension )
501 ) {
502 return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
503 }
504 }
505
506 # check for htmlish code and javascript
507 if ( !$disableUploadScriptChecks ) {
508 if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
509 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
510 if ( $svgStatus !== false ) {
511 return $svgStatus;
512 }
513 }
514 }
515
516 $handler = MediaHandler::getHandler( $mime );
517 if ( $handler ) {
518 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
519 if ( !$handlerStatus->isOK() ) {
520 $errors = $handlerStatus->getErrorsArray();
521
522 return reset( $errors );
523 }
524 }
525
526 $error = true;
527 $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
528 if ( $error !== true ) {
529 if ( !is_array( $error ) ) {
530 $error = [ $error ];
531 }
532 return $error;
533 }
534
535 wfDebug( __METHOD__ . ": all clear; passing." );
536
537 return true;
538 }
539
548 protected function verifyPartialFile() {
549 $config = MediaWikiServices::getInstance()->getMainConfig();
550 $allowJavaUploads = $config->get( 'AllowJavaUploads' );
551 $disableUploadScriptChecks = $config->get( 'DisableUploadScriptChecks' );
552 # getTitle() sets some internal parameters like $this->mFinalExtension
553 $this->getTitle();
554
555 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
556 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
557
558 # check MIME type, if desired
559 $mime = $this->mFileProps['file-mime'];
560 $status = $this->verifyMimeType( $mime );
561 if ( $status !== true ) {
562 return $status;
563 }
564
565 # check for htmlish code and javascript
566 if ( !$disableUploadScriptChecks ) {
567 if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
568 return [ 'uploadscripted' ];
569 }
570 if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
571 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
572 if ( $svgStatus !== false ) {
573 return $svgStatus;
574 }
575 }
576 }
577
578 # Check for Java applets, which if uploaded can bypass cross-site
579 # restrictions.
580 if ( !$allowJavaUploads ) {
581 $this->mJavaDetected = false;
582 $zipStatus = ZipDirectoryReader::read( $this->mTempPath,
583 [ $this, 'zipEntryCallback' ] );
584 if ( !$zipStatus->isOK() ) {
585 $errors = $zipStatus->getErrorsArray();
586 $error = reset( $errors );
587 if ( $error[0] !== 'zip-wrong-format' ) {
588 return $error;
589 }
590 }
591 if ( $this->mJavaDetected ) {
592 return [ 'uploadjava' ];
593 }
594 }
595
596 # Scan the uploaded file for viruses
597 $virus = $this->detectVirus( $this->mTempPath );
598 if ( $virus ) {
599 return [ 'uploadvirus', $virus ];
600 }
601
602 return true;
603 }
604
610 public function zipEntryCallback( $entry ) {
611 $names = [ $entry['name'] ];
612
613 // If there is a null character, cut off the name at it, because JDK's
614 // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
615 // were constructed which had ".class\0" followed by a string chosen to
616 // make the hash collide with the truncated name, that file could be
617 // returned in response to a request for the .class file.
618 $nullPos = strpos( $entry['name'], "\000" );
619 if ( $nullPos !== false ) {
620 $names[] = substr( $entry['name'], 0, $nullPos );
621 }
622
623 // If there is a trailing slash in the file name, we have to strip it,
624 // because that's what ZIP_GetEntry() does.
625 if ( preg_grep( '!\.class/?$!', $names ) ) {
626 $this->mJavaDetected = true;
627 }
628 }
629
639 public function verifyPermissions( Authority $performer ) {
640 return $this->verifyTitlePermissions( $performer );
641 }
642
654 public function verifyTitlePermissions( Authority $performer ) {
659 $nt = $this->getTitle();
660 if ( $nt === null ) {
661 return true;
662 }
663
664 $status = PermissionStatus::newEmpty();
665 $performer->authorizeWrite( 'edit', $nt, $status );
666 $performer->authorizeWrite( 'upload', $nt, $status );
667 if ( !$status->isGood() ) {
668 return $status->toLegacyErrorArray();
669 }
670
671 $overwriteError = $this->checkOverwrite( $performer );
672 if ( $overwriteError !== true ) {
673 return [ $overwriteError ];
674 }
675
676 return true;
677 }
678
688 public function checkWarnings( $user = null ) {
689 if ( $user === null ) {
690 // TODO check uses and hard deprecate
691 $user = RequestContext::getMain()->getUser();
692 }
693
694 $warnings = [];
695
696 $localFile = $this->getLocalFile();
697 $localFile->load( File::READ_LATEST );
698 $filename = $localFile->getName();
699 $hash = $this->getTempFileSha1Base36();
700
701 $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
702 if ( $badFileName !== null ) {
703 $warnings['badfilename'] = $badFileName;
704 }
705
706 $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
707 if ( $unwantedFileExtensionDetails !== null ) {
708 $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
709 }
710
711 $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
712 if ( $fileSizeWarnings ) {
713 $warnings = array_merge( $warnings, $fileSizeWarnings );
714 }
715
716 $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
717 if ( $localFileExistsWarnings ) {
718 $warnings = array_merge( $warnings, $localFileExistsWarnings );
719 }
720
721 if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
722 $warnings['was-deleted'] = $filename;
723 }
724
725 // If a file with the same name exists locally then the local file has already been tested
726 // for duplication of content
727 $ignoreLocalDupes = isset( $warnings['exists'] );
728 $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
729 if ( $dupes ) {
730 $warnings['duplicate'] = $dupes;
731 }
732
733 $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
734 if ( $archivedDupes !== null ) {
735 $warnings['duplicate-archive'] = $archivedDupes;
736 }
737
738 return $warnings;
739 }
740
752 public static function makeWarningsSerializable( $warnings ) {
753 array_walk_recursive( $warnings, static function ( &$param, $key ) {
754 if ( $param instanceof File ) {
755 $param = [
756 'fileName' => $param->getName(),
757 'timestamp' => $param->getTimestamp()
758 ];
759 } elseif ( is_object( $param ) ) {
760 throw new InvalidArgumentException(
761 'UploadBase::makeWarningsSerializable: ' .
762 'Unexpected object of class ' . get_class( $param ) );
763 }
764 } );
765 return $warnings;
766 }
767
777 private function checkBadFileName( $filename, $desiredFileName ) {
778 $comparableName = str_replace( ' ', '_', $desiredFileName );
779 $comparableName = Title::capitalize( $comparableName, NS_FILE );
780
781 if ( $desiredFileName != $filename && $comparableName != $filename ) {
782 return $filename;
783 }
784
785 return null;
786 }
787
796 private function checkUnwantedFileExtensions( $fileExtension ) {
797 global $wgLang;
798 $checkFileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( 'CheckFileExtensions' );
799 $fileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( 'FileExtensions' );
800 if ( $checkFileExtensions ) {
801 $extensions = array_unique( $fileExtensions );
802 if ( !$this->checkFileExtension( $fileExtension, $extensions ) ) {
803 return [
804 $fileExtension,
805 $wgLang->commaList( $extensions ),
806 count( $extensions )
807 ];
808 }
809 }
810
811 return null;
812 }
813
819 private function checkFileSize( $fileSize ) {
820 $uploadSizeWarning = MediaWikiServices::getInstance()->getMainConfig()->get( 'UploadSizeWarning' );
821
822 $warnings = [];
823
824 if ( $uploadSizeWarning && ( $fileSize > $uploadSizeWarning ) ) {
825 $warnings['large-file'] = [
826 Message::sizeParam( $uploadSizeWarning ),
827 Message::sizeParam( $fileSize ),
828 ];
829 }
830
831 if ( $fileSize == 0 ) {
832 $warnings['empty-file'] = true;
833 }
834
835 return $warnings;
836 }
837
844 private function checkLocalFileExists( LocalFile $localFile, $hash ) {
845 $warnings = [];
846
847 $exists = self::getExistsWarning( $localFile );
848 if ( $exists !== false ) {
849 $warnings['exists'] = $exists;
850
851 // check if file is an exact duplicate of current file version
852 if ( $hash !== false && $hash === $localFile->getSha1() ) {
853 $warnings['no-change'] = $localFile;
854 }
855
856 // check if file is an exact duplicate of older versions of this file
857 $history = $localFile->getHistory();
858 foreach ( $history as $oldFile ) {
859 if ( $hash === $oldFile->getSha1() ) {
860 $warnings['duplicate-version'][] = $oldFile;
861 }
862 }
863 }
864
865 return $warnings;
866 }
867
868 private function checkLocalFileWasDeleted( LocalFile $localFile ) {
869 return $localFile->wasDeleted() && !$localFile->exists();
870 }
871
878 private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
879 if ( $hash === false ) {
880 return [];
881 }
882 $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
883 $title = $this->getTitle();
884 foreach ( $dupes as $key => $dupe ) {
885 if (
886 ( $dupe instanceof LocalFile ) &&
887 $ignoreLocalDupes &&
888 $title->equals( $dupe->getTitle() )
889 ) {
890 unset( $dupes[$key] );
891 }
892 }
893
894 return $dupes;
895 }
896
904 private function checkAgainstArchiveDupes( $hash, Authority $performer ) {
905 if ( $hash === false ) {
906 return null;
907 }
908 $archivedFile = new ArchivedFile( null, 0, '', $hash );
909 if ( $archivedFile->getID() > 0 ) {
910 if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
911 return $archivedFile->getName();
912 } else {
913 return '';
914 }
915 }
916
917 return null;
918 }
919
937 public function performUpload(
938 $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
939 ) {
940 $this->getLocalFile()->load( File::READ_LATEST );
941 $props = $this->mFileProps;
942
943 $error = null;
944 $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
945 if ( $error ) {
946 if ( !is_array( $error ) ) {
947 $error = [ $error ];
948 }
949 return Status::newFatal( ...$error );
950 }
951
952 $status = $this->getLocalFile()->upload(
953 $this->mTempPath,
954 $comment,
955 $pageText,
956 File::DELETE_SOURCE,
957 $props,
958 false,
959 $user,
960 $tags
961 );
962
963 if ( $status->isGood() ) {
964 if ( $watch ) {
965 MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
966 $user,
967 $this->getLocalFile()->getTitle(),
968 $watchlistExpiry
969 );
970 }
971 $this->getHookRunner()->onUploadComplete( $this );
972
973 $this->postProcessUpload();
974 }
975
976 return $status;
977 }
978
985 public function postProcessUpload() {
986 }
987
994 public function getTitle() {
995 if ( $this->mTitle !== false ) {
996 return $this->mTitle;
997 }
998 if ( !is_string( $this->mDesiredDestName ) ) {
999 $this->mTitleError = self::ILLEGAL_FILENAME;
1000 $this->mTitle = null;
1001
1002 return $this->mTitle;
1003 }
1004 /* Assume that if a user specified File:Something.jpg, this is an error
1005 * and that the namespace prefix needs to be stripped of.
1006 */
1007 $title = Title::newFromText( $this->mDesiredDestName );
1008 if ( $title && $title->getNamespace() === NS_FILE ) {
1009 $this->mFilteredName = $title->getDBkey();
1010 } else {
1011 $this->mFilteredName = $this->mDesiredDestName;
1012 }
1013
1014 # oi_archive_name is max 255 bytes, which include a timestamp and an
1015 # exclamation mark, so restrict file name to 240 bytes.
1016 if ( strlen( $this->mFilteredName ) > 240 ) {
1017 $this->mTitleError = self::FILENAME_TOO_LONG;
1018 $this->mTitle = null;
1019
1020 return $this->mTitle;
1021 }
1022
1028 $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
1029 /* Normalize to title form before we do any further processing */
1030 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1031 if ( $nt === null ) {
1032 $this->mTitleError = self::ILLEGAL_FILENAME;
1033 $this->mTitle = null;
1034
1035 return $this->mTitle;
1036 }
1037 $this->mFilteredName = $nt->getDBkey();
1038
1043 list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
1044
1045 if ( $ext !== [] ) {
1046 $this->mFinalExtension = trim( end( $ext ) );
1047 } else {
1048 $this->mFinalExtension = '';
1049
1050 // No extension, try guessing one from the temporary file
1051 // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1052 // or incomplete test case in UploadBaseTest (T272328)
1053 if ( $this->mTempPath !== null ) {
1054 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1055 $mime = $magic->guessMimeType( $this->mTempPath );
1056 if ( $mime !== 'unknown/unknown' ) {
1057 # Get a space separated list of extensions
1058 $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1059 if ( $mimeExt !== null ) {
1060 # Set the extension to the canonical extension
1061 $this->mFinalExtension = $mimeExt;
1062
1063 # Fix up the other variables
1064 $this->mFilteredName .= ".{$this->mFinalExtension}";
1065 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1066 $ext = [ $this->mFinalExtension ];
1067 }
1068 }
1069 }
1070 }
1071
1072 // Don't allow users to override the list of prohibited file extensions (check file extension)
1073 $config = MediaWikiServices::getInstance()->getMainConfig();
1074 $checkFileExtensions = $config->get( 'CheckFileExtensions' );
1075 $strictFileExtensions = $config->get( 'StrictFileExtensions' );
1076 $fileExtensions = $config->get( 'FileExtensions' );
1077 $prohibitedFileExtensions = $config->get( 'ProhibitedFileExtensions' );
1078
1079 $blackListedExtensions = self::checkFileExtensionList( $ext, $prohibitedFileExtensions );
1080
1081 if ( $this->mFinalExtension == '' ) {
1082 $this->mTitleError = self::FILETYPE_MISSING;
1083 $this->mTitle = null;
1084
1085 return $this->mTitle;
1086 } elseif ( $blackListedExtensions ||
1087 ( $checkFileExtensions && $strictFileExtensions &&
1088 !$this->checkFileExtension( $this->mFinalExtension, $fileExtensions ) )
1089 ) {
1090 $this->mBlackListedExtensions = $blackListedExtensions;
1091 $this->mTitleError = self::FILETYPE_BADTYPE;
1092 $this->mTitle = null;
1093
1094 return $this->mTitle;
1095 }
1096
1097 // Windows may be broken with special characters, see T3780
1098 if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1099 && !MediaWikiServices::getInstance()->getRepoGroup()
1100 ->getLocalRepo()->backendSupportsUnicodePaths()
1101 ) {
1102 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1103 $this->mTitle = null;
1104
1105 return $this->mTitle;
1106 }
1107
1108 # If there was more than one "extension", reassemble the base
1109 # filename to prevent bogus complaints about length
1110 if ( count( $ext ) > 1 ) {
1111 $iterations = count( $ext ) - 1;
1112 for ( $i = 0; $i < $iterations; $i++ ) {
1113 $partname .= '.' . $ext[$i];
1114 }
1115 }
1116
1117 if ( strlen( $partname ) < 1 ) {
1118 $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1119 $this->mTitle = null;
1120
1121 return $this->mTitle;
1122 }
1123
1124 $this->mTitle = $nt;
1125
1126 return $this->mTitle;
1127 }
1128
1135 public function getLocalFile() {
1136 if ( $this->mLocalFile === null ) {
1137 $nt = $this->getTitle();
1138 $this->mLocalFile = $nt === null
1139 ? null
1140 : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1141 }
1142
1143 return $this->mLocalFile;
1144 }
1145
1149 public function getStashFile() {
1150 return $this->mStashFile;
1151 }
1152
1165 public function tryStashFile( User $user, $isPartial = false ) {
1166 if ( !$isPartial ) {
1167 $error = $this->runUploadStashFileHook( $user );
1168 if ( $error ) {
1169 return Status::newFatal( ...$error );
1170 }
1171 }
1172 try {
1173 $file = $this->doStashFile( $user );
1174 return Status::newGood( $file );
1175 } catch ( UploadStashException $e ) {
1176 return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1177 }
1178 }
1179
1184 protected function runUploadStashFileHook( User $user ) {
1185 $props = $this->mFileProps;
1186 $error = null;
1187 $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1188 if ( $error && !is_array( $error ) ) {
1189 $error = [ $error ];
1190 }
1191 return $error;
1192 }
1193
1201 protected function doStashFile( User $user = null ) {
1202 $stash = MediaWikiServices::getInstance()->getRepoGroup()
1203 ->getLocalRepo()->getUploadStash( $user );
1204 $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
1205 $this->mStashFile = $file;
1206
1207 return $file;
1208 }
1209
1214 public function cleanupTempFile() {
1215 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1216 // Delete when all relevant TempFSFile handles go out of scope
1217 wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1218 $this->tempFileObj->autocollect();
1219 }
1220 }
1221
1222 public function getTempPath() {
1223 return $this->mTempPath;
1224 }
1225
1235 public static function splitExtensions( $filename ) {
1236 $bits = explode( '.', $filename );
1237 $basename = array_shift( $bits );
1238
1239 return [ $basename, $bits ];
1240 }
1241
1250 public static function checkFileExtension( $ext, $list ) {
1251 return in_array( strtolower( $ext ), $list );
1252 }
1253
1262 public static function checkFileExtensionList( $ext, $list ) {
1263 return array_intersect( array_map( 'strtolower', $ext ), $list );
1264 }
1265
1273 public static function verifyExtension( $mime, $extension ) {
1274 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1275
1276 if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) {
1277 if ( !$magic->isRecognizableExtension( $extension ) ) {
1278 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1279 "unrecognized extension '$extension', can't verify" );
1280
1281 return true;
1282 } else {
1283 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1284 "recognized extension '$extension', so probably invalid file" );
1285
1286 return false;
1287 }
1288 }
1289
1290 $match = $magic->isMatchingExtension( $extension, $mime );
1291
1292 if ( $match === null ) {
1293 if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1294 wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1295
1296 return false;
1297 } else {
1298 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1299
1300 return true;
1301 }
1302 } elseif ( $match ) {
1303 wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1304
1306 return true;
1307 } else {
1308 wfDebug( __METHOD__
1309 . ": mime type $mime mismatches file extension $extension, rejecting file" );
1310
1311 return false;
1312 }
1313 }
1314
1326 public static function detectScript( $file, $mime, $extension ) {
1327 # ugly hack: for text files, always look at the entire file.
1328 # For binary field, just check the first K.
1329
1330 $isText = strpos( $mime, 'text/' ) === 0;
1331 if ( $isText ) {
1332 $chunk = file_get_contents( $file );
1333 } else {
1334 $fp = fopen( $file, 'rb' );
1335 if ( !$fp ) {
1336 return false;
1337 }
1338 $chunk = fread( $fp, 1024 );
1339 fclose( $fp );
1340 }
1341
1342 $chunk = strtolower( $chunk );
1343
1344 if ( !$chunk ) {
1345 return false;
1346 }
1347
1348 # decode from UTF-16 if needed (could be used for obfuscation).
1349 if ( substr( $chunk, 0, 2 ) == "\xfe\xff" ) {
1350 $enc = 'UTF-16BE';
1351 } elseif ( substr( $chunk, 0, 2 ) == "\xff\xfe" ) {
1352 $enc = 'UTF-16LE';
1353 } else {
1354 $enc = null;
1355 }
1356
1357 if ( $enc !== null ) {
1358 $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1359 }
1360
1361 $chunk = trim( $chunk );
1362
1364 wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1365
1366 # check for HTML doctype
1367 if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1368 return true;
1369 }
1370
1371 // Some browsers will interpret obscure xml encodings as UTF-8, while
1372 // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1373 if ( $extension == 'svg' || strpos( $mime, 'image/svg' ) === 0 ) {
1374 if ( self::checkXMLEncodingMissmatch( $file ) ) {
1375 return true;
1376 }
1377 }
1378
1379 // Quick check for HTML heuristics in old IE and Safari.
1380 //
1381 // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1382 // don't need them all here as it can cause many false positives.
1383 //
1384 // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1385 $tags = [
1386 '<body',
1387 '<head',
1388 '<html', # also in safari
1389 '<script', # also in safari
1390 ];
1391
1392 foreach ( $tags as $tag ) {
1393 if ( strpos( $chunk, $tag ) !== false ) {
1394 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1395
1396 return true;
1397 }
1398 }
1399
1400 /*
1401 * look for JavaScript
1402 */
1403
1404 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1405 $chunk = Sanitizer::decodeCharReferences( $chunk );
1406
1407 # look for script-types
1408 if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
1409 wfDebug( __METHOD__ . ": found script types" );
1410
1411 return true;
1412 }
1413
1414 # look for html-style script-urls
1415 if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1416 wfDebug( __METHOD__ . ": found html-style script urls" );
1417
1418 return true;
1419 }
1420
1421 # look for css-style script-urls
1422 if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1423 wfDebug( __METHOD__ . ": found css-style script urls" );
1424
1425 return true;
1426 }
1427
1428 wfDebug( __METHOD__ . ": no scripts found" );
1429
1430 return false;
1431 }
1432
1440 public static function checkXMLEncodingMissmatch( $file ) {
1441 $svgMetadataCutoff = MediaWikiServices::getInstance()->getMainConfig()->get( 'SVGMetadataCutoff' );
1442 $contents = file_get_contents( $file, false, null, 0, $svgMetadataCutoff );
1443 $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1444
1445 if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1446 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1447 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1448 ) {
1449 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1450
1451 return true;
1452 }
1453 } elseif ( preg_match( "!<\?xml\b!si", $contents ) ) {
1454 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1455 // bytes. There shouldn't be a legitimate reason for this to happen.
1456 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1457
1458 return true;
1459 } elseif ( substr( $contents, 0, 4 ) == "\x4C\x6F\xA7\x94" ) {
1460 // EBCDIC encoded XML
1461 wfDebug( __METHOD__ . ": EBCDIC Encoded XML" );
1462
1463 return true;
1464 }
1465
1466 // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to
1467 // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings
1468 $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1469 foreach ( $attemptEncodings as $encoding ) {
1470 AtEase::suppressWarnings();
1471 $str = iconv( $encoding, 'UTF-8', $contents );
1472 AtEase::restoreWarnings();
1473 if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1474 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1475 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1476 ) {
1477 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1478
1479 return true;
1480 }
1481 } elseif ( $str != '' && preg_match( "!<\?xml\b!si", $str ) ) {
1482 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1483 // bytes. There shouldn't be a legitimate reason for this to happen.
1484 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1485
1486 return true;
1487 }
1488 }
1489
1490 return false;
1491 }
1492
1498 protected function detectScriptInSvg( $filename, $partial ) {
1499 $this->mSVGNSError = false;
1500 $check = new XmlTypeCheck(
1501 $filename,
1502 [ $this, 'checkSvgScriptCallback' ],
1503 true,
1504 [
1505 'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ],
1506 'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ],
1507 ]
1508 );
1509 if ( $check->wellFormed !== true ) {
1510 // Invalid xml (T60553)
1511 // But only when non-partial (T67724)
1512 return $partial ? false : [ 'uploadinvalidxml' ];
1513 } elseif ( $check->filterMatch ) {
1514 if ( $this->mSVGNSError ) {
1515 return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1516 }
1517
1518 return $check->filterMatchType;
1519 }
1520
1521 return false;
1522 }
1523
1530 public static function checkSvgPICallback( $target, $data ) {
1531 // Don't allow external stylesheets (T59550)
1532 if ( preg_match( '/xml-stylesheet/i', $target ) ) {
1533 return [ 'upload-scripted-pi-callback' ];
1534 }
1535
1536 return false;
1537 }
1538
1550 public static function checkSvgExternalDTD( $type, $publicId, $systemId ) {
1551 // This doesn't include the XHTML+MathML+SVG doctype since we don't
1552 // allow XHTML anyways.
1553 $allowedDTDs = [
1554 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1555 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1556 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1557 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1558 // https://phabricator.wikimedia.org/T168856
1559 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1560 ];
1561 if ( $type !== 'PUBLIC'
1562 || !in_array( $systemId, $allowedDTDs )
1563 || strpos( $publicId, "-//W3C//" ) !== 0
1564 ) {
1565 return [ 'upload-scripted-dtd' ];
1566 }
1567 return false;
1568 }
1569
1577 public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1578 list( $namespace, $strippedElement ) = $this->splitXmlNamespace( $element );
1579
1580 // We specifically don't include:
1581 // http://www.w3.org/1999/xhtml (T62771)
1582 static $validNamespaces = [
1583 '',
1584 'adobe:ns:meta/',
1585 'http://creativecommons.org/ns#',
1586 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1587 'http://ns.adobe.com/adobeillustrator/10.0/',
1588 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1589 'http://ns.adobe.com/extensibility/1.0/',
1590 'http://ns.adobe.com/flows/1.0/',
1591 'http://ns.adobe.com/illustrator/1.0/',
1592 'http://ns.adobe.com/imagereplacement/1.0/',
1593 'http://ns.adobe.com/pdf/1.3/',
1594 'http://ns.adobe.com/photoshop/1.0/',
1595 'http://ns.adobe.com/saveforweb/1.0/',
1596 'http://ns.adobe.com/variables/1.0/',
1597 'http://ns.adobe.com/xap/1.0/',
1598 'http://ns.adobe.com/xap/1.0/g/',
1599 'http://ns.adobe.com/xap/1.0/g/img/',
1600 'http://ns.adobe.com/xap/1.0/mm/',
1601 'http://ns.adobe.com/xap/1.0/rights/',
1602 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1603 'http://ns.adobe.com/xap/1.0/stype/font#',
1604 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1605 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1606 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1607 'http://ns.adobe.com/xap/1.0/t/pg/',
1608 'http://purl.org/dc/elements/1.1/',
1609 'http://purl.org/dc/elements/1.1',
1610 'http://schemas.microsoft.com/visio/2003/svgextensions/',
1611 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1612 'http://taptrix.com/inkpad/svg_extensions',
1613 'http://web.resource.org/cc/',
1614 'http://www.freesoftware.fsf.org/bkchem/cdml',
1615 'http://www.inkscape.org/namespaces/inkscape',
1616 'http://www.opengis.net/gml',
1617 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1618 'http://www.w3.org/2000/svg',
1619 'http://www.w3.org/tr/rec-rdf-syntax/',
1620 'http://www.w3.org/2000/01/rdf-schema#',
1621 ];
1622
1623 // Inkscape mangles namespace definitions created by Adobe Illustrator.
1624 // This is nasty but harmless. (T144827)
1625 $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1626
1627 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1628 wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file." );
1630 $this->mSVGNSError = $namespace;
1631
1632 return true;
1633 }
1634
1635 /*
1636 * check for elements that can contain javascript
1637 */
1638 if ( $strippedElement == 'script' ) {
1639 wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file." );
1640
1641 return [ 'uploaded-script-svg', $strippedElement ];
1642 }
1643
1644 # e.g., <svg xmlns="http://www.w3.org/2000/svg">
1645 # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1646 if ( $strippedElement == 'handler' ) {
1647 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1648
1649 return [ 'uploaded-script-svg', $strippedElement ];
1650 }
1651
1652 # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1653 if ( $strippedElement == 'stylesheet' ) {
1654 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1655
1656 return [ 'uploaded-script-svg', $strippedElement ];
1657 }
1658
1659 # Block iframes, in case they pass the namespace check
1660 if ( $strippedElement == 'iframe' ) {
1661 wfDebug( __METHOD__ . ": iframe in uploaded file." );
1662
1663 return [ 'uploaded-script-svg', $strippedElement ];
1664 }
1665
1666 # Check <style> css
1667 if ( $strippedElement == 'style'
1668 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1669 ) {
1670 wfDebug( __METHOD__ . ": hostile css in style element." );
1671 return [ 'uploaded-hostile-svg' ];
1672 }
1673
1674 foreach ( $attribs as $attrib => $value ) {
1675 $stripped = $this->stripXmlNamespace( $attrib );
1676 $value = strtolower( $value );
1677
1678 if ( substr( $stripped, 0, 2 ) == 'on' ) {
1679 wfDebug( __METHOD__
1680 . ": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1681
1682 return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1683 }
1684
1685 # Do not allow relative links, or unsafe url schemas.
1686 # For <a> tags, only data:, http: and https: and same-document
1687 # fragment links are allowed. For all other tags, only data:
1688 # and fragment are allowed.
1689 if ( $stripped == 'href'
1690 && $value !== ''
1691 && strpos( $value, 'data:' ) !== 0
1692 && strpos( $value, '#' ) !== 0
1693 ) {
1694 if ( !( $strippedElement === 'a'
1695 && preg_match( '!^https?://!i', $value ) )
1696 ) {
1697 wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1698 . "'$attrib'='$value' in uploaded file." );
1699
1700 return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1701 }
1702 }
1703
1704 # only allow data: targets that should be safe. This prevents vectors like,
1705 # image/svg, text/xml, application/xml, and text/html, which can contain scripts
1706 if ( $stripped == 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1707 // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
1708 // phpcs:ignore Generic.Files.LineLength
1709 $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1710
1711 if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1712 wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
1713 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1714 return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1715 }
1716 }
1717
1718 # Change href with animate from (http://html5sec.org/#137).
1719 if ( $stripped === 'attributename'
1720 && $strippedElement === 'animate'
1721 && $this->stripXmlNamespace( $value ) == 'href'
1722 ) {
1723 wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1724 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1725
1726 return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1727 }
1728
1729 # use set/animate to add event-handler attribute to parent
1730 if ( ( $strippedElement == 'set' || $strippedElement == 'animate' )
1731 && $stripped == 'attributename'
1732 && substr( $value, 0, 2 ) == 'on'
1733 ) {
1734 wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1735 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1736
1737 return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1738 }
1739
1740 # use set to add href attribute to parent element
1741 if ( $strippedElement == 'set'
1742 && $stripped == 'attributename'
1743 && strpos( $value, 'href' ) !== false
1744 ) {
1745 wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file." );
1746
1747 return [ 'uploaded-setting-href-svg' ];
1748 }
1749
1750 # use set to add a remote / data / script target to an element
1751 if ( $strippedElement == 'set'
1752 && $stripped == 'to'
1753 && preg_match( '!(http|https|data|script):!sim', $value )
1754 ) {
1755 wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file." );
1756
1757 return [ 'uploaded-wrong-setting-svg', $value ];
1758 }
1759
1760 # use handler attribute with remote / data / script
1761 if ( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
1762 wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1763 . "'$attrib'='$value' in uploaded file." );
1764
1765 return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1766 }
1767
1768 # use CSS styles to bring in remote code
1769 if ( $stripped == 'style'
1770 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1771 ) {
1772 wfDebug( __METHOD__ . ": Found svg setting a style with "
1773 . "remote url '$attrib'='$value' in uploaded file." );
1774 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1775 }
1776
1777 # Several attributes can include css, css character escaping isn't allowed
1778 $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1779 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1780 if ( in_array( $stripped, $cssAttrs )
1781 && self::checkCssFragment( $value )
1782 ) {
1783 wfDebug( __METHOD__ . ": Found svg setting a style with "
1784 . "remote url '$attrib'='$value' in uploaded file." );
1785 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1786 }
1787
1788 # image filters can pull in url, which could be svg that executes scripts
1789 # Only allow url( "#foo" ). Do not allow url( http://example.com )
1790 if ( $strippedElement == 'image'
1791 && $stripped == 'filter'
1792 && preg_match( '!url\s*\‍(\s*["\']?[^#]!sim', $value )
1793 ) {
1794 wfDebug( __METHOD__ . ": Found image filter with url: "
1795 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1796
1797 return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1798 }
1799 }
1800
1801 return false; // No scripts detected
1802 }
1803
1810 private static function checkCssFragment( $value ) {
1811 # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1812 if ( stripos( $value, '@import' ) !== false ) {
1813 return true;
1814 }
1815
1816 # We allow @font-face to embed fonts with data: urls, so we snip the string
1817 # 'url' out so this case won't match when we check for urls below
1818 $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1819 $value = preg_replace( $pattern, '$1$2', $value );
1820
1821 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1822 # properties filter and accelerator don't seem to be useful for xss in SVG files.
1823 # Expression and -o-link don't seem to work either, but filtering them here in case.
1824 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1825 # but not local ones such as url("#..., url('#..., url(#....
1826 if ( preg_match( '!expression
1827 | -o-link\s*:
1828 | -o-link-source\s*:
1829 | -o-replace\s*:!imx', $value ) ) {
1830 return true;
1831 }
1832
1833 if ( preg_match_all(
1834 "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1835 $value,
1836 $matches
1837 ) !== 0
1838 ) {
1839 # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1840 foreach ( $matches[1] as $match ) {
1841 if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1842 return true;
1843 }
1844 }
1845 }
1846
1847 if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1848 return true;
1849 }
1850
1851 return false;
1852 }
1853
1859 private static function splitXmlNamespace( $element ) {
1860 // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1861 $parts = explode( ':', strtolower( $element ) );
1862 $name = array_pop( $parts );
1863 $ns = implode( ':', $parts );
1864
1865 return [ $ns, $name ];
1866 }
1867
1872 private function stripXmlNamespace( $name ) {
1873 // 'http://www.w3.org/2000/svg:script' -> 'script'
1874 $parts = explode( ':', strtolower( $name ) );
1875
1876 return array_pop( $parts );
1877 }
1878
1889 public static function detectVirus( $file ) {
1890 global $wgOut;
1891 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1892 $antivirus = $mainConfig->get( 'Antivirus' );
1893 $antivirusSetup = $mainConfig->get( 'AntivirusSetup' );
1894 $antivirusRequired = $mainConfig->get( 'AntivirusRequired' );
1895 if ( !$antivirus ) {
1896 wfDebug( __METHOD__ . ": virus scanner disabled" );
1897
1898 return null;
1899 }
1900
1901 if ( !$antivirusSetup[$antivirus] ) {
1902 wfDebug( __METHOD__ . ": unknown virus scanner: {$antivirus}" );
1903 $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1904 [ 'virus-badscanner', $antivirus ] );
1905
1906 return wfMessage( 'virus-unknownscanner' )->text() . " {$antivirus}";
1907 }
1908
1909 # look up scanner configuration
1910 $command = $antivirusSetup[$antivirus]['command'];
1911 $exitCodeMap = $antivirusSetup[$antivirus]['codemap'];
1912 $msgPattern = $antivirusSetup[$antivirus]['messagepattern'] ?? null;
1913
1914 if ( strpos( $command, "%f" ) === false ) {
1915 # simple pattern: append file to scan
1916 $command .= " " . Shell::escape( $file );
1917 } else {
1918 # complex pattern: replace "%f" with file to scan
1919 $command = str_replace( "%f", Shell::escape( $file ), $command );
1920 }
1921
1922 wfDebug( __METHOD__ . ": running virus scan: $command " );
1923
1924 # execute virus scanner
1925 $exitCode = false;
1926
1927 # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1928 # that does not seem to be worth the pain.
1929 # Ask me (Duesentrieb) about it if it's ever needed.
1930 $output = wfShellExecWithStderr( $command, $exitCode );
1931
1932 # map exit code to AV_xxx constants.
1933 $mappedCode = $exitCode;
1934 if ( $exitCodeMap ) {
1935 if ( isset( $exitCodeMap[$exitCode] ) ) {
1936 $mappedCode = $exitCodeMap[$exitCode];
1937 } elseif ( isset( $exitCodeMap["*"] ) ) {
1938 $mappedCode = $exitCodeMap["*"];
1939 }
1940 }
1941
1942 /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
1943 * so we need the strict equalities === and thus can't use a switch here
1944 */
1945 if ( $mappedCode === AV_SCAN_FAILED ) {
1946 # scan failed (code was mapped to false by $exitCodeMap)
1947 wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1948
1949 $output = $antivirusRequired
1950 ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1951 : null;
1952 } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1953 # scan failed because filetype is unknown (probably imune)
1954 wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1955 $output = null;
1956 } elseif ( $mappedCode === AV_NO_VIRUS ) {
1957 # no virus found
1958 wfDebug( __METHOD__ . ": file passed virus scan." );
1959 $output = false;
1960 } else {
1961 $output = trim( $output );
1962
1963 if ( !$output ) {
1964 $output = true; # if there's no output, return true
1965 } elseif ( $msgPattern ) {
1966 $groups = [];
1967 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1968 $output = $groups[1];
1969 }
1970 }
1971
1972 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
1973 }
1974
1975 return $output;
1976 }
1977
1986 private function checkOverwrite( Authority $performer ) {
1987 // First check whether the local file can be overwritten
1988 $file = $this->getLocalFile();
1989 $file->load( File::READ_LATEST );
1990 if ( $file->exists() ) {
1991 if ( !self::userCanReUpload( $performer, $file ) ) {
1992 return [ 'fileexists-forbidden', $file->getName() ];
1993 } else {
1994 return true;
1995 }
1996 }
1997
1998 $services = MediaWikiServices::getInstance();
1999
2000 /* Check shared conflicts: if the local file does not exist, but
2001 * RepoGroup::findFile finds a file, it exists in a shared repository.
2002 */
2003 $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
2004 if ( $file && !$performer->isAllowed( 'reupload-shared' )
2005 ) {
2006 return [ 'fileexists-shared-forbidden', $file->getName() ];
2007 }
2008
2009 return true;
2010 }
2011
2019 public static function userCanReUpload( Authority $performer, File $img ) {
2020 if ( $performer->isAllowed( 'reupload' ) ) {
2021 return true; // non-conditional
2022 } elseif ( !$performer->isAllowed( 'reupload-own' ) ) {
2023 return false;
2024 }
2025
2026 if ( !( $img instanceof LocalFile ) ) {
2027 return false;
2028 }
2029
2030 return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2031 }
2032
2044 public static function getExistsWarning( $file ) {
2045 if ( $file->exists() ) {
2046 return [ 'warning' => 'exists', 'file' => $file ];
2047 }
2048
2049 if ( $file->getTitle()->getArticleID() ) {
2050 return [ 'warning' => 'page-exists', 'file' => $file ];
2051 }
2052
2053 if ( !strpos( $file->getName(), '.' ) ) {
2054 $partname = $file->getName();
2055 $extension = '';
2056 } else {
2057 $n = strrpos( $file->getName(), '.' );
2058 $extension = substr( $file->getName(), $n + 1 );
2059 $partname = substr( $file->getName(), 0, $n );
2060 }
2061 $normalizedExtension = File::normalizeExtension( $extension );
2062 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2063
2064 if ( $normalizedExtension != $extension ) {
2065 // We're not using the normalized form of the extension.
2066 // Normal form is lowercase, using most common of alternate
2067 // extensions (eg 'jpg' rather than 'JPEG').
2068
2069 // Check for another file using the normalized form...
2070 $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2071 $file_lc = $localRepo->newFile( $nt_lc );
2072
2073 if ( $file_lc->exists() ) {
2074 return [
2075 'warning' => 'exists-normalized',
2076 'file' => $file,
2077 'normalizedFile' => $file_lc
2078 ];
2079 }
2080 }
2081
2082 // Check for files with the same name but a different extension
2083 $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2084 if ( count( $similarFiles ) ) {
2085 return [
2086 'warning' => 'exists-normalized',
2087 'file' => $file,
2088 'normalizedFile' => $similarFiles[0],
2089 ];
2090 }
2091
2092 if ( self::isThumbName( $file->getName() ) ) {
2093 # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2094 $nt_thb = Title::newFromText(
2095 substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2096 NS_FILE
2097 );
2098 $file_thb = $localRepo->newFile( $nt_thb );
2099 if ( $file_thb->exists() ) {
2100 return [
2101 'warning' => 'thumb',
2102 'file' => $file,
2103 'thumbFile' => $file_thb
2104 ];
2105 } else {
2106 // File does not exist, but we just don't like the name
2107 return [
2108 'warning' => 'thumb-name',
2109 'file' => $file,
2110 'thumbFile' => $file_thb
2111 ];
2112 }
2113 }
2114
2115 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2116 if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
2117 return [
2118 'warning' => 'bad-prefix',
2119 'file' => $file,
2120 'prefix' => $prefix
2121 ];
2122 }
2123 }
2124
2125 return false;
2126 }
2127
2133 public static function isThumbName( $filename ) {
2134 $n = strrpos( $filename, '.' );
2135 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2136
2137 return (
2138 substr( $partname, 3, 3 ) == 'px-' ||
2139 substr( $partname, 2, 3 ) == 'px-'
2140 ) &&
2141 preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2142 }
2143
2149 public static function getFilenamePrefixBlacklist() {
2150 $list = [];
2151 $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2152 if ( !$message->isDisabled() ) {
2153 $lines = explode( "\n", $message->plain() );
2154 foreach ( $lines as $line ) {
2155 // Remove comment lines
2156 $comment = substr( trim( $line ), 0, 1 );
2157 if ( $comment == '#' || $comment == '' ) {
2158 continue;
2159 }
2160 // Remove additional comments after a prefix
2161 $comment = strpos( $line, '#' );
2162 if ( $comment > 0 ) {
2163 $line = substr( $line, 0, $comment - 1 );
2164 }
2165 $list[] = trim( $line );
2166 }
2167 }
2168
2169 return $list;
2170 }
2171
2183 public function getImageInfo( $result ) {
2184 $localFile = $this->getLocalFile();
2185 $stashFile = $this->getStashFile();
2186 // Calling a different API module depending on whether the file was stashed is less than optimal.
2187 // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
2188 if ( $stashFile ) {
2190 $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_fill_keys( $imParam, true ), $result );
2191 } else {
2193 $info = ApiQueryImageInfo::getInfo( $localFile, array_fill_keys( $imParam, true ), $result );
2194 }
2195
2196 return $info;
2197 }
2198
2203 public function convertVerifyErrorToStatus( $error ) {
2204 $code = $error['status'];
2205 unset( $code['status'] );
2206
2207 return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2208 }
2209
2217 public static function getMaxUploadSize( $forType = null ) {
2218 $maxUploadSize = MediaWikiServices::getInstance()->getMainConfig()->get( 'MaxUploadSize' );
2219
2220 if ( is_array( $maxUploadSize ) ) {
2221 if ( $forType !== null && isset( $maxUploadSize[$forType] ) ) {
2222 return $maxUploadSize[$forType];
2223 } else {
2224 return $maxUploadSize['*'];
2225 }
2226 } else {
2227 return intval( $maxUploadSize );
2228 }
2229 }
2230
2238 public static function getMaxPhpUploadSize() {
2239 $phpMaxFileSize = wfShorthandToInteger(
2240 ini_get( 'upload_max_filesize' ),
2241 PHP_INT_MAX
2242 );
2243 $phpMaxPostSize = wfShorthandToInteger(
2244 ini_get( 'post_max_size' ),
2245 PHP_INT_MAX
2246 ) ?: PHP_INT_MAX;
2247 return min( $phpMaxFileSize, $phpMaxPostSize );
2248 }
2249
2261 public static function getSessionStatus( UserIdentity $user, $statusKey ) {
2262 $store = self::getUploadSessionStore();
2263 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2264
2265 return $store->get( $key );
2266 }
2267
2280 public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
2281 $store = self::getUploadSessionStore();
2282 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2283
2284 if ( $value === false ) {
2285 $store->delete( $key );
2286 } else {
2287 $store->set( $key, $value, $store::TTL_DAY );
2288 }
2289 }
2290
2297 private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
2298 return $store->makeKey(
2299 'uploadstatus',
2300 $user->getId() ?: md5( $user->getName() ),
2301 $statusKey
2302 );
2303 }
2304
2308 private static function getUploadSessionStore() {
2309 return MediaWikiServices::getInstance()->getMainObjectStash();
2310 }
2311}
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
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgOut
Definition Setup.php:927
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition Setup.php:927
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:287
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:67
wasDeleted()
Was this file ever deleted from the wiki?
Definition File.php:2082
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:1262
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:47
getDBkey()
Get the main part with underscores.
Definition Title.php:1056
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition Title.php:367
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...
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 sensible.
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.
string false $mSVGNSError
checkAgainstExistingDupes( $hash, $ignoreLocalDupes)
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:68
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
$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