Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.59% covered (success)
93.59%
73 / 78
57.14% covered (warning)
57.14%
4 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
LegacyTagHandler
93.59% covered (success)
93.59%
73 / 78
57.14% covered (warning)
57.14%
4 / 7
24.15
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
 handle
97.06% covered (success)
97.06%
33 / 34
0.00% covered (danger)
0.00%
0 / 1
6
 render
n/a
0 / 0
n/a
0 / 0
0
 saveData
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
4.01
 finalParseStep
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
8
 getTargetLanguage
62.50% covered (warning)
62.50%
5 / 8
0.00% covered (danger)
0.00%
0 / 1
3.47
 getTargetLanguageCode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOutput
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 *
4 * @license MIT
5 * @file
6 *
7 * @author Yuri Astrakhan
8 */
9
10namespace Kartographer\Tag;
11
12use FormatJson;
13use Kartographer\ParserFunctionTracker;
14use Kartographer\PartialWikitextParser;
15use Kartographer\SimpleStyleParser;
16use Kartographer\State;
17use Language;
18use MediaWiki\Config\Config;
19use MediaWiki\Config\ConfigException;
20use MediaWiki\Languages\LanguageNameUtils;
21use MediaWiki\Logger\LoggerFactory;
22use MediaWiki\Title\Title;
23use Parser;
24use PPFrame;
25use Wikimedia\Parsoid\Core\ContentMetadataCollector;
26
27/**
28 * Base class for all <map...> tags
29 *
30 * @license MIT
31 */
32abstract class LegacyTagHandler {
33
34    /**
35     * Lower case name of the XML-style parser tag, e.g. "mapframe". Currently expected to start
36     * with "map…" by the {@see State} class.
37     */
38    public const TAG = '';
39
40    protected MapTagArgumentValidator $args;
41    protected Config $config;
42    protected Parser $parser;
43    private Language $targetLanguage;
44    private LanguageNameUtils $languageCodeValidator;
45
46    public function __construct(
47        Config $config,
48        LanguageNameUtils $languageCodeValidator
49    ) {
50        $this->config = $config;
51        $this->languageCodeValidator = $languageCodeValidator;
52    }
53
54    /**
55     * Entry point for all tags
56     *
57     * @param string|null $input
58     * @param array<string,string> $args
59     * @param Parser $parser
60     * @param PPFrame $frame
61     * @return string
62     */
63    public function handle( ?string $input, array $args, Parser $parser, PPFrame $frame ): string {
64        $mapServer = $this->config->get( 'KartographerMapServer' );
65        if ( !$mapServer ) {
66            throw new ConfigException( '$wgKartographerMapServer doesn\'t have a default, please set your own' );
67        }
68
69        $this->parser = $parser;
70        // Can only be StubUserLang on special pages, but these can't contain <map…> tags
71        $this->targetLanguage = $parser->getTargetLanguage();
72        $options = $parser->getOptions();
73        $isPreview = $options->getIsPreview() || $options->getIsSectionPreview();
74        $parserOutput = $parser->getOutput();
75
76        $parserOutput->addModuleStyles( [ 'ext.kartographer.style' ] );
77        $parserOutput->addExtraCSPDefaultSrc( $mapServer );
78        $state = State::getOrCreate( $parserOutput );
79        $state->incrementUsage( static::TAG );
80
81        $this->args = new MapTagArgumentValidator(
82            static::TAG,
83            $args,
84            $this->config,
85            $this->getTargetLanguage(),
86            $this->languageCodeValidator
87        );
88        $status = $this->args->status;
89        $geometries = [];
90        if ( $status->isOK() ) {
91            $status = SimpleStyleParser::newFromParser( $parser, $frame )->parse( $input );
92            if ( $status->isOK() ) {
93                $geometries = $status->getValue()['data'];
94            }
95        }
96
97        if ( !$status->isGood() ) {
98            $state->incrementBrokenTags();
99            State::setState( $parserOutput, $state );
100
101            $errorReporter = new ErrorReporter( $this->getTargetLanguageCode() );
102            return $errorReporter->getHtml( $status, static::TAG );
103        }
104
105        $this->saveData( $state, $geometries );
106
107        $result = $this->render( new PartialWikitextParser( $parser, $frame ), !$isPreview );
108
109        State::setState( $parserOutput, $state );
110        return $result;
111    }
112
113    /**
114     * When overridden in a descendant class, returns tag HTML
115     *
116     * @param PartialWikitextParser $parser
117     * @param bool $serverMayRenderOverlays If the map server should attempt to render GeoJSON
118     *  overlays via their group id
119     * @return string
120     */
121    abstract protected function render( PartialWikitextParser $parser, bool $serverMayRenderOverlays ): string;
122
123    protected function saveData( State $state, array $geometries ): void {
124        $state->addRequestedGroups( $this->args->showGroups );
125
126        if ( !$geometries ) {
127            return;
128        }
129
130        // Merge existing data with the new tag's data under the same group name
131
132        // For all GeoJSON items whose marker-symbol value begins with '-counter' and '-letter',
133        // recursively replace them with an automatically incremented marker icon.
134        $counters = $state->getCounters();
135        $marker = SimpleStyleParser::updateMarkerSymbolCounters( $geometries, $counters );
136        if ( $marker ) {
137            $this->args->setFirstMarkerProperties( ...$marker );
138        }
139        $state->setCounters( $counters );
140
141        if ( $this->args->groupId === null ) {
142            // This hash calculation MUST be the same as in ParsoidDomProcessor::wtPostprocess
143            $groupId = '_' . sha1( FormatJson::encode( $geometries, false, FormatJson::ALL_OK ) );
144            $this->args->groupId = $groupId;
145            $this->args->showGroups[] = $groupId;
146            // no need to array_unique() because it's impossible to manually add a private group
147        } else {
148            $groupId = (string)$this->args->groupId;
149        }
150
151        $state->addData( $groupId, $geometries );
152    }
153
154    /**
155     * Handles the last step of parse process
156     *
157     * @param State $state
158     * @param ContentMetadataCollector $parserOutput
159     * @param bool $outputAllLiveData
160     * @param ParserFunctionTracker $tracker
161     */
162    public static function finalParseStep(
163        State $state,
164        ContentMetadataCollector $parserOutput,
165        bool $outputAllLiveData,
166        ParserFunctionTracker $tracker
167    ): void {
168        foreach ( $state->getUsages() as $key => $count ) {
169            // Resulting page property names are "kartographer_links" and "kartographer_frames"
170            $name = 'kartographer_' . preg_replace( '/^map/', '', $key );
171            $parserOutput->setNumericPageProperty( $name, $count );
172        }
173
174        $tracker->addTrackingCategories( [
175            'kartographer-broken-category' => $state->hasBrokenTags(),
176            'kartographer-tracking-category' => $state->hasValidTags(),
177        ] );
178
179        // https://phabricator.wikimedia.org/T145615 - include all data in previews
180        $data = $state->getData();
181        if ( $data && $outputAllLiveData ) {
182            $parserOutput->setJsConfigVar( 'wgKartographerLiveData', $data );
183        } else {
184            $interact = $state->getInteractiveGroups();
185            $requested = $state->getRequestedGroups();
186            if ( $interact || $requested ) {
187                $liveData = array_intersect_key( $data, array_flip( $interact ) );
188                // Prevent pointless API requests for missing groups
189                foreach ( $requested as $groupId ) {
190                    if ( !isset( $data[$groupId] ) ) {
191                        $liveData[$groupId] = [];
192                    }
193                }
194                $parserOutput->setJsConfigVar( 'wgKartographerLiveData', (object)$liveData );
195            }
196        }
197    }
198
199    private function getTargetLanguage(): Language {
200        // Log if the user language is different from the page language (T311592)
201        $page = $this->parser->getPage();
202        if ( $page ) {
203            $pageLang = Title::castFromPageReference( $page )->getPageLanguage();
204            if ( $this->targetLanguage->getCode() !== $pageLang->getCode() ) {
205                LoggerFactory::getInstance( 'Kartographer' )->notice( 'Target language (' .
206                    $this->targetLanguage->getCode() . ') is different than page language (' .
207                    $pageLang->getCode() . ') (T311592)' );
208            }
209        }
210
211        return $this->targetLanguage;
212    }
213
214    protected function getTargetLanguageCode(): string {
215        return $this->getTargetLanguage()->getCode();
216    }
217
218    protected function getOutput(): ContentMetadataCollector {
219        return $this->parser->getOutput();
220    }
221
222}