MediaWiki REL1_35
UploadBase.php
Go to the documentation of this file.
1<?php
24use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
28
45abstract class UploadBase {
46 use ProtectedHookAccessorTrait;
47
49 protected $mTempPath;
51 protected $tempFileObj;
55 protected $mDestName;
59 protected $mSourceType;
61 protected $mTitle = false;
63 protected $mTitleError = 0;
65 protected $mFilteredName;
69 protected $mLocalFile;
71 protected $mStashFile;
73 protected $mFileSize;
75 protected $mFileProps;
79 protected $mJavaDetected;
81 protected $mSVGNSError;
82
83 protected static $safeXmlEncodings = [
84 'UTF-8',
85 'ISO-8859-1',
86 'ISO-8859-2',
87 'UTF-16',
88 'UTF-32',
89 'WINDOWS-1250',
90 'WINDOWS-1251',
91 'WINDOWS-1252',
92 'WINDOWS-1253',
93 'WINDOWS-1254',
94 'WINDOWS-1255',
95 'WINDOWS-1256',
96 'WINDOWS-1257',
97 'WINDOWS-1258',
98 ];
99
100 public const SUCCESS = 0;
101 public const OK = 0;
102 public const EMPTY_FILE = 3;
103 public const MIN_LENGTH_PARTNAME = 4;
104 public const ILLEGAL_FILENAME = 5;
105 public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
106 public const FILETYPE_MISSING = 8;
107 public const FILETYPE_BADTYPE = 9;
108 public const VERIFICATION_ERROR = 10;
109 public const HOOK_ABORTED = 11;
110 public const FILE_TOO_LARGE = 12;
111 public const WINDOWS_NONASCII_FILENAME = 13;
112 public const FILENAME_TOO_LONG = 14;
113
118 public function getVerificationErrorCode( $error ) {
119 $code_to_status = [
120 self::EMPTY_FILE => 'empty-file',
121 self::FILE_TOO_LARGE => 'file-too-large',
122 self::FILETYPE_MISSING => 'filetype-missing',
123 self::FILETYPE_BADTYPE => 'filetype-banned',
124 self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
125 self::ILLEGAL_FILENAME => 'illegal-filename',
126 self::OVERWRITE_EXISTING_FILE => 'overwrite',
127 self::VERIFICATION_ERROR => 'verification-error',
128 self::HOOK_ABORTED => 'hookaborted',
129 self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
130 self::FILENAME_TOO_LONG => 'filename-toolong',
131 ];
132 return $code_to_status[$error] ?? 'unknown-error';
133 }
134
141 public static function isEnabled() {
142 global $wgEnableUploads;
143
144 return $wgEnableUploads && wfIniGetBool( 'file_uploads' );
145 }
146
155 public static function isAllowed( UserIdentity $user ) {
156 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
157 foreach ( [ 'upload', 'edit' ] as $permission ) {
158 if ( !$permissionManager->userHasRight( $user, $permission ) ) {
159 return $permission;
160 }
161 }
162
163 return true;
164 }
165
172 public static function isThrottled( $user ) {
173 return $user->pingLimiter( 'upload' );
174 }
175
176 // Upload handlers. Should probably just be a global.
177 private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
178
186 public static function createFromRequest( &$request, $type = null ) {
187 $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
188
189 if ( !$type ) {
190 return null;
191 }
192
193 // Get the upload class
194 $type = ucfirst( $type );
195
196 // Give hooks the chance to handle this request
198 $className = null;
199 Hooks::runner()->onUploadCreateFromRequest( $type, $className );
200 if ( $className === null ) {
201 $className = 'UploadFrom' . $type;
202 wfDebug( __METHOD__ . ": class name: $className" );
203 if ( !in_array( $type, self::$uploadHandlers ) ) {
204 return null;
205 }
206 }
207
208 // Check whether this upload class is enabled
209 if ( !$className::isEnabled() ) {
210 return null;
211 }
212
213 // Check whether the request is valid
214 if ( !$className::isValidRequest( $request ) ) {
215 return null;
216 }
217
219 $handler = new $className;
220
221 $handler->initializeFromRequest( $request );
222
223 return $handler;
224 }
225
231 public static function isValidRequest( $request ) {
232 return false;
233 }
234
238 public function __construct() {
239 }
240
248 public function getSourceType() {
249 return null;
250 }
251
259 public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
260 $this->mDesiredDestName = $name;
261 if ( FileBackend::isStoragePath( $tempPath ) ) {
262 throw new MWException( __METHOD__ . " given storage path `$tempPath`." );
263 }
264
265 $this->setTempFile( $tempPath, $fileSize );
266 $this->mRemoveTempFile = $removeTempFile;
267 }
268
274 abstract public function initializeFromRequest( &$request );
275
280 protected function setTempFile( $tempPath, $fileSize = null ) {
281 $this->mTempPath = $tempPath ?? '';
282 $this->mFileSize = $fileSize ?: null;
283 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
284 $this->tempFileObj = new TempFSFile( $this->mTempPath );
285 if ( !$fileSize ) {
286 $this->mFileSize = filesize( $this->mTempPath );
287 }
288 } else {
289 $this->tempFileObj = null;
290 }
291 }
292
298 public function fetchFile() {
299 return Status::newGood();
300 }
301
306 public function isEmptyFile() {
307 return empty( $this->mFileSize );
308 }
309
314 public function getFileSize() {
315 return $this->mFileSize;
316 }
317
323 public function getTempFileSha1Base36() {
324 return FSFile::getSha1Base36FromPath( $this->mTempPath );
325 }
326
331 public function getRealPath( $srcPath ) {
332 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
333 if ( FileRepo::isVirtualUrl( $srcPath ) ) {
337 $tmpFile = $repo->getLocalCopy( $srcPath );
338 if ( $tmpFile ) {
339 $tmpFile->bind( $this ); // keep alive with $this
340 }
341 $path = $tmpFile ? $tmpFile->getPath() : false;
342 } else {
343 $path = $srcPath;
344 }
345
346 return $path;
347 }
348
366 public function verifyUpload() {
370 if ( $this->isEmptyFile() ) {
371 return [ 'status' => self::EMPTY_FILE ];
372 }
373
377 $maxSize = self::getMaxUploadSize( $this->getSourceType() );
378 if ( $this->mFileSize > $maxSize ) {
379 return [
380 'status' => self::FILE_TOO_LARGE,
381 'max' => $maxSize,
382 ];
383 }
384
390 $verification = $this->verifyFile();
391 if ( $verification !== true ) {
392 return [
393 'status' => self::VERIFICATION_ERROR,
394 'details' => $verification
395 ];
396 }
397
401 $result = $this->validateName();
402 if ( $result !== true ) {
403 return $result;
404 }
405
406 return [ 'status' => self::OK ];
407 }
408
415 public function validateName() {
416 $nt = $this->getTitle();
417 if ( $nt === null ) {
418 $result = [ 'status' => $this->mTitleError ];
419 if ( $this->mTitleError == self::ILLEGAL_FILENAME ) {
420 $result['filtered'] = $this->mFilteredName;
421 }
422 if ( $this->mTitleError == self::FILETYPE_BADTYPE ) {
423 $result['finalExt'] = $this->mFinalExtension;
424 if ( count( $this->mBlackListedExtensions ) ) {
425 $result['blacklistedExt'] = $this->mBlackListedExtensions;
426 }
427 }
428
429 return $result;
430 }
431 $this->mDestName = $this->getLocalFile()->getName();
432
433 return true;
434 }
435
445 protected function verifyMimeType( $mime ) {
447 if ( $wgVerifyMimeType ) {
448 wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" );
451 return [ 'filetype-badmime', $mime ];
452 }
453
454 if ( $wgVerifyMimeTypeIE ) {
455 # Check what Internet Explorer would detect
456 $fp = fopen( $this->mTempPath, 'rb' );
457 $chunk = fread( $fp, 256 );
458 fclose( $fp );
459
460 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
461 $extMime = $magic->getMimeTypeFromExtensionOrNull( $this->mFinalExtension );
462 $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
463 foreach ( $ieTypes as $ieType ) {
464 if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) {
465 return [ 'filetype-bad-ie-mime', $ieType ];
466 }
467 }
468 }
469 }
470
471 return true;
472 }
473
479 protected function verifyFile() {
481
482 $status = $this->verifyPartialFile();
483 if ( $status !== true ) {
484 return $status;
485 }
486
487 $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
488 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
489 $mime = $this->mFileProps['mime'];
490
491 if ( $wgVerifyMimeType ) {
492 # XXX: Missing extension will be caught by validateName() via getTitle()
493 if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) {
494 return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
495 }
496 }
497
498 # check for htmlish code and javascript
500 if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
501 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
502 if ( $svgStatus !== false ) {
503 return $svgStatus;
504 }
505 }
506 }
507
508 $handler = MediaHandler::getHandler( $mime );
509 if ( $handler ) {
510 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
511 if ( !$handlerStatus->isOK() ) {
512 $errors = $handlerStatus->getErrorsArray();
513
514 return reset( $errors );
515 }
516 }
517
518 $error = true;
519 $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
520 if ( $error !== true ) {
521 if ( !is_array( $error ) ) {
522 $error = [ $error ];
523 }
524 return $error;
525 }
526
527 wfDebug( __METHOD__ . ": all clear; passing." );
528
529 return true;
530 }
531
540 protected function verifyPartialFile() {
542
543 # getTitle() sets some internal parameters like $this->mFinalExtension
544 $this->getTitle();
545
546 $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
547 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
548
549 # check MIME type, if desired
550 $mime = $this->mFileProps['file-mime'];
551 $status = $this->verifyMimeType( $mime );
552 if ( $status !== true ) {
553 return $status;
554 }
555
556 # check for htmlish code and javascript
558 if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
559 return [ 'uploadscripted' ];
560 }
561 if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
562 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
563 if ( $svgStatus !== false ) {
564 return $svgStatus;
565 }
566 }
567 }
568
569 # Check for Java applets, which if uploaded can bypass cross-site
570 # restrictions.
571 if ( !$wgAllowJavaUploads ) {
572 $this->mJavaDetected = false;
573 $zipStatus = ZipDirectoryReader::read( $this->mTempPath,
574 [ $this, 'zipEntryCallback' ] );
575 if ( !$zipStatus->isOK() ) {
576 $errors = $zipStatus->getErrorsArray();
577 $error = reset( $errors );
578 if ( $error[0] !== 'zip-wrong-format' ) {
579 return $error;
580 }
581 }
582 if ( $this->mJavaDetected ) {
583 return [ 'uploadjava' ];
584 }
585 }
586
587 # Scan the uploaded file for viruses
588 $virus = $this->detectVirus( $this->mTempPath );
589 if ( $virus ) {
590 return [ 'uploadvirus', $virus ];
591 }
592
593 return true;
594 }
595
601 public function zipEntryCallback( $entry ) {
602 $names = [ $entry['name'] ];
603
604 // If there is a null character, cut off the name at it, because JDK's
605 // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
606 // were constructed which had ".class\0" followed by a string chosen to
607 // make the hash collide with the truncated name, that file could be
608 // returned in response to a request for the .class file.
609 $nullPos = strpos( $entry['name'], "\000" );
610 if ( $nullPos !== false ) {
611 $names[] = substr( $entry['name'], 0, $nullPos );
612 }
613
614 // If there is a trailing slash in the file name, we have to strip it,
615 // because that's what ZIP_GetEntry() does.
616 if ( preg_grep( '!\.class/?$!', $names ) ) {
617 $this->mJavaDetected = true;
618 }
619 }
620
630 public function verifyPermissions( $user ) {
631 return $this->verifyTitlePermissions( $user );
632 }
633
645 public function verifyTitlePermissions( $user ) {
650 $nt = $this->getTitle();
651 if ( $nt === null ) {
652 return true;
653 }
654 $permManager = MediaWikiServices::getInstance()->getPermissionManager();
655 $permErrors = $permManager->getPermissionErrors( 'edit', $user, $nt );
656 $permErrorsUpload = $permManager->getPermissionErrors( 'upload', $user, $nt );
657 if ( !$nt->exists() ) {
658 $permErrorsCreate = $permManager->getPermissionErrors( 'create', $user, $nt );
659 } else {
660 $permErrorsCreate = [];
661 }
662 if ( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
663 $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
664 $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) );
665
666 return $permErrors;
667 }
668
669 $overwriteError = $this->checkOverwrite( $user );
670 if ( $overwriteError !== true ) {
671 return [ $overwriteError ];
672 }
673
674 return true;
675 }
676
686 public function checkWarnings( $user = null ) {
687 if ( $user === null ) {
688 // TODO check uses and hard deprecate
689 $user = RequestContext::getMain()->getUser();
690 }
691
692 $warnings = [];
693
694 $localFile = $this->getLocalFile();
695 $localFile->load( File::READ_LATEST );
696 $filename = $localFile->getName();
697 $hash = $this->getTempFileSha1Base36();
698
699 $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
700 if ( $badFileName !== null ) {
701 $warnings['badfilename'] = $badFileName;
702 }
703
704 $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( $this->mFinalExtension );
705 if ( $unwantedFileExtensionDetails !== null ) {
706 $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
707 }
708
709 $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
710 if ( $fileSizeWarnings ) {
711 $warnings = array_merge( $warnings, $fileSizeWarnings );
712 }
713
714 $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
715 if ( $localFileExistsWarnings ) {
716 $warnings = array_merge( $warnings, $localFileExistsWarnings );
717 }
718
719 if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
720 $warnings['was-deleted'] = $filename;
721 }
722
723 // If a file with the same name exists locally then the local file has already been tested
724 // for duplication of content
725 $ignoreLocalDupes = isset( $warnings['exists'] );
726 $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
727 if ( $dupes ) {
728 $warnings['duplicate'] = $dupes;
729 }
730
731 $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
732 if ( $archivedDupes !== null ) {
733 $warnings['duplicate-archive'] = $archivedDupes;
734 }
735
736 return $warnings;
737 }
738
750 public static function makeWarningsSerializable( $warnings ) {
751 array_walk_recursive( $warnings, function ( &$param, $key ) {
752 if ( $param instanceof File ) {
753 $param = [
754 'fileName' => $param->getName(),
755 'timestamp' => $param->getTimestamp()
756 ];
757 } elseif ( is_object( $param ) ) {
758 throw new InvalidArgumentException(
759 'UploadBase::makeWarningsSerializable: ' .
760 'Unexpected object of class ' . get_class( $param ) );
761 }
762 } );
763 return $warnings;
764 }
765
775 private function checkBadFileName( $filename, $desiredFileName ) {
776 $comparableName = str_replace( ' ', '_', $desiredFileName );
777 $comparableName = Title::capitalize( $comparableName, NS_FILE );
778
779 if ( $desiredFileName != $filename && $comparableName != $filename ) {
780 return $filename;
781 }
782
783 return null;
784 }
785
794 private function checkUnwantedFileExtensions( $fileExtension ) {
796
798 $extensions = array_unique( $wgFileExtensions );
799 if ( !$this->checkFileExtension( $fileExtension, $extensions ) ) {
800 return [
801 $fileExtension,
802 $wgLang->commaList( $extensions ),
803 count( $extensions )
804 ];
805 }
806 }
807
808 return null;
809 }
810
816 private function checkFileSize( $fileSize ) {
818
819 $warnings = [];
820
821 if ( $wgUploadSizeWarning && ( $fileSize > $wgUploadSizeWarning ) ) {
822 $warnings['large-file'] = [
824 Message::sizeParam( $fileSize ),
825 ];
826 }
827
828 if ( $fileSize == 0 ) {
829 $warnings['empty-file'] = true;
830 }
831
832 return $warnings;
833 }
834
841 private function checkLocalFileExists( LocalFile $localFile, $hash ) {
842 $warnings = [];
843
844 $exists = self::getExistsWarning( $localFile );
845 if ( $exists !== false ) {
846 $warnings['exists'] = $exists;
847
848 // check if file is an exact duplicate of current file version
849 if ( $hash === $localFile->getSha1() ) {
850 $warnings['no-change'] = $localFile;
851 }
852
853 // check if file is an exact duplicate of older versions of this file
854 $history = $localFile->getHistory();
855 foreach ( $history as $oldFile ) {
856 if ( $hash === $oldFile->getSha1() ) {
857 $warnings['duplicate-version'][] = $oldFile;
858 }
859 }
860 }
861
862 return $warnings;
863 }
864
865 private function checkLocalFileWasDeleted( LocalFile $localFile ) {
866 return $localFile->wasDeleted() && !$localFile->exists();
867 }
868
875 private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
876 $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
877 $title = $this->getTitle();
878 foreach ( $dupes as $key => $dupe ) {
879 if (
880 ( $dupe instanceof LocalFile ) &&
881 $ignoreLocalDupes &&
882 $title->equals( $dupe->getTitle() )
883 ) {
884 unset( $dupes[$key] );
885 }
886 }
887
888 return $dupes;
889 }
890
898 private function checkAgainstArchiveDupes( $hash, User $user ) {
899 $archivedFile = new ArchivedFile( null, 0, '', $hash );
900 if ( $archivedFile->getID() > 0 ) {
901 if ( $archivedFile->userCan( File::DELETED_FILE, $user ) ) {
902 return $archivedFile->getName();
903 } else {
904 return '';
905 }
906 }
907
908 return null;
909 }
910
928 public function performUpload(
929 $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
930 ) {
931 $this->getLocalFile()->load( File::READ_LATEST );
932 $props = $this->mFileProps;
933
934 $error = null;
935 $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
936 if ( $error ) {
937 if ( !is_array( $error ) ) {
938 $error = [ $error ];
939 }
940 return Status::newFatal( ...$error );
941 }
942
943 $status = $this->getLocalFile()->upload(
944 $this->mTempPath,
945 $comment,
946 $pageText,
947 File::DELETE_SOURCE,
948 $props,
949 false,
950 $user,
951 $tags
952 );
953
954 if ( $status->isGood() ) {
955 if ( $watch ) {
957 $this->getLocalFile()->getTitle(),
958 $user,
960 $watchlistExpiry
961 );
962 }
963 $this->getHookRunner()->onUploadComplete( $this );
964
965 $this->postProcessUpload();
966 }
967
968 return $status;
969 }
970
977 public function postProcessUpload() {
978 }
979
986 public function getTitle() {
987 if ( $this->mTitle !== false ) {
988 return $this->mTitle;
989 }
990 if ( !is_string( $this->mDesiredDestName ) ) {
991 $this->mTitleError = self::ILLEGAL_FILENAME;
992 $this->mTitle = null;
993
994 return $this->mTitle;
995 }
996 /* Assume that if a user specified File:Something.jpg, this is an error
997 * and that the namespace prefix needs to be stripped of.
998 */
999 $title = Title::newFromText( $this->mDesiredDestName );
1000 if ( $title && $title->getNamespace() == NS_FILE ) {
1001 $this->mFilteredName = $title->getDBkey();
1002 } else {
1003 $this->mFilteredName = $this->mDesiredDestName;
1004 }
1005
1006 # oi_archive_name is max 255 bytes, which include a timestamp and an
1007 # exclamation mark, so restrict file name to 240 bytes.
1008 if ( strlen( $this->mFilteredName ) > 240 ) {
1009 $this->mTitleError = self::FILENAME_TOO_LONG;
1010 $this->mTitle = null;
1011
1012 return $this->mTitle;
1013 }
1014
1020 $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
1021 /* Normalize to title form before we do any further processing */
1022 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1023 if ( $nt === null ) {
1024 $this->mTitleError = self::ILLEGAL_FILENAME;
1025 $this->mTitle = null;
1026
1027 return $this->mTitle;
1028 }
1029 $this->mFilteredName = $nt->getDBkey();
1030
1035 list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
1036
1037 if ( $ext !== [] ) {
1038 $this->mFinalExtension = trim( end( $ext ) );
1039 } else {
1040 $this->mFinalExtension = '';
1041
1042 // No extension, try guessing one from the temporary file
1043 // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1044 // or incomplete test case in UploadBaseTest (T272328)
1045 if ( $this->mTempPath !== null ) {
1046 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
1047 $mime = $magic->guessMimeType( $this->mTempPath );
1048 if ( $mime !== 'unknown/unknown' ) {
1049 # Get a space separated list of extensions
1050 $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1051 if ( $mimeExt !== null ) {
1052 # Set the extension to the canonical extension
1053 $this->mFinalExtension = $mimeExt;
1054
1055 # Fix up the other variables
1056 $this->mFilteredName .= ".{$this->mFinalExtension}";
1057 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1058 $ext = [ $this->mFinalExtension ];
1059 }
1060 }
1061 }
1062 }
1063
1064 /* Don't allow users to override the blacklist (check file extension) */
1067
1068 $blackListedExtensions = $this->checkFileExtensionList( $ext, $wgFileBlacklist );
1069
1070 if ( $this->mFinalExtension == '' ) {
1071 $this->mTitleError = self::FILETYPE_MISSING;
1072 $this->mTitle = null;
1073
1074 return $this->mTitle;
1075 } elseif ( $blackListedExtensions ||
1077 !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) )
1078 ) {
1079 $this->mBlackListedExtensions = $blackListedExtensions;
1080 $this->mTitleError = self::FILETYPE_BADTYPE;
1081 $this->mTitle = null;
1082
1083 return $this->mTitle;
1084 }
1085
1086 // Windows may be broken with special characters, see T3780
1087 if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1088 && !MediaWikiServices::getInstance()->getRepoGroup()
1089 ->getLocalRepo()->backendSupportsUnicodePaths()
1090 ) {
1091 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1092 $this->mTitle = null;
1093
1094 return $this->mTitle;
1095 }
1096
1097 # If there was more than one "extension", reassemble the base
1098 # filename to prevent bogus complaints about length
1099 if ( count( $ext ) > 1 ) {
1100 $iterations = count( $ext ) - 1;
1101 for ( $i = 0; $i < $iterations; $i++ ) {
1102 $partname .= '.' . $ext[$i];
1103 }
1104 }
1105
1106 if ( strlen( $partname ) < 1 ) {
1107 $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1108 $this->mTitle = null;
1109
1110 return $this->mTitle;
1111 }
1112
1113 $this->mTitle = $nt;
1114
1115 return $this->mTitle;
1116 }
1117
1124 public function getLocalFile() {
1125 if ( $this->mLocalFile === null ) {
1126 $nt = $this->getTitle();
1127 $this->mLocalFile = $nt === null
1128 ? null
1129 : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1130 }
1131
1132 return $this->mLocalFile;
1133 }
1134
1138 public function getStashFile() {
1139 return $this->mStashFile;
1140 }
1141
1154 public function tryStashFile( User $user, $isPartial = false ) {
1155 if ( !$isPartial ) {
1156 $error = $this->runUploadStashFileHook( $user );
1157 if ( $error ) {
1158 return Status::newFatal( ...$error );
1159 }
1160 }
1161 try {
1162 $file = $this->doStashFile( $user );
1163 return Status::newGood( $file );
1164 } catch ( UploadStashException $e ) {
1165 return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1166 }
1167 }
1168
1173 protected function runUploadStashFileHook( User $user ) {
1174 $props = $this->mFileProps;
1175 $error = null;
1176 $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1177 if ( $error && !is_array( $error ) ) {
1178 $error = [ $error ];
1179 }
1180 return $error;
1181 }
1182
1202 public function stashFile( User $user = null ) {
1203 wfDeprecated( __METHOD__, '1.28' );
1204
1205 return $this->doStashFile( $user );
1206 }
1207
1215 protected function doStashFile( User $user = null ) {
1216 $stash = MediaWikiServices::getInstance()->getRepoGroup()
1217 ->getLocalRepo()->getUploadStash( $user );
1218 $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
1219 $this->mStashFile = $file;
1220
1221 return $file;
1222 }
1223
1228 public function cleanupTempFile() {
1229 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1230 // Delete when all relevant TempFSFile handles go out of scope
1231 wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1232 $this->tempFileObj->autocollect();
1233 }
1234 }
1235
1236 public function getTempPath() {
1237 return $this->mTempPath;
1238 }
1239
1249 public static function splitExtensions( $filename ) {
1250 $bits = explode( '.', $filename );
1251 $basename = array_shift( $bits );
1252
1253 return [ $basename, $bits ];
1254 }
1255
1264 public static function checkFileExtension( $ext, $list ) {
1265 return in_array( strtolower( $ext ), $list );
1266 }
1267
1276 public static function checkFileExtensionList( $ext, $list ) {
1277 return array_intersect( array_map( 'strtolower', $ext ), $list );
1278 }
1279
1287 public static function verifyExtension( $mime, $extension ) {
1288 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
1289
1290 if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) {
1291 if ( !$magic->isRecognizableExtension( $extension ) ) {
1292 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1293 "unrecognized extension '$extension', can't verify" );
1294
1295 return true;
1296 } else {
1297 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1298 "recognized extension '$extension', so probably invalid file" );
1299
1300 return false;
1301 }
1302 }
1303
1304 $match = $magic->isMatchingExtension( $extension, $mime );
1305
1306 if ( $match === null ) {
1307 if ( $magic->getTypesForExtension( $extension ) !== null ) {
1308 wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1309
1310 return false;
1311 } else {
1312 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1313
1314 return true;
1315 }
1316 } elseif ( $match === true ) {
1317 wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1318
1320 return true;
1321 } else {
1322 wfDebug( __METHOD__
1323 . ": mime type $mime mismatches file extension $extension, rejecting file" );
1324
1325 return false;
1326 }
1327 }
1328
1340 public static function detectScript( $file, $mime, $extension ) {
1341 # ugly hack: for text files, always look at the entire file.
1342 # For binary field, just check the first K.
1343
1344 $isText = strpos( $mime, 'text/' ) === 0;
1345 if ( $isText ) {
1346 $chunk = file_get_contents( $file );
1347 } else {
1348 $fp = fopen( $file, 'rb' );
1349 $chunk = fread( $fp, 1024 );
1350 fclose( $fp );
1351 }
1352
1353 $chunk = strtolower( $chunk );
1354
1355 if ( !$chunk ) {
1356 return false;
1357 }
1358
1359 # decode from UTF-16 if needed (could be used for obfuscation).
1360 if ( substr( $chunk, 0, 2 ) == "\xfe\xff" ) {
1361 $enc = 'UTF-16BE';
1362 } elseif ( substr( $chunk, 0, 2 ) == "\xff\xfe" ) {
1363 $enc = 'UTF-16LE';
1364 } else {
1365 $enc = null;
1366 }
1367
1368 if ( $enc !== null ) {
1369 $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1370 }
1371
1372 $chunk = trim( $chunk );
1373
1375 wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1376
1377 # check for HTML doctype
1378 if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1379 return true;
1380 }
1381
1382 // Some browsers will interpret obscure xml encodings as UTF-8, while
1383 // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1384 if ( $extension == 'svg' || strpos( $mime, 'image/svg' ) === 0 ) {
1385 if ( self::checkXMLEncodingMissmatch( $file ) ) {
1386 return true;
1387 }
1388 }
1389
1390 // Quick check for HTML heuristics in old IE and Safari.
1391 //
1392 // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1393 // don't need them all here as it can cause many false positives.
1394 //
1395 // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1396 $tags = [
1397 '<body',
1398 '<head',
1399 '<html', # also in safari
1400 '<script', # also in safari
1401 ];
1402
1403 foreach ( $tags as $tag ) {
1404 if ( strpos( $chunk, $tag ) !== false ) {
1405 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1406
1407 return true;
1408 }
1409 }
1410
1411 /*
1412 * look for JavaScript
1413 */
1414
1415 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1416 $chunk = Sanitizer::decodeCharReferences( $chunk );
1417
1418 # look for script-types
1419 if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
1420 wfDebug( __METHOD__ . ": found script types" );
1421
1422 return true;
1423 }
1424
1425 # look for html-style script-urls
1426 if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1427 wfDebug( __METHOD__ . ": found html-style script urls" );
1428
1429 return true;
1430 }
1431
1432 # look for css-style script-urls
1433 if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1434 wfDebug( __METHOD__ . ": found css-style script urls" );
1435
1436 return true;
1437 }
1438
1439 wfDebug( __METHOD__ . ": no scripts found" );
1440
1441 return false;
1442 }
1443
1451 public static function checkXMLEncodingMissmatch( $file ) {
1452 global $wgSVGMetadataCutoff;
1453 $contents = file_get_contents( $file, false, null, 0, $wgSVGMetadataCutoff );
1454 $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1455
1456 if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1457 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1458 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1459 ) {
1460 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1461
1462 return true;
1463 }
1464 } elseif ( preg_match( "!<\?xml\b!si", $contents ) ) {
1465 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1466 // bytes. There shouldn't be a legitimate reason for this to happen.
1467 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1468
1469 return true;
1470 } elseif ( substr( $contents, 0, 4 ) == "\x4C\x6F\xA7\x94" ) {
1471 // EBCDIC encoded XML
1472 wfDebug( __METHOD__ . ": EBCDIC Encoded XML" );
1473
1474 return true;
1475 }
1476
1477 // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to
1478 // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings
1479 $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1480 foreach ( $attemptEncodings as $encoding ) {
1481 Wikimedia\suppressWarnings();
1482 $str = iconv( $encoding, 'UTF-8', $contents );
1483 Wikimedia\restoreWarnings();
1484 if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1485 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1486 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1487 ) {
1488 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1489
1490 return true;
1491 }
1492 } elseif ( $str != '' && preg_match( "!<\?xml\b!si", $str ) ) {
1493 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1494 // bytes. There shouldn't be a legitimate reason for this to happen.
1495 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1496
1497 return true;
1498 }
1499 }
1500
1501 return false;
1502 }
1503
1509 protected function detectScriptInSvg( $filename, $partial ) {
1510 $this->mSVGNSError = false;
1511 $check = new XmlTypeCheck(
1512 $filename,
1513 [ $this, 'checkSvgScriptCallback' ],
1514 true,
1515 [
1516 'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ],
1517 'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ],
1518 ]
1519 );
1520 if ( $check->wellFormed !== true ) {
1521 // Invalid xml (T60553)
1522 // But only when non-partial (T67724)
1523 return $partial ? false : [ 'uploadinvalidxml' ];
1524 } elseif ( $check->filterMatch ) {
1525 if ( $this->mSVGNSError ) {
1526 return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1527 }
1528
1529 return $check->filterMatchType;
1530 }
1531
1532 return false;
1533 }
1534
1541 public static function checkSvgPICallback( $target, $data ) {
1542 // Don't allow external stylesheets (T59550)
1543 if ( preg_match( '/xml-stylesheet/i', $target ) ) {
1544 return [ 'upload-scripted-pi-callback' ];
1545 }
1546
1547 return false;
1548 }
1549
1561 public static function checkSvgExternalDTD( $type, $publicId, $systemId ) {
1562 // This doesn't include the XHTML+MathML+SVG doctype since we don't
1563 // allow XHTML anyways.
1564 $allowedDTDs = [
1565 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1566 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1567 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1568 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1569 // https://phabricator.wikimedia.org/T168856
1570 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1571 ];
1572 if ( $type !== 'PUBLIC'
1573 || !in_array( $systemId, $allowedDTDs )
1574 || strpos( $publicId, "-//W3C//" ) !== 0
1575 ) {
1576 return [ 'upload-scripted-dtd' ];
1577 }
1578 return false;
1579 }
1580
1588 public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1589 list( $namespace, $strippedElement ) = $this->splitXmlNamespace( $element );
1590
1591 // We specifically don't include:
1592 // http://www.w3.org/1999/xhtml (T62771)
1593 static $validNamespaces = [
1594 '',
1595 'adobe:ns:meta/',
1596 'http://creativecommons.org/ns#',
1597 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1598 'http://ns.adobe.com/adobeillustrator/10.0/',
1599 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1600 'http://ns.adobe.com/extensibility/1.0/',
1601 'http://ns.adobe.com/flows/1.0/',
1602 'http://ns.adobe.com/illustrator/1.0/',
1603 'http://ns.adobe.com/imagereplacement/1.0/',
1604 'http://ns.adobe.com/pdf/1.3/',
1605 'http://ns.adobe.com/photoshop/1.0/',
1606 'http://ns.adobe.com/saveforweb/1.0/',
1607 'http://ns.adobe.com/variables/1.0/',
1608 'http://ns.adobe.com/xap/1.0/',
1609 'http://ns.adobe.com/xap/1.0/g/',
1610 'http://ns.adobe.com/xap/1.0/g/img/',
1611 'http://ns.adobe.com/xap/1.0/mm/',
1612 'http://ns.adobe.com/xap/1.0/rights/',
1613 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1614 'http://ns.adobe.com/xap/1.0/stype/font#',
1615 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1616 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1617 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1618 'http://ns.adobe.com/xap/1.0/t/pg/',
1619 'http://purl.org/dc/elements/1.1/',
1620 'http://purl.org/dc/elements/1.1',
1621 'http://schemas.microsoft.com/visio/2003/svgextensions/',
1622 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1623 'http://taptrix.com/inkpad/svg_extensions',
1624 'http://web.resource.org/cc/',
1625 'http://www.freesoftware.fsf.org/bkchem/cdml',
1626 'http://www.inkscape.org/namespaces/inkscape',
1627 'http://www.opengis.net/gml',
1628 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1629 'http://www.w3.org/2000/svg',
1630 'http://www.w3.org/tr/rec-rdf-syntax/',
1631 'http://www.w3.org/2000/01/rdf-schema#',
1632 ];
1633
1634 // Inkscape mangles namespace definitions created by Adobe Illustrator.
1635 // This is nasty but harmless. (T144827)
1636 $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1637
1638 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1639 wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file." );
1641 $this->mSVGNSError = $namespace;
1642
1643 return true;
1644 }
1645
1646 /*
1647 * check for elements that can contain javascript
1648 */
1649 if ( $strippedElement == 'script' ) {
1650 wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file." );
1651
1652 return [ 'uploaded-script-svg', $strippedElement ];
1653 }
1654
1655 # e.g., <svg xmlns="http://www.w3.org/2000/svg">
1656 # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1657 if ( $strippedElement == 'handler' ) {
1658 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1659
1660 return [ 'uploaded-script-svg', $strippedElement ];
1661 }
1662
1663 # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1664 if ( $strippedElement == 'stylesheet' ) {
1665 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1666
1667 return [ 'uploaded-script-svg', $strippedElement ];
1668 }
1669
1670 # Block iframes, in case they pass the namespace check
1671 if ( $strippedElement == 'iframe' ) {
1672 wfDebug( __METHOD__ . ": iframe in uploaded file." );
1673
1674 return [ 'uploaded-script-svg', $strippedElement ];
1675 }
1676
1677 # Check <style> css
1678 if ( $strippedElement == 'style'
1679 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1680 ) {
1681 wfDebug( __METHOD__ . ": hostile css in style element." );
1682 return [ 'uploaded-hostile-svg' ];
1683 }
1684
1685 foreach ( $attribs as $attrib => $value ) {
1686 $stripped = $this->stripXmlNamespace( $attrib );
1687 $value = strtolower( $value );
1688
1689 if ( substr( $stripped, 0, 2 ) == 'on' ) {
1690 wfDebug( __METHOD__
1691 . ": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1692
1693 return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1694 }
1695
1696 # Do not allow relative links, or unsafe url schemas.
1697 # For <a> tags, only data:, http: and https: and same-document
1698 # fragment links are allowed. For all other tags, only data:
1699 # and fragment are allowed.
1700 if ( $stripped == 'href'
1701 && $value !== ''
1702 && strpos( $value, 'data:' ) !== 0
1703 && strpos( $value, '#' ) !== 0
1704 ) {
1705 if ( !( $strippedElement === 'a'
1706 && preg_match( '!^https?://!i', $value ) )
1707 ) {
1708 wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1709 . "'$attrib'='$value' in uploaded file." );
1710
1711 return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1712 }
1713 }
1714
1715 # only allow data: targets that should be safe. This prevents vectors like,
1716 # image/svg, text/xml, application/xml, and text/html, which can contain scripts
1717 if ( $stripped == 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1718 // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
1719 // phpcs:ignore Generic.Files.LineLength
1720 $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1721
1722 if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1723 wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
1724 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1725 return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1726 }
1727 }
1728
1729 # Change href with animate from (http://html5sec.org/#137).
1730 if ( $stripped === 'attributename'
1731 && $strippedElement === 'animate'
1732 && $this->stripXmlNamespace( $value ) == 'href'
1733 ) {
1734 wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1735 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1736
1737 return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1738 }
1739
1740 # use set/animate to add event-handler attribute to parent
1741 if ( ( $strippedElement == 'set' || $strippedElement == 'animate' )
1742 && $stripped == 'attributename'
1743 && substr( $value, 0, 2 ) == 'on'
1744 ) {
1745 wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1746 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1747
1748 return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1749 }
1750
1751 # use set to add href attribute to parent element
1752 if ( $strippedElement == 'set'
1753 && $stripped == 'attributename'
1754 && strpos( $value, 'href' ) !== false
1755 ) {
1756 wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file." );
1757
1758 return [ 'uploaded-setting-href-svg' ];
1759 }
1760
1761 # use set to add a remote / data / script target to an element
1762 if ( $strippedElement == 'set'
1763 && $stripped == 'to'
1764 && preg_match( '!(http|https|data|script):!sim', $value )
1765 ) {
1766 wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file." );
1767
1768 return [ 'uploaded-wrong-setting-svg', $value ];
1769 }
1770
1771 # use handler attribute with remote / data / script
1772 if ( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
1773 wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1774 . "'$attrib'='$value' in uploaded file." );
1775
1776 return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1777 }
1778
1779 # use CSS styles to bring in remote code
1780 if ( $stripped == 'style'
1781 && self::checkCssFragment( Sanitizer::normalizeCss( $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 # Several attributes can include css, css character escaping isn't allowed
1789 $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1790 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1791 if ( in_array( $stripped, $cssAttrs )
1792 && self::checkCssFragment( $value )
1793 ) {
1794 wfDebug( __METHOD__ . ": Found svg setting a style with "
1795 . "remote url '$attrib'='$value' in uploaded file." );
1796 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1797 }
1798
1799 # image filters can pull in url, which could be svg that executes scripts
1800 # Only allow url( "#foo" ). Do not allow url( http://example.com )
1801 if ( $strippedElement == 'image'
1802 && $stripped == 'filter'
1803 && preg_match( '!url\s*\‍(\s*["\']?[^#]!sim', $value )
1804 ) {
1805 wfDebug( __METHOD__ . ": Found image filter with url: "
1806 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1807
1808 return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1809 }
1810 }
1811
1812 return false; // No scripts detected
1813 }
1814
1821 private static function checkCssFragment( $value ) {
1822 # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1823 if ( stripos( $value, '@import' ) !== false ) {
1824 return true;
1825 }
1826
1827 # We allow @font-face to embed fonts with data: urls, so we snip the string
1828 # 'url' out so this case won't match when we check for urls below
1829 $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1830 $value = preg_replace( $pattern, '$1$2', $value );
1831
1832 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1833 # properties filter and accelerator don't seem to be useful for xss in SVG files.
1834 # Expression and -o-link don't seem to work either, but filtering them here in case.
1835 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1836 # but not local ones such as url("#..., url('#..., url(#....
1837 if ( preg_match( '!expression
1838 | -o-link\s*:
1839 | -o-link-source\s*:
1840 | -o-replace\s*:!imx', $value ) ) {
1841 return true;
1842 }
1843
1844 if ( preg_match_all(
1845 "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1846 $value,
1847 $matches
1848 ) !== 0
1849 ) {
1850 # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1851 foreach ( $matches[1] as $match ) {
1852 if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1853 return true;
1854 }
1855 }
1856 }
1857
1858 if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1859 return true;
1860 }
1861
1862 return false;
1863 }
1864
1870 private static function splitXmlNamespace( $element ) {
1871 // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1872 $parts = explode( ':', strtolower( $element ) );
1873 $name = array_pop( $parts );
1874 $ns = implode( ':', $parts );
1875
1876 return [ $ns, $name ];
1877 }
1878
1883 private function stripXmlNamespace( $name ) {
1884 // 'http://www.w3.org/2000/svg:script' -> 'script'
1885 $parts = explode( ':', strtolower( $name ) );
1886
1887 return array_pop( $parts );
1888 }
1889
1900 public static function detectVirus( $file ) {
1902
1903 if ( !$wgAntivirus ) {
1904 wfDebug( __METHOD__ . ": virus scanner disabled" );
1905
1906 return null;
1907 }
1908
1910 wfDebug( __METHOD__ . ": unknown virus scanner: $wgAntivirus" );
1911 $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1912 [ 'virus-badscanner', $wgAntivirus ] );
1913
1914 return wfMessage( 'virus-unknownscanner' )->text() . " $wgAntivirus";
1915 }
1916
1917 # look up scanner configuration
1919 $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]['codemap'];
1920 $msgPattern = $wgAntivirusSetup[$wgAntivirus]['messagepattern'] ?? null;
1921
1922 if ( strpos( $command, "%f" ) === false ) {
1923 # simple pattern: append file to scan
1924 $command .= " " . Shell::escape( $file );
1925 } else {
1926 # complex pattern: replace "%f" with file to scan
1927 $command = str_replace( "%f", Shell::escape( $file ), $command );
1928 }
1929
1930 wfDebug( __METHOD__ . ": running virus scan: $command " );
1931
1932 # execute virus scanner
1933 $exitCode = false;
1934
1935 # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1936 # that does not seem to be worth the pain.
1937 # Ask me (Duesentrieb) about it if it's ever needed.
1938 $output = wfShellExecWithStderr( $command, $exitCode );
1939
1940 # map exit code to AV_xxx constants.
1941 $mappedCode = $exitCode;
1942 if ( $exitCodeMap ) {
1943 if ( isset( $exitCodeMap[$exitCode] ) ) {
1944 $mappedCode = $exitCodeMap[$exitCode];
1945 } elseif ( isset( $exitCodeMap["*"] ) ) {
1946 $mappedCode = $exitCodeMap["*"];
1947 }
1948 }
1949
1950 /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
1951 * so we need the strict equalities === and thus can't use a switch here
1952 */
1953 if ( $mappedCode === AV_SCAN_FAILED ) {
1954 # scan failed (code was mapped to false by $exitCodeMap)
1955 wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1956
1957 $output = $wgAntivirusRequired
1958 ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1959 : null;
1960 } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1961 # scan failed because filetype is unknown (probably imune)
1962 wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1963 $output = null;
1964 } elseif ( $mappedCode === AV_NO_VIRUS ) {
1965 # no virus found
1966 wfDebug( __METHOD__ . ": file passed virus scan." );
1967 $output = false;
1968 } else {
1969 $output = trim( $output );
1970
1971 if ( !$output ) {
1972 $output = true; # if there's no output, return true
1973 } elseif ( $msgPattern ) {
1974 $groups = [];
1975 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1976 $output = $groups[1];
1977 }
1978 }
1979
1980 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
1981 }
1982
1983 return $output;
1984 }
1985
1994 private function checkOverwrite( $user ) {
1995 // First check whether the local file can be overwritten
1996 $file = $this->getLocalFile();
1997 $file->load( File::READ_LATEST );
1998 if ( $file->exists() ) {
1999 if ( !self::userCanReUpload( $user, $file ) ) {
2000 return [ 'fileexists-forbidden', $file->getName() ];
2001 } else {
2002 return true;
2003 }
2004 }
2005
2006 $services = MediaWikiServices::getInstance();
2007
2008 /* Check shared conflicts: if the local file does not exist, but
2009 * RepoGroup::findFile finds a file, it exists in a shared repository.
2010 */
2011 $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
2012 if ( $file && !$services->getPermissionManager()
2013 ->userHasRight( $user, 'reupload-shared' )
2014 ) {
2015 return [ 'fileexists-shared-forbidden', $file->getName() ];
2016 }
2017
2018 return true;
2019 }
2020
2028 public static function userCanReUpload( User $user, File $img ) {
2029 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
2030 if ( $permissionManager->userHasRight( $user, 'reupload' ) ) {
2031 return true; // non-conditional
2032 } elseif ( !$permissionManager->userHasRight( $user, 'reupload-own' ) ) {
2033 return false;
2034 }
2035
2036 if ( !( $img instanceof LocalFile ) ) {
2037 return false;
2038 }
2039
2040 $img->load();
2041
2042 return $user->getId() == $img->getUser( 'id' );
2043 }
2044
2056 public static function getExistsWarning( $file ) {
2057 if ( $file->exists() ) {
2058 return [ 'warning' => 'exists', 'file' => $file ];
2059 }
2060
2061 if ( $file->getTitle()->getArticleID() ) {
2062 return [ 'warning' => 'page-exists', 'file' => $file ];
2063 }
2064
2065 if ( strpos( $file->getName(), '.' ) == false ) {
2066 $partname = $file->getName();
2067 $extension = '';
2068 } else {
2069 $n = strrpos( $file->getName(), '.' );
2070 $extension = substr( $file->getName(), $n + 1 );
2071 $partname = substr( $file->getName(), 0, $n );
2072 }
2073 $normalizedExtension = File::normalizeExtension( $extension );
2074 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2075
2076 if ( $normalizedExtension != $extension ) {
2077 // We're not using the normalized form of the extension.
2078 // Normal form is lowercase, using most common of alternate
2079 // extensions (eg 'jpg' rather than 'JPEG').
2080
2081 // Check for another file using the normalized form...
2082 $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2083 $file_lc = $localRepo->newFile( $nt_lc );
2084
2085 if ( $file_lc->exists() ) {
2086 return [
2087 'warning' => 'exists-normalized',
2088 'file' => $file,
2089 'normalizedFile' => $file_lc
2090 ];
2091 }
2092 }
2093
2094 // Check for files with the same name but a different extension
2095 $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2096 if ( count( $similarFiles ) ) {
2097 return [
2098 'warning' => 'exists-normalized',
2099 'file' => $file,
2100 'normalizedFile' => $similarFiles[0],
2101 ];
2102 }
2103
2104 if ( self::isThumbName( $file->getName() ) ) {
2105 # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2106 $nt_thb = Title::newFromText(
2107 substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2108 NS_FILE
2109 );
2110 $file_thb = $localRepo->newFile( $nt_thb );
2111 if ( $file_thb->exists() ) {
2112 return [
2113 'warning' => 'thumb',
2114 'file' => $file,
2115 'thumbFile' => $file_thb
2116 ];
2117 } else {
2118 // File does not exist, but we just don't like the name
2119 return [
2120 'warning' => 'thumb-name',
2121 'file' => $file,
2122 'thumbFile' => $file_thb
2123 ];
2124 }
2125 }
2126
2127 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2128 if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
2129 return [
2130 'warning' => 'bad-prefix',
2131 'file' => $file,
2132 'prefix' => $prefix
2133 ];
2134 }
2135 }
2136
2137 return false;
2138 }
2139
2145 public static function isThumbName( $filename ) {
2146 $n = strrpos( $filename, '.' );
2147 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2148
2149 return (
2150 substr( $partname, 3, 3 ) == 'px-' ||
2151 substr( $partname, 2, 3 ) == 'px-'
2152 ) &&
2153 preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2154 }
2155
2161 public static function getFilenamePrefixBlacklist() {
2162 $blacklist = [];
2163 $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2164 if ( !$message->isDisabled() ) {
2165 $lines = explode( "\n", $message->plain() );
2166 foreach ( $lines as $line ) {
2167 // Remove comment lines
2168 $comment = substr( trim( $line ), 0, 1 );
2169 if ( $comment == '#' || $comment == '' ) {
2170 continue;
2171 }
2172 // Remove additional comments after a prefix
2173 $comment = strpos( $line, '#' );
2174 if ( $comment > 0 ) {
2175 $line = substr( $line, 0, $comment - 1 );
2176 }
2177 $blacklist[] = trim( $line );
2178 }
2179 }
2180
2181 return $blacklist;
2182 }
2183
2195 public function getImageInfo( $result ) {
2196 $localFile = $this->getLocalFile();
2197 $stashFile = $this->getStashFile();
2198 // Calling a different API module depending on whether the file was stashed is less than optimal.
2199 // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
2200 if ( $stashFile ) {
2202 $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_flip( $imParam ), $result );
2203 } else {
2205 $info = ApiQueryImageInfo::getInfo( $localFile, array_flip( $imParam ), $result );
2206 }
2207
2208 return $info;
2209 }
2210
2215 public function convertVerifyErrorToStatus( $error ) {
2216 $code = $error['status'];
2217 unset( $code['status'] );
2218
2219 return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2220 }
2221
2229 public static function getMaxUploadSize( $forType = null ) {
2230 global $wgMaxUploadSize;
2231
2232 if ( is_array( $wgMaxUploadSize ) ) {
2233 if ( $forType !== null && isset( $wgMaxUploadSize[$forType] ) ) {
2234 return $wgMaxUploadSize[$forType];
2235 } else {
2236 return $wgMaxUploadSize['*'];
2237 }
2238 } else {
2239 return intval( $wgMaxUploadSize );
2240 }
2241 }
2242
2250 public static function getMaxPhpUploadSize() {
2251 $phpMaxFileSize = wfShorthandToInteger(
2252 ini_get( 'upload_max_filesize' ),
2253 PHP_INT_MAX
2254 );
2255 $phpMaxPostSize = wfShorthandToInteger(
2256 ini_get( 'post_max_size' ),
2257 PHP_INT_MAX
2258 ) ?: PHP_INT_MAX;
2259 return min( $phpMaxFileSize, $phpMaxPostSize );
2260 }
2261
2271 public static function getSessionStatus( User $user, $statusKey ) {
2272 $store = self::getUploadSessionStore();
2273 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2274
2275 return $store->get( $key );
2276 }
2277
2290 public static function setSessionStatus( User $user, $statusKey, $value ) {
2291 $store = self::getUploadSessionStore();
2292 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2293
2294 if ( $value === false ) {
2295 $store->delete( $key );
2296 } else {
2297 $store->set( $key, $value, $store::TTL_DAY );
2298 }
2299 }
2300
2307 private static function getUploadSessionKey( BagOStuff $store, User $user, $statusKey ) {
2308 return $store->makeKey(
2309 'uploadstatus',
2310 $user->getId() ?: md5( $user->getName() ),
2311 $statusKey
2312 );
2313 }
2314
2318 private static function getUploadSessionStore() {
2319 return ObjectCache::getInstance( 'db-replicated' );
2320 }
2321}
$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.
$wgFileBlacklist
Files with these extensions will never be allowed as uploads.
$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.
$wgMimeTypeBlacklist
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.
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.
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with '-'.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Title null $mTitle
$wgOut
Definition Setup.php:786
$wgLang
Definition Setup.php:781
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:71
makeKey( $class,... $components)
Make a cache key, scoped to this instance's keyspace.
static getSha1Base36FromPath( $path)
Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case encoding,...
Definition FSFile.php:225
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Definition FileRepo.php:283
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:63
wasDeleted()
Was this file ever deleted from the wiki?
Definition File.php:2012
Class to represent a local file in the wiki's own database.
Definition LocalFile.php:59
exists()
canRender inherited
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
getSha1()
Stable to override.
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.
Executes shell commands.
Definition Shell.php:44
static sizeParam( $size)
Definition Message.php:1108
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:42
getDBkey()
Get the main part with underscores.
Definition Title.php:1032
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition Title.php:329
UploadBase and subclasses are the backend of MediaWiki's file uploads.
__construct()
Stable to call.
getSourceType()
Returns the upload type.
checkOverwrite( $user)
Check if there's an overwrite conflict and, if so, if restrictions forbid this user from performing t...
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
int $mTitleError
string null $mRemoveTempFile
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.
verifyTitlePermissions( $user)
Check whether the user can edit, upload and create the image.
checkSvgScriptCallback( $element, $attribs, $data=null)
checkLocalFileExists(LocalFile $localFile, $hash)
getLocalFile()
Return the local file and initializes if necessary.
const SUCCESS
stripXmlNamespace( $name)
string $mTempPath
Local file system path to the file to upload (or a local copy)
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.
verifyPermissions( $user)
Alias for verifyTitlePermissions.
runUploadStashFileHook(User $user)
static getSessionStatus(User $user, $statusKey)
Get the current status of a chunked upload (used for polling)
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.
static isAllowed(UserIdentity $user)
Returns true if the user can use this upload module or else a string identifying the missing permissi...
static getFilenamePrefixBlacklist()
Get a list of blacklisted filename prefixes from [[MediaWiki:Filename-prefix-blacklist]].
const OVERWRITE_EXISTING_FILE
setTempFile( $tempPath, $fileSize=null)
static checkXMLEncodingMissmatch( $file)
Check a whitelist of xml encodings that are known not to be interpreted differently by the server's x...
static $uploadHandlers
doStashFile(User $user=null)
Implementation for stashFile() and tryStashFile().
string null $mDestName
string[] $mBlackListedExtensions
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.
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.
static checkSvgExternalDTD( $type, $publicId, $systemId)
Verify that DTD urls referenced are only the standard dtds.
getTempFileSha1Base36()
Get the base 36 SHA1 of the file Stable to override.
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.
static getUploadSessionKey(BagOStuff $store, User $user, $statusKey)
fetchFile()
Fetch the file.
checkAgainstArchiveDupes( $hash, User $user)
checkWarnings( $user=null)
Check for non fatal problems with the file.
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
checkLocalFileWasDeleted(LocalFile $localFile)
getFileSize()
Return the file size.
verifyUpload()
Verify whether the upload is sane.
stashFile(User $user=null)
If the user does not supply all necessary information in the first upload form submission (either by ...
const ILLEGAL_FILENAME
const MIN_LENGTH_PARTNAME
static checkFileExtensionList( $ext, $list)
Perform case-insensitive match against a list of file extensions.
static detectVirus( $file)
Generic wrapper function for a virus scanner program.
checkUnwantedFileExtensions( $fileExtension)
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.
static setSessionStatus(User $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling)
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:60
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2150
getId()
Get the user's ID.
Definition User.php:2121
const IGNORE_USER_RIGHTS
Definition User.php:94
static doWatch(Title $title, User $user, $checkRights=User::CHECK_USER_RIGHTS, ?string $expiry=null)
Watch a page.
static read( $fileName, $callback, $options=[])
Read a ZIP file and call a function for each file discovered in it.
const AV_SCAN_FAILED
Definition Defines.php:104
const NS_FILE
Definition Defines.php:76
const AV_SCAN_ABORTED
Definition Defines.php:103
const AV_NO_VIRUS
Definition Defines.php:101
Interface for objects representing user identity.
$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