Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
ParsoidDomProcessor
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 6
1056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 wtPostprocess
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
90
 processKartographerNode
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
272
 updateSrc
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 updateSrcSet
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 updateUrl
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace Kartographer\Tag;
4
5use Kartographer\ParsoidUtils;
6use Kartographer\SimpleStyleParser;
7use MediaWiki\Config\Config;
8use MediaWiki\Json\FormatJson;
9use MediaWiki\Parser\ParserOutputStringSets;
10use MediaWiki\Title\Title;
11use stdClass;
12use Wikimedia\Parsoid\DOM\Element;
13use Wikimedia\Parsoid\DOM\Node;
14use Wikimedia\Parsoid\Ext\DOMDataUtils;
15use Wikimedia\Parsoid\Ext\DOMProcessor;
16use Wikimedia\Parsoid\Ext\ParsoidExtensionAPI;
17use Wikimedia\Parsoid\Utils\DOMCompat;
18use Wikimedia\Parsoid\Utils\DOMTraverser;
19
20/**
21 * @license MIT
22 */
23class ParsoidDomProcessor extends DOMProcessor {
24
25    private Config $config;
26
27    public function __construct(
28        Config $config
29    ) {
30        $this->config = $config;
31    }
32
33    /** @inheritDoc */
34    public function wtPostprocess( ParsoidExtensionAPI $extApi, Node $root, array $options ): void {
35        if ( !( $root instanceof Element ) ) {
36            return;
37        }
38
39        $state = [
40            'broken' => 0,
41            'interactiveGroups' => [],
42            'requestedGroups' => [],
43            'counters' => [],
44            'maplinks' => 0,
45            'mapframes' => 0,
46            'data' => [],
47        ];
48
49        // FIXME This only selects data-mw-kartographer nodes without exploring HTML that may be stored in
50        // attributes. We need to expand the traversal to find these as well.
51        $kartnodes = DOMCompat::querySelectorAll( $root, '*[data-mw-kartographer]' );
52
53        // let's avoid adding data to the page if there's no kartographer nodes!
54        if ( !$kartnodes ) {
55            return;
56        }
57
58        $mapServer = $this->config->get( 'KartographerMapServer' );
59        $extApi->getMetadata()->addModuleStyles( [ 'ext.kartographer.style' ] );
60        $extApi->getMetadata()->appendOutputStrings( ParserOutputStringSets::EXTRA_CSP_DEFAULT_SRC, [ $mapServer ] );
61
62        $traverser = new DOMTraverser( false, true );
63        $traverser->addHandler( null, function ( $node ) use ( &$state, $extApi ) {
64            if ( $node instanceof Element && $node->hasAttribute( 'data-mw-kartographer' ) ) {
65                $this->processKartographerNode( $node, $extApi, $state );
66            }
67            return true;
68        } );
69        $traverser->traverse( $extApi, $root );
70
71        if ( $state['broken'] > 0 ) {
72            ParsoidUtils::addCategory( $extApi, 'kartographer-broken-category' );
73        }
74        if ( $state['maplinks'] + $state['mapframes'] > $state['broken'] ) {
75            ParsoidUtils::addCategory( $extApi, 'kartographer-tracking-category' );
76        }
77
78        $interactive = $state['interactiveGroups'];
79        $state['interactiveGroups'] = array_keys( $state['interactiveGroups'] );
80        $state['requestedGroups'] = array_keys( $state['requestedGroups'] );
81        $state['parsoidIntVersion'] = $this->config->get( 'KartographerParsoidVersion' );
82        $extApi->getMetadata()->setExtensionData( 'kartographer', $state );
83
84        foreach ( $interactive as $req ) {
85            if ( !isset( $state['data'][$req] ) ) {
86                $state['data'][$req] = [];
87            }
88        }
89        $extApi->getMetadata()->setJsConfigVar( 'wgKartographerLiveData', $state['data'] ?? [] );
90    }
91
92    private function processKartographerNode( Element $kartnode, ParsoidExtensionAPI $extApi, array &$state ): void {
93        $tagName = $kartnode->getAttribute( 'data-mw-kartographer' ) ?? '';
94        if ( $tagName !== '' ) {
95            $state[$tagName . 's' ]++;
96        }
97
98        $markerStr = $kartnode->getAttribute( 'data-kart' );
99        $kartnode->removeAttribute( 'data-kart' );
100        if ( $markerStr === 'error' ) {
101            $state['broken']++;
102            return;
103        }
104        $marker = json_decode( $markerStr ?? '' );
105
106        $state['requestedGroups'] = array_merge( $state['requestedGroups'], $marker->showGroups ?? [] );
107        if ( $tagName === ParsoidMapFrame::TAG ) {
108            $state['interactiveGroups'] = array_merge( $state['interactiveGroups'], $marker->showGroups ?? [] );
109        }
110
111        if ( !$marker || !$marker->geometries || !$marker->geometries[0] instanceof stdClass ) {
112            return;
113        }
114        [ $counter, $props ] = SimpleStyleParser::updateMarkerSymbolCounters( $marker->geometries,
115            $state['counters'] );
116        if ( $tagName === ParsoidMapLink::TAG && $counter ) {
117            if ( !isset( DOMDataUtils::getDataMw( $kartnode )->attrs->text ) ) {
118                $text = $extApi->getTopLevelDoc()->createTextNode( $counter );
119                $kartnode->replaceChild( $text, $kartnode->firstChild );
120            }
121        }
122
123        $data = $marker->geometries;
124
125        if ( $counter ) {
126            // If we have a counter, we update the marker data prior to encoding the groupId, and we remove
127            // the (previously computed) groupId from showGroups
128            if ( ( $marker->groupId[0] ?? '' ) === '_' ) {
129                // TODO unclear if this is necessary or if we could simply set it to [].
130                $marker->showGroups = array_values( array_diff( $marker->showGroups, [ $marker->groupId ] ) );
131                $marker->groupId = null;
132            }
133        }
134
135        $groupId = $marker->groupId ?? null;
136        if ( $groupId === null ) {
137            // This hash calculation MUST be the same as in LegacyTagHandler::updateState
138            $groupId = '_' . sha1( FormatJson::encode( $marker->geometries, false, FormatJson::ALL_OK ) );
139            $marker->groupId = $groupId;
140            $marker->showGroups[] = $groupId;
141            $kartnode->setAttribute( 'data-overlays', FormatJson::encode( $marker->showGroups ) );
142            $img = $kartnode->firstChild;
143            // this should always be the case, but let make phan aware of it
144            if ( $img instanceof Element ) {
145                $this->updateSrc( $img, $groupId, $extApi );
146                $this->updateSrcSet( $img, $groupId, $extApi );
147            }
148        }
149
150        // There is no way to ever add anything to a private group starting with `_`
151        if ( isset( $state['data'][$groupId] ) && !str_starts_with( $groupId, '_' ) ) {
152            $state['data'][$groupId] = array_merge( $state['data'][$groupId], $data );
153        } else {
154            $state['data'][$groupId] = $data;
155        }
156    }
157
158    private function updateSrc( Element $firstChild, string $groupId, ParsoidExtensionAPI $extApi ): void {
159        $src = $firstChild->getAttribute( 'src' ) ?? '';
160        if ( $src !== '' ) {
161            $src = $this->updateUrl( $src, $extApi, $groupId );
162            $firstChild->setAttribute( 'src', $src );
163        }
164    }
165
166    private function updateSrcSet( Element $firstChild, string $groupId, ParsoidExtensionAPI $extApi ): void {
167        $srcset = $firstChild->getAttribute( 'srcset' ) ?? '';
168        if ( $srcset !== '' ) {
169            $arr = explode( ', ', $srcset );
170            $srcsets = [];
171            foreach ( $arr as $plop ) {
172                $toks = explode( ' ', $plop );
173                $toks[0] = $this->updateUrl( $toks[0], $extApi, $groupId );
174                $srcsets[] = implode( ' ', $toks );
175            }
176            $firstChild->setAttribute( 'srcset', implode( ', ', $srcsets ) );
177        }
178    }
179
180    private function updateUrl( string $src, ParsoidExtensionAPI $extApi, string $groupId ): string {
181        $url = explode( '?', $src );
182        $attrs = wfCgiToArray( $url[1] );
183
184        $linkTarget = $extApi->getPageConfig()->getLinkTarget();
185        $pagetitle = Title::newFromLinkTarget( $linkTarget )->getPrefixedDBkey();
186        $revisionId = $extApi->getPageConfig()->getRevisionId();
187        $attrs = array_merge( $attrs,
188            MapFrameAttributeGenerator::getUrlAttrs( $this->config, $pagetitle, $revisionId, [ $groupId ] )
189        );
190
191        return $url[0] . '?' . wfArrayToCgi( $attrs );
192    }
193
194}