Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 346
AddMediaInfo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 14
14762
0.00% covered (danger)
0.00%
0 / 346
 handleSize
0.00% covered (danger)
0.00%
0 / 1
420
0.00% covered (danger)
0.00%
0 / 27
 parseTimeString
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 15
 parseFrag
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 14
 addSources
0.00% covered (danger)
0.00%
0 / 1
56
0.00% covered (danger)
0.00%
0 / 25
 addTracks
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 17
 getPath
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 6
 handleAudio
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 14
 handleVideo
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 15
 handleImage
0.00% covered (danger)
0.00%
0 / 1
72
0.00% covered (danger)
0.00%
0 / 25
 makeErr
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 4
 addErrors
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 8
 copyOverAttribute
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 6
 replaceAnchor
0.00% covered (danger)
0.00%
0 / 1
156
0.00% covered (danger)
0.00%
0 / 37
 run
0.00% covered (danger)
0.00%
0 / 1
1482
0.00% covered (danger)
0.00%
0 / 133
<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Wt2Html\PP\Processors;
use stdClass;
use Wikimedia\Assert\Assert;
use Wikimedia\Parsoid\Config\Env;
use Wikimedia\Parsoid\Core\Sanitizer;
use Wikimedia\Parsoid\DOM\DocumentFragment;
use Wikimedia\Parsoid\DOM\Element;
use Wikimedia\Parsoid\DOM\Node;
use Wikimedia\Parsoid\Html2Wt\WTSUtils;
use Wikimedia\Parsoid\Utils\ContentUtils;
use Wikimedia\Parsoid\Utils\DOMCompat;
use Wikimedia\Parsoid\Utils\DOMDataUtils;
use Wikimedia\Parsoid\Utils\DOMUtils;
use Wikimedia\Parsoid\Utils\Title;
use Wikimedia\Parsoid\Utils\WTUtils;
use Wikimedia\Parsoid\Wikitext\Consts;
use Wikimedia\Parsoid\Wt2Html\PegTokenizer;
use Wikimedia\Parsoid\Wt2Html\Wt2HtmlDOMProcessor;
class AddMediaInfo implements Wt2HtmlDOMProcessor {
    /**
     * Extract the dimensions for media.
     *
     * @param Env $env
     * @param array $attrs
     * @param array $info
     * @phan-param array{size:array{height?:int,width?:int},format:string} $attrs
     * @return array
     */
    private static function handleSize( Env $env, array $attrs, array $info ): array {
        $height = $info['height'];
        $width = $info['width'];
        Assert::invariant(
            is_numeric( $height ) && $height !== NAN,
            'Expected $height as a valid number'
        );
        Assert::invariant(
            is_numeric( $width ) && $width !== NAN,
            'Expected $width as a valid number'
        );
        if ( !empty( $info['thumburl'] ) && !empty( $info['thumbheight'] ) ) {
            $height = $info['thumbheight'];
        }
        if ( !empty( $info['thumburl'] ) && !empty( $info['thumbwidth'] ) ) {
            $width = $info['thumbwidth'];
        }
        // Audio files don't have dimensions, so we fallback to these arbitrary
        // defaults, and the "mw-default-audio-height" class is added.
        if ( $info['mediatype'] === 'AUDIO' ) {
            $height = /* height || */32; // Arguably, audio should respect a defined height
            $width = $width ?: $env->getSiteConfig()->widthOption();
        }
        // Handle client-side upscaling (including 'border')
        $mustRender = $info['mustRender'] ?? $info['mediatype'] !== 'BITMAP';
        // Calculate the scaling ratio from the user-specified width and height
        $ratio = null;
        if ( !empty( $attrs['dims']['height'] ) && !empty( $info['height'] ) ) {
            $ratio = $attrs['dims']['height'] / $info['height'];
        }
        if ( !empty( $attrs['dims']['width'] ) && !empty( $info['width'] ) ) {
            $r = $attrs['dims']['width'] / $info['width'];
            $ratio = ( $ratio === null || $r < $ratio ) ? $r : $ratio;
        }
        // If the user requested upscaling, then this is denied in the thumbnail
        // and frameless format, except for files with mustRender.
        if (
            $ratio !== null && $ratio > 1 && !$mustRender &&
            ( $attrs['format'] === 'Thumb' || $attrs['format'] === 'Frameless' )
        ) {
            // Upscaling denied
            $height = $info['height'];
            $width = $info['width'];
        }
        return [ 'height' => $height, 'width' => $width ];
    }
    /**
     * This is a port of TMH's parseTimeString()
     *
     * @param string $timeString
     * @param int|float|null $length
     * @return int|float|null
     */
    private static function parseTimeString(
        string $timeString, $length = null
    ) {
        $parts = explode( ':', $timeString );
        $time = 0;
        $countParts = count( $parts );
        if ( $countParts > 3 ) {
            return null;
        }
        for ( $i = 0;  $i < $countParts;  $i++ ) {
            if ( !is_numeric( $parts[$i] ) ) {
                return null;
            }
            $time += floatval( $parts[$i] ) * pow( 60, $countParts - 1 - $i );
        }
        if ( $time < 0 ) {
            $time = 0;
        } elseif ( $length !== null ) {
            if ( $time > $length ) {
                $time = $length - 1;
            }
        }
        return $time;
    }
    /**
     * Handle media fragments
     * https://www.w3.org/TR/media-frags/
     *
     * @param array $info
     * @param stdClass $dataMw
     * @return string
     */
    private static function parseFrag( array $info, stdClass $dataMw ): string {
        $frag = '';
        $starttime = WTSUtils::getAttrFromDataMw( $dataMw, 'starttime', true );
        $endtime = WTSUtils::getAttrFromDataMw( $dataMw, 'endtime', true );
        if ( $starttime || $endtime ) {
            $frag .= '#t=';
            if ( $starttime ) {
                $time = self::parseTimeString( $starttime[1]->txt, $info['duration'] ?? null );
                if ( $time !== null ) {
                    $frag .= $time;
                }
            }
            if ( $endtime ) {
                $time = self::parseTimeString( $endtime[1]->txt, $info['duration'] ?? null );
                if ( $time !== null ) {
                    $frag .= ',' . $time;
                }
            }
        }
        return $frag;
    }
    /**
     * @param Element $elt
     * @param array $info
     * @param stdClass $dataMw
     * @param bool $hasDimension
     */
    private static function addSources(
        Element $elt, array $info, stdClass $dataMw, bool $hasDimension
    ): void {
        $doc = $elt->ownerDocument;
        $frag = self::parseFrag( $info, $dataMw );
        $dataFromTMH = true;
        if ( is_array( $info['thumbdata']['derivatives'] ?? null ) ) {
            // BatchAPI's `getAPIData`
            $derivatives = $info['thumbdata']['derivatives'];
        } elseif ( is_array( $info['derivatives'] ?? null ) ) {
            // "videoinfo" prop
            $derivatives = $info['derivatives'];
        } else {
            $derivatives = [
                [
                    'src' => $info['url'],
                    'type' => $info['mime'],
                    'width' => (string)$info['width'],
                    'height' => (string)$info['height'],
                ],
            ];
            $dataFromTMH = false;
        }
        foreach ( $derivatives as $o ) {
            $source = $doc->createElement( 'source' );
            $source->setAttribute( 'src', $o['src'] . $frag );
            $source->setAttribute( 'type', $o['type'] );
            $fromFile = isset( $o['transcodekey'] ) ? '' : '-file';
            if ( $hasDimension ) {
                $source->setAttribute( 'data' . $fromFile . '-width', (string)$o['width'] );
                $source->setAttribute( 'data' . $fromFile . '-height', (string)$o['height'] );
            }
            if ( $dataFromTMH ) {
                $source->setAttribute( 'data-title', $o['title'] );
                $source->setAttribute( 'data-shorttitle', $o['shorttitle'] );
            }
            $elt->appendChild( $source );
        }
    }
    /**
     * @param Element $elt
     * @param array $info
     */
    private static function addTracks( Element $elt, array $info ): void {
        $doc = $elt->ownerDocument;
        if ( is_array( $info['thumbdata']['timedtext'] ?? null ) ) {
            // BatchAPI's `getAPIData`
            $timedtext = $info['thumbdata']['timedtext'];
        } elseif ( is_array( $info['timedtext'] ?? null ) ) {
            // "videoinfo" prop
            $timedtext = $info['timedtext'];
        } else {
            $timedtext = [];
        }
        foreach ( $timedtext as $o ) {
            $track = $doc->createElement( 'track' );
            $track->setAttribute( 'kind', $o['kind'] ?? '' );
            $track->setAttribute( 'type', $o['type'] ?? '' );
            $track->setAttribute( 'src', $o['src'] ?? '' );
            $track->setAttribute( 'srclang', $o['srclang'] ?? '' );
            $track->setAttribute( 'label', $o['label'] ?? '' );
            $track->setAttribute( 'data-mwtitle', $o['title'] ?? '' );
            $track->setAttribute( 'data-dir', $o['dir'] ?? '' );
            $elt->appendChild( $track );
        }
    }
    /**
     * Abstract way to get the path for an image given an info object.
     *
     * @param array $info
     * @return string
     */
    private static function getPath( array $info ) {
        $path = '';
        if ( !empty( $info['thumburl'] ) ) {
            $path = $info['thumburl'];
        } elseif ( !empty( $info['url'] ) ) {
            $path = $info['url'];
        }
        return $path;
    }
    /**
     * @param Env $env
     * @param Element $span
     * @param array $attrs
     * @param array $info
     * @param stdClass $dataMw
     * @param Element $container
     * @param string|null $captionText Unused, but matches the signature of handlers
     * @return Element
     */
    private static function handleAudio(
        Env $env, Element $span, array $attrs, array $info, stdClass $dataMw,
        Element $container, ?string $captionText
    ): Element {
        $doc = $span->ownerDocument;
        $audio = $doc->createElement( 'audio' );
        $audio->setAttribute( 'controls', '' );
        $audio->setAttribute( 'preload', 'none' );
        $size = self::handleSize( $env, $attrs, $info );
        DOMDataUtils::addNormalizedAttribute( $audio, 'height', (string)$size['height'], null, true );
        DOMDataUtils::addNormalizedAttribute( $audio, 'width', (string)$size['width'], null, true );
        // Hardcoded until defined heights are respected.
        // See `AddMediaInfo.handleSize`
        DOMCompat::getClassList( $container )->add( 'mw-default-audio-height' );
        self::copyOverAttribute( $audio, $span, 'resource' );
        if ( $span->hasAttribute( 'lang' ) ) {
            self::copyOverAttribute( $audio, $span, 'lang' );
        }
        self::addSources( $audio, $info, $dataMw, false );
        self::addTracks( $audio, $info );
        return $audio;
    }
    /**
     * @param Env $env
     * @param Element $span
     * @param array $attrs
     * @param array $info
     * @param stdClass $dataMw
     * @param Element $container
     * @param string|null $captionText Unused, but matches the signature of handlers
     * @return Element
     */
    private static function handleVideo(
        Env $env, Element $span, array $attrs, array $info, stdClass $dataMw,
        Element $container, ?string $captionText
    ): Element {
        $doc = $span->ownerDocument;
        $video = $doc->createElement( 'video' );
        if ( !empty( $info['thumburl'] ) ) {
            $video->setAttribute( 'poster', self::getPath( $info ) );
        }
        $video->setAttribute( 'controls', '' );
        $video->setAttribute( 'preload', 'none' );
        $size = self::handleSize( $env, $attrs, $info );
        DOMDataUtils::addNormalizedAttribute( $video, 'height', (string)$size['height'], null, true );
        DOMDataUtils::addNormalizedAttribute( $video, 'width', (string)$size['width'], null, true );
        self::copyOverAttribute( $video, $span, 'resource' );
        if ( $span->hasAttribute( 'lang' ) ) {
            self::copyOverAttribute( $video, $span, 'lang' );
        }
        self::addSources( $video, $info, $dataMw, true );
        self::addTracks( $video, $info );
        return $video;
    }
    /**
     * Set up the actual image structure, attributes, etc.
     *
     * @param Env $env
     * @param Element $span
     * @param array $attrs
     * @param array $info
     * @param stdClass $dataMw
     * @param Element $container
     * @param string|null $captionText
     * @return Element
     */
    private static function handleImage(
        Env $env, Element $span, array $attrs, array $info, stdClass $dataMw,
        Element $container, ?string $captionText
    ): Element {
        $doc = $span->ownerDocument;
        $img = $doc->createElement( 'img' );
        $attr = WTSUtils::getAttrFromDataMw( $dataMw, 'alt', false );
        if ( $attr !== null ) {
            $img->setAttribute( 'alt', $attr[1]->txt );
        } elseif ( $captionText ) {
            $img->setAttribute( 'alt', $captionText );
        }
        self::copyOverAttribute( $img, $span, 'resource' );
        $img->setAttribute( 'src', self::getPath( $info ) );
        $img->setAttribute( 'decoding', 'async' );
        if ( $span->hasAttribute( 'lang' ) ) {
            self::copyOverAttribute( $img, $span, 'lang' );
        }
        // Add (read-only) information about original file size (T64881)
        $img->setAttribute( 'data-file-width', (string)$info['width'] );
        $img->setAttribute( 'data-file-height', (string)$info['height'] );
        $img->setAttribute( 'data-file-type', strtolower( $info['mediatype'] ?? '' ) );
        $size = self::handleSize( $env, $attrs, $info );
        DOMDataUtils::addNormalizedAttribute( $img, 'height', (string)$size['height'], null, true );
        DOMDataUtils::addNormalizedAttribute( $img, 'width', (string)$size['width'], null, true );
        // Handle "responsive" images, i.e. srcset
        if ( !empty( $info['responsiveUrls'] ) ) {
            $candidates = [];
            foreach ( $info['responsiveUrls'] as $density => $url ) {
                $candidates[] = $url . ' ' . $density . 'x';
            }
            if ( $candidates ) {
                $img->setAttribute( 'srcset', implode( ', ', $candidates ) );
            }
        }
        return $img;
    }
    /**
     * @param string $key
     * @param string $message
     * @param ?array $params
     * @return array
     */
    private static function makeErr(
        string $key, string $message, ?array $params = null
    ): array {
        $e = [ 'key' => $key, 'message' => $message ];
        // Additional error info for clients that could fix the error.
        if ( $params !== null ) {
            $e['params'] = $params;
        }
        return $e;
    }
    /**
     * @param Element $container
     * @param array $errs
     * @param stdClass $dataMw
     */
    private static function addErrors( Element $container, array $errs, stdClass $dataMw ): void {
        if ( !DOMUtils::hasTypeOf( $container, 'mw:Error' ) ) {
            $typeOf = $container->getAttribute( 'typeof' ) ?? '';
            $typeOf = 'mw:Error' . ( $typeOf ? ' ' . $typeOf : '' );
            $container->setAttribute( 'typeof', $typeOf );
        }
        if ( is_array( $dataMw->errors ?? null ) ) {
            $errs = array_merge( $dataMw->errors, $errs );
        }
        $dataMw->errors = $errs;
    }
    /**
     * @param Element $elt
     * @param Element $span
     * @param string $attribute
     */
    private static function copyOverAttribute(
        Element $elt, Element $span, string $attribute
    ): void {
        DOMDataUtils::addNormalizedAttribute(
            $elt,
            $attribute,
            $span->getAttribute( $attribute ) ?? '',
            WTSUtils::getAttributeShadowInfo( $span, $attribute )['value']
        );
    }
    /**
     * @param Env $env
     * @param PegTokenizer $urlParser
     * @param Element $container
     * @param Element $oldAnchor
     * @param array $attrs
     * @param stdClass $dataMw
     * @param bool $isImage
     * @param string|null $captionText
     * @param int $page
     * @param string $lang
     * @return Element
     */
    private static function replaceAnchor(
        Env $env, PegTokenizer $urlParser, Element $container,
        Element $oldAnchor, array $attrs, stdClass $dataMw, bool $isImage,
        ?string $captionText, int $page, string $lang
    ): Element {
        $doc = $oldAnchor->ownerDocument;
        $attr = WTSUtils::getAttrFromDataMw( $dataMw, 'link', true );
        if ( $isImage ) {
            $anchor = $doc->createElement( 'a' );
            $addDescriptionLink = static function ( Title $title ) use ( $env, $anchor, $page, $lang ) {
                $href = $env->makeLink( $title );
                $qs = [];
                if ( $page > 0 ) {
                    $qs['page'] = $page;
                }
                if ( $lang ) {
                    $qs['lang'] = $lang;
                }
                if ( $qs ) {
                    $href .= '?' . http_build_query( $qs );
                }
                $anchor->setAttribute( 'href', $href );
                $anchor->setAttribute( 'class', 'mw-file-description' );
            };
            if ( $attr !== null ) {
                $discard = true;
                $val = $attr[1]->txt;
                if ( $val === '' ) {
                    // No href if link= was specified
                    $anchor = $doc->createElement( 'span' );
                } elseif ( $urlParser->tokenizeURL( $val ) !== false ) {
                    // An external link!
                    $href = Sanitizer::cleanUrl( $env->getSiteConfig(), $val, 'external' );
                    $anchor->setAttribute( 'href', $href );
                } else {
                    $link = $env->makeTitleFromText( $val, null, true );
                    if ( $link !== null ) {
                        $anchor->setAttribute( 'href', $env->makeLink( $link ) );
                        $anchor->setAttribute( 'title', $link->getPrefixedText() );
                    } else {
                        // Treat same as if link weren't present
                        $addDescriptionLink( $attrs['title'] );
                        // but preserve for roundtripping
                        $discard = false;
                    }
                }
                if ( $discard ) {
                    WTSUtils::getAttrFromDataMw( $dataMw, 'link', /* keep */false );
                }
            } else {
                $addDescriptionLink( $attrs['title'] );
            }
        } else {
            $anchor = $doc->createElement( 'span' );
        }
        if ( $captionText ) {
            $anchor->setAttribute( 'title', $captionText );
        }
        $oldAnchor->parentNode->replaceChild( $anchor, $oldAnchor );
        return $anchor;
    }
    /**
     * @inheritDoc
     */
    public function run(
        Env $env, Node $root, array $options = [], bool $atTopLevel = false
    ): void {
        '@phan-var Element|DocumentFragment $root';  // @var Element|DocumentFragment $root
        $urlParser = new PegTokenizer( $env );
        $validContainers = [];
        $files = [];
        $containers = DOMCompat::querySelectorAll( $root, '[typeof*="mw:File"]' );
        foreach ( $containers as $container ) {
            // DOMFragmentWrappers assume the element name of their outermost
            // content so, depending how the above query is written, we're
            // protecting against getting a figure of the wrong type.  However,
            // since we're currently using typeof, it shouldn't be a problem.
            // Also note that info for the media nested in the fragment has
            // already been added in their respective pipeline.
            Assert::invariant(
                !WTUtils::isDOMFragmentWrapper( $container ),
                'Media info for fragment was already added'
            );
            // We expect this structure to be predictable based on how it's
            // emitted in the TT/WikiLinkHandler but treebuilding may have
            // messed that up for us.
            $anchor = $container;
            do {
                // An active formatting element may have been reopened inside
                // the wrapper if a content model violation was encountered
                // during treebuiling.  Try to be a little lenient about that
                // instead of bailing out
                $anchor = $anchor->firstChild;
                $anchorNodeName = DOMCompat::nodeName( $anchor );
            } while (
                $anchor instanceof Element && $anchorNodeName !== 'a' &&
                isset( Consts::$HTML['FormattingTags'][$anchorNodeName] )
            );
            if ( !( $anchor instanceof Element && $anchorNodeName === 'a' ) ) {
                $env->log( 'error', 'Unexpected structure when adding media info.' );
                continue;
            }
            $span = $anchor->firstChild;
            if ( !( $span instanceof Element && DOMCompat::nodeName( $span ) === 'span' ) ) {
                $env->log( 'error', 'Unexpected structure when adding media info.' );
                continue;
            }
            $dataMw = DOMDataUtils::getDataMw( $container );
            $dims = [
                'width' => (int)$span->getAttribute( 'data-width' ) ?: null,
                'height' => (int)$span->getAttribute( 'data-height' ) ?: null,
            ];
            $page = WTSUtils::getAttrFromDataMw( $dataMw, 'page', true );
            if ( $page ) {
                $dims['page'] = $page[1]->txt;
            }
            if ( $span->hasAttribute( 'lang' ) ) {
                $dims['lang'] = $span->getAttribute( 'lang' );
            }
            // "starttime" should be used if "thumbtime" isn't present,
            // but only for rendering.
            // "starttime" should be used if "thumbtime" isn't present,
            // but only for rendering.
            $thumbtime = WTSUtils::getAttrFromDataMw( $dataMw, 'thumbtime', true );
            $starttime = WTSUtils::getAttrFromDataMw( $dataMw, 'starttime', true );
            if ( $thumbtime || $starttime ) {
                $seek = isset( $thumbtime[1] )
                    ? $thumbtime[1]->txt
                    : ( isset( $starttime[1] ) ? $starttime[1]->txt : '' );
                $seek = self::parseTimeString( $seek );
                if ( $seek !== null ) {
                    $dims['seek'] = $seek;
                }
            }
            $attrs = [
                'dims' => $dims,
                'format' => WTUtils::getMediaFormat( $container ),
                'title' => $env->makeTitleFromText( $span->textContent ),
            ];
            $file = [ $attrs['title']->getKey(), $dims ];
            $infoKey = md5( json_encode( $file ) );
            $files[$infoKey] = $file;
            $manualKey = null;
            $manualthumb = WTSUtils::getAttrFromDataMw( $dataMw, 'manualthumb', true );
            if ( $manualthumb !== null ) {
                $val = $manualthumb[1]->txt;
                $title = $env->makeTitleFromText( $val, $attrs['title']->getNamespace(), true );
                if ( $title === null ) {
                    $errs = [
                        self::makeErr(
                            'apierror-invalidtitle',
                            'Invalid thumbnail title.',
                            [ 'name' => $val ]
                        )
                    ];
                    self::addErrors( $container, $errs, $dataMw );
                    continue;
                } else {
                    $file = [ $title->getKey(), $dims ];
                    $manualKey = md5( json_encode( $file ) );
                    $files[$manualKey] = $file;
                }
            }
            $validContainers[] = [
                'container' => $container,
                'attrs' => $attrs,
                // Pass the anchor because we did some work to find it above
                'anchor' => $anchor,
                'infoKey' => $infoKey,
                'manualKey' => $manualKey,
            ];
        }
        if ( !$validContainers ) {
            return;
        }
        $start = microtime( true );
        $infos = $env->getDataAccess()->getFileInfo(
            $env->getPageConfig(),
            array_values( $files )
        );
        if ( $env->profiling() ) {
            $profile = $env->getCurrentProfile();
            $profile->bumpMWTime( "Media", 1000 * ( microtime( true ) - $start ), "api" );
            $profile->bumpCount( "Media" );
        }
        $files = array_combine(
            array_keys( $files ),
            $infos
        );
        foreach ( $validContainers as $c ) {
            $container = $c['container'];
            $anchor = $c['anchor'];
            $span = $anchor->firstChild;
            $attrs = $c['attrs'];
            $dataMw = DOMDataUtils::getDataMw( $container );
            $errs = [];
            $info = $files[$c['infoKey']];
            if ( !$info ) {
                $errs[] = self::makeErr( 'apierror-filedoesnotexist', 'This image does not exist.' );
            } elseif ( isset( $info['thumberror'] ) ) {
                $errs[] = self::makeErr( 'apierror-unknownerror', $info['thumberror'] );
            }
            // FIXME: Should we fallback to $info if there are errors with $manualinfo?
            // What does the legacy parser do?
            if ( $c['manualKey'] !== null ) {
                $manualinfo = $files[$c['manualKey']];
                if ( !$manualinfo ) {
                    $errs[] = self::makeErr( 'apierror-filedoesnotexist', 'This image does not exist.' );
                } elseif ( isset( $manualinfo['thumberror'] ) ) {
                    $errs[] = self::makeErr( 'apierror-unknownerror', $manualinfo['thumberror'] );
                } else {
                    $info = $manualinfo;
                }
            }
            if ( $info['badFile'] ?? false ) {
                $errs[] = self::makeErr( 'apierror-badfile', 'This image is on the bad file list.' );
            }
            // Add mw:Error to the RDFa type.
            if ( $errs ) {
                self::addErrors( $container, $errs, $dataMw );
                continue;
            }
            // Info relates to the thumb, not necessarily the file.
            // The distinction matters for manualthumb, in which case only
            // the "resource" copied over from the span relates to the file.
            '@phan-var array $info';  // @var array $info
            switch ( $info['mediatype'] ) {
                case 'AUDIO':
                    $handler = 'handleAudio';
                    $isImage = false;
                    break;
                case 'VIDEO':
                    $handler = 'handleVideo';
                    $isImage = false;
                    break;
                default:
                    $handler = 'handleImage';
                    $isImage = true;
                    break;
            }
            if ( WTUtils::hasVisibleCaption( $container ) ) {
                $captionText = null;
            } else {
                if ( WTUtils::isInlineMedia( $container ) ) {
                    $caption = ContentUtils::createAndLoadDocumentFragment(
                        $container->ownerDocument, $dataMw->caption ?? ''
                    );
                } else {
                    $caption = DOMCompat::querySelector( $container, 'figcaption' );
                    // If the caption had tokens, it was placed in a DOMFragment
                    // and we haven't unpacked yet
                    if (
                        $caption->firstChild &&
                        DOMUtils::hasTypeOf( $caption->firstChild, 'mw:DOMFragment' )
                    ) {
                        $id = DOMDataUtils::getDataParsoid( $caption->firstChild )->html;
                        $caption = $env->getDOMFragment( $id );
                    }
                }
                $captionText = trim( WTUtils::textContentFromCaption( $caption ) );
                // The sanitizer isn't going to do anything with a string value
                // for alt/title and since we're going to use dom element setters,
                // quote escaping should be fine.  Note that if santization does
                // happen here, it should also be done to $altFromCaption so that
                // string comparison matches, where necessary.
                //
                // $sanitizedArgs = Sanitizer::sanitizeTagAttrs( $env->getSiteConfig(), 'img', null, [
                //     new KV( 'alt', $captionText )  // Could be a 'title' too
                // ] );
                // $captionText = $sanitizedArgs['alt'][0];
            }
            $elt = self::$handler( $env, $span, $attrs, $info, $dataMw, $container, $captionText );
            $anchor = self::replaceAnchor(
                $env, $urlParser, $container, $anchor, $attrs, $dataMw, $isImage, $captionText,
                (int)( $attrs['dims']['page'] ?? 0 ),
                $attrs['dims']['lang'] ?? ''
            );
            $anchor->appendChild( $elt );
            if ( isset( $dataMw->attribs ) && count( $dataMw->attribs ) === 0 ) {
                unset( $dataMw->attribs );
            }
        }
    }
}