115 self::EMPTY_FILE =>
'empty-file',
116 self::FILE_TOO_LARGE =>
'file-too-large',
117 self::FILETYPE_MISSING =>
'filetype-missing',
118 self::FILETYPE_BADTYPE =>
'filetype-banned',
119 self::MIN_LENGTH_PARTNAME =>
'filename-tooshort',
120 self::ILLEGAL_FILENAME =>
'illegal-filename',
121 self::OVERWRITE_EXISTING_FILE =>
'overwrite',
122 self::VERIFICATION_ERROR =>
'verification-error',
123 self::HOOK_ABORTED =>
'hookaborted',
124 self::WINDOWS_NONASCII_FILENAME =>
'windows-nonascii-filename',
125 self::FILENAME_TOO_LONG =>
'filename-toolong',
127 return $code_to_status[$error] ??
'unknown-error';
142 # Check php's file_uploads setting
155 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
156 foreach ( [
'upload',
'edit' ] as $permission ) {
157 if ( !$permissionManager->userHasRight( $user, $permission ) ) {
172 return $user->pingLimiter(
'upload' );
186 $type =
$type ?: $request->getVal(
'wpSourceType',
'File' );
197 Hooks::run(
'UploadCreateFromRequest', [
$type, &$className ] );
198 if ( is_null( $className ) ) {
199 $className =
'UploadFrom' .
$type;
200 wfDebug( __METHOD__ .
": class name: $className\n" );
201 if ( !in_array(
$type, self::$uploadHandlers ) ) {
207 if ( !call_user_func( [ $className,
'isEnabled' ] ) ) {
212 if ( !call_user_func( [ $className,
'isValidRequest' ], $request ) ) {
217 $handler =
new $className;
219 $handler->initializeFromRequest( $request );
255 $this->mDesiredDestName = $name;
257 throw new MWException( __METHOD__ .
" given storage path `$tempPath`." );
261 $this->mRemoveTempFile = $removeTempFile;
276 $this->mTempPath = $tempPath;
277 $this->mFileSize = $fileSize ?:
null;
278 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
279 $this->tempFileObj =
new TempFSFile( $this->mTempPath );
281 $this->mFileSize = filesize( $this->mTempPath );
284 $this->tempFileObj =
null;
293 return Status::newGood();
301 return empty( $this->mFileSize );
325 $repo = RepoGroup::singleton()->getLocalRepo();
330 $tmpFile = $repo->getLocalCopy( $srcPath );
332 $tmpFile->bind( $this );
334 $path = $tmpFile ? $tmpFile->getPath() :
false;
358 if ( $this->mFileSize > $maxSize ) {
371 if ( $verification !==
true ) {
374 'details' => $verification
382 if ( $result !==
true ) {
397 if ( is_null( $nt ) ) {
399 if ( $this->mTitleError == self::ILLEGAL_FILENAME ) {
402 if ( $this->mTitleError == self::FILETYPE_BADTYPE ) {
404 if ( count( $this->mBlackListedExtensions ) ) {
428 wfDebug(
"mime: <$mime> extension: <{$this->mFinalExtension}>\n" );
431 return [
'filetype-badmime', $mime ];
435 # Check what Internet Explorer would detect
436 $fp = fopen( $this->mTempPath,
'rb' );
437 $chunk = fread( $fp, 256 );
440 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
441 $extMime = $magic->guessTypesForExtension( $this->mFinalExtension );
442 $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
443 foreach ( $ieTypes as $ieType ) {
445 return [
'filetype-bad-ie-mime', $ieType ];
463 if ( $status !==
true ) {
468 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
469 $mime = $this->mFileProps[
'mime'];
472 # XXX: Missing extension will be caught by validateName() via getTitle()
473 if ( $this->mFinalExtension !=
'' && !$this->
verifyExtension( $mime, $this->mFinalExtension ) ) {
478 # check for htmlish code and javascript
480 if ( $this->mFinalExtension ==
'svg' || $mime ==
'image/svg+xml' ) {
482 if ( $svgStatus !==
false ) {
490 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
491 if ( !$handlerStatus->isOK() ) {
492 $errors = $handlerStatus->getErrorsArray();
494 return reset( $errors );
499 Hooks::run(
'UploadVerifyFile', [ $this, $mime, &$error ] );
500 if ( $error !==
true ) {
501 if ( !is_array( $error ) ) {
507 wfDebug( __METHOD__ .
": all clear; passing.\n" );
523 # getTitle() sets some internal parameters like $this->mFinalExtension
527 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
529 # check MIME type, if desired
530 $mime = $this->mFileProps[
'file-mime'];
532 if ( $status !==
true ) {
536 # check for htmlish code and javascript
538 if ( self::detectScript( $this->mTempPath, $mime, $this->mFinalExtension ) ) {
539 return [
'uploadscripted' ];
541 if ( $this->mFinalExtension ==
'svg' || $mime ==
'image/svg+xml' ) {
543 if ( $svgStatus !==
false ) {
549 # Check for Java applets, which if uploaded can bypass cross-site
552 $this->mJavaDetected =
false;
554 [ $this,
'zipEntryCallback' ] );
555 if ( !$zipStatus->isOK() ) {
556 $errors = $zipStatus->getErrorsArray();
557 $error = reset( $errors );
558 if ( $error[0] !==
'zip-wrong-format' ) {
562 if ( $this->mJavaDetected ) {
563 return [
'uploadjava' ];
567 # Scan the uploaded file for viruses
570 return [
'uploadvirus', $virus ];
582 $names = [ $entry[
'name'] ];
589 $nullPos = strpos( $entry[
'name'],
"\000" );
590 if ( $nullPos !==
false ) {
591 $names[] = substr( $entry[
'name'], 0, $nullPos );
596 if ( preg_grep(
'!\.class/?$!', $names ) ) {
597 $this->mJavaDetected =
true;
631 if ( is_null( $nt ) ) {
634 $permErrors = $nt->getUserPermissionsErrors(
'edit', $user );
635 $permErrorsUpload = $nt->getUserPermissionsErrors(
'upload', $user );
636 if ( !$nt->exists() ) {
637 $permErrorsCreate = $nt->getUserPermissionsErrors(
'create', $user );
639 $permErrorsCreate = [];
641 if ( $permErrors || $permErrorsUpload || $permErrorsCreate ) {
642 $permErrors = array_merge( $permErrors,
wfArrayDiff2( $permErrorsUpload, $permErrors ) );
643 $permErrors = array_merge( $permErrors,
wfArrayDiff2( $permErrorsCreate, $permErrors ) );
649 if ( $overwriteError !==
true ) {
650 return [ $overwriteError ];
667 $localFile->load( File::READ_LATEST );
668 $filename = $localFile->getName();
671 $badFileName = $this->
checkBadFileName( $filename, $this->mDesiredDestName );
672 if ( $badFileName !==
null ) {
673 $warnings[
'badfilename'] = $badFileName;
677 if ( $unwantedFileExtensionDetails !==
null ) {
678 $warnings[
'filetype-unwanted-type'] = $unwantedFileExtensionDetails;
681 $fileSizeWarnings = $this->
checkFileSize( $this->mFileSize );
682 if ( $fileSizeWarnings ) {
683 $warnings = array_merge( $warnings, $fileSizeWarnings );
687 if ( $localFileExistsWarnings ) {
688 $warnings = array_merge( $warnings, $localFileExistsWarnings );
692 $warnings[
'was-deleted'] = $filename;
697 $ignoreLocalDupes = isset( $warnings[
'exists '] );
700 $warnings[
'duplicate'] = $dupes;
704 if ( $archivedDupes !==
null ) {
705 $warnings[
'duplicate-archive'] = $archivedDupes;
723 array_walk_recursive( $warnings,
function ( &$param, $key ) {
724 if ( $param instanceof
File ) {
726 'fileName' => $param->getName(),
727 'timestamp' => $param->getTimestamp()
729 } elseif ( is_object( $param ) ) {
730 throw new InvalidArgumentException(
731 'UploadBase::makeWarningsSerializable: ' .
732 'Unexpected object of class ' . get_class( $param ) );
748 $comparableName = str_replace(
' ',
'_', $desiredFileName );
749 $comparableName = Title::capitalize( $comparableName,
NS_FILE );
751 if ( $desiredFileName != $filename && $comparableName != $filename ) {
774 $wgLang->commaList( $extensions ),
797 if ( $fileSize == 0 ) {
798 $warnings[
'empty-file'] =
true;
814 if ( $exists !==
false ) {
815 $warnings[
'exists'] = $exists;
818 if ( $hash === $localFile->
getSha1() ) {
819 $warnings[
'no-change'] = $localFile;
824 foreach ( $history as $oldFile ) {
825 if ( $hash === $oldFile->getSha1() ) {
826 $warnings[
'duplicate-version'][] = $oldFile;
845 $dupes = RepoGroup::singleton()->findBySha1( $hash );
847 foreach ( $dupes as $key => $dupe ) {
851 $title->equals( $dupe->getTitle() )
853 unset( $dupes[$key] );
868 if ( $archivedFile->getID() > 0 ) {
869 if ( $archivedFile->userCan( File::DELETED_FILE ) ) {
870 return $archivedFile->getName();
892 public function performUpload( $comment, $pageText, $watch, $user, $tags = [] ) {
897 Hooks::run(
'UploadVerifyUpload', [ $this, $user, $props, $comment, $pageText, &$error ] );
899 if ( !is_array( $error ) ) {
902 return Status::newFatal( ...$error );
916 if ( $status->isGood() ) {
921 User::IGNORE_USER_RIGHTS
926 Hooks::run(
'UploadComplete', [ &$uploadBase ] );
949 if ( $this->mTitle !==
false ) {
952 if ( !is_string( $this->mDesiredDestName ) ) {
954 $this->mTitle =
null;
961 $title = Title::newFromText( $this->mDesiredDestName );
963 $this->mFilteredName =
$title->getDBkey();
968 # oi_archive_name is max 255 bytes, which include a timestamp and an
969 # exclamation mark, so restrict file name to 240 bytes.
970 if ( strlen( $this->mFilteredName ) > 240 ) {
972 $this->mTitle =
null;
984 $nt = Title::makeTitleSafe(
NS_FILE, $this->mFilteredName );
985 if ( is_null( $nt ) ) {
987 $this->mTitle =
null;
991 $this->mFilteredName = $nt->
getDBkey();
1000 $this->mFinalExtension = trim( end(
$ext ) );
1002 $this->mFinalExtension =
'';
1004 # No extension, try guessing one
1005 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
1006 $mime = $magic->guessMimeType( $this->mTempPath );
1007 if ( $mime !==
'unknown/unknown' ) {
1008 # Get a space separated list of extensions
1009 $extList = $magic->getExtensionsForType( $mime );
1011 # Set the extension to the canonical extension
1012 $this->mFinalExtension = strtok( $extList,
' ' );
1014 # Fix up the other variables
1015 $this->mFilteredName .=
".{$this->mFinalExtension}";
1016 $nt = Title::makeTitleSafe(
NS_FILE, $this->mFilteredName );
1028 if ( $this->mFinalExtension ==
'' ) {
1030 $this->mTitle =
null;
1033 } elseif ( $blackListedExtensions ||
1037 $this->mBlackListedExtensions = $blackListedExtensions;
1039 $this->mTitle =
null;
1045 if ( !preg_match(
'/^[\x0-\x7f]*$/', $nt->getText() )
1046 && !RepoGroup::singleton()->getLocalRepo()->backendSupportsUnicodePaths()
1049 $this->mTitle =
null;
1054 # If there was more than one "extension", reassemble the base
1055 # filename to prevent bogus complaints about length
1056 if ( count(
$ext ) > 1 ) {
1057 $iterations = count(
$ext ) - 1;
1058 for ( $i = 0; $i < $iterations; $i++ ) {
1059 $partname .=
'.' .
$ext[$i];
1063 if ( strlen( $partname ) < 1 ) {
1065 $this->mTitle =
null;
1070 $this->mTitle = $nt;
1081 if ( is_null( $this->mLocalFile ) ) {
1083 $this->mLocalFile = is_null( $nt ) ? null :
wfLocalFile( $nt );
1108 if ( !$isPartial ) {
1111 return Status::newFatal( ...$error );
1116 return Status::newGood(
$file );
1118 return Status::newFatal(
'uploadstash-exception', get_class( $e ), $e->getMessage() );
1129 Hooks::run(
'UploadStashFile', [ $this, $user, $props, &$error ] );
1130 if ( $error && !is_array( $error ) ) {
1131 $error = [ $error ];
1168 $stash = RepoGroup::singleton()->getLocalRepo()->getUploadStash( $user );
1170 $this->mStashFile =
$file;
1180 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1182 wfDebug( __METHOD__ .
": Marked temporary file '{$this->mTempPath}' for removal\n" );
1183 $this->tempFileObj->autocollect();
1201 $bits = explode(
'.', $filename );
1202 $basename = array_shift( $bits );
1204 return [ $basename, $bits ];
1216 return in_array( strtolower(
$ext ), $list );
1228 return array_intersect( array_map(
'strtolower',
$ext ), $list );
1239 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
1241 if ( !$mime || $mime ==
'unknown' || $mime ==
'unknown/unknown' ) {
1242 if ( !$magic->isRecognizableExtension( $extension ) ) {
1243 wfDebug( __METHOD__ .
": passing file with unknown detected mime type; " .
1244 "unrecognized extension '$extension', can't verify\n" );
1248 wfDebug( __METHOD__ .
": rejecting file with unknown detected mime type; " .
1249 "recognized extension '$extension', so probably invalid file\n" );
1255 $match = $magic->isMatchingExtension( $extension, $mime );
1257 if ( $match ===
null ) {
1258 if ( $magic->getTypesForExtension( $extension ) !==
null ) {
1259 wfDebug( __METHOD__ .
": No extension known for $mime, but we know a mime for $extension\n" );
1263 wfDebug( __METHOD__ .
": no file extension known for mime type $mime, passing file\n" );
1267 } elseif ( $match ===
true ) {
1268 wfDebug( __METHOD__ .
": mime type $mime matches extension $extension, passing file\n" );
1274 .
": mime type $mime mismatches file extension $extension, rejecting file\n" );
1292 # ugly hack: for text files, always look at the entire file.
1293 # For binary field, just check the first K.
1295 $isText = strpos( $mime,
'text/' ) === 0;
1297 $chunk = file_get_contents(
$file );
1299 $fp = fopen(
$file,
'rb' );
1300 $chunk = fread( $fp, 1024 );
1304 $chunk = strtolower( $chunk );
1310 # decode from UTF-16 if needed (could be used for obfuscation).
1311 if ( substr( $chunk, 0, 2 ) ==
"\xfe\xff" ) {
1313 } elseif ( substr( $chunk, 0, 2 ) ==
"\xff\xfe" ) {
1319 if ( $enc !==
null ) {
1320 $chunk = iconv( $enc,
"ASCII//IGNORE", $chunk );
1323 $chunk = trim( $chunk );
1326 wfDebug( __METHOD__ .
": checking for embedded scripts and HTML stuff\n" );
1328 # check for HTML doctype
1329 if ( preg_match(
"/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1335 if ( $extension ==
'svg' || strpos( $mime,
'image/svg' ) === 0 ) {
1336 if ( self::checkXMLEncodingMissmatch(
$file ) ) {
1350 '<html', # also in safari
1351 '<script', # also in safari
1354 foreach ( $tags as $tag ) {
1355 if ( strpos( $chunk, $tag ) !==
false ) {
1356 wfDebug( __METHOD__ .
": found something that may make it be mistaken for html: $tag\n" );
1366 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1367 $chunk = Sanitizer::decodeCharReferences( $chunk );
1369 # look for script-types
1370 if ( preg_match(
'!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
1371 wfDebug( __METHOD__ .
": found script types\n" );
1376 # look for html-style script-urls
1377 if ( preg_match(
'!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1378 wfDebug( __METHOD__ .
": found html-style script urls\n" );
1383 # look for css-style script-urls
1384 if ( preg_match(
'!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1385 wfDebug( __METHOD__ .
": found css-style script urls\n" );
1390 wfDebug( __METHOD__ .
": no scripts found\n" );
1405 $encodingRegex =
'!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1407 if ( preg_match(
"!<\?xml\b(.*?)\?>!si", $contents,
$matches ) ) {
1408 if ( preg_match( $encodingRegex,
$matches[1], $encMatch )
1409 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1411 wfDebug( __METHOD__ .
": Found unsafe XML encoding '{$encMatch[1]}'\n" );
1415 } elseif ( preg_match(
"!<\?xml\b!si", $contents ) ) {
1418 wfDebug( __METHOD__ .
": Unmatched XML declaration start\n" );
1421 } elseif ( substr( $contents, 0, 4 ) ==
"\x4C\x6F\xA7\x94" ) {
1423 wfDebug( __METHOD__ .
": EBCDIC Encoded XML\n" );
1430 $attemptEncodings = [
'UTF-16',
'UTF-16BE',
'UTF-32',
'UTF-32BE' ];
1431 foreach ( $attemptEncodings as $encoding ) {
1432 Wikimedia\suppressWarnings();
1433 $str = iconv( $encoding,
'UTF-8', $contents );
1434 Wikimedia\restoreWarnings();
1435 if ( $str !=
'' && preg_match(
"!<\?xml\b(.*?)\?>!si", $str,
$matches ) ) {
1436 if ( preg_match( $encodingRegex,
$matches[1], $encMatch )
1437 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1439 wfDebug( __METHOD__ .
": Found unsafe XML encoding '{$encMatch[1]}'\n" );
1443 } elseif ( $str !=
'' && preg_match(
"!<\?xml\b!si", $str ) ) {
1446 wfDebug( __METHOD__ .
": Unmatched XML declaration start\n" );
1461 $this->mSVGNSError =
false;
1464 [ $this,
'checkSvgScriptCallback' ],
1467 'processing_instruction_handler' =>
'UploadBase::checkSvgPICallback',
1468 'external_dtd_handler' =>
'UploadBase::checkSvgExternalDTD',
1471 if ( $check->wellFormed !==
true ) {
1474 return $partial ? false : [
'uploadinvalidxml' ];
1475 } elseif ( $check->filterMatch ) {
1476 if ( $this->mSVGNSError ) {
1480 return $check->filterMatchType;
1494 if ( preg_match(
'/xml-stylesheet/i', $target ) ) {
1495 return [
'upload-scripted-pi-callback' ];
1516 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1517 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1518 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1519 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1521 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1523 if (
$type !==
'PUBLIC'
1524 || !in_array( $systemId, $allowedDTDs )
1525 || strpos( $publicId,
"-//W3C//" ) !== 0
1527 return [
'upload-scripted-dtd' ];
1544 static $validNamespaces = [
1547 'http://creativecommons.org/ns#',
1548 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1549 'http://ns.adobe.com/adobeillustrator/10.0/',
1550 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1551 'http://ns.adobe.com/extensibility/1.0/',
1552 'http://ns.adobe.com/flows/1.0/',
1553 'http://ns.adobe.com/illustrator/1.0/',
1554 'http://ns.adobe.com/imagereplacement/1.0/',
1555 'http://ns.adobe.com/pdf/1.3/',
1556 'http://ns.adobe.com/photoshop/1.0/',
1557 'http://ns.adobe.com/saveforweb/1.0/',
1558 'http://ns.adobe.com/variables/1.0/',
1559 'http://ns.adobe.com/xap/1.0/',
1560 'http://ns.adobe.com/xap/1.0/g/',
1561 'http://ns.adobe.com/xap/1.0/g/img/',
1562 'http://ns.adobe.com/xap/1.0/mm/',
1563 'http://ns.adobe.com/xap/1.0/rights/',
1564 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1565 'http://ns.adobe.com/xap/1.0/stype/font#',
1566 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1567 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1568 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1569 'http://ns.adobe.com/xap/1.0/t/pg/',
1570 'http://purl.org/dc/elements/1.1/',
1571 'http://purl.org/dc/elements/1.1',
1572 'http://schemas.microsoft.com/visio/2003/svgextensions/',
1573 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1574 'http://taptrix.com/inkpad/svg_extensions',
1575 'http://web.resource.org/cc/',
1576 'http://www.freesoftware.fsf.org/bkchem/cdml',
1577 'http://www.inkscape.org/namespaces/inkscape',
1578 'http://www.opengis.net/gml',
1579 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1580 'http://www.w3.org/2000/svg',
1581 'http://www.w3.org/tr/rec-rdf-syntax/',
1582 'http://www.w3.org/2000/01/rdf-schema#',
1587 $isBuggyInkscape = preg_match(
'/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1589 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1590 wfDebug( __METHOD__ .
": Non-svg namespace '$namespace' in uploaded file.\n" );
1592 $this->mSVGNSError = $namespace;
1600 if ( $strippedElement ==
'script' ) {
1601 wfDebug( __METHOD__ .
": Found script element '$element' in uploaded file.\n" );
1603 return [
'uploaded-script-svg', $strippedElement ];
1606 # e.g., <svg xmlns="http://www.w3.org/2000/svg">
1607 # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1608 if ( $strippedElement ==
'handler' ) {
1609 wfDebug( __METHOD__ .
": Found scriptable element '$element' in uploaded file.\n" );
1611 return [
'uploaded-script-svg', $strippedElement ];
1614 # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1615 if ( $strippedElement ==
'stylesheet' ) {
1616 wfDebug( __METHOD__ .
": Found scriptable element '$element' in uploaded file.\n" );
1618 return [
'uploaded-script-svg', $strippedElement ];
1621 # Block iframes, in case they pass the namespace check
1622 if ( $strippedElement ==
'iframe' ) {
1623 wfDebug( __METHOD__ .
": iframe in uploaded file.\n" );
1625 return [
'uploaded-script-svg', $strippedElement ];
1629 if ( $strippedElement ==
'style'
1630 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1632 wfDebug( __METHOD__ .
": hostile css in style element.\n" );
1633 return [
'uploaded-hostile-svg' ];
1636 foreach ( $attribs as $attrib => $value ) {
1638 $value = strtolower( $value );
1640 if ( substr( $stripped, 0, 2 ) ==
'on' ) {
1642 .
": Found event-handler attribute '$attrib'='$value' in uploaded file.\n" );
1644 return [
'uploaded-event-handler-on-svg', $attrib, $value ];
1647 # Do not allow relative links, or unsafe url schemas.
1648 # For <a> tags, only data:, http: and https: and same-document
1649 # fragment links are allowed. For all other tags, only data:
1650 # and fragment are allowed.
1651 if ( $stripped ==
'href'
1653 && strpos( $value,
'data:' ) !== 0
1654 && strpos( $value,
'#' ) !== 0
1656 if ( !( $strippedElement ===
'a'
1657 && preg_match(
'!^https?://!i', $value ) )
1659 wfDebug( __METHOD__ .
": Found href attribute <$strippedElement "
1660 .
"'$attrib'='$value' in uploaded file.\n" );
1662 return [
'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1666 # only allow data: targets that should be safe. This prevents vectors like,
1667 # image/svg, text/xml, application/xml, and text/html, which can contain scripts
1668 if ( $stripped ==
'href' && strncasecmp(
'data:', $value, 5 ) === 0 ) {
1671 $parameters =
'(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1673 if ( !preg_match(
"!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1674 wfDebug( __METHOD__ .
": Found href to unwhitelisted data: uri "
1675 .
"\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
1676 return [
'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1680 # Change href with animate from (http:
1681 if ( $stripped ===
'attributename'
1682 && $strippedElement ===
'animate'
1685 wfDebug( __METHOD__ .
": Found animate that might be changing href using from "
1686 .
"\"<$strippedElement '$attrib'='$value'...\" in uploaded file.\n" );
1688 return [
'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1691 # use set/animate to add event-handler attribute to parent
1692 if ( ( $strippedElement ==
'set' || $strippedElement ==
'animate' )
1693 && $stripped ==
'attributename'
1694 && substr( $value, 0, 2 ) ==
'on'
1696 wfDebug( __METHOD__ .
": Found svg setting event-handler attribute with "
1697 .
"\"<$strippedElement $stripped='$value'...\" in uploaded file.\n" );
1699 return [
'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1702 # use set to add href attribute to parent element
1703 if ( $strippedElement ==
'set'
1704 && $stripped ==
'attributename'
1705 && strpos( $value,
'href' ) !==
false
1707 wfDebug( __METHOD__ .
": Found svg setting href attribute '$value' in uploaded file.\n" );
1709 return [
'uploaded-setting-href-svg' ];
1712 # use set to add a remote / data / script target to an element
1713 if ( $strippedElement ==
'set'
1714 && $stripped ==
'to'
1715 && preg_match(
'!(http|https|data|script):!sim', $value )
1717 wfDebug( __METHOD__ .
": Found svg setting attribute to '$value' in uploaded file.\n" );
1719 return [
'uploaded-wrong-setting-svg', $value ];
1722 # use handler attribute with remote / data / script
1723 if ( $stripped ==
'handler' && preg_match(
'!(http|https|data|script):!sim', $value ) ) {
1724 wfDebug( __METHOD__ .
": Found svg setting handler with remote/data/script "
1725 .
"'$attrib'='$value' in uploaded file.\n" );
1727 return [
'uploaded-setting-handler-svg', $attrib, $value ];
1730 # use CSS styles to bring in remote code
1731 if ( $stripped ==
'style'
1732 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
1734 wfDebug( __METHOD__ .
": Found svg setting a style with "
1735 .
"remote url '$attrib'='$value' in uploaded file.\n" );
1736 return [
'uploaded-remote-url-svg', $attrib, $value ];
1739 # Several attributes can include css, css character escaping isn't allowed
1740 $cssAttrs = [
'font',
'clip-path',
'fill',
'filter',
'marker',
1741 'marker-end',
'marker-mid',
'marker-start',
'mask',
'stroke' ];
1742 if ( in_array( $stripped, $cssAttrs )
1743 && self::checkCssFragment( $value )
1745 wfDebug( __METHOD__ .
": Found svg setting a style with "
1746 .
"remote url '$attrib'='$value' in uploaded file.\n" );
1747 return [
'uploaded-remote-url-svg', $attrib, $value ];
1750 # image filters can pull in url, which could be svg that executes scripts
1751 # Only allow url( "#foo" ). Do not allow url( http:
1752 if ( $strippedElement ==
'image'
1753 && $stripped ==
'filter'
1754 && preg_match(
'!url\s*\(\s*["\']?[^#]!sim', $value )
1756 wfDebug( __METHOD__ .
": Found image filter with url: "
1757 .
"\"<$strippedElement $stripped='$value'...\" in uploaded file.\n" );
1759 return [
'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1773 # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1774 if ( stripos( $value,
'@import' ) !==
false ) {
1778 # We allow @font-face to embed fonts with data: urls, so we snip the string
1779 # 'url' out so this case won't match when we check for urls below
1780 $pattern =
'!(@font-face\s*{[^}]*src:)url(\("data:;base64,)!im';
1781 $value = preg_replace( $pattern,
'$1$2', $value );
1783 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1784 # properties filter and accelerator don't seem to be useful for xss in SVG files.
1785 # Expression and -o-link don't seem to work either, but filtering them here in case.
1786 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1787 # but not local ones such as url("#..., url('#..., url(#....
1788 if ( preg_match(
'!expression
1790 | -o-link-source\s*:
1791 | -o-replace\s*:!imx', $value ) ) {
1795 if ( preg_match_all(
1796 "!(\s*(url|image|image-set)\s*\(\s*[\"']?\s*[^#]+.*?\))!sim",
1801 # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1802 foreach (
$matches[1] as $match ) {
1803 if ( !preg_match(
"!\s*(url|image|image-set)\s*\(\s*(#|'#|\"#)!im", $match ) ) {
1809 if ( preg_match(
'/[\000-\010\013\016-\037\177]/', $value ) ) {
1823 $parts = explode(
':', strtolower( $element ) );
1824 $name = array_pop( $parts );
1825 $ns = implode(
':', $parts );
1827 return [ $ns, $name ];
1836 $parts = explode(
':', strtolower( $name ) );
1838 return array_pop( $parts );
1855 wfDebug( __METHOD__ .
": virus scanner disabled\n" );
1861 wfDebug( __METHOD__ .
": unknown virus scanner: $wgAntivirus\n" );
1862 $wgOut->wrapWikiMsg(
"<div class=\"error\">\n$1\n</div>",
1865 return wfMessage(
'virus-unknownscanner' )->text() .
" $wgAntivirus";
1868 # look up scanner configuration
1873 if ( strpos(
$command,
"%f" ) ===
false ) {
1874 # simple pattern: append file to scan
1877 # complex pattern: replace "%f" with file to scan
1881 wfDebug( __METHOD__ .
": running virus scan: $command \n" );
1883 # execute virus scanner
1886 # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1887 # that does not seem to be worth the pain.
1888 # Ask me (Duesentrieb) about it if it's ever needed.
1891 # map exit code to AV_xxx constants.
1892 $mappedCode = $exitCode;
1893 if ( $exitCodeMap ) {
1894 if ( isset( $exitCodeMap[$exitCode] ) ) {
1895 $mappedCode = $exitCodeMap[$exitCode];
1896 } elseif ( isset( $exitCodeMap[
"*"] ) ) {
1897 $mappedCode = $exitCodeMap[
"*"];
1905 # scan failed (code was mapped to false by $exitCodeMap)
1906 wfDebug( __METHOD__ .
": failed to scan $file (code $exitCode).\n" );
1909 ?
wfMessage(
'virus-scanfailed', [ $exitCode ] )->text()
1912 # scan failed because filetype is unknown (probably imune)
1913 wfDebug( __METHOD__ .
": unsupported file type $file (code $exitCode).\n" );
1917 wfDebug( __METHOD__ .
": file passed virus scan.\n" );
1920 $output = trim( $output );
1923 $output =
true; #
if there
's no output, return true
1924 } elseif ( $msgPattern ) {
1926 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1927 $output = $groups[1];
1931 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output \n" );
1945 private function checkOverwrite( $user ) {
1946 // First check whether the local file can be overwritten
1947 $file = $this->getLocalFile();
1948 $file->load( File::READ_LATEST );
1949 if ( $file->exists() ) {
1950 if ( !self::userCanReUpload( $user, $file ) ) {
1951 return [ 'fileexists-forbidden
', $file->getName() ];
1957 /* Check shared conflicts: if the local file does not exist, but
1958 * wfFindFile finds a file, it exists in a shared repository.
1960 $file = wfFindFile( $this->getTitle(), [ 'latest
' => true ] );
1961 if ( $file && !MediaWikiServices::getInstance()
1962 ->getPermissionManager()
1963 ->userHasRight( $user, 'reupload-shared
' )
1965 return [ 'fileexists-shared-forbidden
', $file->getName() ];
1978 public static function userCanReUpload( User $user, File $img ) {
1979 $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
1980 if ( $permissionManager->userHasRight( $user, 'reupload
' ) ) {
1981 return true; // non-conditional
1982 } elseif ( !$permissionManager->userHasRight( $user, 'reupload-own
' ) ) {
1986 if ( !( $img instanceof LocalFile ) ) {
1992 return $user->getId() == $img->getUser( 'id' );
2006 public static function getExistsWarning( $file ) {
2007 if ( $file->exists() ) {
2008 return [ 'warning
' => 'exists
', 'file
' => $file ];
2011 if ( $file->getTitle()->getArticleID() ) {
2012 return [ 'warning
' => 'page-exists
', 'file
' => $file ];
2015 if ( strpos( $file->getName(), '.
' ) == false ) {
2016 $partname = $file->getName();
2019 $n = strrpos( $file->getName(), '.
' );
2020 $extension = substr( $file->getName(), $n + 1 );
2021 $partname = substr( $file->getName(), 0, $n );
2023 $normalizedExtension = File::normalizeExtension( $extension );
2025 if ( $normalizedExtension != $extension ) {
2026 // We're not
using the normalized form of the extension.
2031 $nt_lc = Title::makeTitle(
NS_FILE,
"{$partname}.{$normalizedExtension}" );
2034 if ( $file_lc->exists() ) {
2036 'warning' =>
'exists-normalized',
2038 'normalizedFile' => $file_lc
2045 "{$partname}.", 1 );
2046 if ( count( $similarFiles ) ) {
2048 'warning' =>
'exists-normalized',
2050 'normalizedFile' => $similarFiles[0],
2054 if ( self::isThumbName(
$file->getName() ) ) {
2055 # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2057 substr( $partname, strpos( $partname,
'-' ) + 1 ) .
'.' . $extension,
2061 if ( $file_thb->exists() ) {
2063 'warning' =>
'thumb',
2065 'thumbFile' => $file_thb
2070 'warning' =>
'thumb-name',
2072 'thumbFile' => $file_thb
2077 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2078 if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
2080 'warning' =>
'bad-prefix',
2096 $n = strrpos( $filename,
'.' );
2097 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2100 substr( $partname, 3, 3 ) ==
'px-' ||
2101 substr( $partname, 2, 3 ) ==
'px-'
2103 preg_match(
"/[0-9]{2}/", substr( $partname, 0, 2 ) );
2113 $message =
wfMessage(
'filename-prefix-blacklist' )->inContentLanguage();
2114 if ( !$message->isDisabled() ) {
2115 $lines = explode(
"\n", $message->plain() );
2118 $comment = substr( trim(
$line ), 0, 1 );
2119 if ( $comment ==
'#' || $comment ==
'' ) {
2123 $comment = strpos(
$line,
'#' );
2124 if ( $comment > 0 ) {
2127 $blacklist[] = trim(
$line );
2166 $code = $error[
'status'];
2167 unset( $code[
'status'] );
2202 ini_get(
'upload_max_filesize' ) ?: ini_get(
'hhvm.server.upload.upload_max_file_size' ),
2206 ini_get(
'post_max_size' ) ?: ini_get(
'hhvm.server.max_post_size' ),
2209 return min( $phpMaxFileSize, $phpMaxPostSize );
2225 return $store->get( $key );
2244 if ( $value ===
false ) {
2245 $store->delete( $key );
2247 $store->set( $key, $value, $store::TTL_DAY );
2269 return ObjectCache::getInstance(
'db-replicated' );
$wgAntivirus
Internal name of virus scanner.
$wgFileExtensions
This is the list of preferred extensions for uploading files.
$wgCheckFileExtensions
This is a flag to determine whether or not to check file extensions on upload.
$wgAntivirusRequired
Determines if a failed virus scan (AV_SCAN_FAILED) will cause the file to be rejected.
$wgUploadSizeWarning
Warn if uploaded files are larger than this (in bytes), or false to disable.
$wgDisableUploadScriptChecks
Setting this to true will disable the upload system's checks for HTML/JavaScript.
$wgVerifyMimeType
Determines if the MIME type of uploaded files should be checked.
$wgAntivirusSetup
Configuration for different virus scanners.
$wgVerifyMimeTypeIE
Determines whether extra checks for IE type detection should be applied.
$wgFileBlacklist
Files with these extensions will never be allowed as uploads.
$wgEnableUploads
Allow users to upload files.
$wgAllowJavaUploads
Allow Java archive uploads.
$wgStrictFileExtensions
If this is turned off, users may override the warning for files not covered by $wgFileExtensions.
$wgMimeTypeBlacklist
Files with these MIME types will never be allowed as uploads if $wgVerifyMimeType is enabled.
$wgMaxUploadSize
Max size for uploads, in bytes.
$wgSVGMetadataCutoff
Don't read SVG metadata beyond this point.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfIniGetBool( $setting)
Safety wrapper around ini_get() for boolean settings.
wfLocalFile( $title)
Get an object referring to a locally registered file.
wfArrayDiff2( $a, $b)
Like array_diff( $a, $b ) except that it works with two-dimensional arrays.
wfShellExecWithStderr( $cmd, &$retval=null, $environ=[], $limits=[])
Execute a shell command, returning both stdout and stderr.
wfStripIllegalFilenameChars( $name)
Replace all invalid characters with '-'.
wfShorthandToInteger( $string='', $default=-1)
Converts shorthand byte notation to integer form.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
wfIsHHVM()
Check if we are running under HHVM.
static getPropertyNames( $filter=[])
Returns all possible parameters to iiprop.
static getInfo( $file, $prop, $result, $thumbParams=null, $opts=false)
Get result information for an image revision.
static getPropertyNames( $filter=null)
Returns all possible parameters to siiprop.
Class representing a row of the 'filearchive' table.
Class representing a cache/ephemeral data store.
makeKey( $class,... $components)
Make a cache key, scoped to this instance's keyspace.
static getSha1Base36FromPath( $path)
Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case encoding,...
static isStoragePath( $path)
Check if a given path is a "mwstore://" path.
static isVirtualUrl( $url)
Determine if a string is an mwrepo:// URL.
Implements some public methods and some protected utility functions which are required by multiple ch...
wasDeleted()
Was this file ever deleted from the wiki?
Class to represent a local file in the wiki's own database.
exists()
canRender inherited
getHistory( $limit=null, $start=null, $end=null, $inc=true)
purgeDescription inherited
MimeMagic helper wrapper.
This class is used to hold the location and do limited manipulation of files stored temporarily (this...
Represents a title within MediaWiki.
getDBkey()
Get the main part with underscores.
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
UploadBase and subclasses are the backend of MediaWiki's file uploads.
getSourceType()
Returns the upload type.
checkOverwrite( $user)
Check if there's an overwrite conflict and, if so, if restrictions forbid this user from performing t...
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
string null $mRemoveTempFile
static verifyExtension( $mime, $extension)
Checks if the MIME type of the uploaded file matches the file extension.
postProcessUpload()
Perform extra steps after a successful upload.
verifyTitlePermissions( $user)
Check whether the user can edit, upload and create the image.
checkSvgScriptCallback( $element, $attribs, $data=null)
checkLocalFileExists(LocalFile $localFile, $hash)
getLocalFile()
Return the local file and initializes if necessary.
stripXmlNamespace( $name)
string $mTempPath
Local file system path to the file to upload (or a local copy)
string null $mFilteredName
checkBadFileName( $filename, $desiredFileName)
Check whether the resulting filename is different from the desired one, but ignore things like ucfirs...
UploadStashFile $mStashFile
static createFromRequest(&$request, $type=null)
Create a form of UploadBase depending on wpSourceType and initializes it.
verifyPermissions( $user)
Alias for verifyTitlePermissions.
runUploadStashFileHook(User $user)
static getSessionStatus(User $user, $statusKey)
Get the current status of a chunked upload (used for polling)
zipEntryCallback( $entry)
Callback for ZipDirectoryReader to detect Java class files.
static checkSvgPICallback( $target, $data)
Callback to filter SVG Processing Instructions.
static isValidRequest( $request)
Check whether a request if valid for this handler.
convertVerifyErrorToStatus( $error)
string null $mFinalExtension
verifyPartialFile()
A verification routine suitable for partial files.
static detectScript( $file, $mime, $extension)
Heuristic for detecting files that could contain JavaScript instructions or things that may look like...
verifyFile()
Verifies that it's ok to include the uploaded file.
static isEnabled()
Returns true if uploads are enabled.
static isThumbName( $filename)
Helper function that checks whether the filename looks like a thumbnail.
static getUploadSessionStore()
getVerificationErrorCode( $error)
string null $mDesiredDestName
static checkCssFragment( $value)
Check a block of CSS or CSS fragment for anything that looks like it is bringing in remote code.
static isAllowed(UserIdentity $user)
Returns true if the user can use this upload module or else a string identifying the missing permissi...
static getFilenamePrefixBlacklist()
Get a list of blacklisted filename prefixes from [[MediaWiki:Filename-prefix-blacklist]].
checkAgainstArchiveDupes( $hash)
const OVERWRITE_EXISTING_FILE
setTempFile( $tempPath, $fileSize=null)
static checkXMLEncodingMissmatch( $file)
Check a whitelist of xml encodings that are known not to be interpreted differently by the server's x...
doStashFile(User $user=null)
Implementation for stashFile() and tryStashFile().
string[] $mBlackListedExtensions
const WINDOWS_NONASCII_FILENAME
cleanupTempFile()
If we've modified the upload file we need to manually remove it on exit to clean up.
validateName()
Verify that the name is valid and, if necessary, that we can overwrite.
checkFileSize( $fileSize)
isEmptyFile()
Return true if the file is empty.
static checkFileExtension( $ext, $list)
Perform case-insensitive match against a list of file extensions.
tryStashFile(User $user, $isPartial=false)
Like stashFile(), but respects extensions' wishes to prevent the stashing.
getTitle()
Returns the title of the file to be uploaded.
initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile=false)
Initialize the path information.
static getMaxUploadSize( $forType=null)
Get the MediaWiki maximum uploaded file size for given type of upload, based on $wgMaxUploadSize.
static checkSvgExternalDTD( $type, $publicId, $systemId)
Verify that DTD urls referenced are only the standard dtds.
getTempFileSha1Base36()
Get the base 36 SHA1 of the file.
static splitXmlNamespace( $element)
Divide the element name passed by the xml parser to the callback into URI and prifix.
getImageInfo( $result)
Gets image info about the file just uploaded.
detectScriptInSvg( $filename, $partial)
static splitExtensions( $filename)
Split a file into a base name and all dot-delimited 'extensions' on the end.
static getUploadSessionKey(BagOStuff $store, User $user, $statusKey)
fetchFile()
Fetch the file.
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
checkLocalFileWasDeleted(LocalFile $localFile)
performUpload( $comment, $pageText, $watch, $user, $tags=[])
Really perform the upload.
getFileSize()
Return the file size.
verifyUpload()
Verify whether the upload is sane.
stashFile(User $user=null)
If the user does not supply all necessary information in the first upload form submission (either by ...
const MIN_LENGTH_PARTNAME
static checkFileExtensionList( $ext, $list)
Perform case-insensitive match against a list of file extensions.
checkWarnings()
Check for non fatal problems with the file.
static detectVirus( $file)
Generic wrapper function for a virus scanner program.
checkUnwantedFileExtensions( $fileExtension)
TempFSFile null $tempFileObj
Wrapper to handle deleting the temp file.
static getExistsWarning( $file)
Helper function that does various existence checks for a file.
static getMaxPhpUploadSize()
Get the PHP maximum uploaded file size, based on ini settings.
verifyMimeType( $mime)
Verify the MIME type.
static setSessionStatus(User $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling)
initializeFromRequest(&$request)
Initialize from a WebRequest.
checkAgainstExistingDupes( $hash, $ignoreLocalDupes)
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
getName()
Get the user name, or the IP of an anonymous user.
getId()
Get the user's ID.
static doWatch(Title $title, User $user, $checkRights=User::CHECK_USER_RIGHTS)
Watch a page.
static read( $fileName, $callback, $options=[])
Read a ZIP file and call a function for each file discovered in it.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
if(!is_readable( $file)) $ext