MediaWiki  master
UploadBase.php
Go to the documentation of this file.
1 <?php
25 use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
33 use Wikimedia\AtEase\AtEase;
34 
51 abstract class UploadBase {
52  use ProtectedHookAccessorTrait;
53 
55  protected $mTempPath;
57  protected $tempFileObj;
59  protected $mDesiredDestName;
61  protected $mDestName;
63  protected $mRemoveTempFile;
65  protected $mSourceType;
67  protected $mTitle = false;
69  protected $mTitleError = 0;
71  protected $mFilteredName;
73  protected $mFinalExtension;
75  protected $mLocalFile;
77  protected $mStashFile;
79  protected $mFileSize;
81  protected $mFileProps;
85  protected $mJavaDetected;
87  protected $mSVGNSError;
88 
89  protected static $safeXmlEncodings = [
90  'UTF-8',
91  'US-ASCII',
92  'ISO-8859-1',
93  'ISO-8859-2',
94  'UTF-16',
95  'UTF-32',
96  'WINDOWS-1250',
97  'WINDOWS-1251',
98  'WINDOWS-1252',
99  'WINDOWS-1253',
100  'WINDOWS-1254',
101  'WINDOWS-1255',
102  'WINDOWS-1256',
103  'WINDOWS-1257',
104  'WINDOWS-1258',
105  ];
106 
107  public const SUCCESS = 0;
108  public const OK = 0;
109  public const EMPTY_FILE = 3;
110  public const MIN_LENGTH_PARTNAME = 4;
111  public const ILLEGAL_FILENAME = 5;
112  public const OVERWRITE_EXISTING_FILE = 7; # Not used anymore; handled by verifyTitlePermissions()
113  public const FILETYPE_MISSING = 8;
114  public const FILETYPE_BADTYPE = 9;
115  public const VERIFICATION_ERROR = 10;
116  public const HOOK_ABORTED = 11;
117  public const FILE_TOO_LARGE = 12;
118  public const WINDOWS_NONASCII_FILENAME = 13;
119  public const FILENAME_TOO_LONG = 14;
120 
125  public function getVerificationErrorCode( $error ) {
126  $code_to_status = [
127  self::EMPTY_FILE => 'empty-file',
128  self::FILE_TOO_LARGE => 'file-too-large',
129  self::FILETYPE_MISSING => 'filetype-missing',
130  self::FILETYPE_BADTYPE => 'filetype-banned',
131  self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
132  self::ILLEGAL_FILENAME => 'illegal-filename',
133  self::OVERWRITE_EXISTING_FILE => 'overwrite',
134  self::VERIFICATION_ERROR => 'verification-error',
135  self::HOOK_ABORTED => 'hookaborted',
136  self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
137  self::FILENAME_TOO_LONG => 'filename-toolong',
138  ];
139  return $code_to_status[$error] ?? 'unknown-error';
140  }
141 
148  public static function isEnabled() {
149  $enableUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnableUploads );
150 
151  return $enableUploads && wfIniGetBool( 'file_uploads' );
152  }
153 
162  public static function isAllowed( Authority $performer ) {
163  foreach ( [ 'upload', 'edit' ] as $permission ) {
164  if ( !$performer->isAllowed( $permission ) ) {
165  return $permission;
166  }
167  }
168 
169  return true;
170  }
171 
178  public static function isThrottled( $user ) {
179  return $user->pingLimiter( 'upload' );
180  }
181 
183  private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
184 
192  public static function createFromRequest( &$request, $type = null ) {
193  $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
194 
195  if ( !$type ) {
196  return null;
197  }
198 
199  // Get the upload class
200  $type = ucfirst( $type );
201 
202  // Give hooks the chance to handle this request
204  $className = null;
205  ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
206  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
207  ->onUploadCreateFromRequest( $type, $className );
208  if ( $className === null ) {
209  $className = 'UploadFrom' . $type;
210  wfDebug( __METHOD__ . ": class name: $className" );
211  if ( !in_array( $type, self::$uploadHandlers ) ) {
212  return null;
213  }
214  }
215 
216  // Check whether this upload class is enabled
217  if ( !$className::isEnabled() ) {
218  return null;
219  }
220 
221  // Check whether the request is valid
222  if ( !$className::isValidRequest( $request ) ) {
223  return null;
224  }
225 
227  $handler = new $className;
228 
229  $handler->initializeFromRequest( $request );
230 
231  return $handler;
232  }
233 
239  public static function isValidRequest( $request ) {
240  return false;
241  }
242 
246  public function __construct() {
247  }
248 
256  public function getSourceType() {
257  return null;
258  }
259 
267  public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
268  $this->mDesiredDestName = $name;
269  if ( FileBackend::isStoragePath( $tempPath ) ) {
270  throw new MWException( __METHOD__ . " given storage path `$tempPath`." );
271  }
272 
273  $this->setTempFile( $tempPath, $fileSize );
274  $this->mRemoveTempFile = $removeTempFile;
275  }
276 
282  abstract public function initializeFromRequest( &$request );
283 
288  protected function setTempFile( $tempPath, $fileSize = null ) {
289  $this->mTempPath = $tempPath ?? '';
290  $this->mFileSize = $fileSize ?: null;
291  if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
292  $this->tempFileObj = new TempFSFile( $this->mTempPath );
293  if ( !$fileSize ) {
294  $this->mFileSize = filesize( $this->mTempPath );
295  }
296  } else {
297  $this->tempFileObj = null;
298  }
299  }
300 
306  public function fetchFile() {
307  return Status::newGood();
308  }
309 
314  public function isEmptyFile() {
315  return empty( $this->mFileSize );
316  }
317 
322  public function getFileSize() {
323  return $this->mFileSize;
324  }
325 
331  public function getTempFileSha1Base36() {
332  return FSFile::getSha1Base36FromPath( $this->mTempPath );
333  }
334 
339  public function getRealPath( $srcPath ) {
340  $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
341  if ( FileRepo::isVirtualUrl( $srcPath ) ) {
345  $tmpFile = $repo->getLocalCopy( $srcPath );
346  if ( $tmpFile ) {
347  $tmpFile->bind( $this ); // keep alive with $this
348  }
349  $path = $tmpFile ? $tmpFile->getPath() : false;
350  } else {
351  $path = $srcPath;
352  }
353 
354  return $path;
355  }
356 
374  public function verifyUpload() {
378  if ( $this->isEmptyFile() ) {
379  return [ 'status' => self::EMPTY_FILE ];
380  }
381 
385  $maxSize = self::getMaxUploadSize( $this->getSourceType() );
386  if ( $this->mFileSize > $maxSize ) {
387  return [
388  'status' => self::FILE_TOO_LARGE,
389  'max' => $maxSize,
390  ];
391  }
392 
398  $verification = $this->verifyFile();
399  if ( $verification !== true ) {
400  return [
401  'status' => self::VERIFICATION_ERROR,
402  'details' => $verification
403  ];
404  }
405 
409  $result = $this->validateName();
410  if ( $result !== true ) {
411  return $result;
412  }
413 
414  return [ 'status' => self::OK ];
415  }
416 
423  public function validateName() {
424  $nt = $this->getTitle();
425  if ( $nt === null ) {
426  $result = [ 'status' => $this->mTitleError ];
427  if ( $this->mTitleError === self::ILLEGAL_FILENAME ) {
428  $result['filtered'] = $this->mFilteredName;
429  }
430  if ( $this->mTitleError === self::FILETYPE_BADTYPE ) {
431  $result['finalExt'] = $this->mFinalExtension;
432  if ( count( $this->mBlackListedExtensions ) ) {
433  $result['blacklistedExt'] = $this->mBlackListedExtensions;
434  }
435  }
436 
437  return $result;
438  }
439  $this->mDestName = $this->getLocalFile()->getName();
440 
441  return true;
442  }
443 
453  protected function verifyMimeType( $mime ) {
454  $verifyMimeType = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeType );
455  if ( $verifyMimeType ) {
456  wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" );
457  $mimeTypeExclusions = MediaWikiServices::getInstance()->getMainConfig()
458  ->get( MainConfigNames::MimeTypeExclusions );
459  if ( self::checkFileExtension( $mime, $mimeTypeExclusions ) ) {
460  return [ 'filetype-badmime', $mime ];
461  }
462  }
463 
464  return true;
465  }
466 
472  protected function verifyFile() {
473  $config = MediaWikiServices::getInstance()->getMainConfig();
474  $verifyMimeType = $config->get( MainConfigNames::VerifyMimeType );
475  $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
476  $status = $this->verifyPartialFile();
477  if ( $status !== true ) {
478  return $status;
479  }
480 
481  $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
482  $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
483  $mime = $this->mFileProps['mime'];
484 
485  if ( $verifyMimeType ) {
486  # XXX: Missing extension will be caught by validateName() via getTitle()
487  if ( (string)$this->mFinalExtension !== '' &&
488  !self::verifyExtension( $mime, $this->mFinalExtension )
489  ) {
490  return [ 'filetype-mime-mismatch', $this->mFinalExtension, $mime ];
491  }
492  }
493 
494  # check for htmlish code and javascript
495  if ( !$disableUploadScriptChecks ) {
496  if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
497  $svgStatus = $this->detectScriptInSvg( $this->mTempPath, false );
498  if ( $svgStatus !== false ) {
499  return $svgStatus;
500  }
501  }
502  }
503 
504  $handler = MediaHandler::getHandler( $mime );
505  if ( $handler ) {
506  $handlerStatus = $handler->verifyUpload( $this->mTempPath );
507  if ( !$handlerStatus->isOK() ) {
508  $errors = $handlerStatus->getErrorsArray();
509 
510  return reset( $errors );
511  }
512  }
513 
514  $error = true;
515  $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
516  if ( $error !== true ) {
517  if ( !is_array( $error ) ) {
518  $error = [ $error ];
519  }
520  return $error;
521  }
522 
523  wfDebug( __METHOD__ . ": all clear; passing." );
524 
525  return true;
526  }
527 
537  protected function verifyPartialFile() {
538  $config = MediaWikiServices::getInstance()->getMainConfig();
539  $disableUploadScriptChecks = $config->get( MainConfigNames::DisableUploadScriptChecks );
540  # getTitle() sets some internal parameters like $this->mFinalExtension
541  $this->getTitle();
542 
543  $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
544  $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
545 
546  # check MIME type, if desired
547  $mime = $this->mFileProps['file-mime'];
548  $status = $this->verifyMimeType( $mime );
549  if ( $status !== true ) {
550  return $status;
551  }
552 
553  # check for htmlish code and javascript
554  if ( !$disableUploadScriptChecks ) {
555  if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
556  return [ 'uploadscripted' ];
557  }
558  if ( $this->mFinalExtension === 'svg' || $mime === 'image/svg+xml' ) {
559  $svgStatus = $this->detectScriptInSvg( $this->mTempPath, true );
560  if ( $svgStatus !== false ) {
561  return $svgStatus;
562  }
563  }
564  }
565 
566  # Scan the uploaded file for viruses
567  $virus = self::detectVirus( $this->mTempPath );
568  if ( $virus ) {
569  return [ 'uploadvirus', $virus ];
570  }
571 
572  return true;
573  }
574 
580  public function zipEntryCallback( $entry ) {
581  $names = [ $entry['name'] ];
582 
583  // If there is a null character, cut off the name at it, because JDK's
584  // ZIP_GetEntry() uses strcmp() if the name hashes match. If a file name
585  // were constructed which had ".class\0" followed by a string chosen to
586  // make the hash collide with the truncated name, that file could be
587  // returned in response to a request for the .class file.
588  $nullPos = strpos( $entry['name'], "\000" );
589  if ( $nullPos !== false ) {
590  $names[] = substr( $entry['name'], 0, $nullPos );
591  }
592 
593  // If there is a trailing slash in the file name, we have to strip it,
594  // because that's what ZIP_GetEntry() does.
595  if ( preg_grep( '!\.class/?$!', $names ) ) {
596  $this->mJavaDetected = true;
597  }
598  }
599 
609  public function verifyPermissions( Authority $performer ) {
610  return $this->verifyTitlePermissions( $performer );
611  }
612 
624  public function verifyTitlePermissions( Authority $performer ) {
629  $nt = $this->getTitle();
630  if ( $nt === null ) {
631  return true;
632  }
633 
634  $status = PermissionStatus::newEmpty();
635  $performer->authorizeWrite( 'edit', $nt, $status );
636  $performer->authorizeWrite( 'upload', $nt, $status );
637  if ( !$status->isGood() ) {
638  return $status->toLegacyErrorArray();
639  }
640 
641  $overwriteError = $this->checkOverwrite( $performer );
642  if ( $overwriteError !== true ) {
643  return [ $overwriteError ];
644  }
645 
646  return true;
647  }
648 
658  public function checkWarnings( $user = null ) {
659  if ( $user === null ) {
660  // TODO check uses and hard deprecate
661  $user = RequestContext::getMain()->getUser();
662  }
663 
664  $warnings = [];
665 
666  $localFile = $this->getLocalFile();
667  $localFile->load( File::READ_LATEST );
668  $filename = $localFile->getName();
669  $hash = $this->getTempFileSha1Base36();
670 
671  $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
672  if ( $badFileName !== null ) {
673  $warnings['badfilename'] = $badFileName;
674  }
675 
676  $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
677  if ( $unwantedFileExtensionDetails !== null ) {
678  $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
679  }
680 
681  $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
682  if ( $fileSizeWarnings ) {
683  $warnings = array_merge( $warnings, $fileSizeWarnings );
684  }
685 
686  $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
687  if ( $localFileExistsWarnings ) {
688  $warnings = array_merge( $warnings, $localFileExistsWarnings );
689  }
690 
691  if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
692  $warnings['was-deleted'] = $filename;
693  }
694 
695  // If a file with the same name exists locally then the local file has already been tested
696  // for duplication of content
697  $ignoreLocalDupes = isset( $warnings['exists'] );
698  $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
699  if ( $dupes ) {
700  $warnings['duplicate'] = $dupes;
701  }
702 
703  $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
704  if ( $archivedDupes !== null ) {
705  $warnings['duplicate-archive'] = $archivedDupes;
706  }
707 
708  return $warnings;
709  }
710 
722  public static function makeWarningsSerializable( $warnings ) {
723  array_walk_recursive( $warnings, static function ( &$param, $key ) {
724  if ( $param instanceof File ) {
725  $param = [
726  'fileName' => $param->getName(),
727  'timestamp' => $param->getTimestamp()
728  ];
729  } elseif ( is_object( $param ) ) {
730  throw new InvalidArgumentException(
731  'UploadBase::makeWarningsSerializable: ' .
732  'Unexpected object of class ' . get_class( $param ) );
733  }
734  } );
735  return $warnings;
736  }
737 
747  private function checkBadFileName( $filename, $desiredFileName ) {
748  $comparableName = str_replace( ' ', '_', $desiredFileName );
749  $comparableName = Title::capitalize( $comparableName, NS_FILE );
750 
751  if ( $desiredFileName != $filename && $comparableName != $filename ) {
752  return $filename;
753  }
754 
755  return null;
756  }
757 
766  private function checkUnwantedFileExtensions( $fileExtension ) {
767  $checkFileExtensions = MediaWikiServices::getInstance()->getMainConfig()
768  ->get( MainConfigNames::CheckFileExtensions );
769  $fileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FileExtensions );
770  if ( $checkFileExtensions ) {
771  $extensions = array_unique( $fileExtensions );
772  if ( !self::checkFileExtension( $fileExtension, $extensions ) ) {
773  return [
774  $fileExtension,
775  Message::listParam( $extensions, 'comma' ),
776  count( $extensions )
777  ];
778  }
779  }
780 
781  return null;
782  }
783 
789  private function checkFileSize( $fileSize ) {
790  $uploadSizeWarning = MediaWikiServices::getInstance()->getMainConfig()
791  ->get( MainConfigNames::UploadSizeWarning );
792 
793  $warnings = [];
794 
795  if ( $uploadSizeWarning && ( $fileSize > $uploadSizeWarning ) ) {
796  $warnings['large-file'] = [
797  Message::sizeParam( $uploadSizeWarning ),
798  Message::sizeParam( $fileSize ),
799  ];
800  }
801 
802  if ( $fileSize == 0 ) {
803  $warnings['empty-file'] = true;
804  }
805 
806  return $warnings;
807  }
808 
815  private function checkLocalFileExists( LocalFile $localFile, $hash ) {
816  $warnings = [];
817 
818  $exists = self::getExistsWarning( $localFile );
819  if ( $exists !== false ) {
820  $warnings['exists'] = $exists;
821 
822  // check if file is an exact duplicate of current file version
823  if ( $hash !== false && $hash === $localFile->getSha1() ) {
824  $warnings['no-change'] = $localFile;
825  }
826 
827  // check if file is an exact duplicate of older versions of this file
828  $history = $localFile->getHistory();
829  foreach ( $history as $oldFile ) {
830  if ( $hash === $oldFile->getSha1() ) {
831  $warnings['duplicate-version'][] = $oldFile;
832  }
833  }
834  }
835 
836  return $warnings;
837  }
838 
839  private function checkLocalFileWasDeleted( LocalFile $localFile ) {
840  return $localFile->wasDeleted() && !$localFile->exists();
841  }
842 
849  private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
850  if ( $hash === false ) {
851  return [];
852  }
853  $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
854  $title = $this->getTitle();
855  foreach ( $dupes as $key => $dupe ) {
856  if (
857  ( $dupe instanceof LocalFile ) &&
858  $ignoreLocalDupes &&
859  $title->equals( $dupe->getTitle() )
860  ) {
861  unset( $dupes[$key] );
862  }
863  }
864 
865  return $dupes;
866  }
867 
875  private function checkAgainstArchiveDupes( $hash, Authority $performer ) {
876  if ( $hash === false ) {
877  return null;
878  }
879  $archivedFile = new ArchivedFile( null, 0, '', $hash );
880  if ( $archivedFile->getID() > 0 ) {
881  if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
882  return $archivedFile->getName();
883  }
884  return '';
885  }
886 
887  return null;
888  }
889 
907  public function performUpload(
908  $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
909  ) {
910  $this->getLocalFile()->load( File::READ_LATEST );
911  $props = $this->mFileProps;
912 
913  $error = null;
914  // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
915  $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
916  if ( $error ) {
917  if ( !is_array( $error ) ) {
918  $error = [ $error ];
919  }
920  return Status::newFatal( ...$error );
921  }
922 
923  $status = $this->getLocalFile()->upload(
924  $this->mTempPath,
925  $comment,
926  $pageText,
928  $props,
929  false,
930  $user,
931  $tags
932  );
933 
934  if ( $status->isGood() ) {
935  if ( $watch ) {
936  MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
937  $user,
938  $this->getLocalFile()->getTitle(),
939  $watchlistExpiry
940  );
941  }
942  $this->getHookRunner()->onUploadComplete( $this );
943 
944  $this->postProcessUpload();
945  }
946 
947  return $status;
948  }
949 
956  public function postProcessUpload() {
957  }
958 
965  public function getTitle() {
966  if ( $this->mTitle !== false ) {
967  return $this->mTitle;
968  }
969  if ( !is_string( $this->mDesiredDestName ) ) {
970  $this->mTitleError = self::ILLEGAL_FILENAME;
971  $this->mTitle = null;
972 
973  return $this->mTitle;
974  }
975  /* Assume that if a user specified File:Something.jpg, this is an error
976  * and that the namespace prefix needs to be stripped of.
977  */
978  $title = Title::newFromText( $this->mDesiredDestName );
979  if ( $title && $title->getNamespace() === NS_FILE ) {
980  $this->mFilteredName = $title->getDBkey();
981  } else {
982  $this->mFilteredName = $this->mDesiredDestName;
983  }
984 
985  # oi_archive_name is max 255 bytes, which include a timestamp and an
986  # exclamation mark, so restrict file name to 240 bytes.
987  if ( strlen( $this->mFilteredName ) > 240 ) {
988  $this->mTitleError = self::FILENAME_TOO_LONG;
989  $this->mTitle = null;
990 
991  return $this->mTitle;
992  }
993 
999  $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
1000  /* Normalize to title form before we do any further processing */
1001  $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1002  if ( $nt === null ) {
1003  $this->mTitleError = self::ILLEGAL_FILENAME;
1004  $this->mTitle = null;
1005 
1006  return $this->mTitle;
1007  }
1008  $this->mFilteredName = $nt->getDBkey();
1009 
1014  [ $partname, $ext ] = self::splitExtensions( $this->mFilteredName );
1015 
1016  if ( $ext !== [] ) {
1017  $this->mFinalExtension = trim( end( $ext ) );
1018  } else {
1019  $this->mFinalExtension = '';
1020 
1021  // No extension, try guessing one from the temporary file
1022  // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
1023  // or incomplete test case in UploadBaseTest (T272328)
1024  if ( $this->mTempPath !== null ) {
1025  $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1026  $mime = $magic->guessMimeType( $this->mTempPath );
1027  if ( $mime !== 'unknown/unknown' ) {
1028  # Get a space separated list of extensions
1029  $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
1030  if ( $mimeExt !== null ) {
1031  # Set the extension to the canonical extension
1032  $this->mFinalExtension = $mimeExt;
1033 
1034  # Fix up the other variables
1035  $this->mFilteredName .= ".{$this->mFinalExtension}";
1036  $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
1038  }
1039  }
1040  }
1041  }
1042 
1043  // Don't allow users to override the list of prohibited file extensions (check file extension)
1044  $config = MediaWikiServices::getInstance()->getMainConfig();
1045  $checkFileExtensions = $config->get( MainConfigNames::CheckFileExtensions );
1046  $strictFileExtensions = $config->get( MainConfigNames::StrictFileExtensions );
1047  $fileExtensions = $config->get( MainConfigNames::FileExtensions );
1048  $prohibitedFileExtensions = $config->get( MainConfigNames::ProhibitedFileExtensions );
1049 
1050  $blackListedExtensions = self::checkFileExtensionList( $ext, $prohibitedFileExtensions );
1051 
1052  if ( $this->mFinalExtension == '' ) {
1053  $this->mTitleError = self::FILETYPE_MISSING;
1054  $this->mTitle = null;
1055 
1056  return $this->mTitle;
1057  }
1058 
1059  if ( $blackListedExtensions ||
1060  ( $checkFileExtensions && $strictFileExtensions &&
1061  !self::checkFileExtension( $this->mFinalExtension, $fileExtensions ) ) ) {
1062  $this->mBlackListedExtensions = $blackListedExtensions;
1063  $this->mTitleError = self::FILETYPE_BADTYPE;
1064  $this->mTitle = null;
1065 
1066  return $this->mTitle;
1067  }
1068 
1069  // Windows may be broken with special characters, see T3780
1070  if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1071  && !MediaWikiServices::getInstance()->getRepoGroup()
1072  ->getLocalRepo()->backendSupportsUnicodePaths()
1073  ) {
1074  $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1075  $this->mTitle = null;
1076 
1077  return $this->mTitle;
1078  }
1079 
1080  # If there was more than one "extension", reassemble the base
1081  # filename to prevent bogus complaints about length
1082  if ( count( $ext ) > 1 ) {
1083  $iterations = count( $ext ) - 1;
1084  for ( $i = 0; $i < $iterations; $i++ ) {
1085  $partname .= '.' . $ext[$i];
1086  }
1087  }
1088 
1089  if ( strlen( $partname ) < 1 ) {
1090  $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1091  $this->mTitle = null;
1092 
1093  return $this->mTitle;
1094  }
1095 
1096  $this->mTitle = $nt;
1097 
1098  return $this->mTitle;
1099  }
1100 
1107  public function getLocalFile() {
1108  if ( $this->mLocalFile === null ) {
1109  $nt = $this->getTitle();
1110  $this->mLocalFile = $nt === null
1111  ? null
1112  : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1113  }
1114 
1115  return $this->mLocalFile;
1116  }
1117 
1121  public function getStashFile() {
1122  return $this->mStashFile;
1123  }
1124 
1137  public function tryStashFile( User $user, $isPartial = false ) {
1138  if ( !$isPartial ) {
1139  $error = $this->runUploadStashFileHook( $user );
1140  if ( $error ) {
1141  return Status::newFatal( ...$error );
1142  }
1143  }
1144  try {
1145  $file = $this->doStashFile( $user );
1146  return Status::newGood( $file );
1147  } catch ( UploadStashException $e ) {
1148  return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1149  }
1150  }
1151 
1156  protected function runUploadStashFileHook( User $user ) {
1157  $props = $this->mFileProps;
1158  $error = null;
1159  $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1160  if ( $error && !is_array( $error ) ) {
1161  $error = [ $error ];
1162  }
1163  return $error;
1164  }
1165 
1173  protected function doStashFile( User $user = null ) {
1174  $stash = MediaWikiServices::getInstance()->getRepoGroup()
1175  ->getLocalRepo()->getUploadStash( $user );
1176  $file = $stash->stashFile( $this->mTempPath, $this->getSourceType() );
1177  $this->mStashFile = $file;
1178 
1179  return $file;
1180  }
1181 
1186  public function cleanupTempFile() {
1187  if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1188  // Delete when all relevant TempFSFile handles go out of scope
1189  wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1190  $this->tempFileObj->autocollect();
1191  }
1192  }
1193 
1194  public function getTempPath() {
1195  return $this->mTempPath;
1196  }
1197 
1207  public static function splitExtensions( $filename ) {
1208  $bits = explode( '.', $filename );
1209  $basename = array_shift( $bits );
1210 
1211  return [ $basename, $bits ];
1212  }
1213 
1222  public static function checkFileExtension( $ext, $list ) {
1223  return in_array( strtolower( $ext ?? '' ), $list, true );
1224  }
1225 
1234  public static function checkFileExtensionList( $ext, $list ) {
1235  return array_intersect( array_map( 'strtolower', $ext ), $list );
1236  }
1237 
1245  public static function verifyExtension( $mime, $extension ) {
1246  $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
1247 
1248  if ( !$mime || $mime === 'unknown' || $mime === 'unknown/unknown' ) {
1249  if ( !$magic->isRecognizableExtension( $extension ) ) {
1250  wfDebug( __METHOD__ . ": passing file with unknown detected mime type; " .
1251  "unrecognized extension '$extension', can't verify" );
1252 
1253  return true;
1254  }
1255 
1256  wfDebug( __METHOD__ . ": rejecting file with unknown detected mime type; " .
1257  "recognized extension '$extension', so probably invalid file" );
1258  return false;
1259  }
1260 
1261  $match = $magic->isMatchingExtension( $extension, $mime );
1262 
1263  if ( $match === null ) {
1264  if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1265  wfDebug( __METHOD__ . ": No extension known for $mime, but we know a mime for $extension" );
1266 
1267  return false;
1268  }
1269 
1270  wfDebug( __METHOD__ . ": no file extension known for mime type $mime, passing file" );
1271  return true;
1272  }
1273 
1274  if ( $match ) {
1275  wfDebug( __METHOD__ . ": mime type $mime matches extension $extension, passing file" );
1276 
1278  return true;
1279  }
1280 
1281  wfDebug( __METHOD__
1282  . ": mime type $mime mismatches file extension $extension, rejecting file" );
1283 
1284  return false;
1285  }
1286 
1298  public static function detectScript( $file, $mime, $extension ) {
1299  # ugly hack: for text files, always look at the entire file.
1300  # For binary field, just check the first K.
1301 
1302  if ( str_starts_with( $mime ?? '', 'text/' ) ) {
1303  $chunk = file_get_contents( $file );
1304  } else {
1305  $fp = fopen( $file, 'rb' );
1306  if ( !$fp ) {
1307  return false;
1308  }
1309  $chunk = fread( $fp, 1024 );
1310  fclose( $fp );
1311  }
1312 
1313  $chunk = strtolower( $chunk );
1314 
1315  if ( !$chunk ) {
1316  return false;
1317  }
1318 
1319  # decode from UTF-16 if needed (could be used for obfuscation).
1320  if ( str_starts_with( $chunk, "\xfe\xff" ) ) {
1321  $enc = 'UTF-16BE';
1322  } elseif ( str_starts_with( $chunk, "\xff\xfe" ) ) {
1323  $enc = 'UTF-16LE';
1324  } else {
1325  $enc = null;
1326  }
1327 
1328  if ( $enc !== null ) {
1329  $chunk = iconv( $enc, "ASCII//IGNORE", $chunk );
1330  }
1331 
1332  $chunk = trim( $chunk );
1333 
1335  wfDebug( __METHOD__ . ": checking for embedded scripts and HTML stuff" );
1336 
1337  # check for HTML doctype
1338  if ( preg_match( "/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1339  return true;
1340  }
1341 
1342  // Some browsers will interpret obscure xml encodings as UTF-8, while
1343  // PHP/expat will interpret the given encoding in the xml declaration (T49304)
1344  if ( $extension === 'svg' || str_starts_with( $mime ?? '', 'image/svg' ) ) {
1345  if ( self::checkXMLEncodingMissmatch( $file ) ) {
1346  return true;
1347  }
1348  }
1349 
1350  // Quick check for HTML heuristics in old IE and Safari.
1351  //
1352  // The exact heuristics IE uses are checked separately via verifyMimeType(), so we
1353  // don't need them all here as it can cause many false positives.
1354  //
1355  // Check for `<script` and such still to forbid script tags and embedded HTML in SVG:
1356  $tags = [
1357  '<body',
1358  '<head',
1359  '<html', # also in safari
1360  '<script', # also in safari
1361  ];
1362 
1363  foreach ( $tags as $tag ) {
1364  if ( strpos( $chunk, $tag ) !== false ) {
1365  wfDebug( __METHOD__ . ": found something that may make it be mistaken for html: $tag" );
1366 
1367  return true;
1368  }
1369  }
1370 
1371  /*
1372  * look for JavaScript
1373  */
1374 
1375  # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1376  $chunk = Sanitizer::decodeCharReferences( $chunk );
1377 
1378  # look for script-types
1379  if ( preg_match( '!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!im', $chunk ) ) {
1380  wfDebug( __METHOD__ . ": found script types" );
1381 
1382  return true;
1383  }
1384 
1385  # look for html-style script-urls
1386  if ( preg_match( '!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) {
1387  wfDebug( __METHOD__ . ": found html-style script urls" );
1388 
1389  return true;
1390  }
1391 
1392  # look for css-style script-urls
1393  if ( preg_match( '!url\s*\‍(\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) {
1394  wfDebug( __METHOD__ . ": found css-style script urls" );
1395 
1396  return true;
1397  }
1398 
1399  wfDebug( __METHOD__ . ": no scripts found" );
1400 
1401  return false;
1402  }
1403 
1411  public static function checkXMLEncodingMissmatch( $file ) {
1412  $svgMetadataCutoff = MediaWikiServices::getInstance()->getMainConfig()
1413  ->get( MainConfigNames::SVGMetadataCutoff );
1414  $contents = file_get_contents( $file, false, null, 0, $svgMetadataCutoff );
1415  $encodingRegex = '!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1416 
1417  if ( preg_match( "!<\?xml\b(.*?)\?>!si", $contents, $matches ) ) {
1418  if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1419  && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1420  ) {
1421  wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1422 
1423  return true;
1424  }
1425  } elseif ( preg_match( "!<\?xml\b!i", $contents ) ) {
1426  // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1427  // bytes. There shouldn't be a legitimate reason for this to happen.
1428  wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1429 
1430  return true;
1431  } elseif ( str_starts_with( $contents, "\x4C\x6F\xA7\x94" ) ) {
1432  // EBCDIC encoded XML
1433  wfDebug( __METHOD__ . ": EBCDIC Encoded XML" );
1434 
1435  return true;
1436  }
1437 
1438  // It's possible the file is encoded with multi-byte encoding, so re-encode attempt to
1439  // detect the encoding in case is specifies an encoding not whitelisted in self::$safeXmlEncodings
1440  $attemptEncodings = [ 'UTF-16', 'UTF-16BE', 'UTF-32', 'UTF-32BE' ];
1441  foreach ( $attemptEncodings as $encoding ) {
1442  AtEase::suppressWarnings();
1443  $str = iconv( $encoding, 'UTF-8', $contents );
1444  AtEase::restoreWarnings();
1445  if ( $str != '' && preg_match( "!<\?xml\b(.*?)\?>!si", $str, $matches ) ) {
1446  if ( preg_match( $encodingRegex, $matches[1], $encMatch )
1447  && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1448  ) {
1449  wfDebug( __METHOD__ . ": Found unsafe XML encoding '{$encMatch[1]}'" );
1450 
1451  return true;
1452  }
1453  } elseif ( $str != '' && preg_match( "!<\?xml\b!i", $str ) ) {
1454  // Start of XML declaration without an end in the first $wgSVGMetadataCutoff
1455  // bytes. There shouldn't be a legitimate reason for this to happen.
1456  wfDebug( __METHOD__ . ": Unmatched XML declaration start" );
1457 
1458  return true;
1459  }
1460  }
1461 
1462  return false;
1463  }
1464 
1470  protected function detectScriptInSvg( $filename, $partial ) {
1471  $this->mSVGNSError = false;
1472  $check = new XmlTypeCheck(
1473  $filename,
1474  [ $this, 'checkSvgScriptCallback' ],
1475  true,
1476  [
1477  'processing_instruction_handler' => [ __CLASS__, 'checkSvgPICallback' ],
1478  'external_dtd_handler' => [ __CLASS__, 'checkSvgExternalDTD' ],
1479  ]
1480  );
1481  if ( $check->wellFormed !== true ) {
1482  // Invalid xml (T60553)
1483  // But only when non-partial (T67724)
1484  return $partial ? false : [ 'uploadinvalidxml' ];
1485  }
1486 
1487  if ( $check->filterMatch ) {
1488  if ( $this->mSVGNSError ) {
1489  return [ 'uploadscriptednamespace', $this->mSVGNSError ];
1490  }
1491  return $check->filterMatchType;
1492  }
1493 
1494  return false;
1495  }
1496 
1503  public static function checkSvgPICallback( $target, $data ) {
1504  // Don't allow external stylesheets (T59550)
1505  if ( preg_match( '/xml-stylesheet/i', $target ) ) {
1506  return [ 'upload-scripted-pi-callback' ];
1507  }
1508 
1509  return false;
1510  }
1511 
1523  public static function checkSvgExternalDTD( $type, $publicId, $systemId ) {
1524  // This doesn't include the XHTML+MathML+SVG doctype since we don't
1525  // allow XHTML anyways.
1526  $allowedDTDs = [
1527  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1528  'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1529  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1530  'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1531  // https://phabricator.wikimedia.org/T168856
1532  'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1533  ];
1534  if ( $type !== 'PUBLIC'
1535  || !in_array( $systemId, $allowedDTDs )
1536  || !str_starts_with( $publicId, "-//W3C//" )
1537  ) {
1538  return [ 'upload-scripted-dtd' ];
1539  }
1540  return false;
1541  }
1542 
1550  public function checkSvgScriptCallback( $element, $attribs, $data = null ) {
1551  [ $namespace, $strippedElement ] = self::splitXmlNamespace( $element );
1552 
1553  // We specifically don't include:
1554  // http://www.w3.org/1999/xhtml (T62771)
1555  static $validNamespaces = [
1556  '',
1557  'adobe:ns:meta/',
1558  'http://creativecommons.org/ns#',
1559  'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1560  'http://ns.adobe.com/adobeillustrator/10.0/',
1561  'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1562  'http://ns.adobe.com/extensibility/1.0/',
1563  'http://ns.adobe.com/flows/1.0/',
1564  'http://ns.adobe.com/illustrator/1.0/',
1565  'http://ns.adobe.com/imagereplacement/1.0/',
1566  'http://ns.adobe.com/pdf/1.3/',
1567  'http://ns.adobe.com/photoshop/1.0/',
1568  'http://ns.adobe.com/saveforweb/1.0/',
1569  'http://ns.adobe.com/variables/1.0/',
1570  'http://ns.adobe.com/xap/1.0/',
1571  'http://ns.adobe.com/xap/1.0/g/',
1572  'http://ns.adobe.com/xap/1.0/g/img/',
1573  'http://ns.adobe.com/xap/1.0/mm/',
1574  'http://ns.adobe.com/xap/1.0/rights/',
1575  'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1576  'http://ns.adobe.com/xap/1.0/stype/font#',
1577  'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1578  'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1579  'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1580  'http://ns.adobe.com/xap/1.0/t/pg/',
1581  'http://purl.org/dc/elements/1.1/',
1582  'http://purl.org/dc/elements/1.1',
1583  'http://schemas.microsoft.com/visio/2003/svgextensions/',
1584  'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1585  'http://taptrix.com/inkpad/svg_extensions',
1586  'http://web.resource.org/cc/',
1587  'http://www.freesoftware.fsf.org/bkchem/cdml',
1588  'http://www.inkscape.org/namespaces/inkscape',
1589  'http://www.opengis.net/gml',
1590  'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1591  'http://www.w3.org/2000/svg',
1592  'http://www.w3.org/tr/rec-rdf-syntax/',
1593  'http://www.w3.org/2000/01/rdf-schema#',
1594  'http://www.w3.org/2000/02/svg/testsuite/description/', // https://phabricator.wikimedia.org/T278044
1595  ];
1596 
1597  // Inkscape mangles namespace definitions created by Adobe Illustrator.
1598  // This is nasty but harmless. (T144827)
1599  $isBuggyInkscape = preg_match( '/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1600 
1601  if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1602  wfDebug( __METHOD__ . ": Non-svg namespace '$namespace' in uploaded file." );
1604  $this->mSVGNSError = $namespace;
1605 
1606  return true;
1607  }
1608 
1609  /*
1610  * check for elements that can contain javascript
1611  */
1612  if ( $strippedElement === 'script' ) {
1613  wfDebug( __METHOD__ . ": Found script element '$element' in uploaded file." );
1614 
1615  return [ 'uploaded-script-svg', $strippedElement ];
1616  }
1617 
1618  # e.g., <svg xmlns="http://www.w3.org/2000/svg">
1619  # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1620  if ( $strippedElement === 'handler' ) {
1621  wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1622 
1623  return [ 'uploaded-script-svg', $strippedElement ];
1624  }
1625 
1626  # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1627  if ( $strippedElement === 'stylesheet' ) {
1628  wfDebug( __METHOD__ . ": Found scriptable element '$element' in uploaded file." );
1629 
1630  return [ 'uploaded-script-svg', $strippedElement ];
1631  }
1632 
1633  # Block iframes, in case they pass the namespace check
1634  if ( $strippedElement === 'iframe' ) {
1635  wfDebug( __METHOD__ . ": iframe in uploaded file." );
1636 
1637  return [ 'uploaded-script-svg', $strippedElement ];
1638  }
1639 
1640  # Check <style> css
1641  if ( $strippedElement === 'style'
1642  && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1643  ) {
1644  wfDebug( __METHOD__ . ": hostile css in style element." );
1645 
1646  return [ 'uploaded-hostile-svg' ];
1647  }
1648 
1649  foreach ( $attribs as $attrib => $value ) {
1650  // If attributeNamespace is '', it is relative to its element's namespace
1651  [ $attributeNamespace, $stripped ] = self::splitXmlNamespace( $attrib );
1652  $value = strtolower( $value );
1653 
1654  if ( !(
1655  // Inkscape elements have valid attribs that start with on and are safe, fail all others
1656  $namespace === 'http://www.inkscape.org/namespaces/inkscape' &&
1657  $attributeNamespace === ''
1658  ) && str_starts_with( $stripped, 'on' )
1659  ) {
1660  wfDebug( __METHOD__
1661  . ": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1662 
1663  return [ 'uploaded-event-handler-on-svg', $attrib, $value ];
1664  }
1665 
1666  # Do not allow relative links, or unsafe url schemas.
1667  # For <a> tags, only data:, http: and https: and same-document
1668  # fragment links are allowed. For all other tags, only data:
1669  # and fragment are allowed.
1670  if ( $stripped === 'href'
1671  && $value !== ''
1672  && !str_starts_with( $value, 'data:' )
1673  && !str_starts_with( $value, '#' )
1674  ) {
1675  if ( !( $strippedElement === 'a'
1676  && preg_match( '!^https?://!i', $value ) )
1677  ) {
1678  wfDebug( __METHOD__ . ": Found href attribute <$strippedElement "
1679  . "'$attrib'='$value' in uploaded file." );
1680 
1681  return [ 'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1682  }
1683  }
1684 
1685  # only allow data: targets that should be safe. This prevents vectors like,
1686  # image/svg, text/xml, application/xml, and text/html, which can contain scripts
1687  if ( $stripped === 'href' && strncasecmp( 'data:', $value, 5 ) === 0 ) {
1688  // rfc2397 parameters. This is only slightly slower than (;[\w;]+)*.
1689  // phpcs:ignore Generic.Files.LineLength
1690  $parameters = '(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1691 
1692  if ( !preg_match( "!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1693  wfDebug( __METHOD__ . ": Found href to unwhitelisted data: uri "
1694  . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1695  return [ 'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1696  }
1697  }
1698 
1699  # Change href with animate from (http://html5sec.org/#137).
1700  if ( $stripped === 'attributename'
1701  && $strippedElement === 'animate'
1702  && $this->stripXmlNamespace( $value ) === 'href'
1703  ) {
1704  wfDebug( __METHOD__ . ": Found animate that might be changing href using from "
1705  . "\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1706 
1707  return [ 'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1708  }
1709 
1710  # use set/animate to add event-handler attribute to parent
1711  if ( ( $strippedElement === 'set' || $strippedElement === 'animate' )
1712  && $stripped === 'attributename'
1713  && substr( $value, 0, 2 ) === 'on'
1714  ) {
1715  wfDebug( __METHOD__ . ": Found svg setting event-handler attribute with "
1716  . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1717 
1718  return [ 'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1719  }
1720 
1721  # use set to add href attribute to parent element
1722  if ( $strippedElement === 'set'
1723  && $stripped === 'attributename'
1724  && str_contains( $value, 'href' )
1725  ) {
1726  wfDebug( __METHOD__ . ": Found svg setting href attribute '$value' in uploaded file." );
1727 
1728  return [ 'uploaded-setting-href-svg' ];
1729  }
1730 
1731  # use set to add a remote / data / script target to an element
1732  if ( $strippedElement === 'set'
1733  && $stripped === 'to'
1734  && preg_match( '!(http|https|data|script):!im', $value )
1735  ) {
1736  wfDebug( __METHOD__ . ": Found svg setting attribute to '$value' in uploaded file." );
1737 
1738  return [ 'uploaded-wrong-setting-svg', $value ];
1739  }
1740 
1741  # use handler attribute with remote / data / script
1742  if ( $stripped === 'handler' && preg_match( '!(http|https|data|script):!im', $value ) ) {
1743  wfDebug( __METHOD__ . ": Found svg setting handler with remote/data/script "
1744  . "'$attrib'='$value' in uploaded file." );
1745 
1746  return [ 'uploaded-setting-handler-svg', $attrib, $value ];
1747  }
1748 
1749  # use CSS styles to bring in remote code
1750  if ( $stripped === 'style'
1751  && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1752  ) {
1753  wfDebug( __METHOD__ . ": Found svg setting a style with "
1754  . "remote url '$attrib'='$value' in uploaded file." );
1755  return [ 'uploaded-remote-url-svg', $attrib, $value ];
1756  }
1757 
1758  # Several attributes can include css, css character escaping isn't allowed
1759  $cssAttrs = [ 'font', 'clip-path', 'fill', 'filter', 'marker',
1760  'marker-end', 'marker-mid', 'marker-start', 'mask', 'stroke' ];
1761  if ( in_array( $stripped, $cssAttrs, true )
1762  && self::checkCssFragment( $value )
1763  ) {
1764  wfDebug( __METHOD__ . ": Found svg setting a style with "
1765  . "remote url '$attrib'='$value' in uploaded file." );
1766  return [ 'uploaded-remote-url-svg', $attrib, $value ];
1767  }
1768 
1769  # image filters can pull in url, which could be svg that executes scripts
1770  # Only allow url( "#foo" ). Do not allow url( http://example.com )
1771  if ( $strippedElement === 'image'
1772  && $stripped === 'filter'
1773  && preg_match( '!url\s*\‍(\s*["\']?[^#]!im', $value )
1774  ) {
1775  wfDebug( __METHOD__ . ": Found image filter with url: "
1776  . "\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1777 
1778  return [ 'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1779  }
1780  }
1781 
1782  return false; // No scripts detected
1783  }
1784 
1791  private static function checkCssFragment( $value ) {
1792  # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1793  if ( stripos( $value, '@import' ) !== false ) {
1794  return true;
1795  }
1796 
1797  # We allow @font-face to embed fonts with data: urls, so we snip the string
1798  # 'url' out so this case won't match when we check for urls below
1799  $pattern = '!(@font-face\s*{[^}]*src:)url(\‍("data:;base64,)!im';
1800  $value = preg_replace( $pattern, '$1$2', $value );
1801 
1802  # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1803  # properties filter and accelerator don't seem to be useful for xss in SVG files.
1804  # Expression and -o-link don't seem to work either, but filtering them here in case.
1805  # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1806  # but not local ones such as url("#..., url('#..., url(#....
1807  if ( preg_match( '!expression
1808  | -o-link\s*:
1809  | -o-link-source\s*:
1810  | -o-replace\s*:!imx', $value ) ) {
1811  return true;
1812  }
1813 
1814  if ( preg_match_all(
1815  "!(\s*(url|image|image-set)\s*\‍(\s*[\"']?\s*[^#]+.*?\‍))!sim",
1816  $value,
1817  $matches
1818  ) !== 0
1819  ) {
1820  # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1821  foreach ( $matches[1] as $match ) {
1822  if ( !preg_match( "!\s*(url|image|image-set)\s*\‍(\s*(#|'#|\"#)!im", $match ) ) {
1823  return true;
1824  }
1825  }
1826  }
1827 
1828  if ( preg_match( '/[\000-\010\013\016-\037\177]/', $value ) ) {
1829  return true;
1830  }
1831 
1832  return false;
1833  }
1834 
1840  private static function splitXmlNamespace( $element ) {
1841  // 'http://www.w3.org/2000/svg:script' -> [ 'http://www.w3.org/2000/svg', 'script' ]
1842  $parts = explode( ':', strtolower( $element ) );
1843  $name = array_pop( $parts );
1844  $ns = implode( ':', $parts );
1845 
1846  return [ $ns, $name ];
1847  }
1848 
1853  private function stripXmlNamespace( $name ) {
1854  // 'http://www.w3.org/2000/svg:script' -> 'script'
1855  $parts = explode( ':', strtolower( $name ) );
1856 
1857  return array_pop( $parts );
1858  }
1859 
1870  public static function detectVirus( $file ) {
1871  global $wgOut;
1872  $mainConfig = MediaWikiServices::getInstance()->getMainConfig();
1873  $antivirus = $mainConfig->get( MainConfigNames::Antivirus );
1874  $antivirusSetup = $mainConfig->get( MainConfigNames::AntivirusSetup );
1875  $antivirusRequired = $mainConfig->get( MainConfigNames::AntivirusRequired );
1876  if ( !$antivirus ) {
1877  wfDebug( __METHOD__ . ": virus scanner disabled" );
1878 
1879  return null;
1880  }
1881 
1882  if ( !$antivirusSetup[$antivirus] ) {
1883  wfDebug( __METHOD__ . ": unknown virus scanner: {$antivirus}" );
1884  $wgOut->wrapWikiMsg( "<div class=\"error\">\n$1\n</div>",
1885  [ 'virus-badscanner', $antivirus ] );
1886 
1887  return wfMessage( 'virus-unknownscanner' )->text() . " {$antivirus}";
1888  }
1889 
1890  # look up scanner configuration
1891  $command = $antivirusSetup[$antivirus]['command'];
1892  $exitCodeMap = $antivirusSetup[$antivirus]['codemap'];
1893  $msgPattern = $antivirusSetup[$antivirus]['messagepattern'] ?? null;
1894 
1895  if ( !str_contains( $command, "%f" ) ) {
1896  # simple pattern: append file to scan
1897  $command .= " " . Shell::escape( $file );
1898  } else {
1899  # complex pattern: replace "%f" with file to scan
1900  $command = str_replace( "%f", Shell::escape( $file ), $command );
1901  }
1902 
1903  wfDebug( __METHOD__ . ": running virus scan: $command " );
1904 
1905  # execute virus scanner
1906  $exitCode = false;
1907 
1908  # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1909  # that does not seem to be worth the pain.
1910  # Ask me (Duesentrieb) about it if it's ever needed.
1911  $output = wfShellExecWithStderr( $command, $exitCode );
1912 
1913  # map exit code to AV_xxx constants.
1914  $mappedCode = $exitCode;
1915  if ( $exitCodeMap ) {
1916  if ( isset( $exitCodeMap[$exitCode] ) ) {
1917  $mappedCode = $exitCodeMap[$exitCode];
1918  } elseif ( isset( $exitCodeMap["*"] ) ) {
1919  $mappedCode = $exitCodeMap["*"];
1920  }
1921  }
1922 
1923  /* NB: AV_NO_VIRUS is 0 but AV_SCAN_FAILED is false,
1924  * so we need the strict equalities === and thus can't use a switch here
1925  */
1926  if ( $mappedCode === AV_SCAN_FAILED ) {
1927  # scan failed (code was mapped to false by $exitCodeMap)
1928  wfDebug( __METHOD__ . ": failed to scan $file (code $exitCode)." );
1929 
1930  $output = $antivirusRequired
1931  ? wfMessage( 'virus-scanfailed', [ $exitCode ] )->text()
1932  : null;
1933  } elseif ( $mappedCode === AV_SCAN_ABORTED ) {
1934  # scan failed because filetype is unknown (probably immune)
1935  wfDebug( __METHOD__ . ": unsupported file type $file (code $exitCode)." );
1936  $output = null;
1937  } elseif ( $mappedCode === AV_NO_VIRUS ) {
1938  # no virus found
1939  wfDebug( __METHOD__ . ": file passed virus scan." );
1940  $output = false;
1941  } else {
1942  $output = trim( $output );
1943 
1944  if ( !$output ) {
1945  $output = true; # if there's no output, return true
1946  } elseif ( $msgPattern ) {
1947  $groups = [];
1948  if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1949  $output = $groups[1];
1950  }
1951  }
1952 
1953  wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
1954  }
1955 
1956  return $output;
1957  }
1958 
1967  private function checkOverwrite( Authority $performer ) {
1968  // First check whether the local file can be overwritten
1969  $file = $this->getLocalFile();
1970  $file->load( File::READ_LATEST );
1971  if ( $file->exists() ) {
1972  if ( !self::userCanReUpload( $performer, $file ) ) {
1973  return [ 'fileexists-forbidden', $file->getName() ];
1974  }
1975 
1976  return true;
1977  }
1978 
1979  $services = MediaWikiServices::getInstance();
1980 
1981  /* Check shared conflicts: if the local file does not exist, but
1982  * RepoGroup::findFile finds a file, it exists in a shared repository.
1983  */
1984  $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
1985  if ( $file && !$performer->isAllowed( 'reupload-shared' )
1986  ) {
1987  return [ 'fileexists-shared-forbidden', $file->getName() ];
1988  }
1989 
1990  return true;
1991  }
1992 
2000  public static function userCanReUpload( Authority $performer, File $img ) {
2001  if ( $performer->isAllowed( 'reupload' ) ) {
2002  return true; // non-conditional
2003  }
2004 
2005  if ( !$performer->isAllowed( 'reupload-own' ) ) {
2006  return false;
2007  }
2008 
2009  if ( !( $img instanceof LocalFile ) ) {
2010  return false;
2011  }
2012 
2013  return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2014  }
2015 
2027  public static function getExistsWarning( $file ) {
2028  if ( $file->exists() ) {
2029  return [ 'warning' => 'exists', 'file' => $file ];
2030  }
2031 
2032  if ( $file->getTitle()->getArticleID() ) {
2033  return [ 'warning' => 'page-exists', 'file' => $file ];
2034  }
2035 
2036  if ( !strpos( $file->getName(), '.' ) ) {
2037  $partname = $file->getName();
2038  $extension = '';
2039  } else {
2040  $n = strrpos( $file->getName(), '.' );
2041  $extension = substr( $file->getName(), $n + 1 );
2042  $partname = substr( $file->getName(), 0, $n );
2043  }
2044  $normalizedExtension = File::normalizeExtension( $extension );
2045  $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2046 
2047  if ( $normalizedExtension != $extension ) {
2048  // We're not using the normalized form of the extension.
2049  // Normal form is lowercase, using most common of alternate
2050  // extensions (eg 'jpg' rather than 'JPEG').
2051 
2052  // Check for another file using the normalized form...
2053  $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
2054  $file_lc = $localRepo->newFile( $nt_lc );
2055 
2056  if ( $file_lc->exists() ) {
2057  return [
2058  'warning' => 'exists-normalized',
2059  'file' => $file,
2060  'normalizedFile' => $file_lc
2061  ];
2062  }
2063  }
2064 
2065  // Check for files with the same name but a different extension
2066  $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
2067  if ( count( $similarFiles ) ) {
2068  return [
2069  'warning' => 'exists-normalized',
2070  'file' => $file,
2071  'normalizedFile' => $similarFiles[0],
2072  ];
2073  }
2074 
2075  if ( self::isThumbName( $file->getName() ) ) {
2076  # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2077  $nt_thb = Title::newFromText(
2078  substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
2079  NS_FILE
2080  );
2081  $file_thb = $localRepo->newFile( $nt_thb );
2082  if ( $file_thb->exists() ) {
2083  return [
2084  'warning' => 'thumb',
2085  'file' => $file,
2086  'thumbFile' => $file_thb
2087  ];
2088  }
2089 
2090  // File does not exist, but we just don't like the name
2091  return [
2092  'warning' => 'thumb-name',
2093  'file' => $file,
2094  'thumbFile' => $file_thb
2095  ];
2096  }
2097 
2098  foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2099  if ( str_starts_with( $partname, $prefix ) ) {
2100  return [
2101  'warning' => 'bad-prefix',
2102  'file' => $file,
2103  'prefix' => $prefix
2104  ];
2105  }
2106  }
2107 
2108  return false;
2109  }
2110 
2116  public static function isThumbName( $filename ) {
2117  $n = strrpos( $filename, '.' );
2118  $partname = $n ? substr( $filename, 0, $n ) : $filename;
2119 
2120  return (
2121  substr( $partname, 3, 3 ) === 'px-' ||
2122  substr( $partname, 2, 3 ) === 'px-'
2123  ) &&
2124  preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
2125  }
2126 
2132  public static function getFilenamePrefixBlacklist() {
2133  $list = [];
2134  $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
2135  if ( !$message->isDisabled() ) {
2136  $lines = explode( "\n", $message->plain() );
2137  foreach ( $lines as $line ) {
2138  // Remove comment lines
2139  $comment = substr( trim( $line ), 0, 1 );
2140  if ( $comment === '#' || $comment == '' ) {
2141  continue;
2142  }
2143  // Remove additional comments after a prefix
2144  $comment = strpos( $line, '#' );
2145  if ( $comment > 0 ) {
2146  $line = substr( $line, 0, $comment - 1 );
2147  }
2148  $list[] = trim( $line );
2149  }
2150  }
2151 
2152  return $list;
2153  }
2154 
2166  public function getImageInfo( $result ) {
2167  $localFile = $this->getLocalFile();
2168  $stashFile = $this->getStashFile();
2169  // Calling a different API module depending on whether the file was stashed is less than optimal.
2170  // In fact, calling API modules here at all is less than optimal. Maybe it should be refactored.
2171  if ( $stashFile ) {
2173  $info = ApiQueryStashImageInfo::getInfo( $stashFile, array_fill_keys( $imParam, true ), $result );
2174  } else {
2176  $info = ApiQueryImageInfo::getInfo( $localFile, array_fill_keys( $imParam, true ), $result );
2177  }
2178 
2179  return $info;
2180  }
2181 
2186  public function convertVerifyErrorToStatus( $error ) {
2187  $code = $error['status'];
2188  unset( $code['status'] );
2189 
2190  return Status::newFatal( $this->getVerificationErrorCode( $code ), $error );
2191  }
2192 
2200  public static function getMaxUploadSize( $forType = null ) {
2201  $maxUploadSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxUploadSize );
2202 
2203  if ( is_array( $maxUploadSize ) ) {
2204  if ( $forType !== null && isset( $maxUploadSize[$forType] ) ) {
2205  return $maxUploadSize[$forType];
2206  }
2207  return $maxUploadSize['*'];
2208  }
2209  return intval( $maxUploadSize );
2210  }
2211 
2219  public static function getMaxPhpUploadSize() {
2220  $phpMaxFileSize = wfShorthandToInteger(
2221  ini_get( 'upload_max_filesize' ),
2222  PHP_INT_MAX
2223  );
2224  $phpMaxPostSize = wfShorthandToInteger(
2225  ini_get( 'post_max_size' ),
2226  PHP_INT_MAX
2227  ) ?: PHP_INT_MAX;
2228  return min( $phpMaxFileSize, $phpMaxPostSize );
2229  }
2230 
2242  public static function getSessionStatus( UserIdentity $user, $statusKey ) {
2243  $store = self::getUploadSessionStore();
2244  $key = self::getUploadSessionKey( $store, $user, $statusKey );
2245 
2246  return $store->get( $key );
2247  }
2248 
2261  public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
2262  $store = self::getUploadSessionStore();
2263  $key = self::getUploadSessionKey( $store, $user, $statusKey );
2264 
2265  if ( $value === false ) {
2266  $store->delete( $key );
2267  } else {
2268  $store->set( $key, $value, $store::TTL_DAY );
2269  }
2270  }
2271 
2278  private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
2279  return $store->makeKey(
2280  'uploadstatus',
2281  $user->isRegistered() ? $user->getId() : md5( $user->getName() ),
2282  $statusKey
2283  );
2284  }
2285 
2289  private static function getUploadSessionStore() {
2290  return MediaWikiServices::getInstance()->getMainObjectStash();
2291  }
2292 }
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:528
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:286
Implements some public methods and some protected utility functions which are required by multiple ch...
Definition: File.php:68
getName()
Return the name of this file.
Definition: File.php:334
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:2085
Local file in the wiki's own database.
Definition: LocalFile.php:61
exists()
canRender inherited
Definition: LocalFile.php:1289
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
Definition: LocalFile.php:1527
load( $flags=0)
Load file metadata from cache or DB, unless already loaded.
Definition: LocalFile.php:721
MediaWiki exception.
Definition: MWException.php:32
MimeMagic helper wrapper.
Definition: MWFileProps.php:28
static getHandler( $type)
Get a MediaHandler for a given MIME type from the instance cache.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Definition: HookRunner.php:565
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
Represents a title within MediaWiki.
Definition: Title.php:82
getDBkey()
Get the main part with underscores.
Definition: Title.php:1118
static listParam(array $list, $type='text')
Definition: Message.php:1278
static sizeParam( $size)
Definition: Message.php:1245
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:1372
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: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
UploadBase and subclasses are the backend of MediaWiki's file uploads.
Definition: UploadBase.php:51
getSourceType()
Returns the upload type.
Definition: UploadBase.php:256
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
Definition: UploadBase.php:722
int $mTitleError
Definition: UploadBase.php:69
static setSessionStatus(UserIdentity $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling)
const EMPTY_FILE
Definition: UploadBase.php:109
UploadStashFile null $mStashFile
Definition: UploadBase.php:77
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:956
checkSvgScriptCallback( $element, $attribs, $data=null)
Title false null $mTitle
Definition: UploadBase.php:67
verifyPermissions(Authority $performer)
Alias for verifyTitlePermissions.
Definition: UploadBase.php:609
getLocalFile()
Return the local file and initializes if necessary.
const SUCCESS
Definition: UploadBase.php:107
bool null $mJavaDetected
Definition: UploadBase.php:85
string null $mFilteredName
Definition: UploadBase.php:71
getRealPath( $srcPath)
Definition: UploadBase.php:339
static createFromRequest(&$request, $type=null)
Create a form of UploadBase depending on wpSourceType and initializes it.
Definition: UploadBase.php:192
runUploadStashFileHook(User $user)
zipEntryCallback( $entry)
Callback for ZipDirectoryReader to detect Java class files.
Definition: UploadBase.php:580
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:239
const FILETYPE_MISSING
Definition: UploadBase.php:113
convertVerifyErrorToStatus( $error)
string null $mFinalExtension
Definition: UploadBase.php:73
verifyPartialFile()
A verification routine suitable for partial files.
Definition: UploadBase.php:537
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:472
array null $mFileProps
Definition: UploadBase.php:81
static isEnabled()
Returns true if uploads are enabled.
Definition: UploadBase.php:148
static isThumbName( $filename)
Helper function that checks whether the filename looks like a thumbnail.
getVerificationErrorCode( $error)
Definition: UploadBase.php:125
performUpload( $comment, $pageText, $watch, $user, $tags=[], ?string $watchlistExpiry=null)
Really perform the upload.
Definition: UploadBase.php:907
string null $mDesiredDestName
Definition: UploadBase.php:59
verifyTitlePermissions(Authority $performer)
Check whether the user can edit, upload and create the image.
Definition: UploadBase.php:624
static getFilenamePrefixBlacklist()
Get a list of blacklisted filename prefixes from [[MediaWiki:Filename-prefix-blacklist]].
const OVERWRITE_EXISTING_FILE
Definition: UploadBase.php:112
setTempFile( $tempPath, $fileSize=null)
Definition: UploadBase.php:288
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:116
string null $mDestName
Definition: UploadBase.php:61
const VERIFICATION_ERROR
Definition: UploadBase.php:115
string[] $mBlackListedExtensions
Definition: UploadBase.php:83
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:162
const WINDOWS_NONASCII_FILENAME
Definition: UploadBase.php:118
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:423
string null $mSourceType
Definition: UploadBase.php:65
int null $mFileSize
Definition: UploadBase.php:79
isEmptyFile()
Return true if the file is empty.
Definition: UploadBase.php:314
static checkFileExtension( $ext, $list)
Perform case-insensitive match against a list of file extensions.
const FILETYPE_BADTYPE
Definition: UploadBase.php:114
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:965
initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile=false)
Definition: UploadBase.php:267
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:63
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:331
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:306
checkWarnings( $user=null)
Check for non fatal problems with the file.
Definition: UploadBase.php:658
const FILE_TOO_LARGE
Definition: UploadBase.php:117
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
Definition: UploadBase.php:178
getFileSize()
Return the file size.
Definition: UploadBase.php:322
verifyUpload()
Verify whether the upload is sensible.
Definition: UploadBase.php:374
const ILLEGAL_FILENAME
Definition: UploadBase.php:111
const MIN_LENGTH_PARTNAME
Definition: UploadBase.php:110
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:55
TempFSFile null $tempFileObj
Wrapper to handle deleting the temp file.
Definition: UploadBase.php:57
LocalFile null $mLocalFile
Definition: UploadBase.php:75
static getExistsWarning( $file)
Helper function that does various existence checks for a file.
const FILENAME_TOO_LONG
Definition: UploadBase.php:119
static getMaxPhpUploadSize()
Get the PHP maximum uploaded file size, based on ini settings.
static $safeXmlEncodings
Definition: UploadBase.php:89
verifyMimeType( $mime)
Verify the MIME type.
Definition: UploadBase.php:453
initializeFromRequest(&$request)
Initialize from a WebRequest.
string false $mSVGNSError
Definition: UploadBase.php:87
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition: User.php:71
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