MediaWiki 1.42.0
UploadBase.php
Go to the documentation of this file.
1<?php
26use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
39use Wikimedia\AtEase\AtEase;
40
57abstract class UploadBase {
58 use ProtectedHookAccessorTrait;
59
61 protected $mTempPath;
63 protected $tempFileObj;
67 protected $mDestName;
71 protected $mSourceType;
73 protected $mTitle = false;
75 protected $mTitleError = 0;
77 protected $mFilteredName;
81 protected $mLocalFile;
83 protected $mStashFile;
85 protected $mFileSize;
87 protected $mFileProps;
91 protected $mJavaDetected;
93 protected $mSVGNSError;
94
95 protected static $safeXmlEncodings = [
96 'UTF-8',
97 'US-ASCII',
98 'ISO-8859-1',
99 'ISO-8859-2',
100 'UTF-16',
101 'UTF-32',
102 'WINDOWS-1250',
103 'WINDOWS-1251',
104 'WINDOWS-1252',
105 'WINDOWS-1253',
106 'WINDOWS-1254',
107 'WINDOWS-1255',
108 'WINDOWS-1256',
109 'WINDOWS-1257',
110 'WINDOWS-1258',
111 ];
112
113 public const SUCCESS = 0;
114 public const OK = 0;
115 public const EMPTY_FILE = 3;
116 public const MIN_LENGTH_PARTNAME = 4;
117 public const ILLEGAL_FILENAME = 5;
118 public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
119 public const FILETYPE_MISSING = 8;
120 public const FILETYPE_BADTYPE = 9;
121 public const VERIFICATION_ERROR = 10;
122 public const HOOK_ABORTED = 11;
123 public const FILE_TOO_LARGE = 12;
124 public const WINDOWS_NONASCII_FILENAME = 13;
125 public const FILENAME_TOO_LONG = 14;
126
127 private const CODE_TO_STATUS = [
128 self::EMPTY_FILE => 'empty-file',
129 self::FILE_TOO_LARGE => 'file-too-large',
130 self::FILETYPE_MISSING => 'filetype-missing',
131 self::FILETYPE_BADTYPE => 'filetype-banned',
132 self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
133 self::ILLEGAL_FILENAME => 'illegal-filename',
134 self::OVERWRITE_EXISTING_FILE => 'overwrite',
135 self::VERIFICATION_ERROR => 'verification-error',
136 self::HOOK_ABORTED => 'hookaborted',
137 self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
138 self::FILENAME_TOO_LONG => 'filename-toolong',
139 ];
140
145 public function getVerificationErrorCode( $error ) {
146 return self::CODE_TO_STATUS[$error] ?? 'unknown-error';
147 }
148
155 public static function isEnabled() {
156 $enableUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnableUploads );
157
158 return $enableUploads && wfIniGetBool( 'file_uploads' );
159 }
160
169 public static function isAllowed( Authority $performer ) {
170 foreach ( [ 'upload', 'edit' ] as $permission ) {
171 if ( !$performer->isAllowed( $permission ) ) {
172 return $permission;
173 }
174 }
175
176 return true;
177 }
178
188 public static function isThrottled( $user ) {
189 wfDeprecated( __METHOD__, '1.41' );
190 return $user->pingLimiter( 'upload' );
191 }
192
194 private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
195
203 public static function createFromRequest( &$request, $type = null ) {
204 $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
205
206 if ( !$type ) {
207 return null;
208 }
209
210 // Get the upload class
211 $type = ucfirst( $type );
212
213 // Give hooks the chance to handle this request
215 $className = null;
216 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
217 // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
218 ->onUploadCreateFromRequest( $type, $className );
219 if ( $className === null ) {
220 $className = 'UploadFrom' . $type;
221 wfDebug( __METHOD__ . ": class name: $className" );
222 if ( !in_array( $type, self::$uploadHandlers ) ) {
223 return null;
224 }
225 }
226
227 if ( !$className::isEnabled() || !$className::isValidRequest( $request ) ) {
228 return null;
229 }
230
232 $handler = new $className;
233
234 $handler->initializeFromRequest( $request );
235
236 return $handler;
237 }
238
244 public static function isValidRequest( $request ) {
245 return false;
246 }
247
252 public function getDesiredDestName() {
253 return $this->mDesiredDestName;
254 }
255
259 public function __construct() {
260 }
261
269 public function getSourceType() {
270 return null;
271 }
272
279 public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
280 $this->mDesiredDestName = $name;
281 if ( FileBackend::isStoragePath( $tempPath ) ) {
282 throw new InvalidArgumentException( __METHOD__ . " given storage path `$tempPath`." );
283 }
284
285 $this->setTempFile( $tempPath, $fileSize );
286 $this->mRemoveTempFile = $removeTempFile;
287 }
288
294 abstract public function initializeFromRequest( &$request );
295
300 protected function setTempFile( $tempPath, $fileSize = null ) {
301 $this->mTempPath = $tempPath ?? '';
302 $this->mFileSize = $fileSize ?: null;
303 $this->mFileProps = null;
304 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
305 $this->tempFileObj = new TempFSFile( $this->mTempPath );
306 if ( !$fileSize ) {
307 $this->mFileSize = filesize( $this->mTempPath );
308 }
309 } else {
310 $this->tempFileObj = null;
311 }
312 }
313
319 public function fetchFile() {
320 return Status::newGood();
321 }
322
328 public function canFetchFile() {
329 return Status::newGood();
330 }
331
336 public function isEmptyFile() {
337 return !$this->mFileSize;
338 }
339
344 public function getFileSize() {
345 return $this->mFileSize;
346 }
347
353 public function getTempFileSha1Base36() {
354 // Use cached version if we already have it.
355 if ( $this->mFileProps && is_string( $this->mFileProps['sha1'] ) ) {
356 return $this->mFileProps['sha1'];
357 }
358 return FSFile::getSha1Base36FromPath( $this->mTempPath );
359 }
360
365 public function getRealPath( $srcPath ) {
366 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
367 if ( FileRepo::isVirtualUrl( $srcPath ) ) {
371 $tmpFile = $repo->getLocalCopy( $srcPath );
372 if ( $tmpFile ) {
373 $tmpFile->bind( $this ); // keep alive with $this
374 }
375 $path = $tmpFile ? $tmpFile->getPath() : false;
376 } else {
377 $path = $srcPath;
378 }
379
380 return $path;
381 }
382
400 public function verifyUpload() {
404 if ( $this->isEmptyFile() ) {
405 return [ 'status' => self::EMPTY_FILE ];
406 }
407
411 $maxSize = self::getMaxUploadSize( $this->getSourceType() );
412 if ( $this->mFileSize > $maxSize ) {
413 return [
414 'status' => self::FILE_TOO_LARGE,
415 'max' => $maxSize,
416 ];
417 }
418
424 $verification = $this->verifyFile();
425 if ( $verification !== true ) {
426 return [
427 'status' => self::VERIFICATION_ERROR,
428 'details' => $verification
429 ];
430 }
431
435 $result = $this->validateName();
436 if ( $result !== true ) {
437 return $result;
438 }
439
440 return [ 'status' => self::OK ];
441 }
442
449 public function validateName() {
450 $nt = $this->getTitle();
451 if ( $nt === null ) {
452 $result = [ 'status' => $this->mTitleError ];
453 if ( $this->mTitleError === self::ILLEGAL_FILENAME ) {
454 $result['filtered'] = $this->mFilteredName;
455 }
456 if ( $this->mTitleError === self::FILETYPE_BADTYPE ) {
457 $result['finalExt'] = $this->mFinalExtension;
458 if ( count( $this->mBlackListedExtensions ) ) {
459 $result['blacklistedExt'] = $this->mBlackListedExtensions;
460 }
461 }
462
463 return $result;
464 }
465 $this->mDestName = $this->getLocalFile()->getName();
466
467 return true;
468 }
469
478 protected function verifyMimeType( $mime ) {
479 $verifyMimeType = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeType );
480 if ( $verifyMimeType ) {
481 wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" );
482 $mimeTypeExclusions = MediaWikiServices::getInstance()->getMainConfig()
483 ->get( MainConfigNames::MimeTypeExclusions );
484 if ( self::checkFileExtension( $mime, $mimeTypeExclusions ) ) {
485 return [ 'filetype-badmime', $mime ];
486 }
487 }
488
489 return true;
490 }
491
497 protected function verifyFile() {
498 $config = MediaWikiServices::getInstance()->getMainConfig();
499 $verifyMimeType = $config->get( MainConfigNames::VerifyMimeType );
500 $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
501 $status = $this->verifyPartialFile();
502 if ( $status !== true ) {
503 return $status;
504 }
505
506 // Calculating props calculates the sha1 which is expensive.
507 // reuse props if we already have them
508 if ( !is_array( $this->mFileProps ) ) {
509 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
510 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
511 }
512 $mime = $this->mFileProps['mime'];
513
514 if ( $verifyMimeType ) {
515 # XXX: Missing extension will be caught by validateName() via getTitle()
516 if ( (string)$this->mFinalExtension !== '' &&
517 !self::verifyExtension( $mime, $this->mFinalExtension )
518 ) {
519 return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
520 }
521 }
522
523 # check for htmlish code and javascript
524 if ( !$disableUploadScriptChecks ) {
525 if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
526 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
527 if ( $svgStatus !== false ) {
528 return $svgStatus;
529 }
530 }
531 }
532
533 $handler = MediaHandler::getHandler( $mime );
534 if ( $handler ) {
535 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
536 if ( !$handlerStatus->isOK() ) {
537 $errors = $handlerStatus->getErrorsArray();
538
539 return reset( $errors );
540 }
541 }
542
543 $error = true;
544 $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
545 if ( $error !== true ) {
546 if ( !is_array( $error ) ) {
547 $error = [ $error ];
548 }
549 return $error;
550 }
551
552 wfDebug( __METHOD__ . ": all clear; passing." );
553
554 return true;
555 }
556
566 protected function verifyPartialFile() {
567 $config = MediaWikiServices::getInstance()->getMainConfig();
568 $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
569 # getTitle() sets some internal parameters like $this->mFinalExtension
570 $this->getTitle();
571
572 // Calculating props calculates the sha1 which is expensive.
573 // reuse props if we already have them (e.g. During stashed upload)
574 if ( !is_array( $this->mFileProps ) ) {
575 $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
576 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
577 }
578
579 # check MIME type, if desired
580 $mime = $this->mFileProps['file-mime'];
581 $status = $this->verifyMimeType( $mime );
582 if ( $status !== true ) {
583 return $status;
584 }
585
586 # check for htmlish code and javascript
587 if ( !$disableUploadScriptChecks ) {
588 if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
589 return [ 'uploadscripted' ];
590 }
591 if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
592 $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
593 if ( $svgStatus !== false ) {
594 return $svgStatus;
595 }
596 }
597 }
598
599 # Scan the uploaded file for viruses
600 $virus = self::detectVirus( $this->mTempPath );
601 if ( $virus ) {
602 return [ 'uploadvirus', $virus ];
603 }
604
605 return true;
606 }
607
613 public function zipEntryCallback( $entry ) {
614 $names = [ $entry['name'] ];
615
616 // If there is a null character, cut off the name at it, because JDK's
617 // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
618 // were constructed which had ".class\0" followed by a string chosen to
619 // make the hash collide with the truncated name, that file could be
620 // returned in response to a request for the .class file.
621 $nullPos = strpos( $entry['name'], "\000" );
622 if ( $nullPos !== false ) {
623 $names[] = substr( $entry['name'], 0, $nullPos );
624 }
625
626 // If there is a trailing slash in the file name, we have to strip it,
627 // because that's what ZIP_GetEntry() does.
628 if ( preg_grep( '!\.class/?$!', $names ) ) {
629 $this->mJavaDetected = true;
630 }
631 }
632
642 public function verifyPermissions( Authority $performer ) {
643 return $this->verifyTitlePermissions( $performer );
644 }
645
657 public function verifyTitlePermissions( Authority $performer ) {
662 $nt = $this->getTitle();
663 if ( $nt === null ) {
664 return true;
665 }
666
667 $status = PermissionStatus::newEmpty();
668 $performer->authorizeWrite( 'edit', $nt, $status );
669 $performer->authorizeWrite( 'upload', $nt, $status );
670 if ( !$status->isGood() ) {
671 return $status->toLegacyErrorArray();
672 }
673
674 $overwriteError = $this->checkOverwrite( $performer );
675 if ( $overwriteError !== true ) {
676 return [ $overwriteError ];
677 }
678
679 return true;
680 }
681
691 public function checkWarnings( $user = null ) {
692 if ( $user === null ) {
693 // TODO check uses and hard deprecate
694 $user = RequestContext::getMain()->getUser();
695 }
696
697 $warnings = [];
698
699 $localFile = $this->getLocalFile();
700 $localFile->load( IDBAccessObject::READ_LATEST );
701 $filename = $localFile->getName();
702 $hash = $this->getTempFileSha1Base36();
703
704 $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
705 if ( $badFileName !== null ) {
706 $warnings['badfilename'] = $badFileName;
707 }
708
709 $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
710 if ( $unwantedFileExtensionDetails !== null ) {
711 $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
712 }
713
714 $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
715 if ( $fileSizeWarnings ) {
716 $warnings = array_merge( $warnings, $fileSizeWarnings );
717 }
718
719 $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
720 if ( $localFileExistsWarnings ) {
721 $warnings = array_merge( $warnings, $localFileExistsWarnings );
722 }
723
724 if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
725 $warnings['was-deleted'] = $filename;
726 }
727
728 // If a file with the same name exists locally then the local file has already been tested
729 // for duplication of content
730 $ignoreLocalDupes = isset( $warnings['exists'] );
731 $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
732 if ( $dupes ) {
733 $warnings['duplicate'] = $dupes;
734 }
735
736 $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
737 if ( $archivedDupes !== null ) {
738 $warnings['duplicate-archive'] = $archivedDupes;
739 }
740
741 return $warnings;
742 }
743
755 public static function makeWarningsSerializable( $warnings ) {
756 array_walk_recursive( $warnings, static function ( &$param, $key ) {
757 if ( $param instanceof File ) {
758 $param = [
759 'fileName' => $param->getName(),
760 'timestamp' => $param->getTimestamp()
761 ];
762 } elseif ( is_object( $param ) ) {
763 throw new InvalidArgumentException(
764 'UploadBase::makeWarningsSerializable: ' .
765 'Unexpected object of class ' . get_class( $param ) );
766 }
767 } );
768 return $warnings;
769 }
770
778 public static function unserializeWarnings( $warnings ) {
779 foreach ( $warnings as $key => $value ) {
780 if ( is_array( $value ) ) {
781 if ( isset( $value['fileName'] ) && isset( $value['timestamp'] ) ) {
782 $warnings[$key] = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
783 $value['fileName'],
784 [ 'time' => $value['timestamp'] ]
785 );
786 } else {
787 $warnings[$key] = self::unserializeWarnings( $value );
788 }
789 }
790 }
791 return $warnings;
792 }
793
803 private function checkBadFileName( $filename, $desiredFileName ) {
804 $comparableName = str_replace( ' ', '_', $desiredFileName );
805 $comparableName = Title::capitalize( $comparableName, NS_FILE );
806
807 if ( $desiredFileName != $filename && $comparableName != $filename ) {
808 return $filename;
809 }
810
811 return null;
812 }
813
822 private function checkUnwantedFileExtensions( $fileExtension ) {
823 $checkFileExtensions = MediaWikiServices::getInstance()->getMainConfig()
824 ->get( MainConfigNames::CheckFileExtensions );
825 $fileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FileExtensions );
826 if ( $checkFileExtensions ) {
827 $extensions = array_unique( $fileExtensions );
828 if ( !self::checkFileExtension( $fileExtension, $extensions ) ) {
829 return [
830 $fileExtension,
831 Message::listParam( $extensions, 'comma' ),
832 count( $extensions )
833 ];
834 }
835 }
836
837 return null;
838 }
839
845 private function checkFileSize( $fileSize ) {
846 $uploadSizeWarning = MediaWikiServices::getInstance()->getMainConfig()
847 ->get( MainConfigNames::UploadSizeWarning );
848
849 $warnings = [];
850
851 if ( $uploadSizeWarning && ( $fileSize > $uploadSizeWarning ) ) {
852 $warnings['large-file'] = [
853 Message::sizeParam( $uploadSizeWarning ),
854 Message::sizeParam( $fileSize ),
855 ];
856 }
857
858 if ( $fileSize == 0 ) {
859 $warnings['empty-file'] = true;
860 }
861
862 return $warnings;
863 }
864
871 private function checkLocalFileExists( LocalFile $localFile, $hash ) {
872 $warnings = [];
873
874 $exists = self::getExistsWarning( $localFile );
875 if ( $exists !== false ) {
876 $warnings['exists'] = $exists;
877
878 // check if file is an exact duplicate of current file version
879 if ( $hash !== false && $hash === $localFile->getSha1() ) {
880 $warnings['no-change'] = $localFile;
881 }
882
883 // check if file is an exact duplicate of older versions of this file
884 $history = $localFile->getHistory();
885 foreach ( $history as $oldFile ) {
886 if ( $hash === $oldFile->getSha1() ) {
887 $warnings['duplicate-version'][] = $oldFile;
888 }
889 }
890 }
891
892 return $warnings;
893 }
894
895 private function checkLocalFileWasDeleted( LocalFile $localFile ) {
896 return $localFile->wasDeleted() && !$localFile->exists();
897 }
898
905 private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
906 if ( $hash === false ) {
907 return [];
908 }
909 $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
910 $title = $this->getTitle();
911 foreach ( $dupes as $key => $dupe ) {
912 if (
913 ( $dupe instanceof LocalFile ) &&
914 $ignoreLocalDupes &&
915 $title->equals( $dupe->getTitle() )
916 ) {
917 unset( $dupes[$key] );
918 }
919 }
920
921 return $dupes;
922 }
923
931 private function checkAgainstArchiveDupes( $hash, Authority $performer ) {
932 if ( $hash === false ) {
933 return null;
934 }
935 $archivedFile = new ArchivedFile( null, 0, '', $hash );
936 if ( $archivedFile->getID() > 0 ) {
937 if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
938 return $archivedFile->getName();
939 }
940 return '';
941 }
942
943 return null;
944 }
945
963 public function performUpload(
964 $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
965 ) {
966 $this->getLocalFile()->load( IDBAccessObject::READ_LATEST );
967 $props = $this->mFileProps;
968
969 $error = null;
970 $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
971 if ( $error ) {
972 if ( !is_array( $error ) ) {
973 $error = [ $error ];
974 }
975 return Status::newFatal( ...$error );
976 }
977
978 $status = $this->getLocalFile()->upload(
979 $this->mTempPath,
980 $comment,
981 $pageText !== false ? $pageText : '',
982 File::DELETE_SOURCE,
983 $props,
984 false,
985 $user,
986 $tags
987 );
988
989 if ( $status->isGood() ) {
990 if ( $watch ) {
991 MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
992 $user,
993 $this->getLocalFile()->getTitle(),
994 $watchlistExpiry
995 );
996 }
997 $this->getHookRunner()->onUploadComplete( $this );
998
999 $this->postProcessUpload();
1000 }
1001
1002 return $status;
1003 }
1004
1011 public function postProcessUpload() {
1012 }
1013
1020 public function getTitle() {
1021 if ( $this->mTitle !== false ) {
1022 return $this->mTitle;
1023 }
1024 if ( !is_string( $this->mDesiredDestName ) ) {
1025 $this->mTitleError = self::ILLEGAL_FILENAME;
1026 $this->mTitle = null;
1027
1028 return $this->mTitle;
1029 }
1030 /* Assume that if a user specified File:Something.jpg, this is an error
1031 * and that the namespace prefix needs to be stripped of.
1032 */
1033 $title = Title::newFromText( $this->mDesiredDestName );
1034 if ( $title && $title->getNamespace() === NS_FILE ) {
1035 $this->mFilteredName = $title->getDBkey();
1036 } else {
1037 $this->mFilteredName = $this->mDesiredDestName;
1038 }
1039
1040 # oi_archive_name is max 255 bytes, which include a timestamp and an
1041 # exclamation mark, so restrict file name to 240 bytes.
1042 if ( strlen( $this->mFilteredName ) > 240 ) {
1043 $this->mTitleError = self::FILENAME_TOO_LONG;
1044 $this->mTitle = null;
1045
1046 return $this->mTitle;
1047 }
1048
1054 $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
1055 /* Normalize to title form before we do any further processing */
1056 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1057 if ( $nt === null ) {
1058 $this->mTitleError = self::ILLEGAL_FILENAME;
1059 $this->mTitle = null;
1060
1061 return $this->mTitle;
1062 }
1063 $this->mFilteredName = $nt->getDBkey();
1064
1069 [ $partname, $ext ] = self::splitExtensions( $this->mFilteredName );
1070
1071 if ( $ext !== [] ) {
1072 $this->mFinalExtension = trim( end( $ext ) );
1073 } else {
1074 $this->mFinalExtension = '';
1075
1076 // No extension, try guessing one from the temporary file
1077 // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1078 // or incomplete test case in UploadBaseTest (T272328)
1079 if ( $this->mTempPath !== null ) {
1080 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1081 $mime = $magic->guessMimeType( $this->mTempPath );
1082 if ( $mime !== 'unknown/unknown' ) {
1083 # Get a space separated list of extensions
1084 $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1085 if ( $mimeExt !== null ) {
1086 # Set the extension to the canonical extension
1087 $this->mFinalExtension = $mimeExt;
1088
1089 # Fix up the other variables
1090 $this->mFilteredName .= ".{$this->mFinalExtension}";
1091 $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1092 $ext = [ $this->mFinalExtension ];
1093 }
1094 }
1095 }
1096 }
1097
1098 // Don't allow users to override the list of prohibited file extensions (check file extension)
1099 $config = MediaWikiServices::getInstance()->getMainConfig();
1100 $checkFileExtensions = $config->get( MainConfigNames::CheckFileExtensions );
1101 $strictFileExtensions = $config->get( MainConfigNames::StrictFileExtensions );
1102 $fileExtensions = $config->get( MainConfigNames::FileExtensions );
1103 $prohibitedFileExtensions = $config->get( MainConfigNames::ProhibitedFileExtensions );
1104
1105 $badList = self::checkFileExtensionList( $ext, $prohibitedFileExtensions );
1106
1107 if ( $this->mFinalExtension == '' ) {
1108 $this->mTitleError = self::FILETYPE_MISSING;
1109 $this->mTitle = null;
1110
1111 return $this->mTitle;
1112 }
1113
1114 if ( $badList ||
1115 ( $checkFileExtensions && $strictFileExtensions &&
1116 !self::checkFileExtension( $this->mFinalExtension, $fileExtensions ) )
1117 ) {
1118 $this->mBlackListedExtensions = $badList;
1119 $this->mTitleError = self::FILETYPE_BADTYPE;
1120 $this->mTitle = null;
1121
1122 return $this->mTitle;
1123 }
1124
1125 // Windows may be broken with special characters, see T3780
1126 if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1127 && !MediaWikiServices::getInstance()->getRepoGroup()
1128 ->getLocalRepo()->backendSupportsUnicodePaths()
1129 ) {
1130 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1131 $this->mTitle = null;
1132
1133 return $this->mTitle;
1134 }
1135
1136 # If there was more than one file "extension", reassemble the base
1137 # filename to prevent bogus complaints about length
1138 if ( count( $ext ) > 1 ) {
1139 $iterations = count( $ext ) - 1;
1140 for ( $i = 0; $i < $iterations; $i++ ) {
1141 $partname .= '.' . $ext[$i];
1142 }
1143 }
1144
1145 if ( strlen( $partname ) < 1 ) {
1146 $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1147 $this->mTitle = null;
1148
1149 return $this->mTitle;
1150 }
1151
1152 $this->mTitle = $nt;
1153
1154 return $this->mTitle;
1155 }
1156
1163 public function getLocalFile() {
1164 if ( $this->mLocalFile === null ) {
1165 $nt = $this->getTitle();
1166 $this->mLocalFile = $nt === null
1167 ? null
1168 : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1169 }
1170
1171 return $this->mLocalFile;
1172 }
1173
1177 public function getStashFile() {
1178 return $this->mStashFile;
1179 }
1180
1193 public function tryStashFile( User $user, $isPartial = false ) {
1194 if ( !$isPartial ) {
1195 $error = $this->runUploadStashFileHook( $user );
1196 if ( $error ) {
1197 return Status::newFatal( ...$error );
1198 }
1199 }
1200 try {
1201 $file = $this->doStashFile( $user );
1202 return Status::newGood( $file );
1203 } catch ( UploadStashException $e ) {
1204 return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1205 }
1206 }
1207
1212 protected function runUploadStashFileHook( User $user ) {
1213 $props = $this->mFileProps;
1214 $error = null;
1215 $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1216 if ( $error && !is_array( $error ) ) {
1217 $error = [ $error ];
1218 }
1219 return $error;
1220 }
1221
1229 protected function doStashFile( User $user = null ) {
1230 $stash = MediaWikiServices::getInstance()->getRepoGroup()
1231 ->getLocalRepo()->getUploadStash( $user );
1232 $file = $stash->stashFile( $this->mTempPath, $this->getSourceType(), $this->mFileProps );
1233 $this->mStashFile = $file;
1234
1235 return $file;
1236 }
1237
1242 public function cleanupTempFile() {
1243 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1244 // Delete when all relevant TempFSFile handles go out of scope
1245 wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1246 $this->tempFileObj->autocollect();
1247 }
1248 }
1249
1253 public function getTempPath() {
1254 return $this->mTempPath;
1255 }
1256
1266 public static function splitExtensions( $filename ) {
1267 $bits = explode( '.', $filename );
1268 $basename = array_shift( $bits );
1269
1270 return [ $basename, $bits ];
1271 }
1272
1280 public static function checkFileExtension( $ext, $list ) {
1281 return in_array( strtolower( $ext ?? '' ), $list, true );
1282 }
1283
1292 public static function checkFileExtensionList( $ext, $list ) {
1293 return array_intersect( array_map( 'strtolower', $ext ), $list );
1294 }
1295
1303 public static function verifyExtension( $mime, $extension ) {
1304 $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1305
1306 if ( !$mime || $mime === 'unknown' || $mime === 'unknown/unknown' ) {
1307 if ( !$magic->isRecognizableExtension( $extension ) ) {
1308 wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1309 "unrecognized extension '$extension', can't verify" );
1310
1311 return true;
1312 }
1313
1314 wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1315 "recognized extension '$extension', so probably invalid file" );
1316 return false;
1317 }
1318
1319 $match = $magic->isMatchingExtension( $extension, $mime );
1320
1321 if ( $match === null ) {
1322 if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1323 wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1324
1325 return false;
1326 }
1327
1328 wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1329 return true;
1330 }
1331
1332 if ( $match ) {
1333 wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1334
1336 return true;
1337 }
1338
1339 wfDebug( __METHOD__
1340 . ": mime type $mime mismatches file extension $extension, rejecting file" );
1341
1342 return false;
1343 }
1344
1356 public static function detectScript( $file, $mime, $extension ) {
1357 # ugly hack: for text files, always look at the entire file.
1358 # For binary field, just check the first K.
1359
1360 if ( str_starts_with( $mime ?? '', 'text/' ) ) {
1361 $chunk = file_get_contents( $file );
1362 } else {
1363 $fp = fopen( $file, 'rb' );
1364 if ( !$fp ) {
1365 return false;
1366 }
1367 $chunk = fread( $fp, 1024 );
1368 fclose( $fp );
1369 }
1370
1371 $chunk = strtolower( $chunk );
1372
1373 if ( !$chunk ) {
1374 return false;
1375 }
1376
1377 # decode from UTF-16 if needed (could be used for obfuscation).
1378 if ( str_starts_with( $chunk, "\xfe\xff" ) ) {
1379 $enc = 'UTF-16BE';
1380 } elseif ( str_starts_with( $chunk, "\xff\xfe" ) ) {
1381 $enc = 'UTF-16LE';
1382 } else {
1383 $enc = null;
1384 }
1385
1386 if ( $enc !== null ) {
1387 $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1388 }
1389
1390 $chunk = trim( $chunk );
1391
1393 wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1394
1395 # check for HTML doctype
1396 if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1397 return true;
1398 }
1399
1400 // Some browsers will interpret obscure xml encodings as UTF-8, while
1401 // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1402 if ( $extension === 'svg' || str_starts_with( $mime ?? '', 'image/svg' ) ) {
1403 if ( self::checkXMLEncodingMissmatch( $file ) ) {
1404 return true;
1405 }
1406 }
1407
1408 // Quick check for HTML heuristics in old IE and Safari.
1409 //
1410 // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1411 // don't need them all here as it can cause many false positives.
1412 //
1413 // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1414 $tags = [
1415 '<body',
1416 '<head',
1417 '<html', # also in safari
1418 '<script', # also in safari
1419 ];
1420
1421 foreach ( $tags as $tag ) {
1422 if ( strpos( $chunk, $tag ) !== false ) {
1423 wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1424
1425 return true;
1426 }
1427 }
1428
1429 /*
1430 * look for JavaScript
1431 */
1432
1433 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1434 $chunk = Sanitizer::decodeCharReferences( $chunk );
1435
1436 # look for script-types
1437 if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!im', $chunk ) ) {
1438 wfDebug( __METHOD__ . ": found script types" );
1439
1440 return true;
1441 }
1442
1443 # look for html-style script-urls
1444 if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) {
1445 wfDebug( __METHOD__ . ": found html-style script urls" );
1446
1447 return true;
1448 }
1449
1450 # look for css-style script-urls
1451 if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) {
1452 wfDebug( __METHOD__ . ": found css-style script urls" );
1453
1454 return true;
1455 }
1456
1457 wfDebug( __METHOD__ . ": no scripts found" );
1458
1459 return false;
1460 }
1461
1469 public static function checkXMLEncodingMissmatch( $file ) {
1470 // https://mimesniff.spec.whatwg.org/#resource-header says browsers
1471 // should read the first 1445 bytes. Do 4096 bytes for good measure.
1472 // XML Spec says XML declaration if present must be first thing in file
1473 // other than BOM
1474 $contents = file_get_contents( $file, false, null, 0, 4096 );
1475 $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1476
1477 if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1478 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1479 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1480 ) {
1481 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1482
1483 return true;
1484 }
1485 } elseif ( preg_match( "!<\?xml\b!i", $contents ) ) {
1486 // Start of XML declaration without an end in the first 4096 bytes
1487 // bytes. There shouldn't be a legitimate reason for this to happen.
1488 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1489
1490 return true;
1491 } elseif ( str_starts_with( $contents, "\x4C\x6F\xA7\x94" ) ) {
1492 // EBCDIC encoded XML
1493 wfDebug( __METHOD__ . ": EBCDIC Encoded XML" );
1494
1495 return true;
1496 }
1497
1498 // It's possible the file is encoded with multibyte encoding, so re-encode attempt to
1499 // detect the encoding in case it specifies an encoding not allowed in self::$safeXmlEncodings
1500 $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1501 foreach ( $attemptEncodings as $encoding ) {
1502 AtEase::suppressWarnings();
1503 $str = iconv( $encoding, 'UTF-8', $contents );
1504 AtEase::restoreWarnings();
1505 if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1506 if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1507 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1508 ) {
1509 wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1510
1511 return true;
1512 }
1513 } elseif ( $str != '' && preg_match( "!<\?xml\b!i", $str ) ) {
1514 // Start of XML declaration without an end in the first 4096 bytes
1515 // bytes. There shouldn't be a legitimate reason for this to happen.
1516 wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1517
1518 return true;
1519 }
1520 }
1521
1522 return false;
1523 }
1524
1530 protected function detectScriptInSvg( $filename, $partial ) {
1531 $this->mSVGNSError = false;
1532 $check = new XmlTypeCheck(
1533 $filename,
1534 [ $this, 'checkSvgScriptCallback' ],
1535 true,
1536 [
1537 'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ],
1538 'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ],
1539 ]
1540 );
1541 if ( $check->wellFormed !== true ) {
1542 // Invalid xml (T60553)
1543 // But only when non-partial (T67724)
1544 return $partial ? false : [ 'uploadinvalidxml' ];
1545 }
1546
1547 if ( $check->filterMatch ) {
1548 if ( $this->mSVGNSError ) {
1549 return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1550 }
1551 return $check->filterMatchType;
1552 }
1553
1554 return false;
1555 }
1556
1564 public static function checkSvgPICallback( $target, $data ) {
1565 // Don't allow external stylesheets (T59550)
1566 if ( preg_match( '/xml-stylesheet/i', $target ) ) {
1567 return [ 'upload-scripted-pi-callback' ];
1568 }
1569
1570 return false;
1571 }
1572
1585 public static function checkSvgExternalDTD( $type, $publicId, $systemId ) {
1586 // This doesn't include the XHTML+MathML+SVG doctype since we don't
1587 // allow XHTML anyway.
1588 static $allowedDTDs = [
1589 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1590 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1591 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1592 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1593 // https://phabricator.wikimedia.org/T168856
1594 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1595 ];
1596 if ( $type !== 'PUBLIC'
1597 || !in_array( $systemId, $allowedDTDs )
1598 || !str_starts_with( $publicId, "-//W3C//" )
1599 ) {
1600 return [ 'upload-scripted-dtd' ];
1601 }
1602 return false;
1603 }
1604
1612 public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1613 [ $namespace, $strippedElement ] = self::splitXmlNamespace( $element );
1614
1615 // We specifically don't include:
1616 // http://www.w3.org/1999/xhtml (T62771)
1617 static $validNamespaces = [
1618 '',
1619 'adobe:ns:meta/',
1620 'http://creativecommons.org/ns#',
1621 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1622 'http://ns.adobe.com/adobeillustrator/10.0/',
1623 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1624 'http://ns.adobe.com/extensibility/1.0/',
1625 'http://ns.adobe.com/flows/1.0/',
1626 'http://ns.adobe.com/illustrator/1.0/',
1627 'http://ns.adobe.com/imagereplacement/1.0/',
1628 'http://ns.adobe.com/pdf/1.3/',
1629 'http://ns.adobe.com/photoshop/1.0/',
1630 'http://ns.adobe.com/saveforweb/1.0/',
1631 'http://ns.adobe.com/variables/1.0/',
1632 'http://ns.adobe.com/xap/1.0/',
1633 'http://ns.adobe.com/xap/1.0/g/',
1634 'http://ns.adobe.com/xap/1.0/g/img/',
1635 'http://ns.adobe.com/xap/1.0/mm/',
1636 'http://ns.adobe.com/xap/1.0/rights/',
1637 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1638 'http://ns.adobe.com/xap/1.0/stype/font#',
1639 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1640 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1641 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1642 'http://ns.adobe.com/xap/1.0/t/pg/',
1643 'http://purl.org/dc/elements/1.1/',
1644 'http://purl.org/dc/elements/1.1',
1645 'http://schemas.microsoft.com/visio/2003/svgextensions/',
1646 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1647 'http://taptrix.com/inkpad/svg_extensions',
1648 'http://web.resource.org/cc/',
1649 'http://www.freesoftware.fsf.org/bkchem/cdml',
1650 'http://www.inkscape.org/namespaces/inkscape',
1651 'http://www.opengis.net/gml',
1652 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1653 'http://www.w3.org/2000/svg',
1654 'http://www.w3.org/tr/rec-rdf-syntax/',
1655 'http://www.w3.org/2000/01/rdf-schema#',
1656 'http://www.w3.org/2000/02/svg/testsuite/description/', // https://phabricator.wikimedia.org/T278044
1657 ];
1658
1659 // Inkscape mangles namespace definitions created by Adobe Illustrator.
1660 // This is nasty but harmless. (T144827)
1661 $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1662
1663 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1664 wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file." );
1666 $this->mSVGNSError = $namespace;
1667
1668 return true;
1669 }
1670
1671 // check for elements that can contain javascript
1672 if ( $strippedElement === 'script' ) {
1673 wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file." );
1674
1675 return [ 'uploaded-script-svg', $strippedElement ];
1676 }
1677
1678 // e.g., <svg xmlns="http://www.w3.org/2000/svg">
1679 // <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1680 if ( $strippedElement === 'handler' ) {
1681 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1682
1683 return [ 'uploaded-script-svg', $strippedElement ];
1684 }
1685
1686 // SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1687 if ( $strippedElement === 'stylesheet' ) {
1688 wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1689
1690 return [ 'uploaded-script-svg', $strippedElement ];
1691 }
1692
1693 // Block iframes, in case they pass the namespace check
1694 if ( $strippedElement === 'iframe' ) {
1695 wfDebug( __METHOD__ . ": iframe in uploaded file." );
1696
1697 return [ 'uploaded-script-svg', $strippedElement ];
1698 }
1699
1700 // Check <style> css
1701 if ( $strippedElement === 'style'
1702 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1703 ) {
1704 wfDebug( __METHOD__ . ": hostile css in style element." );
1705
1706 return [ 'uploaded-hostile-svg' ];
1707 }
1708
1709 static $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1710 'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1711
1712 foreach ( $attribs as $attrib => $value ) {
1713 // If attributeNamespace is '', it is relative to its element's namespace
1714 [ $attributeNamespace, $stripped ] = self::splitXmlNamespace( $attrib );
1715 $value = strtolower( $value );
1716
1717 if ( !(
1718 // Inkscape element's have valid attribs that start with on and are safe, fail all others
1719 $namespace === 'http://www.inkscape.org/namespaces/inkscape' &&
1720 $attributeNamespace === ''
1721 ) && str_starts_with( $stripped, 'on' )
1722 ) {
1723 wfDebug( __METHOD__
1724 . ": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1725
1726 return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1727 }
1728
1729 // Do not allow relative links, or unsafe url schemas.
1730 // For <a> tags, only data:, http: and https: and same-document
1731 // fragment links are allowed.
1732 // For all other tags, only 'data:' and fragments (#) are allowed.
1733 if (
1734 $stripped === 'href'
1735 && $value !== ''
1736 && !str_starts_with( $value, 'data:' )
1737 && !str_starts_with( $value, '#' )
1738 && !( $strippedElement === 'a' && preg_match( '!^https?://!i', $value ) )
1739 ) {
1740 wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1741 . "'$attrib'='$value' in uploaded file." );
1742
1743 return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1744 }
1745
1746 // Only allow 'data:\' targets that should be safe.
1747 // This prevents vectors like image/svg, text/xml, application/xml, and text/html, which can contain scripts
1748 if ( $stripped === 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1749 // RFC2397 parameters.
1750 // This is only slightly slower than (;[\w;]+)*.
1751 // phpcs:ignore Generic.Files.LineLength
1752 $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1753
1754 if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1755 wfDebug( __METHOD__ . ": Found href to allow listed data: uri "
1756 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1757 return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1758 }
1759 }
1760
1761 // Change href with animate from (http://html5sec.org/#137).
1762 if ( $stripped === 'attributename'
1763 && $strippedElement === 'animate'
1764 && $this->stripXmlNamespace( $value ) === 'href'
1765 ) {
1766 wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1767 . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1768
1769 return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1770 }
1771
1772 // Use set/animate to add event-handler attribute to parent.
1773 if ( ( $strippedElement === 'set' || $strippedElement === 'animate' )
1774 && $stripped === 'attributename'
1775 && str_starts_with( $value, 'on' )
1776 ) {
1777 wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1778 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1779
1780 return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1781 }
1782
1783 // use set to add href attribute to parent element.
1784 if ( $strippedElement === 'set'
1785 && $stripped === 'attributename'
1786 && str_contains( $value, 'href' )
1787 ) {
1788 wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file." );
1789
1790 return [ 'uploaded-setting-href-svg' ];
1791 }
1792
1793 // use set to add a remote / data / script target to an element.
1794 if ( $strippedElement === 'set'
1795 && $stripped === 'to'
1796 && preg_match( '!(http|https|data|script):!im', $value )
1797 ) {
1798 wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file." );
1799
1800 return [ 'uploaded-wrong-setting-svg', $value ];
1801 }
1802
1803 // use handler attribute with remote / data / script.
1804 if ( $stripped === 'handler' && preg_match( '!(http|https|data|script):!im', $value ) ) {
1805 wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1806 . "'$attrib'='$value' in uploaded file." );
1807
1808 return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1809 }
1810
1811 // use CSS styles to bring in remote code.
1812 if ( $stripped === 'style'
1813 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1814 ) {
1815 wfDebug( __METHOD__ . ": Found svg setting a style with "
1816 . "remote url '$attrib'='$value' in uploaded file." );
1817 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1818 }
1819
1820 // Several attributes can include css, css character escaping isn't allowed.
1821 if ( in_array( $stripped, $cssAttrs, true )
1822 && self::checkCssFragment( $value )
1823 ) {
1824 wfDebug( __METHOD__ . ": Found svg setting a style with "
1825 . "remote url '$attrib'='$value' in uploaded file." );
1826 return [ 'uploaded-remote-url-svg', $attrib, $value ];
1827 }
1828
1829 // image filters can pull in url, which could be svg that executes scripts.
1830 // Only allow url( "#foo" ).
1831 // Do not allow url( http://example.com )
1832 if ( $strippedElement === 'image'
1833 && $stripped === 'filter'
1834 && preg_match( '!url\s*\‍(\s*["\']?[^#]!im', $value )
1835 ) {
1836 wfDebug( __METHOD__ . ": Found image filter with url: "
1837 . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1838
1839 return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1840 }
1841 }
1842
1843 return false; // No scripts detected
1844 }
1845
1852 private static function checkCssFragment( $value ) {
1853 # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1854 if ( stripos( $value, '@import' ) !== false ) {
1855 return true;
1856 }
1857
1858 # We allow @font-face to embed fonts with data: urls, so we snip the string
1859 # 'url' out so that this case won't match when we check for urls below
1860 $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1861 $value = preg_replace( $pattern, '$1$2', $value );
1862
1863 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1864 # properties filter and accelerator don't seem to be useful for xss in SVG files.
1865 # Expression and -o-link don't seem to work either, but filtering them here in case.
1866 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1867 # but not local ones such as url("#..., url('#..., url(#....
1868 if ( preg_match( '!expression
1869 | -o-link\s*:
1870 | -o-link-source\s*:
1871 | -o-replace\s*:!imx', $value ) ) {
1872 return true;
1873 }
1874
1875 if ( preg_match_all(
1876 "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1877 $value,
1878 $matches
1879 ) !== 0
1880 ) {
1881 # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1882 foreach ( $matches[1] as $match ) {
1883 if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1884 return true;
1885 }
1886 }
1887 }
1888
1889 if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1890 return true;
1891 }
1892
1893 return false;
1894 }
1895
1901 private static function splitXmlNamespace( $element ) {
1902 // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1903 $parts = explode( ':', strtolower( $element ) );
1904 $name = array_pop( $parts );
1905 $ns = implode( ':', $parts );
1906
1907 return [ $ns, $name ];
1908 }
1909
1914 private function stripXmlNamespace( $element ) {
1915 // 'http://www.w3.org/2000/svg:script' -> 'script'
1916 return self::splitXmlNamespace( $element )[1];
1917 }
1918
1929 public static function detectVirus( $file ) {
1930 global $wgOut;
1931 $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1932 $antivirus = $mainConfig->get( MainConfigNames::Antivirus );
1933 $antivirusSetup = $mainConfig->get( MainConfigNames::AntivirusSetup );
1934 $antivirusRequired = $mainConfig->get( MainConfigNames::AntivirusRequired );
1935 if ( !$antivirus ) {
1936 wfDebug( __METHOD__ . ": virus scanner disabled" );
1937
1938 return null;
1939 }
1940
1941 if ( !$antivirusSetup[$antivirus] ) {
1942 wfDebug( __METHOD__ . ": unknown virus scanner: {$antivirus}" );
1943 $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1944 [ 'virus-badscanner', $antivirus ] );
1945
1946 return wfMessage( 'virus-unknownscanner' )->text() . " {$antivirus}";
1947 }
1948
1949 # look up scanner configuration
1950 $command = $antivirusSetup[$antivirus]['command'];
1951 $exitCodeMap = $antivirusSetup[$antivirus]['codemap'];
1952 $msgPattern = $antivirusSetup[$antivirus]['messagepattern'] ?? null;
1953
1954 if ( !str_contains( $command, "%f" ) ) {
1955 # simple pattern: append file to scan
1956 $command .= " " . Shell::escape( $file );
1957 } else {
1958 # complex pattern: replace "%f" with file to scan
1959 $command = str_replace( "%f", Shell::escape( $file ), $command );
1960 }
1961
1962 wfDebug( __METHOD__ . ": running virus scan: $command " );
1963
1964 # execute virus scanner
1965 $exitCode = false;
1966
1967 # NOTE: there's a 50-line workaround to make stderr redirection work on windows, too.
1968 # that does not seem to be worth the pain.
1969 # Ask me (Duesentrieb) about it if it's ever needed.
1970 $output = wfShellExecWithStderr( $command, $exitCode );
1971
1972 # map exit code to AV_xxx constants.
1973 $mappedCode = $exitCode;
1974 if ( $exitCodeMap ) {
1975 if ( isset( $exitCodeMap[$exitCode] ) ) {
1976 $mappedCode = $exitCodeMap[$exitCode];
1977 } elseif ( isset( $exitCodeMap["*"] ) ) {
1978 $mappedCode = $exitCodeMap["*"];
1979 }
1980 }
1981
1982 # NB: AV_NO_VIRUS is 0, but AV_SCAN_FAILED is false,
1983 # so we need the strict equalities === and thus can't use a switch here
1984 if ( $mappedCode === AV_SCAN_FAILED ) {
1985 # scan failed (code was mapped to false by $exitCodeMap)
1986 wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1987
1988 $output = $antivirusRequired
1989 ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1990 : null;
1991 } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1992 # scan failed because filetype is unknown (probably immune)
1993 wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1994 $output = null;
1995 } elseif ( $mappedCode === AV_NO_VIRUS ) {
1996 # no virus found
1997 wfDebug( __METHOD__ . ": file passed virus scan." );
1998 $output = false;
1999 } else {
2000 $output = trim( $output );
2001
2002 if ( !$output ) {
2003 $output = true; # if there's no output, return true
2004 } elseif ( $msgPattern ) {
2005 $groups = [];
2006 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
2007 $output = $groups[1];
2008 }
2009 }
2010
2011 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
2012 }
2013
2014 return $output;
2015 }
2016
2025 private function checkOverwrite( Authority $performer ) {
2026 // First check whether the local file can be overwritten
2027 $file = $this->getLocalFile();
2028 $file->load( IDBAccessObject::READ_LATEST );
2029 if ( $file->exists() ) {
2030 if ( !self::userCanReUpload( $performer, $file ) ) {
2031 return [ 'fileexists-forbidden', $file->getName() ];
2032 }
2033
2034 return true;
2035 }
2036
2037 $services = MediaWikiServices::getInstance();
2038
2039 /* Check shared conflicts: if the local file does not exist, but
2040 * RepoGroup::findFile finds a file, it exists in a shared repository.
2041 */
2042 $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
2043 if ( $file && !$performer->isAllowed( 'reupload-shared' ) ) {
2044 return [ 'fileexists-shared-forbidden', $file->getName() ];
2045 }
2046
2047 return true;
2048 }
2049
2057 public static function userCanReUpload( Authority $performer, File $img ) {
2058 if ( $performer->isAllowed( 'reupload' ) ) {
2059 return true; // non-conditional
2060 }
2061
2062 if ( !$performer->isAllowed( 'reupload-own' ) ) {
2063 return false;
2064 }
2065
2066 if ( !( $img instanceof LocalFile ) ) {
2067 return false;
2068 }
2069
2070 return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2071 }
2072
2084 public static function getExistsWarning( $file ) {
2085 if ( $file->exists() ) {
2086 return [ 'warning' => 'exists', 'file' => $file ];
2087 }
2088
2089 if ( $file->getTitle()->getArticleID() ) {
2090 return [ 'warning' => 'page-exists', 'file' => $file ];
2091 }
2092
2093 $n = strrpos( $file->getName(), '.' );
2094 if ( $n > 0 ) {
2095 $partname = substr( $file->getName(), 0, $n );
2096 $extension = substr( $file->getName(), $n + 1 );
2097 } else {
2098 $partname = $file->getName();
2099 $extension = '';
2100 }
2101 $normalizedExtension = File::normalizeExtension( $extension );
2102 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2103
2104 if ( $normalizedExtension != $extension ) {
2105 // We're not using the normalized form of the extension.
2106 // Normal form is lowercase, using most common of alternate
2107 // extensions (e.g. 'jpg' rather than 'JPEG').
2108
2109 // Check for another file using the normalized form...
2110 $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2111 $file_lc = $localRepo->newFile( $nt_lc );
2112
2113 if ( $file_lc->exists() ) {
2114 return [
2115 'warning' => 'exists-normalized',
2116 'file' => $file,
2117 'normalizedFile' => $file_lc
2118 ];
2119 }
2120 }
2121
2122 // Check for files with the same name but a different extension
2123 $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2124 if ( count( $similarFiles ) ) {
2125 return [
2126 'warning' => 'exists-normalized',
2127 'file' => $file,
2128 'normalizedFile' => $similarFiles[0],
2129 ];
2130 }
2131
2132 if ( self::isThumbName( $file->getName() ) ) {
2133 // Check for filenames like 50px- or 180px-, these are mostly thumbnails
2134 $nt_thb = Title::newFromText(
2135 substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2136 NS_FILE
2137 );
2138 $file_thb = $localRepo->newFile( $nt_thb );
2139 if ( $file_thb->exists() ) {
2140 return [
2141 'warning' => 'thumb',
2142 'file' => $file,
2143 'thumbFile' => $file_thb
2144 ];
2145 }
2146
2147 // The file does not exist, but we just don't like the name
2148 return [
2149 'warning' => 'thumb-name',
2150 'file' => $file,
2151 'thumbFile' => $file_thb
2152 ];
2153 }
2154
2155 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2156 if ( str_starts_with( $partname, $prefix ) ) {
2157 return [
2158 'warning' => 'bad-prefix',
2159 'file' => $file,
2160 'prefix' => $prefix
2161 ];
2162 }
2163 }
2164
2165 return false;
2166 }
2167
2173 public static function isThumbName( $filename ) {
2174 $n = strrpos( $filename, '.' );
2175 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2176
2177 return (
2178 substr( $partname, 3, 3 ) === 'px-' ||
2179 substr( $partname, 2, 3 ) === 'px-'
2180 ) && preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2181 }
2182
2188 public static function getFilenamePrefixBlacklist() {
2189 $list = [];
2190 $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2191 if ( !$message->isDisabled() ) {
2192 $lines = explode( "\n", $message->plain() );
2193 foreach ( $lines as $line ) {
2194 // Remove comment lines
2195 $comment = substr( trim( $line ), 0, 1 );
2196 if ( $comment === '#' || $comment == '' ) {
2197 continue;
2198 }
2199 // Remove additional comments after a prefix
2200 $comment = strpos( $line, '#' );
2201 if ( $comment > 0 ) {
2202 $line = substr( $line, 0, $comment - 1 );
2203 }
2204 $list[] = trim( $line );
2205 }
2206 }
2207
2208 return $list;
2209 }
2210
2220 public function getImageInfo( $result = null ) {
2221 $apiUpload = ApiUpload::getDummyInstance();
2222 return $apiUpload->getUploadImageInfo( $this );
2223 }
2224
2229 public function convertVerifyErrorToStatus( $error ) {
2230 $code = $error['status'];
2231 unset( $code['status'] );
2232
2233 return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2234 }
2235
2243 public static function getMaxUploadSize( $forType = null ) {
2244 $maxUploadSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxUploadSize );
2245
2246 if ( is_array( $maxUploadSize ) ) {
2247 if ( $forType !== null && isset( $maxUploadSize[$forType] ) ) {
2248 return $maxUploadSize[$forType];
2249 }
2250 return $maxUploadSize['*'];
2251 }
2252 return intval( $maxUploadSize );
2253 }
2254
2262 public static function getMaxPhpUploadSize() {
2263 $phpMaxFileSize = wfShorthandToInteger(
2264 ini_get( 'upload_max_filesize' ),
2265 PHP_INT_MAX
2266 );
2267 $phpMaxPostSize = wfShorthandToInteger(
2268 ini_get( 'post_max_size' ),
2269 PHP_INT_MAX
2270 ) ?: PHP_INT_MAX;
2271 return min( $phpMaxFileSize, $phpMaxPostSize );
2272 }
2273
2285 public static function getSessionStatus( UserIdentity $user, $statusKey ) {
2286 $store = self::getUploadSessionStore();
2287 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2288
2289 return $store->get( $key );
2290 }
2291
2304 public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
2305 $store = self::getUploadSessionStore();
2306 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2307 $logger = LoggerFactory::getInstance( 'upload' );
2308
2309 if ( is_array( $value ) && ( $value['result'] ?? '' ) === 'Failure' ) {
2310 $logger->info( 'Upload session {key} for {user} set to failure {status} at {stage}',
2311 [
2312 'result' => $value['result'] ?? '',
2313 'stage' => $value['stage'] ?? 'unknown',
2314 'user' => $user->getName(),
2315 'status' => (string)( $value['status'] ?? '-' ),
2316 'filekey' => $value['filekey'] ?? '',
2317 'key' => $statusKey
2318 ]
2319 );
2320 } elseif ( is_array( $value ) ) {
2321 $logger->debug( 'Upload session {key} for {user} changed {status} at {stage}',
2322 [
2323 'result' => $value['result'] ?? '',
2324 'stage' => $value['stage'] ?? 'unknown',
2325 'user' => $user->getName(),
2326 'status' => (string)( $value['status'] ?? '-' ),
2327 'filekey' => $value['filekey'] ?? '',
2328 'key' => $statusKey
2329 ]
2330 );
2331 } else {
2332 $logger->debug( "Upload session {key} deleted for {user}",
2333 [
2334 'value' => $value,
2335 'key' => $statusKey,
2336 'user' => $user->getName()
2337 ]
2338 );
2339 }
2340
2341 if ( $value === false ) {
2342 $store->delete( $key );
2343 } else {
2344 $store->set( $key, $value, $store::TTL_DAY );
2345 }
2346 }
2347
2354 private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
2355 return $store->makeKey(
2356 'uploadstatus',
2357 $user->isRegistered() ? $user->getId() : md5( $user->getName() ),
2358 $statusKey
2359 );
2360 }
2361
2365 private static function getUploadSessionStore() {
2366 return MediaWikiServices::getInstance()->getMainObjectStash();
2367 }
2368}
const AV_SCAN_FAILED
Definition Defines.php:99
const NS_FILE
Definition Defines.php:70
const AV_SCAN_ABORTED
Definition Defines.php:98
const AV_NO_VIRUS
Definition Defines.php:96
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
wfShorthandToInteger(?string $string='', int $default=-1)
Converts shorthand byte notation to integer form.
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with '-'.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that a deprecated feature was used.
Title null $mTitle
if(!defined( 'MW_NO_SESSION') &&MW_ENTRY_POINT !=='cli' $wgOut
Definition Setup.php:536
static getDummyInstance()
Deleted file in the 'filearchive' table.
Class representing a cache/ephemeral data store.
Definition BagOStuff.php:85
get( $key, $flags=0)
Get an item.
delete( $key, $flags=0)
Delete an item if it exists.
set( $key, $value, $exptime=0, $flags=0)
Set an item.
makeKey( $keygroup,... $components)
Make a cache key from the given components, in the default 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:287
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition File.php:73
getName()
Return the name of this file.
Definition File.php:341
wasDeleted()
Was this file ever deleted from the wiki?
Definition File.php:2092
Local file in the wiki's own database.
Definition LocalFile.php:68
exists()
canRender inherited
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
load( $flags=0)
Load file metadata from cache or DB, unless already loaded.
MimeMagic helper wrapper.
Group all the pieces relevant to the context of a request into one instance.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Create PSR-3 logger objects.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
HTML sanitizer for MediaWiki.
Definition Sanitizer.php:46
A StatusValue for permission errors.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Executes shell commands.
Definition Shell.php:46
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
Represents a title within MediaWiki.
Definition Title.php:78
getDBkey()
Get the main part with underscores.
Definition Title.php:1035
internal since 1.36
Definition User.php:93
This class is used to hold the location and do limited manipulation of files stored temporarily (this...
UploadBase and subclasses are the backend of MediaWiki's file uploads.
getSourceType()
Returns the upload type.
getDesiredDestName()
Get the desired destination name.
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
int $mTitleError
static setSessionStatus(UserIdentity $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling).
const EMPTY_FILE
UploadStashFile null $mStashFile
static verifyExtension( $mime, $extension)
Checks if the MIME type of the uploaded file matches the file extension.
postProcessUpload()
Perform extra steps after a successful upload.
checkSvgScriptCallback( $element, $attribs, $data=null)
verifyPermissions(Authority $performer)
Alias for verifyTitlePermissions.
getLocalFile()
Return the local file and initializes if necessary.
const SUCCESS
bool null $mJavaDetected
string null $mFilteredName
getRealPath( $srcPath)
static createFromRequest(&$request, $type=null)
Create a form of UploadBase depending on wpSourceType and initializes it.
runUploadStashFileHook(User $user)
zipEntryCallback( $entry)
Callback for ZipDirectoryReader to detect Java class files.
static checkSvgPICallback( $target, $data)
Callback to filter SVG Processing Instructions.
static isValidRequest( $request)
Check whether a request if valid for this handler.
convertVerifyErrorToStatus( $error)
string null $mFinalExtension
verifyPartialFile()
A verification routine suitable for partial files.
static detectScript( $file, $mime, $extension)
Heuristic for detecting files that could contain JavaScript instructions or things that may look like...
verifyFile()
Verifies that it's ok to include the uploaded file.
array null $mFileProps
static isEnabled()
Returns true if uploads are enabled.
static isThumbName( $filename)
Helper function that checks whether the filename looks like a thumbnail.
getVerificationErrorCode( $error)
performUpload( $comment, $pageText, $watch, $user, $tags=[], ?string $watchlistExpiry=null)
Really perform the upload.
string null $mDesiredDestName
verifyTitlePermissions(Authority $performer)
Check whether the user can edit, upload and create the image.
static getFilenamePrefixBlacklist()
Get a list of disallowed filename prefixes from [[MediaWiki:Filename-prefix-blacklist]].
const OVERWRITE_EXISTING_FILE
setTempFile( $tempPath, $fileSize=null)
static getSessionStatus(UserIdentity $user, $statusKey)
Get the current status of a chunked upload (used for polling).
static checkXMLEncodingMissmatch( $file)
Check an allowed list of xml encodings that are known not to be interpreted differently by the server...
doStashFile(User $user=null)
Implementation for stashFile() and tryStashFile().
string null $mDestName
string[] $mBlackListedExtensions
static isAllowed(Authority $performer)
Returns true if the user can use this upload module or else a string identifying the missing permissi...
cleanupTempFile()
If we've modified the upload file, then we need to manually remove it on exit to clean up.
getImageInfo( $result=null)
Gets image info about the file just uploaded.
validateName()
Verify that the name is valid and, if necessary, that we can overwrite.
string null $mSourceType
int null $mFileSize
isEmptyFile()
Return true if the file is empty.
static checkFileExtension( $ext, $list)
Perform case-insensitive match against a list of file extensions.
tryStashFile(User $user, $isPartial=false)
Like stashFile(), but respects extensions' wishes to prevent the stashing.
getTitle()
Returns the title of the file to be uploaded.
initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile=false)
static getMaxUploadSize( $forType=null)
Get MediaWiki's maximum uploaded file size for a given type of upload, based on $wgMaxUploadSize.
bool null $mRemoveTempFile
static checkSvgExternalDTD( $type, $publicId, $systemId)
Verify that DTD URLs referenced are only the standard DTDs.
getTempFileSha1Base36()
Get the base 36 SHA1 of the file.
detectScriptInSvg( $filename, $partial)
static splitExtensions( $filename)
Split a file into a base name and all dot-delimited 'extensions' on the end.
fetchFile()
Fetch the file.
checkWarnings( $user=null)
Check for non fatal problems with the file.
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
getFileSize()
Return the file size.
verifyUpload()
Verify whether the upload is sensible.
const ILLEGAL_FILENAME
const MIN_LENGTH_PARTNAME
static checkFileExtensionList( $ext, $list)
Perform case-insensitive match against a list of file extensions.
static detectVirus( $file)
Generic wrapper function for a virus scanner program.
string null $mTempPath
Local file system path to the file to upload (or a local copy)
TempFSFile null $tempFileObj
Wrapper to handle deleting the temp file.
LocalFile null $mLocalFile
canFetchFile()
Perform checks to see if the file can be fetched.
static getMaxPhpUploadSize()
Get the PHP maximum uploaded file size, based on ini settings.
static $safeXmlEncodings
verifyMimeType( $mime)
Verify the MIME type.
static unserializeWarnings( $warnings)
Convert the serialized warnings array created by makeWarningsSerializable() back to the output of che...
initializeFromRequest(&$request)
Initialize from a WebRequest.
string false $mSVGNSError
XML syntax and type checker.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
authorizeWrite(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize write access.
isAllowed(string $permission, PermissionStatus $status=null)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
isRegistered()
This must be equivalent to getId() != 0 and is provided for code readability.
getId( $wikiId=self::LOCAL)
if(!file_exists( $CREDITS)) $lines