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 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 109
ExtensionHandler
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 5
650
0.00% covered (danger)
0.00%
0 / 109
 __construct
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 normalizeExtOptions
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 7
 onExtension
0.00% covered (danger)
0.00%
0 / 1
90
0.00% covered (danger)
0.00%
0 / 49
 onDocumentFragment
0.00% covered (danger)
0.00%
0 / 1
110
0.00% covered (danger)
0.00%
0 / 50
 onTag
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 1
<?php
declare( strict_types = 1 );
namespace Wikimedia\Parsoid\Wt2Html\TT;
use Wikimedia\Assert\Assert;
use Wikimedia\Parsoid\DOM\DocumentFragment;
use Wikimedia\Parsoid\Ext\ExtensionError;
use Wikimedia\Parsoid\Ext\ExtensionTag;
use Wikimedia\Parsoid\Ext\ExtensionTagHandler;
use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
use Wikimedia\Parsoid\Tokens\Token;
use Wikimedia\Parsoid\Utils\DOMDataUtils;
use Wikimedia\Parsoid\Utils\DOMUtils;
use Wikimedia\Parsoid\Utils\PHPUtils;
use Wikimedia\Parsoid\Utils\PipelineUtils;
use Wikimedia\Parsoid\Utils\TokenUtils;
use Wikimedia\Parsoid\Utils\Utils;
use Wikimedia\Parsoid\Utils\WTUtils;
use Wikimedia\Parsoid\Wt2Html\TokenTransformManager;
class ExtensionHandler extends TokenHandler {
    /**
     * @param TokenTransformManager $manager
     * @param array $options
     */
    public function __construct( TokenTransformManager $manager, array $options ) {
        parent::__construct( $manager, $options );
    }
    /**
     * @param array $options
     * @return array
     */
    private static function normalizeExtOptions( array $options ): array {
        // Mimics Sanitizer::decodeTagAttributes from the PHP parser
        //
        // Extension options should always be interpreted as plain text. The
        // tokenizer parses them to tokens in case they are for an HTML tag,
        // but here we use the text source instead.
        $n = count( $options );
        for ( $i = 0; $i < $n; $i++ ) {
            $o = $options[$i];
            // Use the source if present. If not use the value, but ensure it's a
            // string, as it can be a token stream if the parser has recognized it
            // as a directive.
            $v = $o->vsrc ?? TokenUtils::tokensToString( $o->v, false, [ 'includeEntities' => true ] );
            // Normalize whitespace in extension attribute values
            // FIXME: If the option is parsed as wikitext, this normalization
            // can mess with src offsets.
            $o->v = trim( preg_replace( '/[\t\r\n ]+/', ' ', $v ) );
            // Decode character references
            $o->v = Utils::decodeWtEntities( $o->v );
        }
        return $options;
    }
    /**
     * @param Token $token
     * @return TokenHandlerResult
     */
    private function onExtension( Token $token ): TokenHandlerResult {
        $env = $this->env;
        $siteConfig = $env->getSiteConfig();
        $pageConfig = $env->getPageConfig();
        $extensionName = $token->getAttribute( 'name' );
        // Track uses of extensions in the talk namespace
        if ( $siteConfig->namespaceIsTalk( $pageConfig->getNS() ) ) {
            $metrics = $siteConfig->metrics();
            if ( $metrics ) {
                $metrics->increment( "extension.talk.{$extensionName}" );
            }
        }
        $nativeExt = $siteConfig->getExtTagImpl( $extensionName );
        $cachedExpansion = $env->extensionCache[$token->dataAttribs->src] ?? null;
        $options = $token->getAttribute( 'options' );
        $token->setAttribute( 'options', self::normalizeExtOptions( $options ) );
        if ( $nativeExt !== null ) {
            $extContent = Utils::extractExtBody( $token );
            $extArgs = $token->getAttribute( 'options' );
            $extApi = new ParsoidExtensionAPI( $env, [
                'wt2html' => [
                    'frame' => $this->manager->getFrame(),
                    'parseOpts' => $this->options,
                    'extTag' => new ExtensionTag( $token ),
                ],
            ] );
            try {
                $domFragment = $nativeExt->sourceToDom(
                    $extApi, $extContent, $extArgs
                );
                $errors = $extApi->getErrors();
            } catch ( ExtensionError $e ) {
                $domFragment = WTUtils::createLocalizationFragment(
                    $env->topLevelDoc, $e->err
                );
                $errors = [ $e->err ];
                // FIXME: Should we include any errors collected
                // from $extApi->getErrors() here?
            }
            if ( $domFragment !== false ) {
                if ( $domFragment !== null ) {
                    // Turn this document fragment into a token
                    $toks = $this->onDocumentFragment(
                        $nativeExt, $token, $domFragment, $errors
                    );
                    return new TokenHandlerResult( $toks );
                } else {
                    // The extension dropped this instance completely (!!)
                    // Should be a rarity and presumably the extension
                    // knows what it is doing. Ex: nested refs are dropped
                    // in some scenarios.
                    return new TokenHandlerResult( [] );
                }
            }
            // Fall through: this extension is electing not to use
            // a custom sourceToDom method (by returning false from
            // sourceToDom).
        }
        if ( $cachedExpansion ) {
            // WARNING: THIS HAS BEEN UNUSED SINCE 2015, SEE T98995.
            // THIS CODE WAS WRITTEN BUT APPARENTLY NEVER TESTED.
            // NO WARRANTY.  MAY HALT AND CATCH ON FIRE.
            PHPUtils::unreachable( 'Should not be here!' );
            $toks = PipelineUtils::encapsulateExpansionHTML(
                $env, $token, $cachedExpansion, [ 'fromCache' => true ]
            );
        } else {
            $start = microtime( true );
            $ret = $env->getDataAccess()->parseWikitext(
                $pageConfig, $env->getMetadata(), $token->getAttribute( 'source' )
            );
            if ( $env->profiling() ) {
                $profile = $env->getCurrentProfile();
                $profile->bumpMWTime( "Extension", 1000 * ( microtime( true ) - $start ), "api" );
                $profile->bumpCount( "Extension" );
            }
            $domFragment = DOMUtils::parseHTMLToFragment(
                $env->topLevelDoc,
                // Strip a paragraph wrapper, if any, before parsing HTML to DOM
                preg_replace( '#(^<p>)|(\n</p>(' . Utils::COMMENT_REGEXP_FRAGMENT . '|\s)*$)#D', '', $ret )
            );
            $toks = $this->onDocumentFragment(
                $nativeExt, $token, $domFragment, []
            );
        }
        return new TokenHandlerResult( $toks );
    }
    /**
     * DOMFragment-based encapsulation
     *
     * @param ?ExtensionTagHandler $nativeExt
     * @param Token $extToken
     * @param DocumentFragment $domFragment
     * @param array $errors
     * @return array
     */
    private function onDocumentFragment(
        ?ExtensionTagHandler $nativeExt, Token $extToken,
        DocumentFragment $domFragment, array $errors
    ): array {
        $env = $this->env;
        $extensionName = $extToken->getAttribute( 'name' );
        if ( $env->hasDumpFlag( 'extoutput' ) ) {
            $logger = $env->getSiteConfig()->getLogger();
            $logger->warning( str_repeat( '=', 80 ) );
            $logger->warning(
                'EXTENSION INPUT: ' . $extToken->getAttribute( 'source' )
            );
            $logger->warning( str_repeat( '=', 80 ) );
            $logger->warning( "EXTENSION OUTPUT:\n" );
            $logger->warning(
                DOMUtils::getFragmentInnerHTML( $domFragment )
            );
            $logger->warning( str_repeat( '-', 80 ) );
        }
        $argDict = Utils::getExtArgInfo( $extToken )->dict;
        $extTagOffsets = $extToken->dataAttribs->extTagOffsets;
        if ( $extTagOffsets->closeWidth === 0 ) {
            unset( $argDict->body ); // Serialize to self-closing.
        }
        // Give native extensions a chance to manipulate the argDict
        if ( $nativeExt ) {
            $extApi = new ParsoidExtensionAPI( $env );
            $nativeExt->modifyArgDict( $extApi, $argDict );
        }
        $opts = [
            'setDSR' => true, // FIXME: This is the only place that sets this ...
            'wrapperName' => $extensionName,
        ];
        // Check if the tag wants its DOM fragment not to be unpacked.
        // The default setting is to unpack the content DOM fragment automatically.
        $extConfig = $env->getSiteConfig()->getExtTagConfig( $extensionName );
        if ( isset( $extConfig['options']['wt2html'] ) ) {
            $opts += $extConfig['options']['wt2html'];
        }
        // This special case is only because, from the beginning, Parsoid has
        // treated <nowiki>s as core functionality with lean markup (no about,
        // no data-mw, custom typeof).
        //
        // We'll keep this hardcoded to avoid exposing the functionality to
        // other native extensions until it's needed.
        if ( $extensionName !== 'nowiki' ) {
            if ( !$domFragment->hasChildNodes() ) {
                // RT extensions expanding to nothing.
                $domFragment->appendChild(
                    $domFragment->ownerDocument->createElement( 'link' )
                );
            }
            // Wrap the top-level nodes so that we have a firstNode element
            // to annotate with the typeof and to apply about ids.
            PipelineUtils::addSpanWrappers( $domFragment->childNodes );
            // Now get the firstNode
            $firstNode = $domFragment->firstChild;
            DOMUtils::assertElt( $firstNode );
            // Adds the wrapper attributes to the first element
            DOMUtils::addTypeOf( $firstNode, "mw:Extension/{$extensionName}" );
            // FIXME: What happens if $firstNode is template generated, since
            // they have higher precedence?  These questions and more in T214241
            Assert::invariant(
                !DOMUtils::hasTypeOf( $firstNode, 'mw:Transclusion' ),
                'First node of extension content is transcluded.'
            );
            if ( count( $errors ) > 0 ) {
                DOMUtils::addTypeOf( $firstNode, 'mw:Error' );
                $argDict->errors = $errors;
            }
            // Add about to all wrapper tokens.
            $about = $env->newAboutId();
            $n = $firstNode;
            while ( $n ) {
                $n->setAttribute( 'about', $about );
                $n = $n->nextSibling;
            }
            // Set data-mw
            // FIXME: Similar to T214241, we're clobbering $firstNode
            DOMDataUtils::setDataMw( $firstNode, $argDict );
            // Update data-parsoid
            $dp = DOMDataUtils::getDataParsoid( $firstNode );
            $dp->tsr = clone $extToken->dataAttribs->tsr;
            $dp->src = $extToken->dataAttribs->src;
            DOMDataUtils::setDataParsoid( $firstNode, $dp );
        }
        return PipelineUtils::tunnelDOMThroughTokens(
            $env, $extToken, $domFragment, $opts
        );
    }
    /**
     * @inheritDoc
     */
    public function onTag( Token $token ): ?TokenHandlerResult {
        return $token->getName() === 'extension' ? $this->onExtension( $token ) : null;
    }
}