35 private const SAFE_XML_ENCODINGS = [
63 private MimeAnalyzer $mimeAnalyzer;
72 MimeAnalyzer $mimeAnalyzer,
76 $this->config = $config;
77 $this->mimeAnalyzer = $mimeAnalyzer;
89 private function verifyMimeType( $mime ) {
91 if ( $verifyMimeType ) {
93 $mimeTypeExclusions = $this->config
96 return [
'filetype-badmime', $mime ];
119 $config = $this->config;
123 if ( $status !==
true ) {
127 $mime = $fileProps[
'mime'];
129 if ( $verifyMimeType ) {
130 # XXX: Missing extension will be caught by validateName() via getTitle()
134 return [
'filetype-mime-mismatch', $ext, $mime ];
138 # check for htmlish code and javascript
139 if ( !$disableUploadScriptChecks ) {
140 if ( $ext ===
'svg' || $mime ===
'image/svg+xml' ) {
141 $svgStatus = $this->detectScriptInSvg(
$path,
false );
142 if ( $svgStatus !==
false ) {
148 $handler = $mime !==
null ? MediaHandler::getHandler( $mime ) :
null;
150 $handlerStatus = $handler->verifyUpload(
$path );
151 if ( !$handlerStatus->isOK() ) {
152 $errors = $handlerStatus->getErrorsArray();
154 return reset( $errors );
162 wfDebug( __METHOD__ .
": all clear; passing." );
183 $config = $this->config;
186 # check MIME type, if desired
187 $mime = $fileProps[
'file-mime'];
188 $status = $this->verifyMimeType( $mime );
189 if ( $status !==
true ) {
193 # check for htmlish code and javascript
194 if ( !$disableUploadScriptChecks ) {
196 return [
'uploadscripted' ];
198 if ( $ext ===
'svg' || $mime ===
'image/svg+xml' ) {
199 $svgStatus = $this->detectScriptInSvg(
$path,
true );
200 if ( $svgStatus !==
false ) {
206 # Scan the uploaded file for viruses
209 return [
'uploadvirus', $virus ];
224 $magic = $this->mimeAnalyzer;
226 if ( !$mime || $mime ===
'unknown' || $mime ===
'unknown/unknown' ) {
227 if ( !$magic->isRecognizableExtension( $extension ) ) {
228 wfDebug( __METHOD__ .
": passing file with unknown detected mime type; " .
229 "unrecognized extension '$extension', can't verify" );
234 wfDebug( __METHOD__ .
": rejecting file with unknown detected mime type; " .
235 "recognized extension '$extension', so probably invalid file" );
239 $match = $magic->isMatchingExtension( $extension, $mime );
241 if ( $match ===
null ) {
242 if ( $magic->getMimeTypesFromExtension( $extension ) !== [] ) {
243 wfDebug( __METHOD__ .
": No extension known for $mime, but we know a mime for $extension" );
248 wfDebug( __METHOD__ .
": no file extension known for mime type $mime, passing file" );
253 wfDebug( __METHOD__ .
": mime type $mime matches extension $extension, passing file" );
260 .
": mime type $mime mismatches file extension $extension, rejecting file" );
281 # ugly hack: for text files, always look at the entire file.
282 # For binary field, just check the first K.
284 if ( str_starts_with( $mime ??
'',
'text/' ) ) {
285 $chunk = file_get_contents( $file );
287 $fp = fopen( $file,
'rb' );
291 $chunk = fread( $fp, 1024 );
295 $chunk = strtolower( $chunk );
301 # decode from UTF-16 if needed (could be used for obfuscation).
302 if ( str_starts_with( $chunk,
"\xfe\xff" ) ) {
304 } elseif ( str_starts_with( $chunk,
"\xff\xfe" ) ) {
310 if ( $enc !==
null ) {
312 $chunk = @iconv( $enc,
"ASCII//IGNORE", $chunk );
315 $chunk = trim( $chunk );
318 wfDebug( __METHOD__ .
": checking for embedded scripts and HTML stuff" );
320 # check for HTML doctype
321 if ( preg_match(
"/<!DOCTYPE *X?HTML/i", $chunk ) ) {
327 if ( $extension ===
'svg' || str_starts_with( $mime ??
'',
'image/svg' ) ) {
328 if ( $this->checkXMLEncodingMismatch( $file ) ) {
342 '<html', # also in safari
343 '<script', # also in safari
346 foreach ( $tags as $tag ) {
347 if ( str_contains( $chunk, $tag ) ) {
348 wfDebug( __METHOD__ .
": found something that may make it be mistaken for html: $tag" );
358 # resolve entity-refs to look at attributes. may be harsh on big files... cache result?
359 $chunk = Sanitizer::decodeCharReferences( $chunk );
361 # look for script-types
362 if ( preg_match(
'!type\s*=\s*[\'"]?\s*(?:\w*/)?(?:ecma|java)!im', $chunk ) ) {
363 wfDebug( __METHOD__ .
": found script types" );
368 # look for html-style script-urls
369 if ( preg_match(
'!(?:href|src|data)\s*=\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) {
370 wfDebug( __METHOD__ .
": found html-style script urls" );
375 # look for css-style script-urls
376 if ( preg_match(
'!url\s*\(\s*[\'"]?\s*(?:ecma|java)script:!im', $chunk ) ) {
377 wfDebug( __METHOD__ .
": found css-style script urls" );
382 wfDebug( __METHOD__ .
": no scripts found" );
394 private function checkXMLEncodingMismatch( $file ) {
399 $contents = file_get_contents( $file,
false,
null, 0, 4096 );
400 $encodingRegex =
'!encoding[ \t\n\r]*=[ \t\n\r]*[\'"](.*?)[\'"]!si';
402 if ( preg_match(
"!<\?xml\b(.*?)\?>!si", $contents,
$matches ) ) {
403 if ( preg_match( $encodingRegex,
$matches[1], $encMatch )
404 && !in_array( strtoupper( $encMatch[1] ), self::SAFE_XML_ENCODINGS )
406 wfDebug( __METHOD__ .
": Found unsafe XML encoding '{$encMatch[1]}'" );
410 } elseif ( preg_match(
"!<\?xml\b!i", $contents ) ) {
413 wfDebug( __METHOD__ .
": Unmatched XML declaration start" );
416 } elseif ( str_starts_with( $contents,
"\x4C\x6F\xA7\x94" ) ) {
418 wfDebug( __METHOD__ .
": EBCDIC Encoded XML" );
425 $attemptEncodings = [
'UTF-16',
'UTF-16BE',
'UTF-32',
'UTF-32BE' ];
426 foreach ( $attemptEncodings as $encoding ) {
428 $str = @iconv( $encoding,
'UTF-8', $contents );
429 if ( $str !=
'' && preg_match(
"!<\?xml\b(.*?)\?>!si", $str,
$matches ) ) {
430 if ( preg_match( $encodingRegex,
$matches[1], $encMatch )
431 && !in_array( strtoupper( $encMatch[1] ), self::SAFE_XML_ENCODINGS )
433 wfDebug( __METHOD__ .
": Found unsafe XML encoding '{$encMatch[1]}'" );
437 } elseif ( $str !=
'' && preg_match(
"!<\?xml\b!i", $str ) ) {
440 wfDebug( __METHOD__ .
": Unmatched XML declaration start" );
457 private function detectScriptInSvg( $filename, $partial ) {
458 $check =
new XmlTypeCheck(
460 $this->checkSvgScriptCallback( ... ),
463 'processing_instruction_handler' => $this->checkSvgPICallback( ... ),
464 'external_dtd_handler' => $this->checkSvgExternalDTD( ... ),
467 if ( $check->wellFormed !==
true ) {
470 return $partial ? false : [
'uploadinvalidxml' ];
473 if ( $check->filterMatch ) {
474 return $check->filterMatchType;
487 private function checkSvgPICallback( $target, $data ) {
489 if ( preg_match(
'/xml-stylesheet/i', $target ) ) {
490 return [
'upload-scripted-pi-callback' ];
508 private function checkSvgExternalDTD( $type, $publicId, $systemId ) {
511 static $allowedDTDs = [
512 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd',
513 'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd',
514 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-basic.dtd',
515 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd',
517 'http://www.w3.org/TR/2001/PR-SVG-20010719/DTD/svg10.dtd',
519 if ( $type !==
'PUBLIC'
520 || !in_array( $systemId, $allowedDTDs )
521 || !str_starts_with( $publicId,
"-//W3C//" )
523 return [
'upload-scripted-dtd' ];
538 private function checkSvgScriptCallback( $element, $attribs, $data =
null ) {
539 [ $namespace, $strippedElement ] = self::splitXmlNamespace( $element );
543 static $validNamespaces = [
546 'http://cipa.jp/exif/1.0/',
547 'http://creativecommons.org/ns#',
548 'http://developer.sonyericsson.com/cell/1.0/',
549 'http://inkscape.sourceforge.net/dtd/sodipodi-0.dtd',
550 'http://iptc.org/std/iptc4xmpcore/1.0/xmlns/',
551 'http://iptc.org/std/iptc4xmpext/2008-02-29/',
552 'http://leica-camera.com/digital-shift-assistant/1.0/',
553 'http://ns.acdsee.com/iptc/1.0/',
554 'http://ns.acdsee.com/regions/',
555 'http://ns.adobe.com/adobeillustrator/10.0/',
556 'http://ns.adobe.com/adobesvgviewerextensions/3.0/',
557 'http://ns.adobe.com/album/1.0/',
558 'http://ns.adobe.com/camera-raw-defaults/1.0/',
559 'http://ns.adobe.com/camera-raw-embedded-lens-profile/1.0/',
560 'http://ns.adobe.com/camera-raw-saved-settings/1.0/',
561 'http://ns.adobe.com/camera-raw-settings/1.0/',
562 'http://ns.adobe.com/creatoratom/1.0/',
563 'http://ns.adobe.com/dicom/',
564 'http://ns.adobe.com/exif/1.0/',
565 'http://ns.adobe.com/exif/1.0/aux/',
566 'http://ns.adobe.com/extensibility/1.0/',
567 'http://ns.adobe.com/flows/1.0/',
568 'http://ns.adobe.com/hdr-gain-map/1.0/',
569 'http://ns.adobe.com/hdr-metadata/1.0/',
570 'http://ns.adobe.com/ix/1.0/',
571 'http://ns.adobe.com/lightroom/1.0/',
572 'http://ns.adobe.com/illustrator/1.0/',
573 'http://ns.adobe.com/imagereplacement/1.0/',
574 'http://ns.adobe.com/pdf/1.3/',
575 'http://ns.adobe.com/pdfx/1.3/',
576 'http://ns.adobe.com/photoshop/1.0/',
577 'http://ns.adobe.com/photoshop/1.0/camera-profile',
578 'http://ns.adobe.com/photoshop/1.0/panorama-profile',
579 'http://ns.adobe.com/raw/1.0/',
580 'http://ns.adobe.com/swf/1.0/',
581 'http://ns.adobe.com/saveforweb/1.0/',
582 'http://ns.adobe.com/tiff/1.0/',
583 'http://ns.adobe.com/variables/1.0/',
584 'http://ns.adobe.com/xap/1.0/',
585 'http://ns.adobe.com/xap/1.0/bj/',
586 'http://ns.adobe.com/xap/1.0/g/',
587 'http://ns.adobe.com/xap/1.0/g/img/',
588 'http://ns.adobe.com/xap/1.0/mm/',
589 'http://ns.adobe.com/xap/1.0/plus/',
590 'http://ns.adobe.com/xap/1.0/rights/',
591 'http://ns.adobe.com/xap/1.0/stype/dimensions#',
592 'http://ns.adobe.com/xap/1.0/stype/font#',
593 'http://ns.adobe.com/xap/1.0/stype/manifestitem#',
594 'http://ns.adobe.com/xap/1.0/stype/resourceevent#',
595 'http://ns.adobe.com/xap/1.0/stype/resourceref#',
596 'http://ns.adobe.com/xap/1.0/stype/version#',
597 'http://ns.adobe.com/xap/1.0/t/pg/',
598 'http://ns.adobe.com/xmp/1.0/dynamicmedia/',
599 'http://ns.adobe.com/xmp/identifier/qual/1.0/',
600 'http://ns.adobe.com/xmp/note/',
601 'http://ns.adobe.com/xmp/stype/area#',
602 'http://ns.apple.com/adjustment-settings/1.0/',
603 'http://ns.apple.com/faceinfo/1.0/',
604 'http://ns.apple.com/hdrgainmap/1.0/',
605 'http://ns.apple.com/pixeldatainfo/1.0/',
606 'http://ns.exiftool.org/1.0/',
607 'http://ns.extensis.com/extensis/1.0/',
608 'http://ns.fastpictureviewer.com/fpv/1.0/',
609 'http://ns.google.com/photos/1.0/audio/',
610 'http://ns.google.com/photos/1.0/camera/',
611 'http://ns.google.com/photos/1.0/container/',
612 'http://ns.google.com/photos/1.0/creations/',
613 'http://ns.google.com/photos/1.0/depthmap/',
614 'http://ns.google.com/photos/1.0/focus/',
615 'http://ns.google.com/photos/1.0/image/',
616 'http://ns.google.com/photos/1.0/panorama/',
617 'http://ns.google.com/photos/dd/1.0/profile/',
618 'http://ns.google.com/videos/1.0/spherical/',
619 'http://ns.idimager.com/ics/1.0/',
620 'http://ns.iview-multimedia.com/mediapro/1.0/',
621 'http://ns.leiainc.com/photos/1.0/image/',
622 'http://ns.microsoft.com/expressionmedia/1.0/',
623 'http://ns.microsoft.com/photo/1.0',
624 'http://ns.microsoft.com/photo/1.1',
625 'http://ns.microsoft.com/photo/1.2/',
626 'http://ns.microsoft.com/photo/1.2/t/region#',
627 'http://ns.microsoft.com/photo/1.2/t/regioninfo#',
628 'http://ns.nikon.com/asteroid/1.0/',
629 'http://ns.nikon.com/nine/1.0/',
630 'http://ns.nikon.com/sdc/1.0/',
631 'http://ns.optimasc.com/dex/1.0/',
632 'http://ns.seal/2024/1.0/',
633 'http://ns.useplus.org/ldf/xmp/1.0/',
634 'http://prismstandard.org/namespaces/basic/2.0/',
635 'http://prismstandard.org/namespaces/pmi/2.2/',
636 'http://prismstandard.org/namespaces/prismusagerights/2.1/',
637 'http://prismstandard.org/namespaces/prl/2.1/',
638 'http://prismstandard.org/namespaces/prm/3.0/',
639 'http://purl.org/dc/elements/1.1/',
640 'http://purl.org/dc/elements/1.1',
641 'http://rs.tdwg.org/dwc/index.htm',
642 'http://schemas.microsoft.com/visio/2003/svgextensions/',
643 'http://sodipodi.sourceforge.net/dtd/sodipodi-0.dtd',
644 'http://taptrix.com/inkpad/svg_extensions',
645 'http://www.digikam.org/ns/1.0/',
646 'http://www.dji.com/drone-dji/1.0/',
647 'http://www.metadataworkinggroup.com/schemas/collections/',
648 'http://www.metadataworkinggroup.com/schemas/keywords/',
649 'http://www.metadataworkinggroup.com/schemas/regions/',
650 'http://web.resource.org/cc/',
651 'http://www.freesoftware.fsf.org/bkchem/cdml',
652 'http://www.inkscape.org/namespaces/inkscape',
653 'http://www.opengis.net/gml',
654 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
655 'http://www.w3.org/2000/01/rdf-schema#',
656 'http://www.w3.org/2000/svg',
657 'http://www.w3.org/2000/02/svg/testsuite/description/',
658 'http://www.w3.org/tr/rec-rdf-syntax/',
659 'http://xmp.gettyimages.com/gift/1.0/',
664 $isBuggyInkscape = preg_match(
'/^&(#38;)*ns_[a-z_]+;$/', $namespace );
666 if ( !( $isBuggyInkscape || in_array( $namespace, $validNamespaces ) ) ) {
667 wfDebug( __METHOD__ .
": Non-svg namespace '$namespace' in uploaded file." );
668 return [
'uploadscriptednamespace', $namespace ];
672 if ( $strippedElement ===
'script' ) {
673 wfDebug( __METHOD__ .
": Found script element '$element' in uploaded file." );
675 return [
'uploaded-script-svg', $strippedElement ];
680 if ( $strippedElement ===
'handler' ) {
681 wfDebug( __METHOD__ .
": Found scriptable element '$element' in uploaded file." );
683 return [
'uploaded-script-svg', $strippedElement ];
687 if ( $strippedElement ===
'stylesheet' ) {
688 wfDebug( __METHOD__ .
": Found scriptable element '$element' in uploaded file." );
690 return [
'uploaded-script-svg', $strippedElement ];
694 if ( $strippedElement ===
'iframe' ) {
695 wfDebug( __METHOD__ .
": iframe in uploaded file." );
697 return [
'uploaded-script-svg', $strippedElement ];
701 if ( $strippedElement ===
'style' ) {
702 $cssCheck = $this->SVGCSSChecker->checkStyleTag( $data );
703 if ( $cssCheck !==
true ) {
704 wfDebug( __METHOD__ .
": hostile css in style element. " . $cssCheck[0] );
706 return [
'uploaded-hostile-svg', $cssCheck[0], $cssCheck[1], $cssCheck[2] ];
710 static $cssAttrs = [
'font',
'clip-path',
'fill',
'filter',
'marker',
711 'marker-end',
'marker-mid',
'marker-start',
'mask',
'stroke',
'cursor' ];
713 foreach ( $attribs as $attrib => $value ) {
715 [ $attributeNamespace, $stripped ] = self::splitXmlNamespace( $attrib );
716 $value = strtolower( $value );
723 $namespace ===
'http://www.inkscape.org/namespaces/inkscape' &&
724 $attributeNamespace ===
''
725 ) && str_starts_with( $stripped,
'on' )
728 .
": Found event-handler attribute '$attrib'='$value' in uploaded file." );
730 return [
'uploaded-event-handler-on-svg', $attrib, $value ];
740 && !str_starts_with( $value,
'data:' )
741 && !str_starts_with( $value,
'#' )
742 && !( $strippedElement ===
'a' && preg_match(
'!^https?://!i', $value ) )
744 wfDebug( __METHOD__ .
": Found href attribute <$strippedElement "
745 .
"'$attrib'='$value' in uploaded file." );
747 return [
'uploaded-href-attribute-svg', $strippedElement, $attrib, $value ];
752 if ( $stripped ===
'href' && strncasecmp(
'data:', $value, 5 ) === 0 ) {
756 $parameters =
'(?>;[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+=(?>[a-zA-Z0-9\!#$&\'*+.^_`{|}~-]+|"(?>[\0-\x0c\x0e-\x21\x23-\x5b\x5d-\x7f]+|\\\\[\0-\x7f])*"))*(?:;base64)?';
758 if ( !preg_match(
"!^data:\s*image/(gif|jpeg|jpg|a?png|webp|avif)$parameters,!i", $value ) ) {
759 wfDebug( __METHOD__ .
": Found href with data URI with MIME type that is not allowed "
760 .
"\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
761 return [
'uploaded-href-unsafe-target-svg', $strippedElement, $attrib, $value ];
766 if ( $stripped ===
'attributename'
767 && $strippedElement ===
'animate'
768 && $this->stripXmlNamespace( $value ) ===
'href'
770 wfDebug( __METHOD__ .
": Found animate that might be changing href using from "
771 .
"\"<$strippedElement '$attrib'='$value'...\" in uploaded file." );
773 return [
'uploaded-animate-svg', $strippedElement, $attrib, $value ];
777 if ( ( $strippedElement ===
'set' || $strippedElement ===
'animate' )
778 && $stripped ===
'attributename'
779 && str_starts_with( $value,
'on' )
781 wfDebug( __METHOD__ .
": Found svg setting event-handler attribute with "
782 .
"\"<$strippedElement $stripped='$value'...\" in uploaded file." );
784 return [
'uploaded-setting-event-handler-svg', $strippedElement, $stripped, $value ];
788 if ( $strippedElement ===
'set'
789 && $stripped ===
'attributename'
790 && str_contains( $value,
'href' )
792 wfDebug( __METHOD__ .
": Found svg setting href attribute '$value' in uploaded file." );
794 return [
'uploaded-setting-href-svg' ];
798 if ( $strippedElement ===
'set'
799 && $stripped ===
'to'
800 && preg_match(
'!(http|https|data|script):!im', $value )
802 wfDebug( __METHOD__ .
": Found svg setting attribute to '$value' in uploaded file." );
804 return [
'uploaded-wrong-setting-svg', $value ];
808 if ( $stripped ===
'handler' && preg_match(
'!(http|https|data|script):!im', $value ) ) {
809 wfDebug( __METHOD__ .
": Found svg setting handler with remote/data/script "
810 .
"'$attrib'='$value' in uploaded file." );
812 return [
'uploaded-setting-handler-svg', $attrib, $value ];
816 if ( $stripped ===
'style'
817 && $this->SVGCSSChecker->checkStyleAttribute( $value ) !==
true
819 wfDebug( __METHOD__ .
": Found svg setting a style with "
820 .
"remote url '$attrib'='$value' in uploaded file." );
821 return [
'uploaded-remote-url-svg', $attrib, $value ];
825 if ( in_array( $stripped, $cssAttrs,
true )
826 && $this->SVGCSSChecker->checkPresentationalAttribute( $value ) !==
true
828 wfDebug( __METHOD__ .
": Found svg setting a style with "
829 .
"remote url '$attrib'='$value' in uploaded file." );
830 return [
'uploaded-remote-url-svg', $attrib, $value ];
837 if ( $strippedElement ===
'image'
838 && $stripped ===
'filter'
839 && preg_match(
'!url\s*\(\s*["\']?[^#]!im', $value )
841 wfDebug( __METHOD__ .
": Found image filter with url: "
842 .
"\"<$strippedElement $stripped='$value'...\" in uploaded file." );
844 return [
'uploaded-image-filter-svg', $strippedElement, $stripped, $value ];
857 private function splitXmlNamespace( $element ) {
859 $parts = explode(
':', strtolower( $element ) );
860 $name = array_pop( $parts );
861 $ns = implode(
':', $parts );
863 return [ $ns, $name ];
870 private function stripXmlNamespace( $element ) {
872 return self::splitXmlNamespace( $element )[1];
888 $mainConfig = $this->config;
893 wfDebug( __METHOD__ .
": virus scanner disabled" );
898 if ( !( $antivirusSetup[$antivirus] ??
false ) ) {
902 # look up scanner configuration
903 $command = $antivirusSetup[$antivirus][
'command'];
904 $exitCodeMap = $antivirusSetup[$antivirus][
'codemap'];
905 $msgPattern = $antivirusSetup[$antivirus][
'messagepattern'] ??
null;
907 if ( !str_contains( $command,
"%f" ) ) {
908 # simple pattern: append file to scan
909 $command .=
" " . Shell::escape( $file );
911 # complex pattern: replace "%f" with file to scan
912 $command = str_replace(
"%f", Shell::escape( $file ), $command );
915 wfDebug( __METHOD__ .
": running virus scan: $command " );
917 # execute virus scanner
920 # NOTE: there's a 50-line workaround to make stderr redirection work on windows, too.
921 # that does not seem to be worth the pain.
922 # Ask me (Duesentrieb) about it if it's ever needed.
925 # map exit code to AV_xxx constants.
926 $mappedCode = $exitCode;
927 if ( $exitCodeMap ) {
928 if ( isset( $exitCodeMap[$exitCode] ) ) {
929 $mappedCode = $exitCodeMap[$exitCode];
930 } elseif ( isset( $exitCodeMap[
"*"] ) ) {
931 $mappedCode = $exitCodeMap[
"*"];
935 # NB: AV_NO_VIRUS is 0, but AV_SCAN_FAILED is false,
936 # so we need the strict equalities === and thus can't use a switch here
938 # scan failed (code was mapped to false by $exitCodeMap)
939 wfDebug( __METHOD__ .
": failed to scan $file (code $exitCode)." );
941 $output = $antivirusRequired
942 ?
wfMessage(
'virus-scanfailed', [ $exitCode ] )->text()
945 # scan failed because filetype is unknown (probably immune)
946 wfDebug( __METHOD__ .
": unsupported file type $file (code $exitCode)." );
950 wfDebug( __METHOD__ .
": file passed virus scan." );
953 $output = trim( $output );
956 $output =
true; #
if there
's no output, return true
957 } elseif ( $msgPattern ) {
959 if ( preg_match( $msgPattern, $output, $groups ) && $groups[1] ) {
960 $output = $groups[1];
964 wfDebug( __METHOD__ . ": FOUND VIRUS! scanner feedback: $output" );