24use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
48 use ProtectedHookAccessorTrait;
65 protected $mTitleError = 0;
108 public const FILETYPE_MISSING = 8;
109 public const FILETYPE_BADTYPE = 9;
110 public const VERIFICATION_ERROR = 10;
111 public const HOOK_ABORTED = 11;
112 public const FILE_TOO_LARGE = 12;
113 public const WINDOWS_NONASCII_FILENAME = 13;
114 public const FILENAME_TOO_LONG = 14;
120 public function getVerificationErrorCode( $error ) {
122 self::EMPTY_FILE =>
'empty-file',
123 self::FILE_TOO_LARGE =>
'file-too-large',
124 self::FILETYPE_MISSING =>
'filetype-missing',
125 self::FILETYPE_BADTYPE =>
'filetype-banned',
126 self::MIN_LENGTH_PARTNAME =>
'filename-tooshort',
127 self::ILLEGAL_FILENAME =>
'illegal-filename',
128 self::OVERWRITE_EXISTING_FILE =>
'overwrite',
129 self::VERIFICATION_ERROR =>
'verification-error',
130 self::HOOK_ABORTED =>
'hookaborted',
131 self::WINDOWS_NONASCII_FILENAME =>
'windows-nonascii-filename',
132 self::FILENAME_TOO_LONG =>
'filename-toolong',
134 return $code_to_status[$error] ??
'unknown-error';
158 foreach ( [
'upload',
'edit' ] as $permission ) {
159 if ( !$performer->
isAllowed( $permission ) ) {
174 return $user->pingLimiter(
'upload' );
178 private static $uploadHandlers = [
'Stash',
'File',
'Url' ];
188 $type =
$type ?: $request->getVal(
'wpSourceType',
'File' );
200 Hooks::runner()->onUploadCreateFromRequest(
$type, $className );
201 if ( $className ===
null ) {
202 $className =
'UploadFrom' .
$type;
203 wfDebug( __METHOD__ .
": class name: $className" );
204 if ( !in_array(
$type, self::$uploadHandlers ) ) {
210 if ( !$className::isEnabled() ) {
215 if ( !$className::isValidRequest( $request ) ) {
220 $handler =
new $className;
222 $handler->initializeFromRequest( $request );
261 $this->mDesiredDestName = $name;
263 throw new MWException( __METHOD__ .
" given storage path `$tempPath`." );
267 $this->mRemoveTempFile = $removeTempFile;
282 $this->mTempPath = $tempPath ??
'';
283 $this->mFileSize = $fileSize ?:
null;
284 if ( strlen( $this->mTempPath ) && file_exists( $this->mTempPath ) ) {
285 $this->tempFileObj =
new TempFSFile( $this->mTempPath );
287 $this->mFileSize = filesize( $this->mTempPath );
290 $this->tempFileObj =
null;
300 return Status::newGood();
308 return empty( $this->mFileSize );
316 return $this->mFileSize;
333 $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
338 $tmpFile = $repo->getLocalCopy( $srcPath );
340 $tmpFile->bind( $this );
342 $path = $tmpFile ? $tmpFile->getPath() :
false;
372 return [
'status' => self::EMPTY_FILE ];
379 if ( $this->mFileSize > $maxSize ) {
381 'status' => self::FILE_TOO_LARGE,
392 if ( $verification !==
true ) {
394 'status' => self::VERIFICATION_ERROR,
395 'details' => $verification
403 if ( $result !==
true ) {
407 return [
'status' => self::OK ];
418 if ( $nt ===
null ) {
419 $result = [
'status' => $this->mTitleError ];
420 if ( $this->mTitleError == self::ILLEGAL_FILENAME ) {
421 $result[
'filtered'] = $this->mFilteredName;
423 if ( $this->mTitleError == self::FILETYPE_BADTYPE ) {
424 $result[
'finalExt'] = $this->mFinalExtension;
425 if ( count( $this->mBlackListedExtensions ) ) {
426 $result[
'blacklistedExt'] = $this->mBlackListedExtensions;
449 wfDebug(
"mime: <$mime> extension: <{$this->mFinalExtension}>" );
452 return [
'filetype-badmime',
$mime ];
456 # Check what Internet Explorer would detect
457 $fp = fopen( $this->mTempPath,
'rb' );
459 $chunk = fread( $fp, 256 );
462 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
463 $extMime = $magic->getMimeTypeFromExtensionOrNull( (
string)$this->mFinalExtension ) ??
'';
464 $ieTypes = $magic->getIEMimeTypes( $this->mTempPath, $chunk, $extMime );
465 foreach ( $ieTypes as $ieType ) {
467 return [
'filetype-bad-ie-mime', $ieType ];
486 if ( $status !==
true ) {
491 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
492 $mime = $this->mFileProps[
'mime'];
495 # XXX: Missing extension will be caught by validateName() via getTitle()
496 if ( (
string)$this->mFinalExtension !==
'' &&
499 return [
'filetype-mime-mismatch', $this->mFinalExtension,
$mime ];
503 # check for htmlish code and javascript
505 if ( $this->mFinalExtension ==
'svg' ||
$mime ==
'image/svg+xml' ) {
507 if ( $svgStatus !==
false ) {
515 $handlerStatus = $handler->verifyUpload( $this->mTempPath );
516 if ( !$handlerStatus->isOK() ) {
517 $errors = $handlerStatus->getErrorsArray();
519 return reset( $errors );
524 $this->getHookRunner()->onUploadVerifyFile( $this,
$mime, $error );
525 if ( $error !==
true ) {
526 if ( !is_array( $error ) ) {
532 wfDebug( __METHOD__ .
": all clear; passing." );
548 # getTitle() sets some internal parameters like $this->mFinalExtension
552 $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
554 # check MIME type, if desired
555 $mime = $this->mFileProps[
'file-mime'];
557 if ( $status !==
true ) {
561 # check for htmlish code and javascript
563 if ( self::detectScript( $this->mTempPath,
$mime, $this->mFinalExtension ) ) {
564 return [
'uploadscripted' ];
566 if ( $this->mFinalExtension ==
'svg' ||
$mime ==
'image/svg+xml' ) {
568 if ( $svgStatus !==
false ) {
574 # Check for Java applets, which if uploaded can bypass cross-site
577 $this->mJavaDetected =
false;
579 [ $this,
'zipEntryCallback' ] );
580 if ( !$zipStatus->isOK() ) {
581 $errors = $zipStatus->getErrorsArray();
582 $error = reset( $errors );
583 if ( $error[0] !==
'zip-wrong-format' ) {
587 if ( $this->mJavaDetected ) {
588 return [
'uploadjava' ];
592 # Scan the uploaded file for viruses
595 return [
'uploadvirus', $virus ];
607 $names = [ $entry[
'name'] ];
614 $nullPos = strpos( $entry[
'name'],
"\000" );
615 if ( $nullPos !==
false ) {
616 $names[] = substr( $entry[
'name'], 0, $nullPos );
621 if ( preg_grep(
'!\.class/?$!', $names ) ) {
622 $this->mJavaDetected =
true;
656 if ( $nt ===
null ) {
660 $status = PermissionStatus::newEmpty();
663 if ( !$status->isGood() ) {
664 return $status->toLegacyErrorArray();
668 if ( $overwriteError !==
true ) {
669 return [ $overwriteError ];
685 if ( $user ===
null ) {
687 $user = RequestContext::getMain()->getUser();
693 $localFile->load( File::READ_LATEST );
694 $filename = $localFile->getName();
697 $badFileName = $this->
checkBadFileName( $filename, $this->mDesiredDestName );
698 if ( $badFileName !==
null ) {
699 $warnings[
'badfilename'] = $badFileName;
703 if ( $unwantedFileExtensionDetails !==
null ) {
704 $warnings[
'filetype-unwanted-type'] = $unwantedFileExtensionDetails;
707 $fileSizeWarnings = $this->
checkFileSize( $this->mFileSize );
708 if ( $fileSizeWarnings ) {
709 $warnings = array_merge( $warnings, $fileSizeWarnings );
713 if ( $localFileExistsWarnings ) {
714 $warnings = array_merge( $warnings, $localFileExistsWarnings );
718 $warnings[
'was-deleted'] = $filename;
723 $ignoreLocalDupes = isset( $warnings[
'exists'] );
726 $warnings[
'duplicate'] = $dupes;
730 if ( $archivedDupes !==
null ) {
731 $warnings[
'duplicate-archive'] = $archivedDupes;
749 array_walk_recursive( $warnings,
static function ( &$param, $key ) {
750 if ( $param instanceof
File ) {
752 'fileName' => $param->getName(),
753 'timestamp' => $param->getTimestamp()
755 } elseif ( is_object( $param ) ) {
756 throw new InvalidArgumentException(
757 'UploadBase::makeWarningsSerializable: ' .
758 'Unexpected object of class ' . get_class( $param ) );
774 $comparableName = str_replace(
' ',
'_', $desiredFileName );
775 $comparableName = Title::capitalize( $comparableName,
NS_FILE );
777 if ( $desiredFileName != $filename && $comparableName != $filename ) {
800 $wgLang->commaList( $extensions ),
820 $warnings[
'large-file'] = [
826 if ( $fileSize == 0 ) {
827 $warnings[
'empty-file'] =
true;
842 $exists = self::getExistsWarning( $localFile );
843 if ( $exists !==
false ) {
844 $warnings[
'exists'] = $exists;
847 if ( $hash !==
false && $hash === $localFile->
getSha1() ) {
848 $warnings[
'no-change'] = $localFile;
853 foreach ( $history as $oldFile ) {
854 if ( $hash === $oldFile->getSha1() ) {
855 $warnings[
'duplicate-version'][] = $oldFile;
874 if ( $hash ===
false ) {
877 $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
879 foreach ( $dupes as $key => $dupe ) {
883 $title->equals( $dupe->getTitle() )
885 unset( $dupes[$key] );
900 if ( $hash ===
false ) {
904 if ( $archivedFile->getID() > 0 ) {
905 if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
906 return $archivedFile->getName();
933 $comment, $pageText, $watch, $user, $tags = [], ?
string $watchlistExpiry =
null
936 $props = $this->mFileProps;
939 $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
941 if ( !is_array( $error ) ) {
944 return Status::newFatal( ...$error );
958 if ( $status->isGood() ) {
960 MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
966 $this->getHookRunner()->onUploadComplete( $this );
990 if ( $this->mTitle !==
false ) {
993 if ( !is_string( $this->mDesiredDestName ) ) {
994 $this->mTitleError = self::ILLEGAL_FILENAME;
995 $this->mTitle =
null;
1002 $title = Title::newFromText( $this->mDesiredDestName );
1004 $this->mFilteredName =
$title->getDBkey();
1006 $this->mFilteredName = $this->mDesiredDestName;
1009 # oi_archive_name is max 255 bytes, which include a timestamp and an
1010 # exclamation mark, so restrict file name to 240 bytes.
1011 if ( strlen( $this->mFilteredName ) > 240 ) {
1012 $this->mTitleError = self::FILENAME_TOO_LONG;
1013 $this->mTitle =
null;
1025 $nt = Title::makeTitleSafe(
NS_FILE, $this->mFilteredName );
1026 if ( $nt ===
null ) {
1027 $this->mTitleError = self::ILLEGAL_FILENAME;
1028 $this->mTitle =
null;
1032 $this->mFilteredName = $nt->
getDBkey();
1040 if (
$ext !== [] ) {
1041 $this->mFinalExtension = trim( end(
$ext ) );
1043 $this->mFinalExtension =
'';
1048 if ( $this->mTempPath !==
null ) {
1049 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
1050 $mime = $magic->guessMimeType( $this->mTempPath );
1051 if (
$mime !==
'unknown/unknown' ) {
1052 # Get a space separated list of extensions
1053 $mimeExt = $magic->getExtensionFromMimeTypeOrNull(
$mime );
1054 if ( $mimeExt !==
null ) {
1055 # Set the extension to the canonical extension
1056 $this->mFinalExtension = $mimeExt;
1058 # Fix up the other variables
1059 $this->mFilteredName .=
".{$this->mFinalExtension}";
1060 $nt = Title::makeTitleSafe(
NS_FILE, $this->mFilteredName );
1061 $ext = [ $this->mFinalExtension ];
1073 if ( $this->mFinalExtension ==
'' ) {
1074 $this->mTitleError = self::FILETYPE_MISSING;
1075 $this->mTitle =
null;
1078 } elseif ( $blackListedExtensions ||
1082 $this->mBlackListedExtensions = $blackListedExtensions;
1083 $this->mTitleError = self::FILETYPE_BADTYPE;
1084 $this->mTitle =
null;
1090 if ( !preg_match(
'/^[\x0-\x7f]*$/', $nt->getText() )
1091 && !MediaWikiServices::getInstance()->getRepoGroup()
1092 ->getLocalRepo()->backendSupportsUnicodePaths()
1094 $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1095 $this->mTitle =
null;
1100 # If there was more than one "extension", reassemble the base
1101 # filename to prevent bogus complaints about length
1102 if ( count(
$ext ) > 1 ) {
1103 $iterations = count(
$ext ) - 1;
1104 for ( $i = 0; $i < $iterations; $i++ ) {
1105 $partname .=
'.' .
$ext[$i];
1109 if ( strlen( $partname ) < 1 ) {
1110 $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1111 $this->mTitle =
null;
1116 $this->mTitle = $nt;
1128 if ( $this->mLocalFile ===
null ) {
1130 $this->mLocalFile = $nt ===
null
1132 : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1135 return $this->mLocalFile;
1142 return $this->mStashFile;
1158 if ( !$isPartial ) {
1161 return Status::newFatal( ...$error );
1166 return Status::newGood(
$file );
1168 return Status::newFatal(
'uploadstash-exception', get_class( $e ), $e->getMessage() );
1177 $props = $this->mFileProps;
1179 $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1180 if ( $error && !is_array( $error ) ) {
1181 $error = [ $error ];
1194 $stash = MediaWikiServices::getInstance()->getRepoGroup()
1195 ->getLocalRepo()->getUploadStash( $user );
1197 $this->mStashFile =
$file;
1207 if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1209 wfDebug( __METHOD__ .
": Marked temporary file '{$this->mTempPath}' for removal" );
1210 $this->tempFileObj->autocollect();
1215 return $this->mTempPath;
1228 $bits = explode(
'.', $filename );
1229 $basename = array_shift( $bits );
1231 return [ $basename, $bits ];
1243 return in_array( strtolower(
$ext ), $list );
1255 return array_intersect( array_map(
'strtolower',
$ext ), $list );
1266 $magic = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer();
1269 if ( !$magic->isRecognizableExtension( $extension ) ) {
1270 wfDebug( __METHOD__ .
": passing file with unknown detected mime type; " .
1271 "unrecognized extension '$extension', can't verify" );
1275 wfDebug( __METHOD__ .
": rejecting file with unknown detected mime type; " .
1276 "recognized extension '$extension', so probably invalid file" );
1282 $match = $magic->isMatchingExtension( $extension,
$mime );
1284 if ( $match ===
null ) {
1285 if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
1286 wfDebug( __METHOD__ .
": No extension known for $mime, but we know a mime for $extension" );
1290 wfDebug( __METHOD__ .
": no file extension known for mime type $mime, passing file" );
1294 } elseif ( $match ) {
1295 wfDebug( __METHOD__ .
": mime type $mime matches extension $extension, passing file" );
1301 .
": mime type $mime mismatches file extension $extension, rejecting file" );
1319 # ugly hack: for text files, always look at the entire file.
1320 # For binary field, just check the first K.
1322 $isText = strpos(
$mime,
'text/' ) === 0;
1324 $chunk = file_get_contents(
$file );
1326 $fp = fopen(
$file,
'rb' );
1330 $chunk = fread( $fp, 1024 );
1334 $chunk = strtolower( $chunk );
1340 # decode from UTF-16 if needed (could be used for obfuscation).
1341 if ( substr( $chunk, 0, 2 ) ==
"\xfe\xff" ) {
1343 } elseif ( substr( $chunk, 0, 2 ) ==
"\xff\xfe" ) {
1349 if ( $enc !==
null ) {
1350 $chunk = iconv( $enc,
"ASCII//IGNORE", $chunk );
1353 $chunk = trim( $chunk );
1356 wfDebug( __METHOD__ .
": checking for embedded scripts and HTML stuff" );
1358 # check for HTML doctype
1359 if ( preg_match(
"/<!DOCTYPE *X?HTML/i", $chunk ) ) {
1365 if ( $extension ==
'svg' || strpos(
$mime,
'image/svg' ) === 0 ) {
1366 if ( self::checkXMLEncodingMissmatch(
$file ) ) {
1380 '<html', # also in safari
1381 '<script', # also in safari
1384 foreach ( $tags as $tag ) {
1385 if ( strpos( $chunk, $tag ) !==
false ) {
1386 wfDebug( __METHOD__ .
": found something that may make it be mistaken for html: $tag" );
1396 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
1397 $chunk = Sanitizer::decodeCharReferences( $chunk );
1399 # look for script-types
1400 if ( preg_match(
'!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!sim', $chunk ) ) {
1401 wfDebug( __METHOD__ .
": found script types" );
1406 # look for html-style script-urls
1407 if ( preg_match(
'!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1408 wfDebug( __METHOD__ .
": found html-style script urls" );
1413 # look for css-style script-urls
1414 if ( preg_match(
'!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!sim', $chunk ) ) {
1415 wfDebug( __METHOD__ .
": found css-style script urls" );
1420 wfDebug( __METHOD__ .
": no scripts found" );
1435 $encodingRegex =
'!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
1437 if ( preg_match(
"!<\?xml\b(.*?)\?>!si", $contents,
$matches ) ) {
1438 if ( preg_match( $encodingRegex,
$matches[1], $encMatch )
1439 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1441 wfDebug( __METHOD__ .
": Found unsafe XML encoding '{$encMatch[1]}'" );
1445 } elseif ( preg_match(
"!<\?xml\b!si", $contents ) ) {
1448 wfDebug( __METHOD__ .
": Unmatched XML declaration start" );
1451 } elseif ( substr( $contents, 0, 4 ) ==
"\x4C\x6F\xA7\x94" ) {
1453 wfDebug( __METHOD__ .
": EBCDIC Encoded XML" );
1460 $attemptEncodings = [
'UTF-16',
'UTF-16BE',
'UTF-32',
'UTF-32BE' ];
1461 foreach ( $attemptEncodings as $encoding ) {
1462 Wikimedia\suppressWarnings();
1463 $str = iconv( $encoding,
'UTF-8', $contents );
1464 Wikimedia\restoreWarnings();
1465 if ( $str !=
'' && preg_match(
"!<\?xml\b(.*?)\?>!si", $str,
$matches ) ) {
1466 if ( preg_match( $encodingRegex,
$matches[1], $encMatch )
1467 && !in_array( strtoupper( $encMatch[1] ), self::$safeXmlEncodings )
1469 wfDebug( __METHOD__ .
": Found unsafe XML encoding '{$encMatch[1]}'" );
1473 } elseif ( $str !=
'' && preg_match(
"!<\?xml\b!si", $str ) ) {
1476 wfDebug( __METHOD__ .
": Unmatched XML declaration start" );
1491 $this->mSVGNSError =
false;
1494 [ $this,
'checkSvgScriptCallback' ],
1497 'processing_instruction_handler' => [ __CLASS__,
'checkSvgPICallback' ],
1498 'external_dtd_handler' => [ __CLASS__,
'checkSvgExternalDTD' ],
1501 if ( $check->wellFormed !==
true ) {
1504 return $partial ? false : [
'uploadinvalidxml' ];
1505 } elseif ( $check->filterMatch ) {
1506 if ( $this->mSVGNSError ) {
1507 return [
'uploadscriptednamespace', $this->mSVGNSError ];
1510 return $check->filterMatchType;
1524 if ( preg_match(
'/xml-stylesheet/i', $target ) ) {
1525 return [
'upload-scripted-pi-callback' ];
1546 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
1547 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
1548 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
1549 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
1551 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
1553 if (
$type !==
'PUBLIC'
1554 || !in_array( $systemId, $allowedDTDs )
1555 || strpos( $publicId,
"-//W3C//" ) !== 0
1557 return [
'upload-scripted-dtd' ];
1574 static $validNamespaces = [
1577 'http://creativecommons.org/ns#',
1578 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
1579 'http://ns.adobe.com/adobeillustrator/10.0/',
1580 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
1581 'http://ns.adobe.com/extensibility/1.0/',
1582 'http://ns.adobe.com/flows/1.0/',
1583 'http://ns.adobe.com/illustrator/1.0/',
1584 'http://ns.adobe.com/imagereplacement/1.0/',
1585 'http://ns.adobe.com/pdf/1.3/',
1586 'http://ns.adobe.com/photoshop/1.0/',
1587 'http://ns.adobe.com/saveforweb/1.0/',
1588 'http://ns.adobe.com/variables/1.0/',
1589 'http://ns.adobe.com/xap/1.0/',
1590 'http://ns.adobe.com/xap/1.0/g/',
1591 'http://ns.adobe.com/xap/1.0/g/img/',
1592 'http://ns.adobe.com/xap/1.0/mm/',
1593 'http://ns.adobe.com/xap/1.0/rights/',
1594 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
1595 'http://ns.adobe.com/xap/1.0/stype/font#',
1596 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
1597 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
1598 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
1599 'http://ns.adobe.com/xap/1.0/t/pg/',
1600 'http://purl.org/dc/elements/1.1/',
1601 'http://purl.org/dc/elements/1.1',
1602 'http://schemas.microsoft.com/visio/2003/svgextensions/',
1603 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
1604 'http://taptrix.com/inkpad/svg_extensions',
1605 'http://web.resource.org/cc/',
1606 'http://www.freesoftware.fsf.org/bkchem/cdml',
1607 'http://www.inkscape.org/namespaces/inkscape',
1608 'http://www.opengis.net/gml',
1609 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
1610 'http://www.w3.org/2000/svg',
1611 'http://www.w3.org/tr/rec-rdf-syntax/',
1612 'http://www.w3.org/2000/01/rdf-schema#',
1617 $isBuggyInkscape = preg_match(
'/^&(#38;)*ns_[a-z_]+;$/', $namespace );
1619 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
1620 wfDebug( __METHOD__ .
": Non-svg namespace '$namespace' in uploaded file." );
1622 $this->mSVGNSError = $namespace;
1630 if ( $strippedElement ==
'script' ) {
1631 wfDebug( __METHOD__ .
": Found script element '$element' in uploaded file." );
1633 return [
'uploaded-script-svg', $strippedElement ];
1636 # e.g., <svg xmlns="http://www.w3.org/2000/svg">
1637 # <handler xmlns:ev="http://www.w3.org/2001/xml-events" ev:event="load">alert(1)</handler> </svg>
1638 if ( $strippedElement ==
'handler' ) {
1639 wfDebug( __METHOD__ .
": Found scriptable element '$element' in uploaded file." );
1641 return [
'uploaded-script-svg', $strippedElement ];
1644 # SVG reported in Feb '12 that used xml:stylesheet to generate javascript block
1645 if ( $strippedElement ==
'stylesheet' ) {
1646 wfDebug( __METHOD__ .
": Found scriptable element '$element' in uploaded file." );
1648 return [
'uploaded-script-svg', $strippedElement ];
1651 # Block iframes, in case they pass the namespace check
1652 if ( $strippedElement ==
'iframe' ) {
1653 wfDebug( __METHOD__ .
": iframe in uploaded file." );
1655 return [
'uploaded-script-svg', $strippedElement ];
1659 if ( $strippedElement ==
'style'
1660 && self::checkCssFragment( Sanitizer::normalizeCss( $data ) )
1662 wfDebug( __METHOD__ .
": hostile css in style element." );
1663 return [
'uploaded-hostile-svg' ];
1666 foreach ( $attribs as $attrib => $value ) {
1668 $value = strtolower( $value );
1670 if ( substr( $stripped, 0, 2 ) ==
'on' ) {
1672 .
": Found event-handler attribute '$attrib'='$value' in uploaded file." );
1674 return [
'uploaded-event-handler-on-svg', $attrib, $value ];
1677 # Do not allow relative links, or unsafe url schemas.
1678 # For <a> tags, only data:, http: and https: and same-document
1679 # fragment links are allowed. For all other tags, only data:
1680 # and fragment are allowed.
1681 if ( $stripped ==
'href'
1683 && strpos( $value,
'data:' ) !== 0
1684 && strpos( $value,
'#' ) !== 0
1686 if ( !( $strippedElement ===
'a'
1687 && preg_match(
'!^https?://!i', $value ) )
1689 wfDebug( __METHOD__ .
": Found href attribute <$strippedElement "
1690 .
"'$attrib'='$value' in uploaded file." );
1692 return [
'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
1696 # only allow data: targets that should be safe. This prevents vectors like,
1697 # image/svg, text/xml, application/xml, and text/html, which can contain scripts
1698 if ( $stripped ==
'href' && strncasecmp(
'data:', $value, 5 ) === 0 ) {
1701 $parameters =
'(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
1703 if ( !preg_match(
"!^data:\s*image/(gif|jpeg|jpg|png)$parameters,!i", $value ) ) {
1704 wfDebug( __METHOD__ .
": Found href to unwhitelisted data: uri "
1705 .
"\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1706 return [
'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
1710 # Change href with animate from (http:
1711 if ( $stripped ===
'attributename'
1712 && $strippedElement ===
'animate'
1715 wfDebug( __METHOD__ .
": Found animate that might be changing href using from "
1716 .
"\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
1718 return [
'uploaded-animate-svg', $strippedElement, $attrib, $value ];
1721 # use set/animate to add event-handler attribute to parent
1722 if ( ( $strippedElement ==
'set' || $strippedElement ==
'animate' )
1723 && $stripped ==
'attributename'
1724 && substr( $value, 0, 2 ) ==
'on'
1726 wfDebug( __METHOD__ .
": Found svg setting event-handler attribute with "
1727 .
"\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1729 return [
'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
1732 # use set to add href attribute to parent element
1733 if ( $strippedElement ==
'set'
1734 && $stripped ==
'attributename'
1735 && strpos( $value,
'href' ) !==
false
1737 wfDebug( __METHOD__ .
": Found svg setting href attribute '$value' in uploaded file." );
1739 return [
'uploaded-setting-href-svg' ];
1742 # use set to add a remote / data / script target to an element
1743 if ( $strippedElement ==
'set'
1744 && $stripped ==
'to'
1745 && preg_match(
'!(http|https|data|script):!sim', $value )
1747 wfDebug( __METHOD__ .
": Found svg setting attribute to '$value' in uploaded file." );
1749 return [
'uploaded-wrong-setting-svg', $value ];
1752 # use handler attribute with remote / data / script
1753 if ( $stripped ==
'handler' && preg_match(
'!(http|https|data|script):!sim', $value ) ) {
1754 wfDebug( __METHOD__ .
": Found svg setting handler with remote/data/script "
1755 .
"'$attrib'='$value' in uploaded file." );
1757 return [
'uploaded-setting-handler-svg', $attrib, $value ];
1760 # use CSS styles to bring in remote code
1761 if ( $stripped ==
'style'
1762 && self::checkCssFragment( Sanitizer::normalizeCss( $value ) )
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 ];
1769 # Several attributes can include css, css character escaping isn't allowed
1770 $cssAttrs = [
'font',
'clip-path',
'fill',
'filter',
'marker',
1771 'marker-end',
'marker-mid',
'marker-start',
'mask',
'stroke' ];
1772 if ( in_array( $stripped, $cssAttrs )
1773 && self::checkCssFragment( $value )
1775 wfDebug( __METHOD__ .
": Found svg setting a style with "
1776 .
"remote url '$attrib'='$value' in uploaded file." );
1777 return [
'uploaded-remote-url-svg', $attrib, $value ];
1780 # image filters can pull in url, which could be svg that executes scripts
1781 # Only allow url( "#foo" ). Do not allow url( http:
1782 if ( $strippedElement ==
'image'
1783 && $stripped ==
'filter'
1784 && preg_match(
'!url\s*\(\s*["\']?[^#]!sim', $value )
1786 wfDebug( __METHOD__ .
": Found image filter with url: "
1787 .
"\"<$strippedElement $stripped='$value'...\" in uploaded file." );
1789 return [
'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
1803 # Forbid external stylesheets, for both reliability and to protect viewer's privacy
1804 if ( stripos( $value,
'@import' ) !==
false ) {
1808 # We allow @font-face to embed fonts with data: urls, so we snip the string
1809 # 'url' out so this case won't match when we check for urls below
1810 $pattern =
'!(@font-face\s*{[^}]*src:)url(\("data:;base64,)!im';
1811 $value = preg_replace( $pattern,
'$1$2', $value );
1813 # Check for remote and executable CSS. Unlike in Sanitizer::checkCss, the CSS
1814 # properties filter and accelerator don't seem to be useful for xss in SVG files.
1815 # Expression and -o-link don't seem to work either, but filtering them here in case.
1816 # Additionally, we catch remote urls like url("http:..., url('http:..., url(http:...,
1817 # but not local ones such as url("#..., url('#..., url(#....
1818 if ( preg_match(
'!expression
1820 | -o-link-source\s*:
1821 | -o-replace\s*:!imx', $value ) ) {
1825 if ( preg_match_all(
1826 "!(\s*(url|image|image-set)\s*\(\s*[\"']?\s*[^#]+.*?\))!sim",
1831 # TODO: redo this in one regex. Until then, url("#whatever") matches the first
1832 foreach (
$matches[1] as $match ) {
1833 if ( !preg_match(
"!\s*(url|image|image-set)\s*\(\s*(#|'#|\"#)!im", $match ) ) {
1839 if ( preg_match(
'/[\000-\010\013\016-\037\177]/', $value ) ) {
1853 $parts = explode(
':', strtolower( $element ) );
1854 $name = array_pop( $parts );
1855 $ns = implode(
':', $parts );
1857 return [ $ns, $name ];
1866 $parts = explode(
':', strtolower( $name ) );
1868 return array_pop( $parts );
1885 wfDebug( __METHOD__ .
": virus scanner disabled" );
1891 wfDebug( __METHOD__ .
": unknown virus scanner: $wgAntivirus" );
1892 $wgOut->wrapWikiMsg(
"<div class=\"error\">\n$1\n</div>",
1895 return wfMessage(
'virus-unknownscanner' )->text() .
" $wgAntivirus";
1898 # look up scanner configuration
1903 if ( strpos(
$command,
"%f" ) ===
false ) {
1904 # simple pattern: append file to scan
1907 # complex pattern: replace "%f" with file to scan
1911 wfDebug( __METHOD__ .
": running virus scan: $command " );
1913 # execute virus scanner
1916 # NOTE: there's a 50 line workaround to make stderr redirection work on windows, too.
1917 # that does not seem to be worth the pain.
1918 # Ask me (Duesentrieb) about it if it's ever needed.
1921 # map exit code to AV_xxx constants.
1922 $mappedCode = $exitCode;
1923 if ( $exitCodeMap ) {
1924 if ( isset( $exitCodeMap[$exitCode] ) ) {
1925 $mappedCode = $exitCodeMap[$exitCode];
1926 } elseif ( isset( $exitCodeMap[
"*"] ) ) {
1927 $mappedCode = $exitCodeMap[
"*"];
1935 # scan failed (code was mapped to false by $exitCodeMap)
1936 wfDebug( __METHOD__ .
": failed to scan $file (code $exitCode)." );
1939 ?
wfMessage(
'virus-scanfailed', [ $exitCode ] )->text()
1942 # scan failed because filetype is unknown (probably imune)
1943 wfDebug( __METHOD__ .
": unsupported file type $file (code $exitCode)." );
1947 wfDebug( __METHOD__ .
": file passed virus scan." );
1950 $output = trim( $output );
1953 $output =
true; #
if there
's no output, return true
1954 } elseif ( $msgPattern ) {
1956 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
1957 $output = $groups[1];
1961 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );
1975 private function checkOverwrite( Authority $performer ) {
1976 // First check whether the local file can be overwritten
1977 $file = $this->getLocalFile();
1978 $file->load( File::READ_LATEST );
1979 if ( $file->exists() ) {
1980 if ( !self::userCanReUpload( $performer, $file ) ) {
1981 return [ 'fileexists-forbidden
', $file->getName() ];
1987 $services = MediaWikiServices::getInstance();
1989 /* Check shared conflicts: if the local file does not exist, but
1990 * RepoGroup::findFile finds a file, it exists in a shared repository.
1992 $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest
' => true ] );
1993 if ( $file && !$performer->isAllowed( 'reupload-shared
' )
1995 return [ 'fileexists-shared-forbidden
', $file->getName() ];
2008 public static function userCanReUpload( Authority $performer, File $img ) {
2009 if ( $performer->isAllowed( 'reupload
' ) ) {
2010 return true; // non-conditional
2011 } elseif ( !$performer->isAllowed( 'reupload-own
' ) ) {
2015 if ( !( $img instanceof LocalFile ) ) {
2019 return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
2033 public static function getExistsWarning( $file ) {
2034 if ( $file->exists() ) {
2035 return [ 'warning
' => 'exists
', 'file
' => $file ];
2038 if ( $file->getTitle()->getArticleID() ) {
2039 return [ 'warning
' => 'page-exists
', 'file
' => $file ];
2042 if ( !strpos( $file->getName(), '.
' ) ) {
2043 $partname = $file->getName();
2046 $n = strrpos( $file->getName(), '.
' );
2047 $extension = substr( $file->getName(), $n + 1 );
2048 $partname = substr( $file->getName(), 0, $n );
2050 $normalizedExtension = File::normalizeExtension( $extension );
2051 $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
2053 if ( $normalizedExtension != $extension ) {
2054 // We're not
using the normalized form of the extension.
2059 $nt_lc = Title::makeTitle(
NS_FILE,
"{$partname}.{$normalizedExtension}" );
2060 $file_lc = $localRepo->newFile( $nt_lc );
2062 if ( $file_lc->exists() ) {
2064 'warning' =>
'exists-normalized',
2066 'normalizedFile' => $file_lc
2072 $similarFiles = $localRepo->findFilesByPrefix(
"{$partname}.", 1 );
2073 if ( count( $similarFiles ) ) {
2075 'warning' =>
'exists-normalized',
2077 'normalizedFile' => $similarFiles[0],
2081 if ( self::isThumbName(
$file->getName() ) ) {
2082 # Check for filenames like 50px- or 180px-, these are mostly thumbnails
2084 substr( $partname, strpos( $partname,
'-' ) + 1 ) .
'.' . $extension,
2087 $file_thb = $localRepo->newFile( $nt_thb );
2088 if ( $file_thb->exists() ) {
2090 'warning' =>
'thumb',
2092 'thumbFile' => $file_thb
2097 'warning' =>
'thumb-name',
2099 'thumbFile' => $file_thb
2104 foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
2105 if ( substr( $partname, 0, strlen( $prefix ) ) == $prefix ) {
2107 'warning' =>
'bad-prefix',
2123 $n = strrpos( $filename,
'.' );
2124 $partname = $n ? substr( $filename, 0, $n ) : $filename;
2127 substr( $partname, 3, 3 ) ==
'px-' ||
2128 substr( $partname, 2, 3 ) ==
'px-'
2130 preg_match(
"/[0-9]{2}/", substr( $partname, 0, 2 ) );
2140 $message =
wfMessage(
'filename-prefix-blacklist' )->inContentLanguage();
2141 if ( !$message->isDisabled() ) {
2142 $lines = explode(
"\n", $message->plain() );
2145 $comment = substr( trim(
$line ), 0, 1 );
2146 if ( $comment ==
'#' || $comment ==
'' ) {
2150 $comment = strpos(
$line,
'#' );
2151 if ( $comment > 0 ) {
2154 $list[] = trim(
$line );
2193 $code = $error[
'status'];
2194 unset( $code[
'status'] );
2229 ini_get(
'upload_max_filesize' ),
2233 ini_get(
'post_max_size' ),
2236 return min( $phpMaxFileSize, $phpMaxPostSize );
2249 $store = self::getUploadSessionStore();
2250 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2252 return $store->get( $key );
2268 $store = self::getUploadSessionStore();
2269 $key = self::getUploadSessionKey( $store, $user, $statusKey );
2271 if ( $value ===
false ) {
2272 $store->delete( $key );
2274 $store->set( $key, $value, $store::TTL_DAY );
2296 return ObjectCache::getInstance(
'db-replicated' );
$wgProhibitedFileExtensions
Files with these extensions will never be allowed as uploads.
$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.
$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.
$wgMimeTypeExclusions
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.
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.
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( $collection,... $components)
Make a cache key for the global keyspace and given components.
static getSha1Base36FromPath( $path)
Get a SHA-1 hash of a file in the local filesystem, in base-36 lower case encoding,...
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.
static string[] $uploadHandlers
Upload handlers.
getSourceType()
Returns the upload type.
static makeWarningsSerializable( $warnings)
Convert the warnings array returned by checkWarnings() to something that can be serialized.
static setSessionStatus(UserIdentity $user, $statusKey, $value)
Set the current status of a chunked upload (used for polling)
UploadStashFile null $mStashFile
static verifyExtension( $mime, $extension)
Checks if the MIME type of the uploaded file matches the file extension.
postProcessUpload()
Perform extra steps after a successful upload.
checkSvgScriptCallback( $element, $attribs, $data=null)
verifyPermissions(Authority $performer)
Alias for verifyTitlePermissions.
checkLocalFileExists(LocalFile $localFile, $hash)
getLocalFile()
Return the local file and initializes if necessary.
stripXmlNamespace( $name)
string null $mFilteredName
checkBadFileName( $filename, $desiredFileName)
Check whether the resulting filename is different from the desired one, but ignore things like ucfirs...
static createFromRequest(&$request, $type=null)
Create a form of UploadBase depending on wpSourceType and initializes it.
runUploadStashFileHook(User $user)
zipEntryCallback( $entry)
Callback for ZipDirectoryReader to detect Java class files.
static checkSvgPICallback( $target, $data)
Callback to filter SVG Processing Instructions.
static isValidRequest( $request)
Check whether a request if valid for this handler.
convertVerifyErrorToStatus( $error)
string null $mFinalExtension
verifyPartialFile()
A verification routine suitable for partial files.
static detectScript( $file, $mime, $extension)
Heuristic for detecting files that could contain JavaScript instructions or things that may look like...
verifyFile()
Verifies that it's ok to include the uploaded file.
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)
performUpload( $comment, $pageText, $watch, $user, $tags=[], ?string $watchlistExpiry=null)
Really perform the upload.
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.
verifyTitlePermissions(Authority $performer)
Check whether the user can edit, upload and create the image.
static getFilenamePrefixBlacklist()
Get a list of blacklisted filename prefixes from [[MediaWiki:Filename-prefix-blacklist]].
const OVERWRITE_EXISTING_FILE
setTempFile( $tempPath, $fileSize=null)
static getSessionStatus(UserIdentity $user, $statusKey)
Get the current status of a chunked upload (used for polling)
static checkXMLEncodingMissmatch( $file)
Check 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
static isAllowed(Authority $performer)
Returns true if the user can use this upload module or else a string identifying the missing permissi...
cleanupTempFile()
If we've modified the upload file 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.
checkAgainstArchiveDupes( $hash, Authority $performer)
getTitle()
Returns the title of the file to be uploaded.
initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile=false)
static getMaxUploadSize( $forType=null)
Get MediaWiki's maximum uploaded file size for given type of upload, based on $wgMaxUploadSize.
bool null $mRemoveTempFile
static checkSvgExternalDTD( $type, $publicId, $systemId)
Verify that DTD urls referenced are only the standard dtds.
getTempFileSha1Base36()
Get the base 36 SHA1 of the file.
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.
fetchFile()
Fetch the file.
checkWarnings( $user=null)
Check for non fatal problems with the file.
static getUploadSessionKey(BagOStuff $store, UserIdentity $user, $statusKey)
static isThrottled( $user)
Returns true if the user has surpassed the upload rate limit, false otherwise.
checkLocalFileWasDeleted(LocalFile $localFile)
getFileSize()
Return the file size.
verifyUpload()
Verify whether the upload is sane.
checkOverwrite(Authority $performer)
Check if there's an overwrite conflict and, if so, if restrictions forbid this user from performing t...
const MIN_LENGTH_PARTNAME
static checkFileExtensionList( $ext, $list)
Perform case-insensitive match against a list of file extensions.
static detectVirus( $file)
Generic wrapper function for a virus scanner program.
checkUnwantedFileExtensions( $fileExtension)
string null $mTempPath
Local file system path to the file to upload (or a local copy)
TempFSFile null $tempFileObj
Wrapper to handle deleting the temp file.
LocalFile null $mLocalFile
static getMaxPhpUploadSize()
Get the PHP maximum uploaded file size, based on ini settings.
verifyMimeType( $mime)
Verify the MIME type.
initializeFromRequest(&$request)
Initialize from a WebRequest.
checkAgainstExistingDupes( $hash, $ignoreLocalDupes)
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
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
if(!file_exists( $CREDITS)) $lines