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