MediaWiki  master
UploadBase.php
Go to the documentation of this file.
1 <?php
24 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
31 use Wikimedia\AtEase\AtEase;
32 
49 abstract class UploadBase {
50  use ProtectedHookAccessorTrait;
51 
53  protected $mTempPath;
55  protected $tempFileObj;
57  protected $mDesiredDestName;
59  protected $mDestName;
61  protected $mRemoveTempFile;
63  protected $mSourceType;
65  protected $mTitle = false;
67  protected $mTitleError = 0;
69  protected $mFilteredName;
71  protected $mFinalExtension;
73  protected $mLocalFile;
75  protected $mStashFile;
77  protected $mFileSize;
79  protected $mFileProps;
83  protected $mJavaDetected;
85  protected $mSVGNSError;
86 
87  protected static $safeXmlEncodings = [
88  'UTF-8',
89  'US-ASCII',
90  'ISO-8859-1',
91  'ISO-8859-2',
92  'UTF-16',
93  'UTF-32',
94  'WINDOWS-1250',
95  'WINDOWS-1251',
96  'WINDOWS-1252',
97  'WINDOWS-1253',
98  'WINDOWS-1254',
99  'WINDOWS-1255',
100  'WINDOWS-1256',
101  'WINDOWS-1257',
102  'WINDOWS-1258',
103  ];
104 
105  public const SUCCESS = 0;
106  public const OK = 0;
107  public const EMPTY_FILE = 3;
108  public const MIN_LENGTH_PARTNAME = 4;
109  public const ILLEGAL_FILENAME = 5;
110  public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
111  public const FILETYPE_MISSING = 8;
112  public const FILETYPE_BADTYPE = 9;
113  public const VERIFICATION_ERROR = 10;
114  public const HOOK_ABORTED = 11;
115  public const FILE_TOO_LARGE = 12;
116  public const WINDOWS_NONASCII_FILENAME = 13;
117  public const FILENAME_TOO_LONG = 14;
118 
123  public function getVerificationErrorCode( $error ) {
124  $code_to_status = [
125  self::EMPTY_FILE => 'empty-file',
126  self::FILE_TOO_LARGE => 'file-too-large',
127  self::FILETYPE_MISSING => 'filetype-missing',
128  self::FILETYPE_BADTYPE => 'filetype-banned',
129  self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
130  self::ILLEGAL_FILENAME => 'illegal-filename',
131  self::OVERWRITE_EXISTING_FILE => 'overwrite',
132  self::VERIFICATION_ERROR => 'verification-error',
133  self::HOOK_ABORTED => 'hookaborted',
134  self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
135  self::FILENAME_TOO_LONG => 'filename-toolong',
136  ];
137  return $code_to_status[$error] ?? 'unknown-error';
138  }
139 
146  public static function isEnabled() {
147  $enableUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnableUploads );
148 
149  return $enableUploads && wfIniGetBool( 'file_uploads' );
150  }
151 
160  public static function isAllowed( Authority $performer ) {
161  foreach ( [ 'upload', 'edit' ] as $permission ) {
162  if ( !$performer->isAllowed( $permission ) ) {
163  return $permission;
164  }
165  }
166 
167  return true;
168  }
169 
176  public static function isThrottled( $user ) {
177  return $user->pingLimiter( 'upload' );
178  }
179 
181  private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
182 
190  public static function createFromRequest( &$request, $type = null ) {
191  $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
192 
193  if ( !$type ) {
194  return null;
195  }
196 
197  // Get the upload class
198  $type = ucfirst( $type );
199 
200  // Give hooks the chance to handle this request
202  $className = null;
203  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
204  Hooks::runner()->onUploadCreateFromRequest( $type, $className );
205  if ( $className === null ) {
206  $className = 'UploadFrom' . $type;
207  wfDebug( __METHOD__ . ": class name: $className" );
208  if ( !in_array( $type, self::$uploadHandlers ) ) {
209  return null;
210  }
211  }
212 
213  // Check whether this upload class is enabled
214  if ( !$className::isEnabled() ) {
215  return null;
216  }
217 
218  // Check whether the request is valid
219  if ( !$className::isValidRequest( $request ) ) {
220  return null;
221  }
222 
224  $handler = new $className;
225 
226  $handler->initializeFromRequest( $request );
227 
228  return $handler;
229  }
230 
236  public static function isValidRequest( $request ) {
237  return false;
238  }
239 
243  public function __construct() {
244  }
245 
253  public function getSourceType() {
254  return null;
255  }
256 
264  public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
265  $this->mDesiredDestName = $name;
266  if ( FileBackend::isStoragePath( $tempPath ) ) {
267  throw new MWException( __METHOD__ . " given storage path `$tempPath`." );
268  }
269 
270  $this->setTempFile( $tempPath, $fileSize );
271  $this->mRemoveTempFile = $removeTempFile;
272  }
273 
279  abstract public function initializeFromRequest( &$request );
280 
285  protected function setTempFile( $tempPath, $fileSize = null ) {
286  $this->mTempPath = $tempPath ?? '';
287  $this->mFileSize = $fileSize ?: null;
288  if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
289  $this->tempFileObj = new TempFSFile( $this->mTempPath );
290  if ( !$fileSize ) {
291  $this->mFileSize = filesize( $this->mTempPath );
292  }
293  } else {
294  $this->tempFileObj = null;
295  }
296  }
297 
303  public function fetchFile() {
304  return Status::newGood();
305  }
306 
311  public function isEmptyFile() {
312  return empty( $this->mFileSize );
313  }
314 
319  public function getFileSize() {
320  return $this->mFileSize;
321  }
322 
328  public function getTempFileSha1Base36() {
329  return FSFile::getSha1Base36FromPath( $this->mTempPath );
330  }
331 
336  public function getRealPath( $srcPath ) {
337  $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
338  if ( FileRepo::isVirtualUrl( $srcPath ) ) {
342  $tmpFile = $repo->getLocalCopy( $srcPath );
343  if ( $tmpFile ) {
344  $tmpFile->bind( $this ); // keep alive with $this
345  }
346  $path = $tmpFile ? $tmpFile->getPath() : false;
347  } else {
348  $path = $srcPath;
349  }
350 
351  return $path;
352  }
353 
371  public function verifyUpload() {
375  if ( $this->isEmptyFile() ) {
376  return [ 'status' => self::EMPTY_FILE ];
377  }
378 
382  $maxSize = self::getMaxUploadSize( $this->getSourceType() );
383  if ( $this->mFileSize > $maxSize ) {
384  return [
385  'status' => self::FILE_TOO_LARGE,
386  'max' => $maxSize,
387  ];
388  }
389 
395  $verification = $this->verifyFile();
396  if ( $verification !== true ) {
397  return [
398  'status' => self::VERIFICATION_ERROR,
399  'details' => $verification
400  ];
401  }
402 
406  $result = $this->validateName();
407  if ( $result !== true ) {
408  return $result;
409  }
410 
411  return [ 'status' => self::OK ];
412  }
413 
420  public function validateName() {
421  $nt = $this->getTitle();
422  if ( $nt === null ) {
423  $result = [ 'status' => $this->mTitleError ];
424  if ( $this->mTitleError === self::ILLEGAL_FILENAME ) {
425  $result['filtered'] = $this->mFilteredName;
426  }
427  if ( $this->mTitleError === self::FILETYPE_BADTYPE ) {
428  $result['finalExt'] = $this->mFinalExtension;
429  if ( count( $this->mBlackListedExtensions ) ) {
430  $result['blacklistedExt'] = $this->mBlackListedExtensions;
431  }
432  }
433 
434  return $result;
435  }
436  $this->mDestName = $this->getLocalFile()->getName();
437 
438  return true;
439  }
440 
450  protected function verifyMimeType( $mime ) {
451  $verifyMimeType = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeType );
452  $verifyMimeTypeIE = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeTypeIE );
453  if ( $verifyMimeType ) {
454  wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" );
455  $mimeTypeExclusions = MediaWikiServices::getInstance()->getMainConfig()
456  ->get( MainConfigNames::MimeTypeExclusions );
457  if ( self::checkFileExtension( $mime, $mimeTypeExclusions ) ) {
458  return [ 'filetype-badmime', $mime ];
459  }
460 
461  if ( $verifyMimeTypeIE ) {
462  # Check what Internet Explorer would detect
463  $fp = fopen( $this->mTempPath, 'rb' );
464  if ( $fp ) {
465  $chunk = fread( $fp, 256 );
466  fclose( $fp );
467 
468  $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
469  $extMime = $magic->getMimeTypeFromExtensionOrNull( (string)$this->mFinalExtension ) ?? '';
470  $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
471  foreach ( $ieTypes as $ieType ) {
472  if ( self::checkFileExtension( $ieType, $mimeTypeExclusions ) ) {
473  return [ 'filetype-bad-ie-mime', $ieType ];
474  }
475  }
476  }
477  }
478  }
479 
480  return true;
481  }
482 
488  protected function verifyFile() {
489  $config = MediaWikiServices::getInstance()->getMainConfig();
490  $verifyMimeType = $config->get( MainConfigNames::VerifyMimeType );
491  $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
492  $status = $this->verifyPartialFile();
493  if ( $status !== true ) {
494  return $status;
495  }
496 
497  $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
498  $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
499  $mime = $this->mFileProps['mime'];
500 
501  if ( $verifyMimeType ) {
502  # XXX: Missing extension will be caught by validateName() via getTitle()
503  if ( (string)$this->mFinalExtension !== '' &&
504  !self::verifyExtension( $mime, $this->mFinalExtension )
505  ) {
506  return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
507  }
508  }
509 
510  # check for htmlish code and javascript
511  if ( !$disableUploadScriptChecks ) {
512  if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
513  $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
514  if ( $svgStatus !== false ) {
515  return $svgStatus;
516  }
517  }
518  }
519 
520  $handler = MediaHandler::getHandler( $mime );
521  if ( $handler ) {
522  $handlerStatus = $handler->verifyUpload( $this->mTempPath );
523  if ( !$handlerStatus->isOK() ) {
524  $errors = $handlerStatus->getErrorsArray();
525 
526  return reset( $errors );
527  }
528  }
529 
530  $error = true;
531  $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
532  if ( $error !== true ) {
533  if ( !is_array( $error ) ) {
534  $error = [ $error ];
535  }
536  return $error;
537  }
538 
539  wfDebug( __METHOD__ . ": all clear; passing." );
540 
541  return true;
542  }
543 
553  protected function verifyPartialFile() {
554  $config = MediaWikiServices::getInstance()->getMainConfig();
555  $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
556  # getTitle() sets some internal parameters like $this->mFinalExtension
557  $this->getTitle();
558 
559  $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
560  $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
561 
562  # check MIME type, if desired
563  $mime = $this->mFileProps['file-mime'];
564  $status = $this->verifyMimeType( $mime );
565  if ( $status !== true ) {
566  return $status;
567  }
568 
569  # check for htmlish code and javascript
570  if ( !$disableUploadScriptChecks ) {
571  if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
572  return [ 'uploadscripted' ];
573  }
574  if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
575  $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
576  if ( $svgStatus !== false ) {
577  return $svgStatus;
578  }
579  }
580  }
581 
582  # Scan the uploaded file for viruses
583  $virus = self::detectVirus( $this->mTempPath );
584  if ( $virus ) {
585  return [ 'uploadvirus', $virus ];
586  }
587 
588  return true;
589  }
590 
596  public function zipEntryCallback( $entry ) {
597  $names = [ $entry['name'] ];
598 
599  // If there is a null character, cut off the name at it, because JDK's
600  // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
601  // were constructed which had ".class\0" followed by a string chosen to
602  // make the hash collide with the truncated name, that file could be
603  // returned in response to a request for the .class file.
604  $nullPos = strpos( $entry['name'], "\000" );
605  if ( $nullPos !== false ) {
606  $names[] = substr( $entry['name'], 0, $nullPos );
607  }
608 
609  // If there is a trailing slash in the file name, we have to strip it,
610  // because that's what ZIP_GetEntry() does.
611  if ( preg_grep( '!\.class/?$!', $names ) ) {
612  $this->mJavaDetected = true;
613  }
614  }
615 
625  public function verifyPermissions( Authority $performer ) {
626  return $this->verifyTitlePermissions( $performer );
627  }
628 
640  public function verifyTitlePermissions( Authority $performer ) {
645  $nt = $this->getTitle();
646  if ( $nt === null ) {
647  return true;
648  }
649 
650  $status = PermissionStatus::newEmpty();
651  $performer->authorizeWrite( 'edit', $nt, $status );
652  $performer->authorizeWrite( 'upload', $nt, $status );
653  if ( !$status->isGood() ) {
654  return $status->toLegacyErrorArray();
655  }
656 
657  $overwriteError = $this->checkOverwrite( $performer );
658  if ( $overwriteError !== true ) {
659  return [ $overwriteError ];
660  }
661 
662  return true;
663  }
664 
674  public function checkWarnings( $user = null ) {
675  if ( $user === null ) {
676  // TODO check uses and hard deprecate
677  $user = RequestContext::getMain()->getUser();
678  }
679 
680  $warnings = [];
681 
682  $localFile = $this->getLocalFile();
683  $localFile->load( File::READ_LATEST );
684  $filename = $localFile->getName();
685  $hash = $this->getTempFileSha1Base36();
686 
687  $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
688  if ( $badFileName !== null ) {
689  $warnings['badfilename'] = $badFileName;
690  }
691 
692  $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
693  if ( $unwantedFileExtensionDetails !== null ) {
694  $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
695  }
696 
697  $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
698  if ( $fileSizeWarnings ) {
699  $warnings = array_merge( $warnings, $fileSizeWarnings );
700  }
701 
702  $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
703  if ( $localFileExistsWarnings ) {
704  $warnings = array_merge( $warnings, $localFileExistsWarnings );
705  }
706 
707  if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
708  $warnings['was-deleted'] = $filename;
709  }
710 
711  // If a file with the same name exists locally then the local file has already been tested
712  // for duplication of content
713  $ignoreLocalDupes = isset( $warnings['exists'] );
714  $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
715  if ( $dupes ) {
716  $warnings['duplicate'] = $dupes;
717  }
718 
719  $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
720  if ( $archivedDupes !== null ) {
721  $warnings['duplicate-archive'] = $archivedDupes;
722  }
723 
724  return $warnings;
725  }
726 
738  public static function makeWarningsSerializable( $warnings ) {
739  array_walk_recursive( $warnings, static function ( &$param, $key ) {
740  if ( $param instanceof File ) {
741  $param = [
742  'fileName' => $param->getName(),
743  'timestamp' => $param->getTimestamp()
744  ];
745  } elseif ( is_object( $param ) ) {
746  throw new InvalidArgumentException(
747  'UploadBase::makeWarningsSerializable: ' .
748  'Unexpected object of class ' . get_class( $param ) );
749  }
750  } );
751  return $warnings;
752  }
753 
763  private function checkBadFileName( $filename, $desiredFileName ) {
764  $comparableName = str_replace( ' ', '_', $desiredFileName );
765  $comparableName = Title::capitalize( $comparableName, NS_FILE );
766 
767  if ( $desiredFileName != $filename && $comparableName != $filename ) {
768  return $filename;
769  }
770 
771  return null;
772  }
773 
782  private function checkUnwantedFileExtensions( $fileExtension ) {
783  $checkFileExtensions = MediaWikiServices::getInstance()->getMainConfig()
784  ->get( MainConfigNames::CheckFileExtensions );
785  $fileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FileExtensions );
786  if ( $checkFileExtensions ) {
787  $extensions = array_unique( $fileExtensions );
788  if ( !self::checkFileExtension( $fileExtension, $extensions ) ) {
789  return [
790  $fileExtension,
791  Message::listParam( $extensions, 'comma' ),
792  count( $extensions )
793  ];
794  }
795  }
796 
797  return null;
798  }
799 
805  private function checkFileSize( $fileSize ) {
806  $uploadSizeWarning = MediaWikiServices::getInstance()->getMainConfig()
807  ->get( MainConfigNames::UploadSizeWarning );
808 
809  $warnings = [];
810 
811  if ( $uploadSizeWarning && ( $fileSize > $uploadSizeWarning ) ) {
812  $warnings['large-file'] = [
813  Message::sizeParam( $uploadSizeWarning ),
814  Message::sizeParam( $fileSize ),
815  ];
816  }
817 
818  if ( $fileSize == 0 ) {
819  $warnings['empty-file'] = true;
820  }
821 
822  return $warnings;
823  }
824 
831  private function checkLocalFileExists( LocalFile $localFile, $hash ) {
832  $warnings = [];
833 
834  $exists = self::getExistsWarning( $localFile );
835  if ( $exists !== false ) {
836  $warnings['exists'] = $exists;
837 
838  // check if file is an exact duplicate of current file version
839  if ( $hash !== false && $hash === $localFile->getSha1() ) {
840  $warnings['no-change'] = $localFile;
841  }
842 
843  // check if file is an exact duplicate of older versions of this file
844  $history = $localFile->getHistory();
845  foreach ( $history as $oldFile ) {
846  if ( $hash === $oldFile->getSha1() ) {
847  $warnings['duplicate-version'][] = $oldFile;
848  }
849  }
850  }
851 
852  return $warnings;
853  }
854 
855  private function checkLocalFileWasDeleted( LocalFile $localFile ) {
856  return $localFile->wasDeleted() && !$localFile->exists();
857  }
858 
865  private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
866  if ( $hash === false ) {
867  return [];
868  }
869  $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
870  $title = $this->getTitle();
871  foreach ( $dupes as $key => $dupe ) {
872  if (
873  ( $dupe instanceof LocalFile ) &&
874  $ignoreLocalDupes &&
875  $title->equals( $dupe->getTitle() )
876  ) {
877  unset( $dupes[$key] );
878  }
879  }
880 
881  return $dupes;
882  }
883 
891  private function checkAgainstArchiveDupes( $hash, Authority $performer ) {
892  if ( $hash === false ) {
893  return null;
894  }
895  $archivedFile = new ArchivedFile( null, 0, '', $hash );
896  if ( $archivedFile->getID() > 0 ) {
897  if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
898  return $archivedFile->getName();
899  }
900  return '';
901  }
902 
903  return null;
904  }
905 
923  public function performUpload(
924  $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
925  ) {
926  $this->getLocalFile()->load( File::READ_LATEST );
927  $props = $this->mFileProps;
928 
929  $error = null;
930  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
931  $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
932  if ( $error ) {
933  if ( !is_array( $error ) ) {
934  $error = [ $error ];
935  }
936  return Status::newFatal( ...$error );
937  }
938 
939  $status = $this->getLocalFile()->upload(
940  $this->mTempPath,
941  $comment,
942  $pageText,
944  $props,
945  false,
946  $user,
947  $tags
948  );
949 
950  if ( $status->isGood() ) {
951  if ( $watch ) {
952  MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
953  $user,
954  $this->getLocalFile()->getTitle(),
955  $watchlistExpiry
956  );
957  }
958  $this->getHookRunner()->onUploadComplete( $this );
959 
960  $this->postProcessUpload();
961  }
962 
963  return $status;
964  }
965 
972  public function postProcessUpload() {
973  }
974 
981  public function getTitle() {
982  if ( $this->mTitle !== false ) {
983  return $this->mTitle;
984  }
985  if ( !is_string( $this->mDesiredDestName ) ) {
986  $this->mTitleError = self::ILLEGAL_FILENAME;
987  $this->mTitle = null;
988 
989  return $this->mTitle;
990  }
991  /* Assume that if a user specified File:Something.jpg, this is an error
992  * and that the namespace prefix needs to be stripped of.
993  */
994  $title = Title::newFromText( $this->mDesiredDestName );
995  if ( $title && $title->getNamespace() === NS_FILE ) {
996  $this->mFilteredName = $title->getDBkey();
997  } else {
998  $this->mFilteredName = $this->mDesiredDestName;
999  }
1000 
1001  # oi_archive_name is max 255 bytes, which include a timestamp and an
1002  # exclamation mark, so restrict file name to 240 bytes.
1003  if ( strlen( $this->mFilteredName ) > 240 ) {
1004  $this->mTitleError = self::FILENAME_TOO_LONG;
1005  $this->mTitle = null;
1006 
1007  return $this->mTitle;
1008  }
1009 
1015  $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
1016  /* Normalize to title form before we do any further processing */
1017  $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1018  if ( $nt === null ) {
1019  $this->mTitleError = self::ILLEGAL_FILENAME;
1020  $this->mTitle = null;
1021 
1022  return $this->mTitle;
1023  }
1024  $this->mFilteredName = $nt->getDBkey();
1025 
1030  [ $partname, $ext ] = self::splitExtensions( $this->mFilteredName );
1031 
1032  if ( $ext !== [] ) {
1033  $this->mFinalExtension = trim( end( $ext ) );
1034  } else {
1035  $this->mFinalExtension = '';
1036 
1037  // No extension, try guessing one from the temporary file
1038  // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1039  // or incomplete test case in UploadBaseTest (T272328)
1040  if ( $this->mTempPath !== null ) {
1041  $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1042  $mime = $magic->guessMimeType( $this->mTempPath );
1043  if ( $mime !== 'unknown/unknown' ) {
1044  # Get a space separated list of extensions
1045  $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1046  if ( $mimeExt !== null ) {
1047  # Set the extension to the canonical extension
1048  $this->mFinalExtension = $mimeExt;
1049 
1050  # Fix up the other variables
1051  $this->mFilteredName .= ".{$this->mFinalExtension}";
1052  $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1054  }
1055  }
1056  }
1057  }
1058 
1059  // Don't allow users to override the list of prohibited file extensions (check file extension)
1060  $config = MediaWikiServices::getInstance()->getMainConfig();
1061  $checkFileExtensions = $config->get( MainConfigNames::CheckFileExtensions );
1062  $strictFileExtensions = $config->get( MainConfigNames::StrictFileExtensions );
1063  $fileExtensions = $config->get( MainConfigNames::FileExtensions );
1064  $prohibitedFileExtensions = $config->get( MainConfigNames::ProhibitedFileExtensions );
1065 
1066  $blackListedExtensions = self::checkFileExtensionList( $ext, $prohibitedFileExtensions );
1067 
1068  if ( $this->mFinalExtension == '' ) {
1069  $this->mTitleError = self::FILETYPE_MISSING;
1070  $this->mTitle = null;
1071 
1072  return $this->mTitle;
1073  }
1074 
1075  if ( $blackListedExtensions ||
1076  ( $checkFileExtensions && $strictFileExtensions &&
1077  !self::checkFileExtension( $this->mFinalExtension, $fileExtensions ) ) ) {
1078  $this->mBlackListedExtensions = $blackListedExtensions;
1079  $this->mTitleError = self::FILETYPE_BADTYPE;
1080  $this->mTitle = null;
1081 
1082  return $this->mTitle;
1083  }
1084 
1085  // Windows may be broken with special characters, see T3780
1086  if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1087  && !MediaWikiServices::getInstance()->getRepoGroup()
1088  ->getLocalRepo()->backendSupportsUnicodePaths()
1089  ) {
1090  $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1091  $this->mTitle = null;
1092 
1093  return $this->mTitle;
1094  }
1095 
1096  # If there was more than one "extension", reassemble the base
1097  # filename to prevent bogus complaints about length
1098  if ( count( $ext ) > 1 ) {
1099  $iterations = count( $ext ) - 1;
1100  for ( $i = 0; $i < $iterations; $i++ ) {
1101  $partname .= '.' . $ext[$i];
1102  }
1103  }
1104 
1105  if ( strlen( $partname ) < 1 ) {
1106  $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1107  $this->mTitle = null;
1108 
1109  return $this->mTitle;
1110  }
1111 
1112  $this->mTitle = $nt;
1113 
1114  return $this->mTitle;
1115  }
1116 
1123  public function getLocalFile() {
1124  if ( $this->mLocalFile === null ) {
1125  $nt = $this->getTitle();
1126  $this->mLocalFile = $nt === null
1127  ? null
1128  : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1129  }
1130 
1131  return $this->mLocalFile;
1132  }
1133 
1137  public function getStashFile() {
1138  return $this->mStashFile;
1139  }
1140 
1153  public function tryStashFile( User $user, $isPartial = false ) {
1154  if ( !$isPartial ) {
1155  $error = $this->runUploadStashFileHook( $user );
1156  if ( $error ) {
1157  return Status::newFatal( ...$error );
1158  }
1159  }
1160  try {
1161  $file = $this->doStashFile( $user );
1162  return Status::newGood( $file );
1163  } catch ( UploadStashException $e ) {
1164  return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1165  }
1166  }
1167 
1172  protected function runUploadStashFileHook( User $user ) {
1173  $props = $this->mFileProps;
1174  $error = null;
1175  $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1176  if ( $error && !is_array( $error ) ) {
1177  $error = [ $error ];
1178  }
1179  return $error;
1180  }
1181 
1189  protected function doStashFile( User $user = null ) {
1190  $stash = MediaWikiServices::getInstance()->getRepoGroup()
1191  ->getLocalRepo()->getUploadStash( $user );
1192  $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
1193  $this->mStashFile = $file;
1194 
1195  return $file;
1196  }
1197 
1202  public function cleanupTempFile() {
1203  if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1204  // Delete when all relevant TempFSFile handles go out of scope
1205  wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1206  $this->tempFileObj->autocollect();
1207  }
1208  }
1209 
1210  public function getTempPath() {
1211  return $this->mTempPath;
1212  }
1213 
1223  public static function splitExtensions( $filename ) {
1224  $bits = explode( '.', $filename );
1225  $basename = array_shift( $bits );
1226 
1227  return [ $basename, $bits ];
1228  }
1229 
1238  public static function checkFileExtension( $ext, $list ) {
1239  return in_array( strtolower( $ext ), $list, true );
1240  }
1241 
1250  public static function checkFileExtensionList( $ext, $list ) {
1251  return array_intersect( array_map( 'strtolower', $ext ), $list );
1252  }
1253 
1261  public static function verifyExtension( $mime, $extension ) {
1262  $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1263 
1264  if ( !$mime || $mime === 'unknown' || $mime === 'unknown/unknown' ) {
1265  if ( !$magic->isRecognizableExtension( $extension ) ) {
1266  wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1267  "unrecognized extension '$extension', can't verify" );
1268 
1269  return true;
1270  }
1271 
1272  wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1273  "recognized extension '$extension', so probably invalid file" );
1274  return false;
1275  }
1276 
1277  $match = $magic->isMatchingExtension( $extension, $mime );
1278 
1279  if ( $match === null ) {
1280  if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1281  wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1282 
1283  return false;
1284  }
1285 
1286  wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1287  return true;
1288  }
1289 
1290  if ( $match ) {
1291  wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1292 
1294  return true;
1295  }
1296 
1297  wfDebug( __METHOD__
1298  . ": mime type $mime mismatches file extension $extension, rejecting file" );
1299 
1300  return false;
1301  }
1302 
1314  public static function detectScript( $file, $mime, $extension ) {
1315  # ugly hack: for text files, always look at the entire file.
1316  # For binary field, just check the first K.
1317 
1318  if ( str_starts_with( $mime, 'text/' ) ) {
1319  $chunk = file_get_contents( $file );
1320  } else {
1321  $fp = fopen( $file, 'rb' );
1322  if ( !$fp ) {
1323  return false;
1324  }
1325  $chunk = fread( $fp, 1024 );
1326  fclose( $fp );
1327  }
1328 
1329  $chunk = strtolower( $chunk );
1330 
1331  if ( !$chunk ) {
1332  return false;
1333  }
1334 
1335  # decode from UTF-16 if needed (could be used for obfuscation).
1336  if ( str_starts_with( $chunk, "\xfe\xff" ) ) {
1337  $enc = 'UTF-16BE';
1338  } elseif ( str_starts_with( $chunk, "\xff\xfe" ) ) {
1339  $enc = 'UTF-16LE';
1340  } else {
1341  $enc = null;
1342  }
1343 
1344  if ( $enc !== null ) {
1345  $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1346  }
1347 
1348  $chunk = trim( $chunk );
1349 
1351  wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1352 
1353  # check for HTML doctype
1354  if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1355  return true;
1356  }
1357 
1358  // Some browsers will interpret obscure xml encodings as UTF-8, while
1359  // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1360  if ( $extension === 'svg' || str_starts_with( $mime, 'image/svg' ) ) {
1361  if ( self::checkXMLEncodingMissmatch( $file ) ) {
1362  return true;
1363  }
1364  }
1365 
1366  // Quick check for HTML heuristics in old IE and Safari.
1367  //
1368  // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1369  // don't need them all here as it can cause many false positives.
1370  //
1371  // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1372  $tags = [
1373  '<body',
1374  '<head',
1375  '<html', # also in safari
1376  '<script', # also in safari
1377  ];
1378 
1379  foreach ( $tags as $tag ) {
1380  if ( strpos( $chunk, $tag ) !== false ) {
1381  wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1382 
1383  return true;
1384  }
1385  }
1386 
1387  /*
1388  * look for JavaScript
1389  */
1390 
1391  # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1392  $chunk = Sanitizer::decodeCharReferences( $chunk );
1393 
1394  # look for script-types
1395  if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
1396  wfDebug( __METHOD__ . ": found script types" );
1397 
1398  return true;
1399  }
1400 
1401  # look for html-style script-urls
1402  if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1403  wfDebug( __METHOD__ . ": found html-style script urls" );
1404 
1405  return true;
1406  }
1407 
1408  # look for css-style script-urls
1409  if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1410  wfDebug( __METHOD__ . ": found css-style script urls" );
1411 
1412  return true;
1413  }
1414 
1415  wfDebug( __METHOD__ . ": no scripts found" );
1416 
1417  return false;
1418  }
1419 
1427  public static function checkXMLEncodingMissmatch( $file ) {
1428  $svgMetadataCutoff = MediaWikiServices::getInstance()->getMainConfig()
1429  ->get( MainConfigNames::SVGMetadataCutoff );
1430  $contents = file_get_contents( $file, false, null, 0, $svgMetadataCutoff );
1431  $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1432 
1433  if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1434  if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1435  && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1436  ) {
1437  wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1438 
1439  return true;
1440  }
1441  } elseif ( preg_match( "!<\?xml\b!si", $contents ) ) {
1442  // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1443  // bytes. There shouldn't be a legitimate reason for this to happen.
1444  wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1445 
1446  return true;
1447  } elseif ( str_starts_with( $contents, "\x4C\x6F\xA7\x94" ) ) {
1448  // EBCDIC encoded XML
1449  wfDebug( __METHOD__ . ": EBCDIC Encoded XML" );
1450 
1451  return true;
1452  }
1453 
1454  // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to
1455  // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings
1456  $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1457  foreach ( $attemptEncodings as $encoding ) {
1458  AtEase::suppressWarnings();
1459  $str = iconv( $encoding, 'UTF-8', $contents );
1460  AtEase::restoreWarnings();
1461  if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1462  if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1463  && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1464  ) {
1465  wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1466 
1467  return true;
1468  }
1469  } elseif ( $str != '' && preg_match( "!<\?xml\b!si", $str ) ) {
1470  // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1471  // bytes. There shouldn't be a legitimate reason for this to happen.
1472  wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1473 
1474  return true;
1475  }
1476  }
1477 
1478  return false;
1479  }
1480 
1486  protected function detectScriptInSvg( $filename, $partial ) {
1487  $this->mSVGNSError = false;
1488  $check = new XmlTypeCheck(
1489  $filename,
1490  [ $this, 'checkSvgScriptCallback' ],
1491  true,
1492  [
1493  'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ],
1494  'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ],
1495  ]
1496  );
1497  if ( $check->wellFormed !== true ) {
1498  // Invalid xml (T60553)
1499  // But only when non-partial (T67724)
1500  return $partial ? false : [ 'uploadinvalidxml' ];
1501  }
1502 
1503  if ( $check->filterMatch ) {
1504  if ( $this->mSVGNSError ) {
1505  return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1506  }
1507  return $check->filterMatchType;
1508  }
1509 
1510  return false;
1511  }
1512 
1519  public static function checkSvgPICallback( $target, $data ) {
1520  // Don't allow external stylesheets (T59550)
1521  if ( preg_match( '/xml-stylesheet/i', $target ) ) {
1522  return [ 'upload-scripted-pi-callback' ];
1523  }
1524 
1525  return false;
1526  }
1527 
1539  public static function checkSvgExternalDTD( $type, $publicId, $systemId ) {
1540  // This doesn't include the XHTML+MathML+SVG doctype since we don't
1541  // allow XHTML anyways.
1542  $allowedDTDs = [
1543  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1544  'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1545  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1546  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1547  // https://phabricator.wikimedia.org/T168856
1548  'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1549  ];
1550  if ( $type !== 'PUBLIC'
1551  || !in_array( $systemId, $allowedDTDs )
1552  || !str_starts_with( $publicId, "-//W3C//" )
1553  ) {
1554  return [ 'upload-scripted-dtd' ];
1555  }
1556  return false;
1557  }
1558 
1566  public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1567  [ $namespace, $strippedElement ] = self::splitXmlNamespace( $element );
1568 
1569  // We specifically don't include:
1570  // http://www.w3.org/1999/xhtml (T62771)
1571  static $validNamespaces = [
1572  '',
1573  'adobe:ns:meta/',
1574  'http://creativecommons.org/ns#',
1575  'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1576  'http://ns.adobe.com/adobeillustrator/10.0/',
1577  'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1578  'http://ns.adobe.com/extensibility/1.0/',
1579  'http://ns.adobe.com/flows/1.0/',
1580  'http://ns.adobe.com/illustrator/1.0/',
1581  'http://ns.adobe.com/imagereplacement/1.0/',
1582  'http://ns.adobe.com/pdf/1.3/',
1583  'http://ns.adobe.com/photoshop/1.0/',
1584  'http://ns.adobe.com/saveforweb/1.0/',
1585  'http://ns.adobe.com/variables/1.0/',
1586  'http://ns.adobe.com/xap/1.0/',
1587  'http://ns.adobe.com/xap/1.0/g/',
1588  'http://ns.adobe.com/xap/1.0/g/img/',
1589  'http://ns.adobe.com/xap/1.0/mm/',
1590  'http://ns.adobe.com/xap/1.0/rights/',
1591  'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1592  'http://ns.adobe.com/xap/1.0/stype/font#',
1593  'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1594  'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1595  'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1596  'http://ns.adobe.com/xap/1.0/t/pg/',
1597  'http://purl.org/dc/elements/1.1/',
1598  'http://purl.org/dc/elements/1.1',
1599  'http://schemas.microsoft.com/visio/2003/svgextensions/',
1600  'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1601  'http://taptrix.com/inkpad/svg_extensions',
1602  'http://web.resource.org/cc/',
1603  'http://www.freesoftware.fsf.org/bkchem/cdml',
1604  'http://www.inkscape.org/namespaces/inkscape',
1605  'http://www.opengis.net/gml',
1606  'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1607  'http://www.w3.org/2000/svg',
1608  'http://www.w3.org/tr/rec-rdf-syntax/',
1609  'http://www.w3.org/2000/01/rdf-schema#',
1610  'http://www.w3.org/2000/02/svg/testsuite/description/', // https://phabricator.wikimedia.org/T278044
1611  ];
1612 
1613  // Inkscape mangles namespace definitions created by Adobe Illustrator.
1614  // This is nasty but harmless. (T144827)
1615  $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1616 
1617  if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1618  wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file." );
1620  $this->mSVGNSError = $namespace;
1621 
1622  return true;
1623  }
1624 
1625  /*
1626  * check for elements that can contain javascript
1627  */
1628  if ( $strippedElement === 'script' ) {
1629  wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file." );
1630 
1631  return [ 'uploaded-script-svg', $strippedElement ];
1632  }
1633 
1634  # e.g., <svg xmlns="http://www.w3.org/2000/svg">
1635  # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1636  if ( $strippedElement === 'handler' ) {
1637  wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1638 
1639  return [ 'uploaded-script-svg', $strippedElement ];
1640  }
1641 
1642  # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1643  if ( $strippedElement === 'stylesheet' ) {
1644  wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1645 
1646  return [ 'uploaded-script-svg', $strippedElement ];
1647  }
1648 
1649  # Block iframes, in case they pass the namespace check
1650  if ( $strippedElement === 'iframe' ) {
1651  wfDebug( __METHOD__ . ": iframe in uploaded file." );
1652 
1653  return [ 'uploaded-script-svg', $strippedElement ];
1654  }
1655 
1656  # Check <style> css
1657  if ( $strippedElement === 'style'
1658  && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1659  ) {
1660  wfDebug( __METHOD__ . ": hostile css in style element." );
1661 
1662  return [ 'uploaded-hostile-svg' ];
1663  }
1664 
1665  foreach ( $attribs as $attrib => $value ) {
1666  $stripped = $this->stripXmlNamespace( $attrib );
1667  $value = strtolower( $value );
1668 
1669  if ( str_starts_with( $stripped, 'on' ) ) {
1670  wfDebug( __METHOD__
1671  . ": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1672 
1673  return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1674  }
1675 
1676  # Do not allow relative links, or unsafe url schemas.
1677  # For <a> tags, only data:, http: and https: and same-document
1678  # fragment links are allowed. For all other tags, only data:
1679  # and fragment are allowed.
1680  if ( $stripped === 'href'
1681  && $value !== ''
1682  && !str_starts_with( $value, 'data:' )
1683  && !str_starts_with( $value, '#' )
1684  ) {
1685  if ( !( $strippedElement === 'a'
1686  && preg_match( '!^https?://!i', $value ) )
1687  ) {
1688  wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1689  . "'$attrib'='$value' in uploaded file." );
1690 
1691  return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1692  }
1693  }
1694 
1695  # only allow data: targets that should be safe. This prevents vectors like,
1696  # image/svg, text/xml, application/xml, and text/html, which can contain scripts
1697  if ( $stripped === 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1698  // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
1699  // phpcs:ignore Generic.Files.LineLength
1700  $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1701 
1702  if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1703  wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
1704  . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1705  return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1706  }
1707  }
1708 
1709  # Change href with animate from (http://html5sec.org/#137).
1710  if ( $stripped === 'attributename'
1711  && $strippedElement === 'animate'
1712  && $this->stripXmlNamespace( $value ) === 'href'
1713  ) {
1714  wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1715  . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1716 
1717  return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1718  }
1719 
1720  # use set/animate to add event-handler attribute to parent
1721  if ( ( $strippedElement === 'set' || $strippedElement === 'animate' )
1722  && $stripped === 'attributename'
1723  && substr( $value, 0, 2 ) === 'on'
1724  ) {
1725  wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1726  . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1727 
1728  return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1729  }
1730 
1731  # use set to add href attribute to parent element
1732  if ( $strippedElement === 'set'
1733  && $stripped === 'attributename'
1734  && str_contains( $value, 'href' )
1735  ) {
1736  wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file." );
1737 
1738  return [ 'uploaded-setting-href-svg' ];
1739  }
1740 
1741  # use set to add a remote / data / script target to an element
1742  if ( $strippedElement === 'set'
1743  && $stripped === 'to'
1744  && preg_match( '!(http|https|data|script):!sim', $value )
1745  ) {
1746  wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file." );
1747 
1748  return [ 'uploaded-wrong-setting-svg', $value ];
1749  }
1750 
1751  # use handler attribute with remote / data / script
1752  if ( $stripped === 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
1753  wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1754  . "'$attrib'='$value' in uploaded file." );
1755 
1756  return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1757  }
1758 
1759  # use CSS styles to bring in remote code
1760  if ( $stripped === 'style'
1761  && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1762  ) {
1763  wfDebug( __METHOD__ . ": Found svg setting a style with "
1764  . "remote url '$attrib'='$value' in uploaded file." );
1765  return [ 'uploaded-remote-url-svg', $attrib, $value ];
1766  }
1767 
1768  # Several attributes can include css, css character escaping isn't allowed
1769  $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1770  'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1771  if ( in_array( $stripped, $cssAttrs, true )
1772  && self::checkCssFragment( $value )
1773  ) {
1774  wfDebug( __METHOD__ . ": Found svg setting a style with "
1775  . "remote url '$attrib'='$value' in uploaded file." );
1776  return [ 'uploaded-remote-url-svg', $attrib, $value ];
1777  }
1778 
1779  # image filters can pull in url, which could be svg that executes scripts
1780  # Only allow url( "#foo" ). Do not allow url( http://example.com )
1781  if ( $strippedElement === 'image'
1782  && $stripped === 'filter'
1783  && preg_match( '!url\s*\‍(\s*["\']?[^#]!sim', $value )
1784  ) {
1785  wfDebug( __METHOD__ . ": Found image filter with url: "
1786  . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1787 
1788  return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1789  }
1790  }
1791 
1792  return false; // No scripts detected
1793  }
1794 
1801  private static function checkCssFragment( $value ) {
1802  # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1803  if ( stripos( $value, '@import' ) !== false ) {
1804  return true;
1805  }
1806 
1807  # We allow @font-face to embed fonts with data: urls, so we snip the string
1808  # 'url' out so this case won't match when we check for urls below
1809  $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1810  $value = preg_replace( $pattern, '$1$2', $value );
1811 
1812  # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1813  # properties filter and accelerator don't seem to be useful for xss in SVG files.
1814  # Expression and -o-link don't seem to work either, but filtering them here in case.
1815  # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1816  # but not local ones such as url("#..., url('#..., url(#....
1817  if ( preg_match( '!expression
1818  | -o-link\s*:
1819  | -o-link-source\s*:
1820  | -o-replace\s*:!imx', $value ) ) {
1821  return true;
1822  }
1823 
1824  if ( preg_match_all(
1825  "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1826  $value,
1827  $matches
1828  ) !== 0
1829  ) {
1830  # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1831  foreach ( $matches[1] as $match ) {
1832  if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1833  return true;
1834  }
1835  }
1836  }
1837 
1838  if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1839  return true;
1840  }
1841 
1842  return false;
1843  }
1844 
1850  private static function splitXmlNamespace( $element ) {
1851  // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1852  $parts = explode( ':', strtolower( $element ) );
1853  $name = array_pop( $parts );
1854  $ns = implode( ':', $parts );
1855 
1856  return [ $ns, $name ];
1857  }
1858 
1863  private function stripXmlNamespace( $name ) {
1864  // 'http://www.w3.org/2000/svg:script' -> 'script'
1865  $parts = explode( ':', strtolower( $name ) );
1866 
1867  return array_pop( $parts );
1868  }
1869 
1880  public static function detectVirus( $file ) {
1881  global $wgOut;
1882  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1883  $antivirus = $mainConfig->get( MainConfigNames::Antivirus );
1884  $antivirusSetup = $mainConfig->get( MainConfigNames::AntivirusSetup );
1885  $antivirusRequired = $mainConfig->get( MainConfigNames::AntivirusRequired );
1886  if ( !$antivirus ) {
1887  wfDebug( __METHOD__ . ": virus scanner disabled" );
1888 
1889  return null;
1890  }
1891 
1892  if ( !$antivirusSetup[$antivirus] ) {
1893  wfDebug( __METHOD__ . ": unknown virus scanner: {$antivirus}" );
1894  $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1895  [ 'virus-badscanner', $antivirus ] );
1896 
1897  return wfMessage( 'virus-unknownscanner' )->text() . " {$antivirus}";
1898  }
1899 
1900  # look up scanner configuration
1901  $command = $antivirusSetup[$antivirus]['command'];
1902  $exitCodeMap = $antivirusSetup[$antivirus]['codemap'];
1903  $msgPattern = $antivirusSetup[$antivirus]['messagepattern'] ?? null;
1904 
1905  if ( !str_contains( $command, "%f" ) ) {
1906  # simple pattern: append file to scan
1907  $command .= " " . Shell::escape( $file );
1908  } else {
1909  # complex pattern: replace "%f" with file to scan
1910  $command = str_replace( "%f", Shell::escape( $file ), $command );
1911  }
1912 
1913  wfDebug( __METHOD__ . ": running virus scan: $command " );
1914 
1915  # execute virus scanner
1916  $exitCode = false;
1917 
1918  # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1919  # that does not seem to be worth the pain.
1920  # Ask me (Duesentrieb) about it if it's ever needed.
1921  $output = wfShellExecWithStderr( $command, $exitCode );
1922 
1923  # map exit code to AV_xxx constants.
1924  $mappedCode = $exitCode;
1925  if ( $exitCodeMap ) {
1926  if ( isset( $exitCodeMap[$exitCode] ) ) {
1927  $mappedCode = $exitCodeMap[$exitCode];
1928  } elseif ( isset( $exitCodeMap["*"] ) ) {
1929  $mappedCode = $exitCodeMap["*"];
1930  }
1931  }
1932 
1933  /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
1934  * so we need the strict equalities === and thus can't use a switch here
1935  */
1936  if ( $mappedCode === AV_SCAN_FAILED ) {
1937  # scan failed (code was mapped to false by $exitCodeMap)
1938  wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1939 
1940  $output = $antivirusRequired
1941  ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1942  : null;
1943  } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1944  # scan failed because filetype is unknown (probably immune)
1945  wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1946  $output = null;
1947  } elseif ( $mappedCode === AV_NO_VIRUS ) {
1948  # no virus found
1949  wfDebug( __METHOD__ . ": file passed virus scan." );
1950  $output = false;
1951  } else {
1952  $output = trim( $output );
1953 
1954  if ( !$output ) {
1955  $output = true; # if there's no output, return true
1956  } elseif ( $msgPattern ) {
1957  $groups = [];
1958  if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1959  $output = $groups[1];
1960  }
1961  }
1962 
1963  wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
1964  }
1965 
1966  return $output;
1967  }
1968 
1977  private function checkOverwrite( Authority $performer ) {
1978  // First check whether the local file can be overwritten
1979  $file = $this->getLocalFile();
1980  $file->load( File::READ_LATEST );
1981  if ( $file->exists() ) {
1982  if ( !self::userCanReUpload( $performer, $file ) ) {
1983  return [ 'fileexists-forbidden', $file->getName() ];
1984  }
1985 
1986  return true;
1987  }
1988 
1989  $services = MediaWikiServices::getInstance();
1990 
1991  /* Check shared conflicts: if the local file does not exist, but
1992  * RepoGroup::findFile finds a file, it exists in a shared repository.
1993  */
1994  $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
1995  if ( $file && !$performer->isAllowed( 'reupload-shared' )
1996  ) {
1997  return [ 'fileexists-shared-forbidden', $file->getName() ];
1998  }
1999 
2000  return true;
2001  }
2002 
2010  public static function userCanReUpload( Authority $performer, File $img ) {
2011  if ( $performer->isAllowed( 'reupload' ) ) {
2012  return true; // non-conditional
2013  }
2014 
2015  if ( !$performer->isAllowed( 'reupload-own' ) ) {
2016  return false;
2017  }
2018 
2019  if ( !( $img instanceof LocalFile ) ) {
2020  return false;
2021  }
2022 
2023  return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2024  }
2025 
2037  public static function getExistsWarning( $file ) {
2038  if ( $file->exists() ) {
2039  return [ 'warning' => 'exists', 'file' => $file ];
2040  }
2041 
2042  if ( $file->getTitle()->getArticleID() ) {
2043  return [ 'warning' => 'page-exists', 'file' => $file ];
2044  }
2045 
2046  if ( !strpos( $file->getName(), '.' ) ) {
2047  $partname = $file->getName();
2048  $extension = '';
2049  } else {
2050  $n = strrpos( $file->getName(), '.' );
2051  $extension = substr( $file->getName(), $n + 1 );
2052  $partname = substr( $file->getName(), 0, $n );
2053  }
2054  $normalizedExtension = File::normalizeExtension( $extension );
2055  $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2056 
2057  if ( $normalizedExtension != $extension ) {
2058  // We're not using the normalized form of the extension.
2059  // Normal form is lowercase, using most common of alternate
2060  // extensions (eg 'jpg' rather than 'JPEG').
2061 
2062  // Check for another file using the normalized form...
2063  $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2064  $file_lc = $localRepo->newFile( $nt_lc );
2065 
2066  if ( $file_lc->exists() ) {
2067  return [
2068  'warning' => 'exists-normalized',
2069  'file' => $file,
2070  'normalizedFile' => $file_lc
2071  ];
2072  }
2073  }
2074 
2075  // Check for files with the same name but a different extension
2076  $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2077  if ( count( $similarFiles ) ) {
2078  return [
2079  'warning' => 'exists-normalized',
2080  'file' => $file,
2081  'normalizedFile' => $similarFiles[0],
2082  ];
2083  }
2084 
2085  if ( self::isThumbName( $file->getName() ) ) {
2086  # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2087  $nt_thb = Title::newFromText(
2088  substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2089  NS_FILE
2090  );
2091  $file_thb = $localRepo->newFile( $nt_thb );
2092  if ( $file_thb->exists() ) {
2093  return [
2094  'warning' => 'thumb',
2095  'file' => $file,
2096  'thumbFile' => $file_thb
2097  ];
2098  }
2099 
2100  // File does not exist, but we just don't like the name
2101  return [
2102  'warning' => 'thumb-name',
2103  'file' => $file,
2104  'thumbFile' => $file_thb
2105  ];
2106  }
2107 
2108  foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2109  if ( str_starts_with( $partname, $prefix ) ) {
2110  return [
2111  'warning' => 'bad-prefix',
2112  'file' => $file,
2113  'prefix' => $prefix
2114  ];
2115  }
2116  }
2117 
2118  return false;
2119  }
2120 
2126  public static function isThumbName( $filename ) {
2127  $n = strrpos( $filename, '.' );
2128  $partname = $n ? substr( $filename, 0, $n ) : $filename;
2129 
2130  return (
2131  substr( $partname, 3, 3 ) === 'px-' ||
2132  substr( $partname, 2, 3 ) === 'px-'
2133  ) &&
2134  preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2135  }
2136 
2142  public static function getFilenamePrefixBlacklist() {
2143  $list = [];
2144  $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2145  if ( !$message->isDisabled() ) {
2146  $lines = explode( "\n", $message->plain() );
2147  foreach ( $lines as $line ) {
2148  // Remove comment lines
2149  $comment = substr( trim( $line ), 0, 1 );
2150  if ( $comment === '#' || $comment == '' ) {
2151  continue;
2152  }
2153  // Remove additional comments after a prefix
2154  $comment = strpos( $line, '#' );
2155  if ( $comment > 0 ) {
2156  $line = substr( $line, 0, $comment - 1 );
2157  }
2158  $list[] = trim( $line );
2159  }
2160  }
2161 
2162  return $list;
2163  }
2164 
2176  public function getImageInfo( $result ) {
2177  $localFile = $this->getLocalFile();
2178  $stashFile = $this->getStashFile();
2179  // Calling a different API module depending on whether the file was stashed is less than optimal.
2180  // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
2181  if ( $stashFile ) {
2183  $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_fill_keys( $imParam, true ), $result );
2184  } else {
2186  $info = ApiQueryImageInfo::getInfo( $localFile, array_fill_keys( $imParam, true ), $result );
2187  }
2188 
2189  return $info;
2190  }
2191 
2196  public function convertVerifyErrorToStatus( $error ) {
2197  $code = $error['status'];
2198  unset( $code['status'] );
2199 
2200  return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2201  }
2202 
2210  public static function getMaxUploadSize( $forType = null ) {
2211  $maxUploadSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxUploadSize );
2212 
2213  if ( is_array( $maxUploadSize ) ) {
2214  if ( $forType !== null && isset( $maxUploadSize[$forType] ) ) {
2215  return $maxUploadSize[$forType];
2216  }
2217  return $maxUploadSize['*'];
2218  }
2219  return intval( $maxUploadSize );
2220  }
2221 
2229  public static function getMaxPhpUploadSize() {
2230  $phpMaxFileSize = wfShorthandToInteger(
2231  ini_get( 'upload_max_filesize' ),
2232  PHP_INT_MAX
2233  );
2234  $phpMaxPostSize = wfShorthandToInteger(
2235  ini_get( 'post_max_size' ),
2236  PHP_INT_MAX
2237  ) ?: PHP_INT_MAX;
2238  return min( $phpMaxFileSize, $phpMaxPostSize );
2239  }
2240 
2252  public static function getSessionStatus( UserIdentity $user, $statusKey ) {
2253  $store = self::getUploadSessionStore();
2254  $key = self::getUploadSessionKey( $store, $user, $statusKey );
2255 
2256  return $store->get( $key );
2257  }
2258 
2271  public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
2272  $store = self::getUploadSessionStore();
2273  $key = self::getUploadSessionKey( $store, $user, $statusKey );
2274 
2275  if ( $value === false ) {
2276  $store->delete( $key );
2277  } else {
2278  $store->set( $key, $value, $store::TTL_DAY );
2279  }
2280  }
2281 
2288  private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
2289  return $store->makeKey(
2290  'uploadstatus',
2291  $user->isRegistered() ? $user->getId() : md5( $user->getName() ),
2292  $statusKey
2293  );
2294  }
2295 
2299  private static function getUploadSessionStore() {
2300  return MediaWikiServices::getInstance()->getMainObjectStash();
2301  }
2302 }
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.
$matches
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgOut
Definition: Setup.php:508
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.
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.
makeKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
set( $key, $value, $exptime=0, $flags=0)
Set an item.
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:285
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:67
getName()
Return the name of this file.
Definition: File.php:333
const DELETE_SOURCE
Definition: File.php:84
const DELETED_FILE
Definition: File.php:71
wasDeleted()
Was this file ever deleted from the wiki?
Definition: File.php:2084
static runner()
Get a HookRunner instance for calling hooks using the new interfaces.
Definition: Hooks.php:173
Local file in the wiki's own database.
Definition: LocalFile.php:60
exists()
canRender inherited
Definition: LocalFile.php:1288
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
Definition: LocalFile.php:1521
load( $flags=0)
Load file metadata from cache or DB, unless already loaded.
Definition: LocalFile.php:720
MediaWiki exception.
Definition: MWException.php:30
MimeMagic helper wrapper.
Definition: MWFileProps.php:28
static getHandler( $type)
Get a MediaHandler for a given MIME type from the instance cache.
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
A StatusValue for permission errors.
Executes shell commands.
Definition: Shell.php:46
static listParam(array $list, $type='text')
Definition: Message.php:1277
static sizeParam( $size)
Definition: Message.php:1244
static getMain()
Get the RequestContext object associated with the main request.
static decodeCharReferences( $text)
Decode any character references, numeric or named entities, in the text and return a UTF-8 string.
Definition: Sanitizer.php:1371
static normalizeCss( $value)
Normalize CSS into a format we can easily search for hostile input.
Definition: Sanitizer.php:696
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:73
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:85
This class is used to hold the location and do limited manipulation of files stored temporarily (this...
Definition: TempFSFile.php:34
static capitalize( $text, $ns=NS_MAIN)
Capitalize a text string for a title if it belongs to a namespace that capitalizes.
Definition: Title.php:2959
getDBkey()
Get the main part with underscores.
Definition: Title.php:1060
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:373
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:667
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:641
UploadBase and subclasses are the backend of MediaWiki's file uploads.
Definition: UploadBase.php:49
getSourceType()
Returns the upload type.
Definition: UploadBase.php:253
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
Definition: UploadBase.php:738
int $mTitleError
Definition: UploadBase.php:67
static setSessionStatus(UserIdentity $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling)
const EMPTY_FILE
Definition: UploadBase.php:107
UploadStashFile null $mStashFile
Definition: UploadBase.php:75
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.
Definition: UploadBase.php:972
checkSvgScriptCallback( $element, $attribs, $data=null)
Title false null $mTitle
Definition: UploadBase.php:65
verifyPermissions(Authority $performer)
Alias for verifyTitlePermissions.
Definition: UploadBase.php:625
getLocalFile()
Return the local file and initializes if necessary.
const SUCCESS
Definition: UploadBase.php:105
bool null $mJavaDetected
Definition: UploadBase.php:83
string null $mFilteredName
Definition: UploadBase.php:69
getRealPath( $srcPath)
Definition: UploadBase.php:336
static createFromRequest(&$request, $type=null)
Create a form of UploadBase depending on wpSourceType and initializes it.
Definition: UploadBase.php:190
runUploadStashFileHook(User $user)
zipEntryCallback( $entry)
Callback for ZipDirectoryReader to detect Java class files.
Definition: UploadBase.php:596
static checkSvgPICallback( $target, $data)
Callback to filter SVG Processing Instructions.
static isValidRequest( $request)
Check whether a request if valid for this handler.
Definition: UploadBase.php:236
const FILETYPE_MISSING
Definition: UploadBase.php:111
convertVerifyErrorToStatus( $error)
string null $mFinalExtension
Definition: UploadBase.php:71
verifyPartialFile()
A verification routine suitable for partial files.
Definition: UploadBase.php:553
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.
Definition: UploadBase.php:488
array null $mFileProps
Definition: UploadBase.php:79
static isEnabled()
Returns true if uploads are enabled.
Definition: UploadBase.php:146
static isThumbName( $filename)
Helper function that checks whether the filename looks like a thumbnail.
getVerificationErrorCode( $error)
Definition: UploadBase.php:123
performUpload( $comment, $pageText, $watch, $user, $tags=[], ?string $watchlistExpiry=null)
Really perform the upload.
Definition: UploadBase.php:923
string null $mDesiredDestName
Definition: UploadBase.php:57
verifyTitlePermissions(Authority $performer)
Check whether the user can edit, upload and create the image.
Definition: UploadBase.php:640
static getFilenamePrefixBlacklist()
Get a list of blacklisted filename prefixes from [[MediaWiki:Filename-prefix-blacklist]].
const OVERWRITE_EXISTING_FILE
Definition: UploadBase.php:110
setTempFile( $tempPath, $fileSize=null)
Definition: UploadBase.php:285
static getSessionStatus(UserIdentity $user, $statusKey)
Get the current status of a chunked upload (used for polling)
static checkXMLEncodingMissmatch( $file)
Check a whitelist of xml encodings that are known not to be interpreted differently by the server's x...
doStashFile(User $user=null)
Implementation for stashFile() and tryStashFile().
const HOOK_ABORTED
Definition: UploadBase.php:114
string null $mDestName
Definition: UploadBase.php:59
const VERIFICATION_ERROR
Definition: UploadBase.php:113
string[] $mBlackListedExtensions
Definition: UploadBase.php:81
static isAllowed(Authority $performer)
Returns true if the user can use this upload module or else a string identifying the missing permissi...
Definition: UploadBase.php:160
const WINDOWS_NONASCII_FILENAME
Definition: UploadBase.php:116
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.
Definition: UploadBase.php:420
string null $mSourceType
Definition: UploadBase.php:63
int null $mFileSize
Definition: UploadBase.php:77
isEmptyFile()
Return true if the file is empty.
Definition: UploadBase.php:311
static checkFileExtension( $ext, $list)
Perform case-insensitive match against a list of file extensions.
const FILETYPE_BADTYPE
Definition: UploadBase.php:112
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.
Definition: UploadBase.php:981
initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile=false)
Definition: UploadBase.php:264
static getMaxUploadSize( $forType=null)
Get MediaWiki's maximum uploaded file size for given type of upload, based on $wgMaxUploadSize.
bool null $mRemoveTempFile
Definition: UploadBase.php:61
static checkSvgExternalDTD( $type, $publicId, $systemId)
Verify that DTD urls referenced are only the standard dtds.
getTempFileSha1Base36()
Get the base 36 SHA1 of the file.
Definition: UploadBase.php:328
getImageInfo( $result)
Gets image info about the file just uploaded.
detectScriptInSvg( $filename, $partial)
static splitExtensions( $filename)
Split a file into a base name and all dot-delimited 'extensions' on the end.
fetchFile()
Fetch the file.
Definition: UploadBase.php:303
checkWarnings( $user=null)
Check for non fatal problems with the file.
Definition: UploadBase.php:674
const FILE_TOO_LARGE
Definition: UploadBase.php:115
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
Definition: UploadBase.php:176
getFileSize()
Return the file size.
Definition: UploadBase.php:319
verifyUpload()
Verify whether the upload is sensible.
Definition: UploadBase.php:371
const ILLEGAL_FILENAME
Definition: UploadBase.php:109
const MIN_LENGTH_PARTNAME
Definition: UploadBase.php:108
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)
Definition: UploadBase.php:53
TempFSFile null $tempFileObj
Wrapper to handle deleting the temp file.
Definition: UploadBase.php:55
LocalFile null $mLocalFile
Definition: UploadBase.php:73
static getExistsWarning( $file)
Helper function that does various existence checks for a file.
const FILENAME_TOO_LONG
Definition: UploadBase.php:117
static getMaxPhpUploadSize()
Get the PHP maximum uploaded file size, based on ini settings.
static $safeXmlEncodings
Definition: UploadBase.php:87
verifyMimeType( $mime)
Verify the MIME type.
Definition: UploadBase.php:450
initializeFromRequest(&$request)
Initialize from a WebRequest.
string false $mSVGNSError
Definition: UploadBase.php:85
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:70
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
authorizeWrite(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize write access.
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)
$mime
Definition: router.php:60
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
if(!is_readable( $file)) $ext
Definition: router.php:48
if(!file_exists( $CREDITS)) $lines