MediaWiki REL1_34
UploadBase.php
Go to the documentation of this file.
1<?php
27
42abstract class UploadBase {
44 protected $mTempPath;
46 protected $tempFileObj;
50 protected $mDestName;
54 protected $mSourceType;
56 protected $mTitle = false;
58 protected $mTitleError = 0;
60 protected $mFilteredName;
64 protected $mLocalFile;
66 protected $mStashFile;
68 protected $mFileSize;
70 protected $mFileProps;
74 protected $mJavaDetected;
76 protected $mSVGNSError;
77
78 protected static $safeXmlEncodings = [
79 'UTF-8',
80 'ISO-8859-1',
81 'ISO-8859-2',
82 'UTF-16',
83 'UTF-32',
84 'WINDOWS-1250',
85 'WINDOWS-1251',
86 'WINDOWS-1252',
87 'WINDOWS-1253',
88 'WINDOWS-1254',
89 'WINDOWS-1255',
90 'WINDOWS-1256',
91 'WINDOWS-1257',
92 'WINDOWS-1258',
93 ];
94
95 const SUCCESS = 0;
96 const OK = 0;
97 const EMPTY_FILE = 3;
100 const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
104 const HOOK_ABORTED = 11;
105 const FILE_TOO_LARGE = 12;
108
113 public function getVerificationErrorCode( $error ) {
114 $code_to_status = [
115 self::EMPTY_FILE => 'empty-file',
116 self::FILE_TOO_LARGE => 'file-too-large',
117 self::FILETYPE_MISSING => 'filetype-missing',
118 self::FILETYPE_BADTYPE => 'filetype-banned',
119 self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
120 self::ILLEGAL_FILENAME => 'illegal-filename',
121 self::OVERWRITE_EXISTING_FILE => 'overwrite',
122 self::VERIFICATION_ERROR => 'verification-error',
123 self::HOOK_ABORTED => 'hookaborted',
124 self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
125 self::FILENAME_TOO_LONG => 'filename-toolong',
126 ];
127 return $code_to_status[$error] ?? 'unknown-error';
128 }
129
135 public static function isEnabled() {
136 global $wgEnableUploads;
137
138 if ( !$wgEnableUploads ) {
139 return false;
140 }
141
142 # Check php's file_uploads setting
143 return wfIsHHVM() || wfIniGetBool( 'file_uploads' );
144 }
145
154 public static function isAllowed( UserIdentity $user ) {
155 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
156 foreach ( [ 'upload', 'edit' ] as $permission ) {
157 if ( !$permissionManager->userHasRight( $user, $permission ) ) {
158 return $permission;
159 }
160 }
161
162 return true;
163 }
164
171 public static function isThrottled( $user ) {
172 return $user->pingLimiter( 'upload' );
173 }
174
175 // Upload handlers. Should probably just be a global.
176 private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
177
185 public static function createFromRequest( &$request, $type = null ) {
186 $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
187
188 if ( !$type ) {
189 return null;
190 }
191
192 // Get the upload class
193 $type = ucfirst( $type );
194
195 // Give hooks the chance to handle this request
196 $className = null;
197 Hooks::run( 'UploadCreateFromRequest', [ $type, &$className ] );
198 if ( is_null( $className ) ) {
199 $className = 'UploadFrom' . $type;
200 wfDebug( __METHOD__ . ": class name: $className\n" );
201 if ( !in_array( $type, self::$uploadHandlers ) ) {
202 return null;
203 }
204 }
205
206 // Check whether this upload class is enabled
207 if ( !call_user_func( [ $className, 'isEnabled' ] ) ) {
208 return null;
209 }
210
211 // Check whether the request is valid
212 if ( !call_user_func( [ $className, 'isValidRequest' ], $request ) ) {
213 return null;
214 }
215
217 $handler = new $className;
218
219 $handler->initializeFromRequest( $request );
220
221 return $handler;
222 }
223
229 public static function isValidRequest( $request ) {
230 return false;
231 }
232
233 public function __construct() {
234 }
235
242 public function getSourceType() {
243 return null;
244 }
245
254 public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
255 $this->mDesiredDestName = $name;
256 if ( FileBackend::isStoragePath( $tempPath ) ) {
257 throw new MWException( __METHOD__ . " given storage path `$tempPath`." );
258 }
259
260 $this->setTempFile( $tempPath, $fileSize );
261 $this->mRemoveTempFile = $removeTempFile;
262 }
263
269 abstract public function initializeFromRequest( &$request );
270
275 protected function setTempFile( $tempPath, $fileSize = null ) {
276 $this->mTempPath = $tempPath;
277 $this->mFileSize = $fileSize ?: null;
278 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
279 $this->tempFileObj = new TempFSFile( $this->mTempPath );
280 if ( !$fileSize ) {
281 $this->mFileSize = filesize( $this->mTempPath );
282 }
283 } else {
284 $this->tempFileObj = null;
285 }
286 }
287
292 public function fetchFile() {
293 return Status::newGood();
294 }
295
300 public function isEmptyFile() {
301 return empty( $this->mFileSize );
302 }
303
308 public function getFileSize() {
309 return $this->mFileSize;
310 }
311
316 public function getTempFileSha1Base36() {
317 return FSFile::getSha1Base36FromPath( $this->mTempPath );
318 }
319
324 public function getRealPath( $srcPath ) {
325 $repo = RepoGroup::singleton()->getLocalRepo();
326 if ( FileRepo::isVirtualUrl( $srcPath ) ) {
330 $tmpFile = $repo->getLocalCopy( $srcPath );
331 if ( $tmpFile ) {
332 $tmpFile->bind( $this ); // keep alive with $this
333 }
334 $path = $tmpFile ? $tmpFile->getPath() : false;
335 } else {
336 $path = $srcPath;
337 }
338
339 return $path;
340 }
341
346 public function verifyUpload() {
350 if ( $this->isEmptyFile() ) {
351 return [ 'status' => self::EMPTY_FILE ];
352 }
353
357 $maxSize = self::getMaxUploadSize( $this->getSourceType() );
358 if ( $this->mFileSize > $maxSize ) {
359 return [
360 'status' => self::FILE_TOO_LARGE,
361 'max' => $maxSize,
362 ];
363 }
364
370 $verification = $this->verifyFile();
371 if ( $verification !== true ) {
372 return [
373 'status' => self::VERIFICATION_ERROR,
374 'details' => $verification
375 ];
376 }
377
381 $result = $this->validateName();
382 if ( $result !== true ) {
383 return $result;
384 }
385
386 return [ 'status' => self::OK ];
387 }
388
395 public function validateName() {
396 $nt = $this->getTitle();
397 if ( is_null( $nt ) ) {
398 $result = [ 'status' => $this->mTitleError ];
399 if ( $this->mTitleError == self::ILLEGAL_FILENAME ) {
400 $result['filtered'] = $this->mFilteredName;
401 }
402 if ( $this->mTitleError == self::FILETYPE_BADTYPE ) {
403 $result['finalExt'] = $this->mFinalExtension;
404 if ( count( $this->mBlackListedExtensions ) ) {
405 $result['blacklistedExt'] = $this->mBlackListedExtensions;
406 }
407 }
408
409 return $result;
410 }
411 $this->mDestName = $this->getLocalFile()->getName();
412
413 return true;
414 }
415
425 protected function verifyMimeType( $mime ) {
427 if ( $wgVerifyMimeType ) {
428 wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>\n" );
430 if ( $this->checkFileExtension( $mime, $wgMimeTypeBlacklist ) ) {
431 return [ 'filetype-badmime', $mime ];
432 }
433
434 if ( $wgVerifyMimeTypeIE ) {
435 # Check what Internet Explorer would detect
436 $fp = fopen( $this->mTempPath, 'rb' );
437 $chunk = fread( $fp, 256 );
438 fclose( $fp );
439
440 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
441 $extMime = $magic->guessTypesForExtension( $this->mFinalExtension );
442 $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
443 foreach ( $ieTypes as $ieType ) {
444 if ( $this->checkFileExtension( $ieType, $wgMimeTypeBlacklist ) ) {
445 return [ 'filetype-bad-ie-mime', $ieType ];
446 }
447 }
448 }
449 }
450
451 return true;
452 }
453
459 protected function verifyFile() {
461
462 $status = $this->verifyPartialFile();
463 if ( $status !== true ) {
464 return $status;
465 }
466
467 $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
468 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
469 $mime = $this->mFileProps['mime'];
470
471 if ( $wgVerifyMimeType ) {
472 # XXX: Missing extension will be caught by validateName() via getTitle()
473 if ( $this->mFinalExtension != '' && !$this->verifyExtension( $mime, $this->mFinalExtension ) ) {
474 return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
475 }
476 }
477
478 # check for htmlish code and javascript
480 if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
481 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
482 if ( $svgStatus !== false ) {
483 return $svgStatus;
484 }
485 }
486 }
487
488 $handler = MediaHandler::getHandler( $mime );
489 if ( $handler ) {
490 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
491 if ( !$handlerStatus->isOK() ) {
492 $errors = $handlerStatus->getErrorsArray();
493
494 return reset( $errors );
495 }
496 }
497
498 $error = true;
499 Hooks::run( 'UploadVerifyFile', [ $this, $mime, &$error ] );
500 if ( $error !== true ) {
501 if ( !is_array( $error ) ) {
502 $error = [ $error ];
503 }
504 return $error;
505 }
506
507 wfDebug( __METHOD__ . ": all clear; passing.\n" );
508
509 return true;
510 }
511
520 protected function verifyPartialFile() {
522
523 # getTitle() sets some internal parameters like $this->mFinalExtension
524 $this->getTitle();
525
526 $mwProps = new MWFileProps( MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer() );
527 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
528
529 # check MIME type, if desired
530 $mime = $this->mFileProps['file-mime'];
531 $status = $this->verifyMimeType( $mime );
532 if ( $status !== true ) {
533 return $status;
534 }
535
536 # check for htmlish code and javascript
538 if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
539 return [ 'uploadscripted' ];
540 }
541 if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
542 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
543 if ( $svgStatus !== false ) {
544 return $svgStatus;
545 }
546 }
547 }
548
549 # Check for Java applets, which if uploaded can bypass cross-site
550 # restrictions.
551 if ( !$wgAllowJavaUploads ) {
552 $this->mJavaDetected = false;
553 $zipStatus = ZipDirectoryReader::read( $this->mTempPath,
554 [ $this, 'zipEntryCallback' ] );
555 if ( !$zipStatus->isOK() ) {
556 $errors = $zipStatus->getErrorsArray();
557 $error = reset( $errors );
558 if ( $error[0] !== 'zip-wrong-format' ) {
559 return $error;
560 }
561 }
562 if ( $this->mJavaDetected ) {
563 return [ 'uploadjava' ];
564 }
565 }
566
567 # Scan the uploaded file for viruses
568 $virus = $this->detectVirus( $this->mTempPath );
569 if ( $virus ) {
570 return [ 'uploadvirus', $virus ];
571 }
572
573 return true;
574 }
575
581 public function zipEntryCallback( $entry ) {
582 $names = [ $entry['name'] ];
583
584 // If there is a null character, cut off the name at it, because JDK's
585 // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
586 // were constructed which had ".class\0" followed by a string chosen to
587 // make the hash collide with the truncated name, that file could be
588 // returned in response to a request for the .class file.
589 $nullPos = strpos( $entry['name'], "\000" );
590 if ( $nullPos !== false ) {
591 $names[] = substr( $entry['name'], 0, $nullPos );
592 }
593
594 // If there is a trailing slash in the file name, we have to strip it,
595 // because that's what ZIP_GetEntry() does.
596 if ( preg_grep( '!\.class/?$!', $names ) ) {
597 $this->mJavaDetected = true;
598 }
599 }
600
610 public function verifyPermissions( $user ) {
611 return $this->verifyTitlePermissions( $user );
612 }
613
625 public function verifyTitlePermissions( $user ) {
630 $nt = $this->getTitle();
631 if ( is_null( $nt ) ) {
632 return true;
633 }
634 $permErrors = $nt->getUserPermissionsErrors( 'edit', $user );
635 $permErrorsUpload = $nt->getUserPermissionsErrors( 'upload', $user );
636 if ( !$nt->exists() ) {
637 $permErrorsCreate = $nt->getUserPermissionsErrors( 'create', $user );
638 } else {
639 $permErrorsCreate = [];
640 }
641 if ( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
642 $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsUpload, $permErrors ) );
643 $permErrors = array_merge( $permErrors, wfArrayDiff2( $permErrorsCreate, $permErrors ) );
644
645 return $permErrors;
646 }
647
648 $overwriteError = $this->checkOverwrite( $user );
649 if ( $overwriteError !== true ) {
650 return [ $overwriteError ];
651 }
652
653 return true;
654 }
655
663 public function checkWarnings() {
664 $warnings = [];
665
666 $localFile = $this->getLocalFile();
667 $localFile->load( File::READ_LATEST );
668 $filename = $localFile->getName();
669 $hash = $this->getTempFileSha1Base36();
670
671 $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
672 if ( $badFileName !== null ) {
673 $warnings['badfilename'] = $badFileName;
674 }
675
676 $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( $this->mFinalExtension );
677 if ( $unwantedFileExtensionDetails !== null ) {
678 $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
679 }
680
681 $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
682 if ( $fileSizeWarnings ) {
683 $warnings = array_merge( $warnings, $fileSizeWarnings );
684 }
685
686 $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
687 if ( $localFileExistsWarnings ) {
688 $warnings = array_merge( $warnings, $localFileExistsWarnings );
689 }
690
691 if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
692 $warnings['was-deleted'] = $filename;
693 }
694
695 // If a file with the same name exists locally then the local file has already been tested
696 // for duplication of content
697 $ignoreLocalDupes = isset( $warnings[ 'exists '] );
698 $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
699 if ( $dupes ) {
700 $warnings['duplicate'] = $dupes;
701 }
702
703 $archivedDupes = $this->checkAgainstArchiveDupes( $hash );
704 if ( $archivedDupes !== null ) {
705 $warnings['duplicate-archive'] = $archivedDupes;
706 }
707
708 return $warnings;
709 }
710
722 public static function makeWarningsSerializable( $warnings ) {
723 array_walk_recursive( $warnings, function ( &$param, $key ) {
724 if ( $param instanceof File ) {
725 $param = [
726 'fileName' => $param->getName(),
727 'timestamp' => $param->getTimestamp()
728 ];
729 } elseif ( is_object( $param ) ) {
730 throw new InvalidArgumentException(
731 'UploadBase::makeWarningsSerializable: ' .
732 'Unexpected object of class ' . get_class( $param ) );
733 }
734 } );
735 return $warnings;
736 }
737
747 private function checkBadFileName( $filename, $desiredFileName ) {
748 $comparableName = str_replace( ' ', '_', $desiredFileName );
749 $comparableName = Title::capitalize( $comparableName, NS_FILE );
750
751 if ( $desiredFileName != $filename && $comparableName != $filename ) {
752 return $filename;
753 }
754
755 return null;
756 }
757
766 private function checkUnwantedFileExtensions( $fileExtension ) {
768
770 $extensions = array_unique( $wgFileExtensions );
771 if ( !$this->checkFileExtension( $fileExtension, $extensions ) ) {
772 return [
773 $fileExtension,
774 $wgLang->commaList( $extensions ),
775 count( $extensions )
776 ];
777 }
778 }
779
780 return null;
781 }
782
788 private function checkFileSize( $fileSize ) {
790
791 $warnings = [];
792
793 if ( $wgUploadSizeWarning && ( $fileSize > $wgUploadSizeWarning ) ) {
794 $warnings['large-file'] = [ $wgUploadSizeWarning, $fileSize ];
795 }
796
797 if ( $fileSize == 0 ) {
798 $warnings['empty-file'] = true;
799 }
800
801 return $warnings;
802 }
803
810 private function checkLocalFileExists( LocalFile $localFile, $hash ) {
811 $warnings = [];
812
813 $exists = self::getExistsWarning( $localFile );
814 if ( $exists !== false ) {
815 $warnings['exists'] = $exists;
816
817 // check if file is an exact duplicate of current file version
818 if ( $hash === $localFile->getSha1() ) {
819 $warnings['no-change'] = $localFile;
820 }
821
822 // check if file is an exact duplicate of older versions of this file
823 $history = $localFile->getHistory();
824 foreach ( $history as $oldFile ) {
825 if ( $hash === $oldFile->getSha1() ) {
826 $warnings['duplicate-version'][] = $oldFile;
827 }
828 }
829 }
830
831 return $warnings;
832 }
833
834 private function checkLocalFileWasDeleted( LocalFile $localFile ) {
835 return $localFile->wasDeleted() && !$localFile->exists();
836 }
837
844 private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
845 $dupes = RepoGroup::singleton()->findBySha1( $hash );
846 $title = $this->getTitle();
847 foreach ( $dupes as $key => $dupe ) {
848 if (
849 ( $dupe instanceof LocalFile ) &&
850 $ignoreLocalDupes &&
851 $title->equals( $dupe->getTitle() )
852 ) {
853 unset( $dupes[$key] );
854 }
855 }
856
857 return $dupes;
858 }
859
866 private function checkAgainstArchiveDupes( $hash ) {
867 $archivedFile = new ArchivedFile( null, 0, '', $hash );
868 if ( $archivedFile->getID() > 0 ) {
869 if ( $archivedFile->userCan( File::DELETED_FILE ) ) {
870 return $archivedFile->getName();
871 } else {
872 return '';
873 }
874 }
875
876 return null;
877 }
878
892 public function performUpload( $comment, $pageText, $watch, $user, $tags = [] ) {
893 $this->getLocalFile()->load( File::READ_LATEST );
894 $props = $this->mFileProps;
895
896 $error = null;
897 Hooks::run( 'UploadVerifyUpload', [ $this, $user, $props, $comment, $pageText, &$error ] );
898 if ( $error ) {
899 if ( !is_array( $error ) ) {
900 $error = [ $error ];
901 }
902 return Status::newFatal( ...$error );
903 }
904
905 $status = $this->getLocalFile()->upload(
906 $this->mTempPath,
907 $comment,
908 $pageText,
910 $props,
911 false,
912 $user,
913 $tags
914 );
915
916 if ( $status->isGood() ) {
917 if ( $watch ) {
919 $this->getLocalFile()->getTitle(),
920 $user,
922 );
923 }
924 // Avoid PHP 7.1 warning of passing $this by reference
925 $uploadBase = $this;
926 Hooks::run( 'UploadComplete', [ &$uploadBase ] );
927
928 $this->postProcessUpload();
929 }
930
931 return $status;
932 }
933
939 public function postProcessUpload() {
940 }
941
948 public function getTitle() {
949 if ( $this->mTitle !== false ) {
950 return $this->mTitle;
951 }
952 if ( !is_string( $this->mDesiredDestName ) ) {
953 $this->mTitleError = self::ILLEGAL_FILENAME;
954 $this->mTitle = null;
955
956 return $this->mTitle;
957 }
958 /* Assume that if a user specified File:Something.jpg, this is an error
959 * and that the namespace prefix needs to be stripped of.
960 */
961 $title = Title::newFromText( $this->mDesiredDestName );
962 if ( $title && $title->getNamespace() == NS_FILE ) {
963 $this->mFilteredName = $title->getDBkey();
964 } else {
965 $this->mFilteredName = $this->mDesiredDestName;
966 }
967
968 # oi_archive_name is max 255 bytes, which include a timestamp and an
969 # exclamation mark, so restrict file name to 240 bytes.
970 if ( strlen( $this->mFilteredName ) > 240 ) {
971 $this->mTitleError = self::FILENAME_TOO_LONG;
972 $this->mTitle = null;
973
974 return $this->mTitle;
975 }
976
982 $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
983 /* Normalize to title form before we do any further processing */
984 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
985 if ( is_null( $nt ) ) {
986 $this->mTitleError = self::ILLEGAL_FILENAME;
987 $this->mTitle = null;
988
989 return $this->mTitle;
990 }
991 $this->mFilteredName = $nt->getDBkey();
992
997 list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
998
999 if ( $ext !== [] ) {
1000 $this->mFinalExtension = trim( end( $ext ) );
1001 } else {
1002 $this->mFinalExtension = '';
1003
1004 # No extension, try guessing one
1005 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
1006 $mime = $magic->guessMimeType( $this->mTempPath );
1007 if ( $mime !== 'unknown/unknown' ) {
1008 # Get a space separated list of extensions
1009 $extList = $magic->getExtensionsForType( $mime );
1010 if ( $extList ) {
1011 # Set the extension to the canonical extension
1012 $this->mFinalExtension = strtok( $extList, ' ' );
1013
1014 # Fix up the other variables
1015 $this->mFilteredName .= ".{$this->mFinalExtension}";
1016 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1018 }
1019 }
1020 }
1021
1022 /* Don't allow users to override the blacklist (check file extension) */
1025
1026 $blackListedExtensions = $this->checkFileExtensionList( $ext, $wgFileBlacklist );
1027
1028 if ( $this->mFinalExtension == '' ) {
1029 $this->mTitleError = self::FILETYPE_MISSING;
1030 $this->mTitle = null;
1031
1032 return $this->mTitle;
1033 } elseif ( $blackListedExtensions ||
1035 !$this->checkFileExtension( $this->mFinalExtension, $wgFileExtensions ) )
1036 ) {
1037 $this->mBlackListedExtensions = $blackListedExtensions;
1038 $this->mTitleError = self::FILETYPE_BADTYPE;
1039 $this->mTitle = null;
1040
1041 return $this->mTitle;
1042 }
1043
1044 // Windows may be broken with special characters, see T3780
1045 if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1046 && !RepoGroup::singleton()->getLocalRepo()->backendSupportsUnicodePaths()
1047 ) {
1048 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1049 $this->mTitle = null;
1050
1051 return $this->mTitle;
1052 }
1053
1054 # If there was more than one "extension", reassemble the base
1055 # filename to prevent bogus complaints about length
1056 if ( count( $ext ) > 1 ) {
1057 $iterations = count( $ext ) - 1;
1058 for ( $i = 0; $i < $iterations; $i++ ) {
1059 $partname .= '.' . $ext[$i];
1060 }
1061 }
1062
1063 if ( strlen( $partname ) < 1 ) {
1064 $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1065 $this->mTitle = null;
1066
1067 return $this->mTitle;
1068 }
1069
1070 $this->mTitle = $nt;
1071
1072 return $this->mTitle;
1073 }
1074
1080 public function getLocalFile() {
1081 if ( is_null( $this->mLocalFile ) ) {
1082 $nt = $this->getTitle();
1083 $this->mLocalFile = is_null( $nt ) ? null : wfLocalFile( $nt );
1084 }
1085
1086 return $this->mLocalFile;
1087 }
1088
1092 public function getStashFile() {
1093 return $this->mStashFile;
1094 }
1095
1107 public function tryStashFile( User $user, $isPartial = false ) {
1108 if ( !$isPartial ) {
1109 $error = $this->runUploadStashFileHook( $user );
1110 if ( $error ) {
1111 return Status::newFatal( ...$error );
1112 }
1113 }
1114 try {
1115 $file = $this->doStashFile( $user );
1116 return Status::newGood( $file );
1117 } catch ( UploadStashException $e ) {
1118 return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1119 }
1120 }
1121
1126 protected function runUploadStashFileHook( User $user ) {
1127 $props = $this->mFileProps;
1128 $error = null;
1129 Hooks::run( 'UploadStashFile', [ $this, $user, $props, &$error ] );
1130 if ( $error && !is_array( $error ) ) {
1131 $error = [ $error ];
1132 }
1133 return $error;
1134 }
1135
1155 public function stashFile( User $user = null ) {
1156 wfDeprecated( __METHOD__, '1.28' );
1157
1158 return $this->doStashFile( $user );
1159 }
1160
1167 protected function doStashFile( User $user = null ) {
1168 $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $user );
1169 $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
1170 $this->mStashFile = $file;
1171
1172 return $file;
1173 }
1174
1179 public function cleanupTempFile() {
1180 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1181 // Delete when all relevant TempFSFile handles go out of scope
1182 wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal\n" );
1183 $this->tempFileObj->autocollect();
1184 }
1185 }
1186
1187 public function getTempPath() {
1188 return $this->mTempPath;
1189 }
1190
1200 public static function splitExtensions( $filename ) {
1201 $bits = explode( '.', $filename );
1202 $basename = array_shift( $bits );
1203
1204 return [ $basename, $bits ];
1205 }
1206
1215 public static function checkFileExtension( $ext, $list ) {
1216 return in_array( strtolower( $ext ), $list );
1217 }
1218
1227 public static function checkFileExtensionList( $ext, $list ) {
1228 return array_intersect( array_map( 'strtolower', $ext ), $list );
1229 }
1230
1238 public static function verifyExtension( $mime, $extension ) {
1239 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
1240
1241 if ( !$mime || $mime == 'unknown' || $mime == 'unknown/unknown' ) {
1242 if ( !$magic->isRecognizableExtension( $extension ) ) {
1243 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1244 "unrecognized extension '$extension', can't verify\n" );
1245
1246 return true;
1247 } else {
1248 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1249 "recognized extension '$extension', so probably invalid file\n" );
1250
1251 return false;
1252 }
1253 }
1254
1255 $match = $magic->isMatchingExtension( $extension, $mime );
1256
1257 if ( $match === null ) {
1258 if ( $magic->getTypesForExtension( $extension ) !== null ) {
1259 wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension\n" );
1260
1261 return false;
1262 } else {
1263 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file\n" );
1264
1265 return true;
1266 }
1267 } elseif ( $match === true ) {
1268 wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file\n" );
1269
1271 return true;
1272 } else {
1273 wfDebug( __METHOD__
1274 . ": mime type $mime mismatches file extension $extension, rejecting file\n" );
1275
1276 return false;
1277 }
1278 }
1279
1291 public static function detectScript( $file, $mime, $extension ) {
1292 # ugly hack: for text files, always look at the entire file.
1293 # For binary field, just check the first K.
1294
1295 $isText = strpos( $mime, 'text/' ) === 0;
1296 if ( $isText ) {
1297 $chunk = file_get_contents( $file );
1298 } else {
1299 $fp = fopen( $file, 'rb' );
1300 $chunk = fread( $fp, 1024 );
1301 fclose( $fp );
1302 }
1303
1304 $chunk = strtolower( $chunk );
1305
1306 if ( !$chunk ) {
1307 return false;
1308 }
1309
1310 # decode from UTF-16 if needed (could be used for obfuscation).
1311 if ( substr( $chunk, 0, 2 ) == "\xfe\xff" ) {
1312 $enc = 'UTF-16BE';
1313 } elseif ( substr( $chunk, 0, 2 ) == "\xff\xfe" ) {
1314 $enc = 'UTF-16LE';
1315 } else {
1316 $enc = null;
1317 }
1318
1319 if ( $enc !== null ) {
1320 $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1321 }
1322
1323 $chunk = trim( $chunk );
1324
1326 wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff\n" );
1327
1328 # check for HTML doctype
1329 if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1330 return true;
1331 }
1332
1333 // Some browsers will interpret obscure xml encodings as UTF-8, while
1334 // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1335 if ( $extension == 'svg' || strpos( $mime, 'image/svg' ) === 0 ) {
1336 if ( self::checkXMLEncodingMissmatch( $file ) ) {
1337 return true;
1338 }
1339 }
1340
1341 // Quick check for HTML heuristics in old IE and Safari.
1342 //
1343 // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1344 // don't need them all here as it can cause many false positives.
1345 //
1346 // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1347 $tags = [
1348 '<body',
1349 '<head',
1350 '<html', # also in safari
1351 '<script', # also in safari
1352 ];
1353
1354 foreach ( $tags as $tag ) {
1355 if ( strpos( $chunk, $tag ) !== false ) {
1356 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag\n" );
1357
1358 return true;
1359 }
1360 }
1361
1362 /*
1363 * look for JavaScript
1364 */
1365
1366 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1367 $chunk = Sanitizer::decodeCharReferences( $chunk );
1368
1369 # look for script-types
1370 if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
1371 wfDebug( __METHOD__ . ": found script types\n" );
1372
1373 return true;
1374 }
1375
1376 # look for html-style script-urls
1377 if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1378 wfDebug( __METHOD__ . ": found html-style script urls\n" );
1379
1380 return true;
1381 }
1382
1383 # look for css-style script-urls
1384 if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1385 wfDebug( __METHOD__ . ": found css-style script urls\n" );
1386
1387 return true;
1388 }
1389
1390 wfDebug( __METHOD__ . ": no scripts found\n" );
1391
1392 return false;
1393 }
1394
1402 public static function checkXMLEncodingMissmatch( $file ) {
1403 global $wgSVGMetadataCutoff;
1404 $contents = file_get_contents( $file, false, null, 0, $wgSVGMetadataCutoff );
1405 $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1406
1407 if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1408 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1409 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1410 ) {
1411 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'\n" );
1412
1413 return true;
1414 }
1415 } elseif ( preg_match( "!<\?xml\b!si", $contents ) ) {
1416 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1417 // bytes. There shouldn't be a legitimate reason for this to happen.
1418 wfDebug( __METHOD__ . ": Unmatched XML declaration start\n" );
1419
1420 return true;
1421 } elseif ( substr( $contents, 0, 4 ) == "\x4C\x6F\xA7\x94" ) {
1422 // EBCDIC encoded XML
1423 wfDebug( __METHOD__ . ": EBCDIC Encoded XML\n" );
1424
1425 return true;
1426 }
1427
1428 // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to
1429 // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings
1430 $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1431 foreach ( $attemptEncodings as $encoding ) {
1432 Wikimedia\suppressWarnings();
1433 $str = iconv( $encoding, 'UTF-8', $contents );
1434 Wikimedia\restoreWarnings();
1435 if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1436 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1437 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1438 ) {
1439 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'\n" );
1440
1441 return true;
1442 }
1443 } elseif ( $str != '' && preg_match( "!<\?xml\b!si", $str ) ) {
1444 // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1445 // bytes. There shouldn't be a legitimate reason for this to happen.
1446 wfDebug( __METHOD__ . ": Unmatched XML declaration start\n" );
1447
1448 return true;
1449 }
1450 }
1451
1452 return false;
1453 }
1454
1460 protected function detectScriptInSvg( $filename, $partial ) {
1461 $this->mSVGNSError = false;
1462 $check = new XmlTypeCheck(
1463 $filename,
1464 [ $this, 'checkSvgScriptCallback' ],
1465 true,
1466 [
1467 'processing_instruction_handler' => 'UploadBase::checkSvgPICallback',
1468 'external_dtd_handler' => 'UploadBase::checkSvgExternalDTD',
1469 ]
1470 );
1471 if ( $check->wellFormed !== true ) {
1472 // Invalid xml (T60553)
1473 // But only when non-partial (T67724)
1474 return $partial ? false : [ 'uploadinvalidxml' ];
1475 } elseif ( $check->filterMatch ) {
1476 if ( $this->mSVGNSError ) {
1477 return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1478 }
1479
1480 return $check->filterMatchType;
1481 }
1482
1483 return false;
1484 }
1485
1492 public static function checkSvgPICallback( $target, $data ) {
1493 // Don't allow external stylesheets (T59550)
1494 if ( preg_match( '/xml-stylesheet/i', $target ) ) {
1495 return [ 'upload-scripted-pi-callback' ];
1496 }
1497
1498 return false;
1499 }
1500
1512 public static function checkSvgExternalDTD( $type, $publicId, $systemId ) {
1513 // This doesn't include the XHTML+MathML+SVG doctype since we don't
1514 // allow XHTML anyways.
1515 $allowedDTDs = [
1516 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1517 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1518 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1519 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1520 // https://phabricator.wikimedia.org/T168856
1521 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1522 ];
1523 if ( $type !== 'PUBLIC'
1524 || !in_array( $systemId, $allowedDTDs )
1525 || strpos( $publicId, "-//W3C//" ) !== 0
1526 ) {
1527 return [ 'upload-scripted-dtd' ];
1528 }
1529 return false;
1530 }
1531
1539 public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1540 list( $namespace, $strippedElement ) = $this->splitXmlNamespace( $element );
1541
1542 // We specifically don't include:
1543 // http://www.w3.org/1999/xhtml (T62771)
1544 static $validNamespaces = [
1545 '',
1546 'adobe:ns:meta/',
1547 'http://creativecommons.org/ns#',
1548 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1549 'http://ns.adobe.com/adobeillustrator/10.0/',
1550 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1551 'http://ns.adobe.com/extensibility/1.0/',
1552 'http://ns.adobe.com/flows/1.0/',
1553 'http://ns.adobe.com/illustrator/1.0/',
1554 'http://ns.adobe.com/imagereplacement/1.0/',
1555 'http://ns.adobe.com/pdf/1.3/',
1556 'http://ns.adobe.com/photoshop/1.0/',
1557 'http://ns.adobe.com/saveforweb/1.0/',
1558 'http://ns.adobe.com/variables/1.0/',
1559 'http://ns.adobe.com/xap/1.0/',
1560 'http://ns.adobe.com/xap/1.0/g/',
1561 'http://ns.adobe.com/xap/1.0/g/img/',
1562 'http://ns.adobe.com/xap/1.0/mm/',
1563 'http://ns.adobe.com/xap/1.0/rights/',
1564 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1565 'http://ns.adobe.com/xap/1.0/stype/font#',
1566 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1567 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1568 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1569 'http://ns.adobe.com/xap/1.0/t/pg/',
1570 'http://purl.org/dc/elements/1.1/',
1571 'http://purl.org/dc/elements/1.1',
1572 'http://schemas.microsoft.com/visio/2003/svgextensions/',
1573 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1574 'http://taptrix.com/inkpad/svg_extensions',
1575 'http://web.resource.org/cc/',
1576 'http://www.freesoftware.fsf.org/bkchem/cdml',
1577 'http://www.inkscape.org/namespaces/inkscape',
1578 'http://www.opengis.net/gml',
1579 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1580 'http://www.w3.org/2000/svg',
1581 'http://www.w3.org/tr/rec-rdf-syntax/',
1582 'http://www.w3.org/2000/01/rdf-schema#',
1583 ];
1584
1585 // Inkscape mangles namespace definitions created by Adobe Illustrator.
1586 // This is nasty but harmless. (T144827)
1587 $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1588
1589 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1590 wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file.\n" );
1592 $this->mSVGNSError = $namespace;
1593
1594 return true;
1595 }
1596
1597 /*
1598 * check for elements that can contain javascript
1599 */
1600 if ( $strippedElement == 'script' ) {
1601 wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file.\n" );
1602
1603 return [ 'uploaded-script-svg', $strippedElement ];
1604 }
1605
1606 # e.g., <svg xmlns="http://www.w3.org/2000/svg">
1607 # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1608 if ( $strippedElement == 'handler' ) {
1609 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file.\n" );
1610
1611 return [ 'uploaded-script-svg', $strippedElement ];
1612 }
1613
1614 # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1615 if ( $strippedElement == 'stylesheet' ) {
1616 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file.\n" );
1617
1618 return [ 'uploaded-script-svg', $strippedElement ];
1619 }
1620
1621 # Block iframes, in case they pass the namespace check
1622 if ( $strippedElement == 'iframe' ) {
1623 wfDebug( __METHOD__ . ": iframe in uploaded file.\n" );
1624
1625 return [ 'uploaded-script-svg', $strippedElement ];
1626 }
1627
1628 # Check <style> css
1629 if ( $strippedElement == 'style'
1630 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1631 ) {
1632 wfDebug( __METHOD__ . ": hostile css in style element.\n" );
1633 return [ 'uploaded-hostile-svg' ];
1634 }
1635
1636 foreach ( $attribs as $attrib => $value ) {
1637 $stripped = $this->stripXmlNamespace( $attrib );
1638 $value = strtolower( $value );
1639
1640 if ( substr( $stripped, 0, 2 ) == 'on' ) {
1641 wfDebug( __METHOD__
1642 . ": Found event-handler attribute '$attrib'='$value' in uploaded file.\n" );
1643
1644 return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1645 }
1646
1647 # Do not allow relative links, or unsafe url schemas.
1648 # For <a> tags, only data:, http: and https: and same-document
1649 # fragment links are allowed. For all other tags, only data:
1650 # and fragment are allowed.
1651 if ( $stripped == 'href'
1652 && $value !== ''
1653 && strpos( $value, 'data:' ) !== 0
1654 && strpos( $value, '#' ) !== 0
1655 ) {
1656 if ( !( $strippedElement === 'a'
1657 && preg_match( '!^https?://!i', $value ) )
1658 ) {
1659 wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1660 . "'$attrib'='$value' in uploaded file.\n" );
1661
1662 return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1663 }
1664 }
1665
1666 # only allow data: targets that should be safe. This prevents vectors like,
1667 # image/svg, text/xml, application/xml, and text/html, which can contain scripts
1668 if ( $stripped == 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1669 // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
1670 // phpcs:ignore Generic.Files.LineLength
1671 $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1672
1673 if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1674 wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
1675 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
1676 return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1677 }
1678 }
1679
1680 # Change href with animate from (http://html5sec.org/#137).
1681 if ( $stripped === 'attributename'
1682 && $strippedElement === 'animate'
1683 && $this->stripXmlNamespace( $value ) == 'href'
1684 ) {
1685 wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1686 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
1687
1688 return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1689 }
1690
1691 # use set/animate to add event-handler attribute to parent
1692 if ( ( $strippedElement == 'set' || $strippedElement == 'animate' )
1693 && $stripped == 'attributename'
1694 && substr( $value, 0, 2 ) == 'on'
1695 ) {
1696 wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1697 . "\"<$strippedElement $stripped='$value'...\" in uploaded file.\n" );
1698
1699 return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1700 }
1701
1702 # use set to add href attribute to parent element
1703 if ( $strippedElement == 'set'
1704 && $stripped == 'attributename'
1705 && strpos( $value, 'href' ) !== false
1706 ) {
1707 wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file.\n" );
1708
1709 return [ 'uploaded-setting-href-svg' ];
1710 }
1711
1712 # use set to add a remote / data / script target to an element
1713 if ( $strippedElement == 'set'
1714 && $stripped == 'to'
1715 && preg_match( '!(http|https|data|script):!sim', $value )
1716 ) {
1717 wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file.\n" );
1718
1719 return [ 'uploaded-wrong-setting-svg', $value ];
1720 }
1721
1722 # use handler attribute with remote / data / script
1723 if ( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
1724 wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1725 . "'$attrib'='$value' in uploaded file.\n" );
1726
1727 return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1728 }
1729
1730 # use CSS styles to bring in remote code
1731 if ( $stripped == 'style'
1732 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1733 ) {
1734 wfDebug( __METHOD__ . ": Found svg setting a style with "
1735 . "remote url '$attrib'='$value' in uploaded file.\n" );
1736 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1737 }
1738
1739 # Several attributes can include css, css character escaping isn't allowed
1740 $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1741 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1742 if ( in_array( $stripped, $cssAttrs )
1743 && self::checkCssFragment( $value )
1744 ) {
1745 wfDebug( __METHOD__ . ": Found svg setting a style with "
1746 . "remote url '$attrib'='$value' in uploaded file.\n" );
1747 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1748 }
1749
1750 # image filters can pull in url, which could be svg that executes scripts
1751 # Only allow url( "#foo" ). Do not allow url( http://example.com )
1752 if ( $strippedElement == 'image'
1753 && $stripped == 'filter'
1754 && preg_match( '!url\s*\‍(\s*["\']?[^#]!sim', $value )
1755 ) {
1756 wfDebug( __METHOD__ . ": Found image filter with url: "
1757 . "\"<$strippedElement $stripped='$value'...\" in uploaded file.\n" );
1758
1759 return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1760 }
1761 }
1762
1763 return false; // No scripts detected
1764 }
1765
1772 private static function checkCssFragment( $value ) {
1773 # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1774 if ( stripos( $value, '@import' ) !== false ) {
1775 return true;
1776 }
1777
1778 # We allow @font-face to embed fonts with data: urls, so we snip the string
1779 # 'url' out so this case won't match when we check for urls below
1780 $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1781 $value = preg_replace( $pattern, '$1$2', $value );
1782
1783 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1784 # properties filter and accelerator don't seem to be useful for xss in SVG files.
1785 # Expression and -o-link don't seem to work either, but filtering them here in case.
1786 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1787 # but not local ones such as url("#..., url('#..., url(#....
1788 if ( preg_match( '!expression
1789 | -o-link\s*:
1790 | -o-link-source\s*:
1791 | -o-replace\s*:!imx', $value ) ) {
1792 return true;
1793 }
1794
1795 if ( preg_match_all(
1796 "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1797 $value,
1798 $matches
1799 ) !== 0
1800 ) {
1801 # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1802 foreach ( $matches[1] as $match ) {
1803 if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1804 return true;
1805 }
1806 }
1807 }
1808
1809 if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1810 return true;
1811 }
1812
1813 return false;
1814 }
1815
1821 private static function splitXmlNamespace( $element ) {
1822 // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1823 $parts = explode( ':', strtolower( $element ) );
1824 $name = array_pop( $parts );
1825 $ns = implode( ':', $parts );
1826
1827 return [ $ns, $name ];
1828 }
1829
1834 private function stripXmlNamespace( $name ) {
1835 // 'http://www.w3.org/2000/svg:script' -> 'script'
1836 $parts = explode( ':', strtolower( $name ) );
1837
1838 return array_pop( $parts );
1839 }
1840
1851 public static function detectVirus( $file ) {
1853
1854 if ( !$wgAntivirus ) {
1855 wfDebug( __METHOD__ . ": virus scanner disabled\n" );
1856
1857 return null;
1858 }
1859
1861 wfDebug( __METHOD__ . ": unknown virus scanner: $wgAntivirus\n" );
1862 $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1863 [ 'virus-badscanner', $wgAntivirus ] );
1864
1865 return wfMessage( 'virus-unknownscanner' )->text() . " $wgAntivirus";
1866 }
1867
1868 # look up scanner configuration
1870 $exitCodeMap = $wgAntivirusSetup[$wgAntivirus]['codemap'];
1871 $msgPattern = $wgAntivirusSetup[$wgAntivirus]['messagepattern'] ?? null;
1872
1873 if ( strpos( $command, "%f" ) === false ) {
1874 # simple pattern: append file to scan
1875 $command .= " " . Shell::escape( $file );
1876 } else {
1877 # complex pattern: replace "%f" with file to scan
1878 $command = str_replace( "%f", Shell::escape( $file ), $command );
1879 }
1880
1881 wfDebug( __METHOD__ . ": running virus scan: $command \n" );
1882
1883 # execute virus scanner
1884 $exitCode = false;
1885
1886 # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1887 # that does not seem to be worth the pain.
1888 # Ask me (Duesentrieb) about it if it's ever needed.
1889 $output = wfShellExecWithStderr( $command, $exitCode );
1890
1891 # map exit code to AV_xxx constants.
1892 $mappedCode = $exitCode;
1893 if ( $exitCodeMap ) {
1894 if ( isset( $exitCodeMap[$exitCode] ) ) {
1895 $mappedCode = $exitCodeMap[$exitCode];
1896 } elseif ( isset( $exitCodeMap["*"] ) ) {
1897 $mappedCode = $exitCodeMap["*"];
1898 }
1899 }
1900
1901 /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
1902 * so we need the strict equalities === and thus can't use a switch here
1903 */
1904 if ( $mappedCode === AV_SCAN_FAILED ) {
1905 # scan failed (code was mapped to false by $exitCodeMap)
1906 wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode).\n" );
1907
1908 $output = $wgAntivirusRequired
1909 ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1910 : null;
1911 } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1912 # scan failed because filetype is unknown (probably imune)
1913 wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode).\n" );
1914 $output = null;
1915 } elseif ( $mappedCode === AV_NO_VIRUS ) {
1916 # no virus found
1917 wfDebug( __METHOD__ . ": file passed virus scan.\n" );
1918 $output = false;
1919 } else {
1920 $output = trim( $output );
1921
1922 if ( !$output ) {
1923 $output = true; # if there's no output, return true
1924 } elseif ( $msgPattern ) {
1925 $groups = [];
1926 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1927 $output = $groups[1];
1928 }
1929 }
1930
1931 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output \n" );
1932 }
1933
1934 return $output;
1935 }
1936
1945 private function checkOverwrite( $user ) {
1946 // First check whether the local file can be overwritten
1947 $file = $this->getLocalFile();
1948 $file->load( File::READ_LATEST );
1949 if ( $file->exists() ) {
1950 if ( !self::userCanReUpload( $user, $file ) ) {
1951 return [ 'fileexists-forbidden', $file->getName() ];
1952 } else {
1953 return true;
1954 }
1955 }
1956
1957 /* Check shared conflicts: if the local file does not exist, but
1958 * wfFindFile finds a file, it exists in a shared repository.
1959 */
1960 $file = wfFindFile( $this->getTitle(), [ 'latest' => true ] );
1961 if ( $file && !MediaWikiServices::getInstance()
1962 ->getPermissionManager()
1963 ->userHasRight( $user, 'reupload-shared' )
1964 ) {
1965 return [ 'fileexists-shared-forbidden', $file->getName() ];
1966 }
1967
1968 return true;
1969 }
1970
1978 public static function userCanReUpload( User $user, File $img ) {
1979 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1980 if ( $permissionManager->userHasRight( $user, 'reupload' ) ) {
1981 return true; // non-conditional
1982 } elseif ( !$permissionManager->userHasRight( $user, 'reupload-own' ) ) {
1983 return false;
1984 }
1985
1986 if ( !( $img instanceof LocalFile ) ) {
1987 return false;
1988 }
1989
1990 $img->load();
1991
1992 return $user->getId() == $img->getUser( 'id' );
1993 }
1994
2006 public static function getExistsWarning( $file ) {
2007 if ( $file->exists() ) {
2008 return [ 'warning' => 'exists', 'file' => $file ];
2009 }
2010
2011 if ( $file->getTitle()->getArticleID() ) {
2012 return [ 'warning' => 'page-exists', 'file' => $file ];
2013 }
2014
2015 if ( strpos( $file->getName(), '.' ) == false ) {
2016 $partname = $file->getName();
2017 $extension = '';
2018 } else {
2019 $n = strrpos( $file->getName(), '.' );
2020 $extension = substr( $file->getName(), $n + 1 );
2021 $partname = substr( $file->getName(), 0, $n );
2022 }
2023 $normalizedExtension = File::normalizeExtension( $extension );
2024
2025 if ( $normalizedExtension != $extension ) {
2026 // We're not using the normalized form of the extension.
2027 // Normal form is lowercase, using most common of alternate
2028 // extensions (eg 'jpg' rather than 'JPEG').
2029
2030 // Check for another file using the normalized form...
2031 $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2032 $file_lc = wfLocalFile( $nt_lc );
2033
2034 if ( $file_lc->exists() ) {
2035 return [
2036 'warning' => 'exists-normalized',
2037 'file' => $file,
2038 'normalizedFile' => $file_lc
2039 ];
2040 }
2041 }
2042
2043 // Check for files with the same name but a different extension
2044 $similarFiles = RepoGroup::singleton()->getLocalRepo()->findFilesByPrefix(
2045 "{$partname}.", 1 );
2046 if ( count( $similarFiles ) ) {
2047 return [
2048 'warning' => 'exists-normalized',
2049 'file' => $file,
2050 'normalizedFile' => $similarFiles[0],
2051 ];
2052 }
2053
2054 if ( self::isThumbName( $file->getName() ) ) {
2055 # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2056 $nt_thb = Title::newFromText(
2057 substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2058 NS_FILE
2059 );
2060 $file_thb = wfLocalFile( $nt_thb );
2061 if ( $file_thb->exists() ) {
2062 return [
2063 'warning' => 'thumb',
2064 'file' => $file,
2065 'thumbFile' => $file_thb
2066 ];
2067 } else {
2068 // File does not exist, but we just don't like the name
2069 return [
2070 'warning' => 'thumb-name',
2071 'file' => $file,
2072 'thumbFile' => $file_thb
2073 ];
2074 }
2075 }
2076
2077 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2078 if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
2079 return [
2080 'warning' => 'bad-prefix',
2081 'file' => $file,
2082 'prefix' => $prefix
2083 ];
2084 }
2085 }
2086
2087 return false;
2088 }
2089
2095 public static function isThumbName( $filename ) {
2096 $n = strrpos( $filename, '.' );
2097 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2098
2099 return (
2100 substr( $partname, 3, 3 ) == 'px-' ||
2101 substr( $partname, 2, 3 ) == 'px-'
2102 ) &&
2103 preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2104 }
2105
2111 public static function getFilenamePrefixBlacklist() {
2112 $blacklist = [];
2113 $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2114 if ( !$message->isDisabled() ) {
2115 $lines = explode( "\n", $message->plain() );
2116 foreach ( $lines as $line ) {
2117 // Remove comment lines
2118 $comment = substr( trim( $line ), 0, 1 );
2119 if ( $comment == '#' || $comment == '' ) {
2120 continue;
2121 }
2122 // Remove additional comments after a prefix
2123 $comment = strpos( $line, '#' );
2124 if ( $comment > 0 ) {
2125 $line = substr( $line, 0, $comment - 1 );
2126 }
2127 $blacklist[] = trim( $line );
2128 }
2129 }
2130
2131 return $blacklist;
2132 }
2133
2145 public function getImageInfo( $result ) {
2146 $localFile = $this->getLocalFile();
2147 $stashFile = $this->getStashFile();
2148 // Calling a different API module depending on whether the file was stashed is less than optimal.
2149 // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
2150 if ( $stashFile ) {
2152 $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_flip( $imParam ), $result );
2153 } else {
2155 $info = ApiQueryImageInfo::getInfo( $localFile, array_flip( $imParam ), $result );
2156 }
2157
2158 return $info;
2159 }
2160
2165 public function convertVerifyErrorToStatus( $error ) {
2166 $code = $error['status'];
2167 unset( $code['status'] );
2168
2169 return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2170 }
2171
2179 public static function getMaxUploadSize( $forType = null ) {
2180 global $wgMaxUploadSize;
2181
2182 if ( is_array( $wgMaxUploadSize ) ) {
2183 if ( !is_null( $forType ) && isset( $wgMaxUploadSize[$forType] ) ) {
2184 return $wgMaxUploadSize[$forType];
2185 } else {
2186 return $wgMaxUploadSize['*'];
2187 }
2188 } else {
2189 return intval( $wgMaxUploadSize );
2190 }
2191 }
2192
2200 public static function getMaxPhpUploadSize() {
2201 $phpMaxFileSize = wfShorthandToInteger(
2202 ini_get( 'upload_max_filesize' ) ?: ini_get( 'hhvm.server.upload.upload_max_file_size' ),
2203 PHP_INT_MAX
2204 );
2205 $phpMaxPostSize = wfShorthandToInteger(
2206 ini_get( 'post_max_size' ) ?: ini_get( 'hhvm.server.max_post_size' ),
2207 PHP_INT_MAX
2208 ) ?: PHP_INT_MAX;
2209 return min( $phpMaxFileSize, $phpMaxPostSize );
2210 }
2211
2221 public static function getSessionStatus( User $user, $statusKey ) {
2222 $store = self::getUploadSessionStore();
2223 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2224
2225 return $store->get( $key );
2226 }
2227
2240 public static function setSessionStatus( User $user, $statusKey, $value ) {
2241 $store = self::getUploadSessionStore();
2242 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2243
2244 if ( $value === false ) {
2245 $store->delete( $key );
2246 } else {
2247 $store->set( $key, $value, $store::TTL_DAY );
2248 }
2249 }
2250
2257 private static function getUploadSessionKey( BagOStuff $store, User $user, $statusKey ) {
2258 return $store->makeKey(
2259 'uploadstatus',
2260 $user->getId() ?: md5( $user->getName() ),
2261 $statusKey
2262 );
2263 }
2264
2268 private static function getUploadSessionStore() {
2269 return ObjectCache::getInstance( 'db-replicated' );
2270 }
2271}
$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.
wfLocalFile( $title)
Get an object referring to a locally registered file.
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 '-'.
wfShorthandToInteger( $string='', $default=-1)
Converts shorthand byte notation to integer form.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
wfIsHHVM()
Check if we are running under HHVM.
$wgOut
Definition Setup.php:885
$wgLang
Definition Setup.php:880
$line
Definition cdb.php:59
$command
Definition cdb.php:65
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:63
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:263
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:61
const DELETE_SOURCE
Definition File.php:76
const DELETED_FILE
Definition File.php:63
wasDeleted()
Was this file ever deleted from the wiki?
Definition File.php:1918
Class to represent a local file in the wiki's own database.
Definition LocalFile.php:56
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.
Executes shell commands.
Definition Shell.php:44
static singleton()
Definition RepoGroup.php:60
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:1013
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition Title.php:316
UploadBase and subclasses are the backend of MediaWiki's file uploads.
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
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)
UploadStashFile $mStashFile
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.
const FILETYPE_MISSING
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)
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]].
checkAgainstArchiveDupes( $hash)
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().
const HOOK_ABORTED
string null $mDestName
const VERIFICATION_ERROR
string[] $mBlackListedExtensions
const WINDOWS_NONASCII_FILENAME
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.
const FILETYPE_BADTYPE
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)
Initialize the path information.
static getMaxUploadSize( $forType=null)
Get the MediaWiki maximum uploaded file size for given type of upload, based on $wgMaxUploadSize.
Title bool $mTitle
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.
static getUploadSessionKey(BagOStuff $store, User $user, $statusKey)
fetchFile()
Fetch the file.
const FILE_TOO_LARGE
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
checkLocalFileWasDeleted(LocalFile $localFile)
performUpload( $comment, $pageText, $watch, $user, $tags=[])
Really perform the upload.
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.
checkWarnings()
Check for non fatal problems with the file.
LocalFile $mLocalFile
static detectVirus( $file)
Generic wrapper function for a virus scanner program.
checkUnwantedFileExtensions( $fileExtension)
TempFSFile null $tempFileObj
Wrapper to handle deleting the temp file.
static getExistsWarning( $file)
Helper function that does various existence checks for a file.
const FILENAME_TOO_LONG
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:51
getName()
Get the user name, or the IP of an anonymous user.
Definition User.php:2364
getId()
Get the user's ID.
Definition User.php:2335
const IGNORE_USER_RIGHTS
Definition User.php:83
static doWatch(Title $title, User $user, $checkRights=User::CHECK_USER_RIGHTS)
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:103
const NS_FILE
Definition Defines.php:75
const AV_SCAN_ABORTED
Definition Defines.php:102
const AV_NO_VIRUS
Definition Defines.php:100
Interface for objects representing user identity.
A helper class for throttling authentication attempts.
$lines
Definition router.php:61
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