Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
86.11% covered (warning)
86.11%
31 / 36
75.00% covered (warning)
75.00%
3 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
ExtractBody
86.11% covered (warning)
86.11%
31 / 36
75.00% covered (warning)
75.00%
3 / 4
15.60
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 shouldRun
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expandRelativeAttrs
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
6
 transformText
66.67% covered (warning)
66.67%
10 / 15
0.00% covered (danger)
0.00%
0 / 1
8.81
1<?php
2
3namespace MediaWiki\OutputTransform\Stages;
4
5use MediaWiki\Config\ServiceOptions;
6use MediaWiki\Html\HtmlHelper;
7use MediaWiki\OutputTransform\ContentTextTransformStage;
8use MediaWiki\Parser\Parser;
9use MediaWiki\Parser\ParserOutput;
10use MediaWiki\Parser\Parsoid\ParsoidParser;
11use ParserOptions;
12use Psr\Log\LoggerInterface;
13use Wikimedia\RemexHtml\Serializer\SerializerNode;
14
15/**
16 * Applies base href, and strip everything but the <body>
17 * @internal
18 */
19class ExtractBody extends ContentTextTransformStage {
20
21    // @phan-suppress-next-line PhanUndeclaredTypeProperty
22    private ?\MobileContext $mobileContext;
23
24    public function __construct(
25        ServiceOptions $options, LoggerInterface $logger,
26        // @phan-suppress-next-line PhanUndeclaredTypeParameter
27        ?\MobileContext $mobileContext
28    ) {
29        parent::__construct( $options, $logger );
30        $this->mobileContext = $mobileContext;
31    }
32
33    public function shouldRun( ParserOutput $po, ?ParserOptions $popts, array $options = [] ): bool {
34        return ( $options['isParsoidContent'] ?? false );
35    }
36
37    private const EXPAND_ELEMENTS = [
38        'a' => true, 'img' => true, 'video' => true, 'audio' => true,
39    ];
40
41    private static function expandRelativeAttrs( string $text, string $baseHref, string $pageFragmentPrefix ): string {
42        // T350952: Expand relative links
43        // What we should be doing here is parsing as a title and then
44        // using Title::getLocalURL()
45        return HtmlHelper::modifyElements(
46            $text,
47            static function ( SerializerNode $node ): bool {
48                if ( !isset( self::EXPAND_ELEMENTS[$node->name] ) ) {
49                    return false;
50                }
51                $attr = $node->name === 'a' ? 'href' : 'resource';
52                return str_starts_with( $node->attrs[$attr] ?? '', './' );
53            },
54            static function ( SerializerNode $node ) use ( $baseHref, $pageFragmentPrefix ): SerializerNode {
55                $attr = $node->name === 'a' ? 'href' : 'resource';
56                $href = $node->attrs[$attr];
57                // Convert page fragment urls to true fragment urls
58                // This ensures that those fragments include any URL query params
59                // and resolve internally. (Ex: on pages with ?useparsoid=1,
60                // cite link fragments should not take you to a different page).
61                if ( $pageFragmentPrefix && str_starts_with( $href, $pageFragmentPrefix ) ) {
62                    $node->attrs[$attr] = substr( $href, strlen( $pageFragmentPrefix ) - 1 );
63                } else {
64                    $href = $baseHref . $href;
65                    $node->attrs[$attr] = wfExpandUrl( $href, PROTO_RELATIVE );
66                }
67                return $node;
68            }
69        );
70    }
71
72    protected function transformText( string $text, ParserOutput $po, ?ParserOptions $popts, array &$options ): string {
73        // T350952: temporary fix for subpage paths: use Parsoid's
74        // <base href> to expand relative links
75        $baseHref = '';
76        if ( preg_match( '{<base href=["\']([^"\']+)["\'][^>]+>}', $text, $matches ) === 1 ) {
77            $baseHref = $matches[1];
78            // @phan-suppress-next-line PhanUndeclaredClassMethod
79            if ( $this->mobileContext !== null && $this->mobileContext->usingMobileDomain() ) {
80                // @phan-suppress-next-line PhanUndeclaredClassMethod
81                $mobileUrl = $this->mobileContext->getMobileUrl( $baseHref );
82                if ( $mobileUrl !== false ) {
83                    $baseHref = $mobileUrl;
84                }
85            }
86        }
87        $title = $po->getExtensionData( ParsoidParser::PARSOID_TITLE_KEY );
88        if ( !$title ) {
89            // We don't think this should ever trigger, but being conservative
90            $this->logger->error( __METHOD__ . ": Missing title information in ParserOutput" );
91        }
92        $pageFragmentPrefix = "./" . $title . "#";
93        foreach ( $po->getIndicators() as $name => $html ) {
94            $po->setIndicator( $name, self::expandRelativeAttrs( $html, $baseHref, $pageFragmentPrefix ) );
95        }
96        $text = Parser::extractBody( $text );
97        return self::expandRelativeAttrs( $text, $baseHref, $pageFragmentPrefix );
98    }
99}