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  !$this->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 
552  protected function verifyPartialFile() {
553  $config = MediaWikiServices::getInstance()->getMainConfig();
554  $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
555  # getTitle() sets some internal parameters like $this->mFinalExtension
556  $this->getTitle();
557 
558  $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
559  $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
560 
561  # check MIME type, if desired
562  $mime = $this->mFileProps['file-mime'];
563  $status = $this->verifyMimeType( $mime );
564  if ( $status !== true ) {
565  return $status;
566  }
567 
568  # check for htmlish code and javascript
569  if ( !$disableUploadScriptChecks ) {
570  if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
571  return [ 'uploadscripted' ];
572  }
573  if ( $this->mFinalExtension == 'svg' || $mime == 'image/svg+xml' ) {
574  $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
575  if ( $svgStatus !== false ) {
576  return $svgStatus;
577  }
578  }
579  }
580 
581  # Scan the uploaded file for viruses
582  $virus = $this->detectVirus( $this->mTempPath );
583  if ( $virus ) {
584  return [ 'uploadvirus', $virus ];
585  }
586 
587  return true;
588  }
589 
595  public function zipEntryCallback( $entry ) {
596  $names = [ $entry['name'] ];
597 
598  // If there is a null character, cut off the name at it, because JDK's
599  // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
600  // were constructed which had ".class\0" followed by a string chosen to
601  // make the hash collide with the truncated name, that file could be
602  // returned in response to a request for the .class file.
603  $nullPos = strpos( $entry['name'], "\000" );
604  if ( $nullPos !== false ) {
605  $names[] = substr( $entry['name'], 0, $nullPos );
606  }
607 
608  // If there is a trailing slash in the file name, we have to strip it,
609  // because that's what ZIP_GetEntry() does.
610  if ( preg_grep( '!\.class/?$!', $names ) ) {
611  $this->mJavaDetected = true;
612  }
613  }
614 
624  public function verifyPermissions( Authority $performer ) {
625  return $this->verifyTitlePermissions( $performer );
626  }
627 
639  public function verifyTitlePermissions( Authority $performer ) {
644  $nt = $this->getTitle();
645  if ( $nt === null ) {
646  return true;
647  }
648 
649  $status = PermissionStatus::newEmpty();
650  $performer->authorizeWrite( 'edit', $nt, $status );
651  $performer->authorizeWrite( 'upload', $nt, $status );
652  if ( !$status->isGood() ) {
653  return $status->toLegacyErrorArray();
654  }
655 
656  $overwriteError = $this->checkOverwrite( $performer );
657  if ( $overwriteError !== true ) {
658  return [ $overwriteError ];
659  }
660 
661  return true;
662  }
663 
673  public function checkWarnings( $user = null ) {
674  if ( $user === null ) {
675  // TODO check uses and hard deprecate
676  $user = RequestContext::getMain()->getUser();
677  }
678 
679  $warnings = [];
680 
681  $localFile = $this->getLocalFile();
682  $localFile->load( File::READ_LATEST );
683  $filename = $localFile->getName();
684  $hash = $this->getTempFileSha1Base36();
685 
686  $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
687  if ( $badFileName !== null ) {
688  $warnings['badfilename'] = $badFileName;
689  }
690 
691  $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
692  if ( $unwantedFileExtensionDetails !== null ) {
693  $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
694  }
695 
696  $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
697  if ( $fileSizeWarnings ) {
698  $warnings = array_merge( $warnings, $fileSizeWarnings );
699  }
700 
701  $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
702  if ( $localFileExistsWarnings ) {
703  $warnings = array_merge( $warnings, $localFileExistsWarnings );
704  }
705 
706  if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
707  $warnings['was-deleted'] = $filename;
708  }
709 
710  // If a file with the same name exists locally then the local file has already been tested
711  // for duplication of content
712  $ignoreLocalDupes = isset( $warnings['exists'] );
713  $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
714  if ( $dupes ) {
715  $warnings['duplicate'] = $dupes;
716  }
717 
718  $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
719  if ( $archivedDupes !== null ) {
720  $warnings['duplicate-archive'] = $archivedDupes;
721  }
722 
723  return $warnings;
724  }
725 
737  public static function makeWarningsSerializable( $warnings ) {
738  array_walk_recursive( $warnings, static function ( &$param, $key ) {
739  if ( $param instanceof File ) {
740  $param = [
741  'fileName' => $param->getName(),
742  'timestamp' => $param->getTimestamp()
743  ];
744  } elseif ( is_object( $param ) ) {
745  throw new InvalidArgumentException(
746  'UploadBase::makeWarningsSerializable: ' .
747  'Unexpected object of class ' . get_class( $param ) );
748  }
749  } );
750  return $warnings;
751  }
752 
762  private function checkBadFileName( $filename, $desiredFileName ) {
763  $comparableName = str_replace( ' ', '_', $desiredFileName );
764  $comparableName = Title::capitalize( $comparableName, NS_FILE );
765 
766  if ( $desiredFileName != $filename && $comparableName != $filename ) {
767  return $filename;
768  }
769 
770  return null;
771  }
772 
781  private function checkUnwantedFileExtensions( $fileExtension ) {
782  global $wgLang;
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 ( !$this->checkFileExtension( $fileExtension, $extensions ) ) {
789  return [
790  $fileExtension,
791  $wgLang->commaList( $extensions ),
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  } else {
900  return '';
901  }
902  }
903 
904  return null;
905  }
906 
924  public function performUpload(
925  $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
926  ) {
927  $this->getLocalFile()->load( File::READ_LATEST );
928  $props = $this->mFileProps;
929 
930  $error = null;
931  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
932  $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
933  if ( $error ) {
934  if ( !is_array( $error ) ) {
935  $error = [ $error ];
936  }
937  return Status::newFatal( ...$error );
938  }
939 
940  $status = $this->getLocalFile()->upload(
941  $this->mTempPath,
942  $comment,
943  $pageText,
945  $props,
946  false,
947  $user,
948  $tags
949  );
950 
951  if ( $status->isGood() ) {
952  if ( $watch ) {
953  MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
954  $user,
955  $this->getLocalFile()->getTitle(),
956  $watchlistExpiry
957  );
958  }
959  $this->getHookRunner()->onUploadComplete( $this );
960 
961  $this->postProcessUpload();
962  }
963 
964  return $status;
965  }
966 
973  public function postProcessUpload() {
974  }
975 
982  public function getTitle() {
983  if ( $this->mTitle !== false ) {
984  return $this->mTitle;
985  }
986  if ( !is_string( $this->mDesiredDestName ) ) {
987  $this->mTitleError = self::ILLEGAL_FILENAME;
988  $this->mTitle = null;
989 
990  return $this->mTitle;
991  }
992  /* Assume that if a user specified File:Something.jpg, this is an error
993  * and that the namespace prefix needs to be stripped of.
994  */
995  $title = Title::newFromText( $this->mDesiredDestName );
996  if ( $title && $title->getNamespace() === NS_FILE ) {
997  $this->mFilteredName = $title->getDBkey();
998  } else {
999  $this->mFilteredName = $this->mDesiredDestName;
1000  }
1001 
1002  # oi_archive_name is max 255 bytes, which include a timestamp and an
1003  # exclamation mark, so restrict file name to 240 bytes.
1004  if ( strlen( $this->mFilteredName ) > 240 ) {
1005  $this->mTitleError = self::FILENAME_TOO_LONG;
1006  $this->mTitle = null;
1007 
1008  return $this->mTitle;
1009  }
1010 
1016  $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
1017  /* Normalize to title form before we do any further processing */
1018  $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1019  if ( $nt === null ) {
1020  $this->mTitleError = self::ILLEGAL_FILENAME;
1021  $this->mTitle = null;
1022 
1023  return $this->mTitle;
1024  }
1025  $this->mFilteredName = $nt->getDBkey();
1026 
1031  list( $partname, $ext ) = $this->splitExtensions( $this->mFilteredName );
1032 
1033  if ( $ext !== [] ) {
1034  $this->mFinalExtension = trim( end( $ext ) );
1035  } else {
1036  $this->mFinalExtension = '';
1037 
1038  // No extension, try guessing one from the temporary file
1039  // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1040  // or incomplete test case in UploadBaseTest (T272328)
1041  if ( $this->mTempPath !== null ) {
1042  $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1043  $mime = $magic->guessMimeType( $this->mTempPath );
1044  if ( $mime !== 'unknown/unknown' ) {
1045  # Get a space separated list of extensions
1046  $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1047  if ( $mimeExt !== null ) {
1048  # Set the extension to the canonical extension
1049  $this->mFinalExtension = $mimeExt;
1050 
1051  # Fix up the other variables
1052  $this->mFilteredName .= ".{$this->mFinalExtension}";
1053  $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1055  }
1056  }
1057  }
1058  }
1059 
1060  // Don't allow users to override the list of prohibited file extensions (check file extension)
1061  $config = MediaWikiServices::getInstance()->getMainConfig();
1062  $checkFileExtensions = $config->get( MainConfigNames::CheckFileExtensions );
1063  $strictFileExtensions = $config->get( MainConfigNames::StrictFileExtensions );
1064  $fileExtensions = $config->get( MainConfigNames::FileExtensions );
1065  $prohibitedFileExtensions = $config->get( MainConfigNames::ProhibitedFileExtensions );
1066 
1067  $blackListedExtensions = self::checkFileExtensionList( $ext, $prohibitedFileExtensions );
1068 
1069  if ( $this->mFinalExtension == '' ) {
1070  $this->mTitleError = self::FILETYPE_MISSING;
1071  $this->mTitle = null;
1072 
1073  return $this->mTitle;
1074  } elseif ( $blackListedExtensions ||
1075  ( $checkFileExtensions && $strictFileExtensions &&
1076  !$this->checkFileExtension( $this->mFinalExtension, $fileExtensions ) )
1077  ) {
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 );
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  } else {
1271  wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1272  "recognized extension '$extension', so probably invalid file" );
1273 
1274  return false;
1275  }
1276  }
1277 
1278  $match = $magic->isMatchingExtension( $extension, $mime );
1279 
1280  if ( $match === null ) {
1281  if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1282  wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1283 
1284  return false;
1285  } else {
1286  wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1287 
1288  return true;
1289  }
1290  } elseif ( $match ) {
1291  wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1292 
1294  return true;
1295  } else {
1296  wfDebug( __METHOD__
1297  . ": mime type $mime mismatches file extension $extension, rejecting file" );
1298 
1299  return false;
1300  }
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  $isText = strpos( $mime, 'text/' ) === 0;
1319  if ( $isText ) {
1320  $chunk = file_get_contents( $file );
1321  } else {
1322  $fp = fopen( $file, 'rb' );
1323  if ( !$fp ) {
1324  return false;
1325  }
1326  $chunk = fread( $fp, 1024 );
1327  fclose( $fp );
1328  }
1329 
1330  $chunk = strtolower( $chunk );
1331 
1332  if ( !$chunk ) {
1333  return false;
1334  }
1335 
1336  # decode from UTF-16 if needed (could be used for obfuscation).
1337  if ( substr( $chunk, 0, 2 ) == "\xfe\xff" ) {
1338  $enc = 'UTF-16BE';
1339  } elseif ( substr( $chunk, 0, 2 ) == "\xff\xfe" ) {
1340  $enc = 'UTF-16LE';
1341  } else {
1342  $enc = null;
1343  }
1344 
1345  if ( $enc !== null ) {
1346  $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1347  }
1348 
1349  $chunk = trim( $chunk );
1350 
1352  wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1353 
1354  # check for HTML doctype
1355  if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1356  return true;
1357  }
1358 
1359  // Some browsers will interpret obscure xml encodings as UTF-8, while
1360  // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1361  if ( $extension == 'svg' || strpos( $mime, 'image/svg' ) === 0 ) {
1362  if ( self::checkXMLEncodingMissmatch( $file ) ) {
1363  return true;
1364  }
1365  }
1366 
1367  // Quick check for HTML heuristics in old IE and Safari.
1368  //
1369  // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1370  // don't need them all here as it can cause many false positives.
1371  //
1372  // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1373  $tags = [
1374  '<body',
1375  '<head',
1376  '<html', # also in safari
1377  '<script', # also in safari
1378  ];
1379 
1380  foreach ( $tags as $tag ) {
1381  if ( strpos( $chunk, $tag ) !== false ) {
1382  wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1383 
1384  return true;
1385  }
1386  }
1387 
1388  /*
1389  * look for JavaScript
1390  */
1391 
1392  # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1393  $chunk = Sanitizer::decodeCharReferences( $chunk );
1394 
1395  # look for script-types
1396  if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
1397  wfDebug( __METHOD__ . ": found script types" );
1398 
1399  return true;
1400  }
1401 
1402  # look for html-style script-urls
1403  if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1404  wfDebug( __METHOD__ . ": found html-style script urls" );
1405 
1406  return true;
1407  }
1408 
1409  # look for css-style script-urls
1410  if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1411  wfDebug( __METHOD__ . ": found css-style script urls" );
1412 
1413  return true;
1414  }
1415 
1416  wfDebug( __METHOD__ . ": no scripts found" );
1417 
1418  return false;
1419  }
1420 
1428  public static function checkXMLEncodingMissmatch( $file ) {
1429  $svgMetadataCutoff = MediaWikiServices::getInstance()->getMainConfig()
1430  ->get( MainConfigNames::SVGMetadataCutoff );
1431  $contents = file_get_contents( $file, false, null, 0, $svgMetadataCutoff );
1432  $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1433 
1434  if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1435  if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1436  && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1437  ) {
1438  wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1439 
1440  return true;
1441  }
1442  } elseif ( preg_match( "!<\?xml\b!si", $contents ) ) {
1443  // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1444  // bytes. There shouldn't be a legitimate reason for this to happen.
1445  wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1446 
1447  return true;
1448  } elseif ( substr( $contents, 0, 4 ) == "\x4C\x6F\xA7\x94" ) {
1449  // EBCDIC encoded XML
1450  wfDebug( __METHOD__ . ": EBCDIC Encoded XML" );
1451 
1452  return true;
1453  }
1454 
1455  // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to
1456  // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings
1457  $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1458  foreach ( $attemptEncodings as $encoding ) {
1459  AtEase::suppressWarnings();
1460  $str = iconv( $encoding, 'UTF-8', $contents );
1461  AtEase::restoreWarnings();
1462  if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1463  if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1464  && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1465  ) {
1466  wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1467 
1468  return true;
1469  }
1470  } elseif ( $str != '' && preg_match( "!<\?xml\b!si", $str ) ) {
1471  // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1472  // bytes. There shouldn't be a legitimate reason for this to happen.
1473  wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1474 
1475  return true;
1476  }
1477  }
1478 
1479  return false;
1480  }
1481 
1487  protected function detectScriptInSvg( $filename, $partial ) {
1488  $this->mSVGNSError = false;
1489  $check = new XmlTypeCheck(
1490  $filename,
1491  [ $this, 'checkSvgScriptCallback' ],
1492  true,
1493  [
1494  'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ],
1495  'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ],
1496  ]
1497  );
1498  if ( $check->wellFormed !== true ) {
1499  // Invalid xml (T60553)
1500  // But only when non-partial (T67724)
1501  return $partial ? false : [ 'uploadinvalidxml' ];
1502  } elseif ( $check->filterMatch ) {
1503  if ( $this->mSVGNSError ) {
1504  return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1505  }
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  || strpos( $publicId, "-//W3C//" ) !== 0
1553  ) {
1554  return [ 'upload-scripted-dtd' ];
1555  }
1556  return false;
1557  }
1558 
1566  public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1567  list( $namespace, $strippedElement ) = $this->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  ];
1611 
1612  // Inkscape mangles namespace definitions created by Adobe Illustrator.
1613  // This is nasty but harmless. (T144827)
1614  $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1615 
1616  if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1617  wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file." );
1619  $this->mSVGNSError = $namespace;
1620 
1621  return true;
1622  }
1623 
1624  /*
1625  * check for elements that can contain javascript
1626  */
1627  if ( $strippedElement == 'script' ) {
1628  wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file." );
1629 
1630  return [ 'uploaded-script-svg', $strippedElement ];
1631  }
1632 
1633  # e.g., <svg xmlns="http://www.w3.org/2000/svg">
1634  # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1635  if ( $strippedElement == 'handler' ) {
1636  wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1637 
1638  return [ 'uploaded-script-svg', $strippedElement ];
1639  }
1640 
1641  # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1642  if ( $strippedElement == 'stylesheet' ) {
1643  wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1644 
1645  return [ 'uploaded-script-svg', $strippedElement ];
1646  }
1647 
1648  # Block iframes, in case they pass the namespace check
1649  if ( $strippedElement == 'iframe' ) {
1650  wfDebug( __METHOD__ . ": iframe in uploaded file." );
1651 
1652  return [ 'uploaded-script-svg', $strippedElement ];
1653  }
1654 
1655  # Check <style> css
1656  if ( $strippedElement == 'style'
1657  && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1658  ) {
1659  wfDebug( __METHOD__ . ": hostile css in style element." );
1660  return [ 'uploaded-hostile-svg' ];
1661  }
1662 
1663  foreach ( $attribs as $attrib => $value ) {
1664  $stripped = $this->stripXmlNamespace( $attrib );
1665  $value = strtolower( $value );
1666 
1667  if ( substr( $stripped, 0, 2 ) == 'on' ) {
1668  wfDebug( __METHOD__
1669  . ": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1670 
1671  return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1672  }
1673 
1674  # Do not allow relative links, or unsafe url schemas.
1675  # For <a> tags, only data:, http: and https: and same-document
1676  # fragment links are allowed. For all other tags, only data:
1677  # and fragment are allowed.
1678  if ( $stripped == 'href'
1679  && $value !== ''
1680  && strpos( $value, 'data:' ) !== 0
1681  && strpos( $value, '#' ) !== 0
1682  ) {
1683  if ( !( $strippedElement === 'a'
1684  && preg_match( '!^https?://!i', $value ) )
1685  ) {
1686  wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1687  . "'$attrib'='$value' in uploaded file." );
1688 
1689  return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1690  }
1691  }
1692 
1693  # only allow data: targets that should be safe. This prevents vectors like,
1694  # image/svg, text/xml, application/xml, and text/html, which can contain scripts
1695  if ( $stripped == 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1696  // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
1697  // phpcs:ignore Generic.Files.LineLength
1698  $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1699 
1700  if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1701  wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
1702  . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1703  return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1704  }
1705  }
1706 
1707  # Change href with animate from (http://html5sec.org/#137).
1708  if ( $stripped === 'attributename'
1709  && $strippedElement === 'animate'
1710  && $this->stripXmlNamespace( $value ) == 'href'
1711  ) {
1712  wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1713  . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1714 
1715  return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1716  }
1717 
1718  # use set/animate to add event-handler attribute to parent
1719  if ( ( $strippedElement == 'set' || $strippedElement == 'animate' )
1720  && $stripped == 'attributename'
1721  && substr( $value, 0, 2 ) == 'on'
1722  ) {
1723  wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1724  . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1725 
1726  return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1727  }
1728 
1729  # use set to add href attribute to parent element
1730  if ( $strippedElement == 'set'
1731  && $stripped == 'attributename'
1732  && strpos( $value, 'href' ) !== false
1733  ) {
1734  wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file." );
1735 
1736  return [ 'uploaded-setting-href-svg' ];
1737  }
1738 
1739  # use set to add a remote / data / script target to an element
1740  if ( $strippedElement == 'set'
1741  && $stripped == 'to'
1742  && preg_match( '!(http|https|data|script):!sim', $value )
1743  ) {
1744  wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file." );
1745 
1746  return [ 'uploaded-wrong-setting-svg', $value ];
1747  }
1748 
1749  # use handler attribute with remote / data / script
1750  if ( $stripped == 'handler' && preg_match( '!(http|https|data|script):!sim', $value ) ) {
1751  wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1752  . "'$attrib'='$value' in uploaded file." );
1753 
1754  return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1755  }
1756 
1757  # use CSS styles to bring in remote code
1758  if ( $stripped == 'style'
1759  && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1760  ) {
1761  wfDebug( __METHOD__ . ": Found svg setting a style with "
1762  . "remote url '$attrib'='$value' in uploaded file." );
1763  return [ 'uploaded-remote-url-svg', $attrib, $value ];
1764  }
1765 
1766  # Several attributes can include css, css character escaping isn't allowed
1767  $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1768  'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1769  if ( in_array( $stripped, $cssAttrs )
1770  && self::checkCssFragment( $value )
1771  ) {
1772  wfDebug( __METHOD__ . ": Found svg setting a style with "
1773  . "remote url '$attrib'='$value' in uploaded file." );
1774  return [ 'uploaded-remote-url-svg', $attrib, $value ];
1775  }
1776 
1777  # image filters can pull in url, which could be svg that executes scripts
1778  # Only allow url( "#foo" ). Do not allow url( http://example.com )
1779  if ( $strippedElement == 'image'
1780  && $stripped == 'filter'
1781  && preg_match( '!url\s*\‍(\s*["\']?[^#]!sim', $value )
1782  ) {
1783  wfDebug( __METHOD__ . ": Found image filter with url: "
1784  . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1785 
1786  return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1787  }
1788  }
1789 
1790  return false; // No scripts detected
1791  }
1792 
1799  private static function checkCssFragment( $value ) {
1800  # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1801  if ( stripos( $value, '@import' ) !== false ) {
1802  return true;
1803  }
1804 
1805  # We allow @font-face to embed fonts with data: urls, so we snip the string
1806  # 'url' out so this case won't match when we check for urls below
1807  $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1808  $value = preg_replace( $pattern, '$1$2', $value );
1809 
1810  # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1811  # properties filter and accelerator don't seem to be useful for xss in SVG files.
1812  # Expression and -o-link don't seem to work either, but filtering them here in case.
1813  # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1814  # but not local ones such as url("#..., url('#..., url(#....
1815  if ( preg_match( '!expression
1816  | -o-link\s*:
1817  | -o-link-source\s*:
1818  | -o-replace\s*:!imx', $value ) ) {
1819  return true;
1820  }
1821 
1822  if ( preg_match_all(
1823  "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1824  $value,
1825  $matches
1826  ) !== 0
1827  ) {
1828  # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1829  foreach ( $matches[1] as $match ) {
1830  if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1831  return true;
1832  }
1833  }
1834  }
1835 
1836  if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1837  return true;
1838  }
1839 
1840  return false;
1841  }
1842 
1848  private static function splitXmlNamespace( $element ) {
1849  // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1850  $parts = explode( ':', strtolower( $element ) );
1851  $name = array_pop( $parts );
1852  $ns = implode( ':', $parts );
1853 
1854  return [ $ns, $name ];
1855  }
1856 
1861  private function stripXmlNamespace( $name ) {
1862  // 'http://www.w3.org/2000/svg:script' -> 'script'
1863  $parts = explode( ':', strtolower( $name ) );
1864 
1865  return array_pop( $parts );
1866  }
1867 
1878  public static function detectVirus( $file ) {
1879  global $wgOut;
1880  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1881  $antivirus = $mainConfig->get( MainConfigNames::Antivirus );
1882  $antivirusSetup = $mainConfig->get( MainConfigNames::AntivirusSetup );
1883  $antivirusRequired = $mainConfig->get( MainConfigNames::AntivirusRequired );
1884  if ( !$antivirus ) {
1885  wfDebug( __METHOD__ . ": virus scanner disabled" );
1886 
1887  return null;
1888  }
1889 
1890  if ( !$antivirusSetup[$antivirus] ) {
1891  wfDebug( __METHOD__ . ": unknown virus scanner: {$antivirus}" );
1892  $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1893  [ 'virus-badscanner', $antivirus ] );
1894 
1895  return wfMessage( 'virus-unknownscanner' )->text() . " {$antivirus}";
1896  }
1897 
1898  # look up scanner configuration
1899  $command = $antivirusSetup[$antivirus]['command'];
1900  $exitCodeMap = $antivirusSetup[$antivirus]['codemap'];
1901  $msgPattern = $antivirusSetup[$antivirus]['messagepattern'] ?? null;
1902 
1903  if ( strpos( $command, "%f" ) === false ) {
1904  # simple pattern: append file to scan
1905  $command .= " " . Shell::escape( $file );
1906  } else {
1907  # complex pattern: replace "%f" with file to scan
1908  $command = str_replace( "%f", Shell::escape( $file ), $command );
1909  }
1910 
1911  wfDebug( __METHOD__ . ": running virus scan: $command " );
1912 
1913  # execute virus scanner
1914  $exitCode = false;
1915 
1916  # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1917  # that does not seem to be worth the pain.
1918  # Ask me (Duesentrieb) about it if it's ever needed.
1919  $output = wfShellExecWithStderr( $command, $exitCode );
1920 
1921  # map exit code to AV_xxx constants.
1922  $mappedCode = $exitCode;
1923  if ( $exitCodeMap ) {
1924  if ( isset( $exitCodeMap[$exitCode] ) ) {
1925  $mappedCode = $exitCodeMap[$exitCode];
1926  } elseif ( isset( $exitCodeMap["*"] ) ) {
1927  $mappedCode = $exitCodeMap["*"];
1928  }
1929  }
1930 
1931  /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
1932  * so we need the strict equalities === and thus can't use a switch here
1933  */
1934  if ( $mappedCode === AV_SCAN_FAILED ) {
1935  # scan failed (code was mapped to false by $exitCodeMap)
1936  wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1937 
1938  $output = $antivirusRequired
1939  ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1940  : null;
1941  } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1942  # scan failed because filetype is unknown (probably immune)
1943  wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1944  $output = null;
1945  } elseif ( $mappedCode === AV_NO_VIRUS ) {
1946  # no virus found
1947  wfDebug( __METHOD__ . ": file passed virus scan." );
1948  $output = false;
1949  } else {
1950  $output = trim( $output );
1951 
1952  if ( !$output ) {
1953  $output = true; # if there's no output, return true
1954  } elseif ( $msgPattern ) {
1955  $groups = [];
1956  if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1957  $output = $groups[1];
1958  }
1959  }
1960 
1961  wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
1962  }
1963 
1964  return $output;
1965  }
1966 
1975  private function checkOverwrite( Authority $performer ) {
1976  // First check whether the local file can be overwritten
1977  $file = $this->getLocalFile();
1978  $file->load( File::READ_LATEST );
1979  if ( $file->exists() ) {
1980  if ( !self::userCanReUpload( $performer, $file ) ) {
1981  return [ 'fileexists-forbidden', $file->getName() ];
1982  } else {
1983  return true;
1984  }
1985  }
1986 
1987  $services = MediaWikiServices::getInstance();
1988 
1989  /* Check shared conflicts: if the local file does not exist, but
1990  * RepoGroup::findFile finds a file, it exists in a shared repository.
1991  */
1992  $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
1993  if ( $file && !$performer->isAllowed( 'reupload-shared' )
1994  ) {
1995  return [ 'fileexists-shared-forbidden', $file->getName() ];
1996  }
1997 
1998  return true;
1999  }
2000 
2008  public static function userCanReUpload( Authority $performer, File $img ) {
2009  if ( $performer->isAllowed( 'reupload' ) ) {
2010  return true; // non-conditional
2011  } elseif ( !$performer->isAllowed( 'reupload-own' ) ) {
2012  return false;
2013  }
2014 
2015  if ( !( $img instanceof LocalFile ) ) {
2016  return false;
2017  }
2018 
2019  return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2020  }
2021 
2033  public static function getExistsWarning( $file ) {
2034  if ( $file->exists() ) {
2035  return [ 'warning' => 'exists', 'file' => $file ];
2036  }
2037 
2038  if ( $file->getTitle()->getArticleID() ) {
2039  return [ 'warning' => 'page-exists', 'file' => $file ];
2040  }
2041 
2042  if ( !strpos( $file->getName(), '.' ) ) {
2043  $partname = $file->getName();
2044  $extension = '';
2045  } else {
2046  $n = strrpos( $file->getName(), '.' );
2047  $extension = substr( $file->getName(), $n + 1 );
2048  $partname = substr( $file->getName(), 0, $n );
2049  }
2050  $normalizedExtension = File::normalizeExtension( $extension );
2051  $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2052 
2053  if ( $normalizedExtension != $extension ) {
2054  // We're not using the normalized form of the extension.
2055  // Normal form is lowercase, using most common of alternate
2056  // extensions (eg 'jpg' rather than 'JPEG').
2057 
2058  // Check for another file using the normalized form...
2059  $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2060  $file_lc = $localRepo->newFile( $nt_lc );
2061 
2062  if ( $file_lc->exists() ) {
2063  return [
2064  'warning' => 'exists-normalized',
2065  'file' => $file,
2066  'normalizedFile' => $file_lc
2067  ];
2068  }
2069  }
2070 
2071  // Check for files with the same name but a different extension
2072  $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2073  if ( count( $similarFiles ) ) {
2074  return [
2075  'warning' => 'exists-normalized',
2076  'file' => $file,
2077  'normalizedFile' => $similarFiles[0],
2078  ];
2079  }
2080 
2081  if ( self::isThumbName( $file->getName() ) ) {
2082  # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2083  $nt_thb = Title::newFromText(
2084  substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2085  NS_FILE
2086  );
2087  $file_thb = $localRepo->newFile( $nt_thb );
2088  if ( $file_thb->exists() ) {
2089  return [
2090  'warning' => 'thumb',
2091  'file' => $file,
2092  'thumbFile' => $file_thb
2093  ];
2094  } else {
2095  // File does not exist, but we just don't like the name
2096  return [
2097  'warning' => 'thumb-name',
2098  'file' => $file,
2099  'thumbFile' => $file_thb
2100  ];
2101  }
2102  }
2103 
2104  foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2105  if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
2106  return [
2107  'warning' => 'bad-prefix',
2108  'file' => $file,
2109  'prefix' => $prefix
2110  ];
2111  }
2112  }
2113 
2114  return false;
2115  }
2116 
2122  public static function isThumbName( $filename ) {
2123  $n = strrpos( $filename, '.' );
2124  $partname = $n ? substr( $filename, 0, $n ) : $filename;
2125 
2126  return (
2127  substr( $partname, 3, 3 ) == 'px-' ||
2128  substr( $partname, 2, 3 ) == 'px-'
2129  ) &&
2130  preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2131  }
2132 
2138  public static function getFilenamePrefixBlacklist() {
2139  $list = [];
2140  $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2141  if ( !$message->isDisabled() ) {
2142  $lines = explode( "\n", $message->plain() );
2143  foreach ( $lines as $line ) {
2144  // Remove comment lines
2145  $comment = substr( trim( $line ), 0, 1 );
2146  if ( $comment == '#' || $comment == '' ) {
2147  continue;
2148  }
2149  // Remove additional comments after a prefix
2150  $comment = strpos( $line, '#' );
2151  if ( $comment > 0 ) {
2152  $line = substr( $line, 0, $comment - 1 );
2153  }
2154  $list[] = trim( $line );
2155  }
2156  }
2157 
2158  return $list;
2159  }
2160 
2172  public function getImageInfo( $result ) {
2173  $localFile = $this->getLocalFile();
2174  $stashFile = $this->getStashFile();
2175  // Calling a different API module depending on whether the file was stashed is less than optimal.
2176  // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
2177  if ( $stashFile ) {
2179  $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_fill_keys( $imParam, true ), $result );
2180  } else {
2182  $info = ApiQueryImageInfo::getInfo( $localFile, array_fill_keys( $imParam, true ), $result );
2183  }
2184 
2185  return $info;
2186  }
2187 
2192  public function convertVerifyErrorToStatus( $error ) {
2193  $code = $error['status'];
2194  unset( $code['status'] );
2195 
2196  return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2197  }
2198 
2206  public static function getMaxUploadSize( $forType = null ) {
2207  $maxUploadSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxUploadSize );
2208 
2209  if ( is_array( $maxUploadSize ) ) {
2210  if ( $forType !== null && isset( $maxUploadSize[$forType] ) ) {
2211  return $maxUploadSize[$forType];
2212  } else {
2213  return $maxUploadSize['*'];
2214  }
2215  } else {
2216  return intval( $maxUploadSize );
2217  }
2218  }
2219 
2227  public static function getMaxPhpUploadSize() {
2228  $phpMaxFileSize = wfShorthandToInteger(
2229  ini_get( 'upload_max_filesize' ),
2230  PHP_INT_MAX
2231  );
2232  $phpMaxPostSize = wfShorthandToInteger(
2233  ini_get( 'post_max_size' ),
2234  PHP_INT_MAX
2235  ) ?: PHP_INT_MAX;
2236  return min( $phpMaxFileSize, $phpMaxPostSize );
2237  }
2238 
2250  public static function getSessionStatus( UserIdentity $user, $statusKey ) {
2251  $store = self::getUploadSessionStore();
2252  $key = self::getUploadSessionKey( $store, $user, $statusKey );
2253 
2254  return $store->get( $key );
2255  }
2256 
2269  public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
2270  $store = self::getUploadSessionStore();
2271  $key = self::getUploadSessionKey( $store, $user, $statusKey );
2272 
2273  if ( $value === false ) {
2274  $store->delete( $key );
2275  } else {
2276  $store->set( $key, $value, $store::TTL_DAY );
2277  }
2278  }
2279 
2286  private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
2287  return $store->makeKey(
2288  'uploadstatus',
2289  $user->isRegistered() ? $user->getId() : md5( $user->getName() ),
2290  $statusKey
2291  );
2292  }
2293 
2297  private static function getUploadSessionStore() {
2298  return MediaWikiServices::getInstance()->getMainObjectStash();
2299  }
2300 }
const AV_SCAN_FAILED
Definition: Defines.php:98
const NS_FILE
Definition: Defines.php:70
const AV_SCAN_ABORTED
Definition: Defines.php:97
const AV_NO_VIRUS
Definition: Defines.php:95
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with '-'.
wfShorthandToInteger( $string='', $default=-1)
Converts shorthand byte notation to integer form.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
$matches
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgOut
Definition: Setup.php:486
if(!defined( 'MW_NO_SESSION') &&! $wgCommandLineMode) $wgLang
Definition: Setup.php:486
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:87
makeKey( $collection,... $components)
Make a cache key for the global keyspace and given components.
static getSha1Base36FromPath( $path)
Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case encoding,...
Definition: FSFile.php:225
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Definition: FileRepo.php:288
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:68
const DELETE_SOURCE
Definition: File.php:85
const DELETED_FILE
Definition: File.php:72
wasDeleted()
Was this file ever deleted from the wiki?
Definition: File.php:2088
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:1293
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
Definition: LocalFile.php:1522
MediaWiki exception.
Definition: MWException.php:29
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.
MediaWikiServices is the service locator for the application scope of MediaWiki.
A StatusValue for permission errors.
Executes shell commands.
Definition: Shell.php:46
static sizeParam( $size)
Definition: Message.php:1265
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:1363
static normalizeCss( $value)
Normalize CSS into a format we can easily search for hostile input.
Definition: Sanitizer.php:697
static newFatal( $message,... $parameters)
Factory function for fatal errors.
Definition: StatusValue.php:70
static newGood( $value=null)
Factory function for good results.
Definition: StatusValue.php:82
This class is used to hold the location and do limited manipulation of files stored temporarily (this...
Definition: TempFSFile.php:35
static capitalize( $text, $ns=NS_MAIN)
Capitalize a text string for a title if it belongs to a namespace that capitalizes.
Definition: Title.php:2968
getDBkey()
Get the main part with underscores.
Definition: Title.php:1056
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:369
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:663
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:637
UploadBase and subclasses are the backend of MediaWiki's file uploads.
Definition: UploadBase.php:49
static string[] $uploadHandlers
Upload handlers.
Definition: UploadBase.php:181
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:737
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:973
checkSvgScriptCallback( $element, $attribs, $data=null)
verifyPermissions(Authority $performer)
Alias for verifyTitlePermissions.
Definition: UploadBase.php:624
checkLocalFileExists(LocalFile $localFile, $hash)
Definition: UploadBase.php:831
getLocalFile()
Return the local file and initializes if necessary.
const SUCCESS
Definition: UploadBase.php:105
stripXmlNamespace( $name)
bool null $mJavaDetected
Definition: UploadBase.php:83
string null $mFilteredName
Definition: UploadBase.php:69
checkBadFileName( $filename, $desiredFileName)
Check whether the resulting filename is different from the desired one, but ignore things like ucfirs...
Definition: UploadBase.php:762
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:595
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:552
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.
static getUploadSessionStore()
getVerificationErrorCode( $error)
Definition: UploadBase.php:123
performUpload( $comment, $pageText, $watch, $user, $tags=[], ?string $watchlistExpiry=null)
Really perform the upload.
Definition: UploadBase.php:924
string null $mDesiredDestName
Definition: UploadBase.php:57
static checkCssFragment( $value)
Check a block of CSS or CSS fragment for anything that looks like it is bringing in remote code.
verifyTitlePermissions(Authority $performer)
Check whether the user can edit, upload and create the image.
Definition: UploadBase.php:639
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
Title bool null $mTitle
Definition: UploadBase.php:65
checkFileSize( $fileSize)
Definition: UploadBase.php:805
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.
checkAgainstArchiveDupes( $hash, Authority $performer)
Definition: UploadBase.php:891
getTitle()
Returns the title of the file to be uploaded.
Definition: UploadBase.php:982
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
static splitXmlNamespace( $element)
Divide the element name passed by the xml parser to the callback into URI and prefix.
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:673
static getUploadSessionKey(BagOStuff $store, UserIdentity $user, $statusKey)
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
checkLocalFileWasDeleted(LocalFile $localFile)
Definition: UploadBase.php:855
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
checkOverwrite(Authority $performer)
Check if there's an overwrite conflict and, if so, if restrictions forbid this user from performing t...
const MIN_LENGTH_PARTNAME
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.
checkUnwantedFileExtensions( $fileExtension)
Definition: UploadBase.php:781
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
checkAgainstExistingDupes( $hash, $ignoreLocalDupes)
Definition: UploadBase.php:865
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:68
This interface represents the authority associated the current execution context, such as a web reque...
Definition: Authority.php:37
authorizeWrite(string $action, PageIdentity $target, PermissionStatus $status=null)
Authorize write access.
isAllowed(string $permission)
Checks whether this authority has the given permission in general.
Interface for objects representing user identity.
getId( $wikiId=self::LOCAL)
$line
Definition: mcc.php:119
$command
Definition: mcc.php:125
$mime
Definition: router.php:60
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
if(!is_readable( $file)) $ext
Definition: router.php:48
if(!file_exists( $CREDITS)) $lines