Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
80.00% |
48 / 60 |
|
33.33% |
3 / 9 |
CRAP | |
0.00% |
0 / 1 |
MapTagArgumentValidator | |
80.00% |
48 / 60 |
|
33.33% |
3 / 9 |
43.25 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
2 | |||
parseArgs | |
96.67% |
29 / 30 |
|
0.00% |
0 / 1 |
15 | |||
parseGroups | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
hasCoordinates | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
usesAutoPosition | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
isValidLanguageCode | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
12 | |||
getLanguageCodeWithDefaultFallback | |
75.00% |
3 / 4 |
|
0.00% |
0 / 1 |
2.06 | |||
setFirstMarkerProperties | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
getTextWithFallback | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace Kartographer\Tag; |
4 | |
5 | use Language; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\Languages\LanguageNameUtils; |
8 | use StatusValue; |
9 | use stdClass; |
10 | |
11 | /** |
12 | * Validator and preprocessor for all arguments that can be used in <mapframe> and <maplink> tags. |
13 | * As of now this class intentionally knows everything about both tags. |
14 | * |
15 | * @license MIT |
16 | */ |
17 | class MapTagArgumentValidator { |
18 | |
19 | public StatusValue $status; |
20 | private Tag $args; |
21 | private Config $config; |
22 | private Language $defaultLanguage; |
23 | private ?LanguageNameUtils $languageCodeValidator; |
24 | |
25 | public ?float $lat; |
26 | public ?float $lon; |
27 | /** @var int|null Typically a number from 0 to 19 */ |
28 | public ?int $zoom; |
29 | /** @var string One of "osm-intl" or "osm" */ |
30 | public string $mapStyle; |
31 | /** @var string|null Number of pixels (without a unit) or "full" */ |
32 | public ?string $width = null; |
33 | public ?int $height; |
34 | /** @var string One of "left", "center", "right", or "none" */ |
35 | public string $align; |
36 | public bool $frameless; |
37 | public string $cssClass; |
38 | /** @var string|null Language code as specified by the user, null if none or invalid */ |
39 | public ?string $specifiedLangCode = null; |
40 | public ?string $text; |
41 | private ?string $fallbackText = null; |
42 | public string $firstMarkerColor = ''; |
43 | |
44 | /** |
45 | * @var string|null Currently parsed group identifier from the group="…" attribute. Only allowed |
46 | * in …WikivoyageMode. Otherwise a private, auto-generated identifier starting with "_". |
47 | */ |
48 | public ?string $groupId = null; |
49 | /** @var string[] List of group identifiers to show */ |
50 | public array $showGroups = []; |
51 | |
52 | /** |
53 | * @param string $tag Tag name, e.g. "maplink" |
54 | * @param array<string,string> $args |
55 | * @param Config $config |
56 | * @param Language $defaultLanguage |
57 | * @param LanguageNameUtils|null $languageCodeValidator |
58 | */ |
59 | public function __construct( |
60 | string $tag, |
61 | array $args, |
62 | Config $config, |
63 | Language $defaultLanguage, |
64 | LanguageNameUtils $languageCodeValidator = null |
65 | ) { |
66 | $this->status = StatusValue::newGood(); |
67 | $this->args = new Tag( $tag, $args, $this->status ); |
68 | $this->config = $config; |
69 | $this->defaultLanguage = $defaultLanguage; |
70 | $this->languageCodeValidator = $languageCodeValidator; |
71 | |
72 | $this->parseArgs(); |
73 | if ( $config->get( 'KartographerWikivoyageMode' ) ) { |
74 | $this->parseGroups(); |
75 | } |
76 | } |
77 | |
78 | private function parseArgs(): void { |
79 | // Required arguments |
80 | if ( $this->args->name === LegacyMapFrame::TAG ) { |
81 | foreach ( [ 'width', 'height' ] as $required ) { |
82 | if ( !$this->args->has( $required ) ) { |
83 | $this->status->fatal( 'kartographer-error-missing-attr', $required ); |
84 | } |
85 | } |
86 | |
87 | // @todo: should these have defaults? |
88 | $this->width = $this->args->getString( 'width', '/^(\d+|([1-9]\d?|100)%|full)$/' ); |
89 | $this->height = $this->args->getInt( 'height' ); |
90 | |
91 | // @todo: deprecate old syntax completely |
92 | if ( $this->width && str_ends_with( $this->width, '%' ) ) { |
93 | $this->width = $this->width === '100%' ? 'full' : '300'; |
94 | } |
95 | } |
96 | |
97 | // Arguments valid for both <mapframe> and <maplink> |
98 | $this->lat = $this->args->getFloat( 'latitude' ); |
99 | $this->lon = $this->args->getFloat( 'longitude' ); |
100 | if ( $this->status->isOK() && ( ( $this->lat === null ) xor ( $this->lon === null ) ) ) { |
101 | $this->lat = null; |
102 | $this->lon = null; |
103 | $this->status->fatal( 'kartographer-error-latlon' ); |
104 | } |
105 | |
106 | $this->zoom = $this->args->getInt( 'zoom' ); |
107 | $regexp = '/^(' . implode( '|', $this->config->get( 'KartographerStyles' ) ) . ')$/'; |
108 | $defaultStyle = $this->config->get( 'KartographerDfltStyle' ); |
109 | $this->mapStyle = $this->args->getString( 'mapstyle', $regexp ) ?? $defaultStyle; |
110 | $this->text = $this->args->getString( 'text' ); |
111 | |
112 | $lang = $this->args->getString( 'lang' ); |
113 | // If the specified language code is invalid, behave as if no language was specified |
114 | if ( $lang && $this->isValidLanguageCode( $lang ) ) { |
115 | $this->specifiedLangCode = $lang; |
116 | } |
117 | |
118 | // Arguments valid only for one of the two tags, but all optional anyway |
119 | if ( $this->width === 'full' ) { |
120 | $this->align = 'none'; |
121 | } elseif ( $this->width !== null ) { |
122 | $defaultAlign = $this->defaultLanguage->alignEnd(); |
123 | $this->align = $this->args->getString( 'align', '/^(left|center|right)$/' ) ?? $defaultAlign; |
124 | } |
125 | $this->frameless = ( $this->text === null || $this->text === '' ) && |
126 | $this->args->getString( 'frameless' ) !== null; |
127 | $this->cssClass = $this->args->getString( 'class', '/^([a-z][\w-]*)?$/i' ) ?? ''; |
128 | } |
129 | |
130 | private function parseGroups(): void { |
131 | $this->groupId = $this->args->getString( 'group', '/^[\w ]+$/u' ); |
132 | |
133 | $show = $this->args->getString( 'show', '/^([\w ]+(\s*,\s*+[\w ]+)*)?$/u' ); |
134 | if ( $show ) { |
135 | $this->showGroups = array_map( 'trim', explode( ',', $show ) ); |
136 | } |
137 | |
138 | // Make sure the current group is shown for this map, even if there is no geojson |
139 | // Private group will be added during the save, as it requires hash calculation |
140 | if ( $this->groupId !== null ) { |
141 | $this->showGroups[] = $this->groupId; |
142 | } |
143 | |
144 | // Make sure there are no group name duplicates |
145 | $this->showGroups = array_values( array_unique( $this->showGroups ) ); |
146 | } |
147 | |
148 | /** |
149 | * @return bool If a complete pair of coordinates is given, e.g. to render a <maplink> label |
150 | */ |
151 | public function hasCoordinates(): bool { |
152 | return $this->lat !== null && $this->lon !== null; |
153 | } |
154 | |
155 | /** |
156 | * @return bool If the map relies on Kartotherian's auto-position feature (extracting a bounding |
157 | * box from the GeoJSON) instead of the arguments alone (coordinates and zoom) |
158 | */ |
159 | public function usesAutoPosition(): bool { |
160 | return $this->zoom === null || !$this->hasCoordinates(); |
161 | } |
162 | |
163 | private function isValidLanguageCode( string $code ): bool { |
164 | return $code === 'local' || |
165 | // Everything is valid without a validator, should only be used in test scenarios |
166 | !$this->languageCodeValidator || |
167 | $this->languageCodeValidator->isKnownLanguageTag( $code ); |
168 | } |
169 | |
170 | public function getLanguageCodeWithDefaultFallback(): string { |
171 | return $this->specifiedLangCode ?? |
172 | ( $this->config->get( 'KartographerUsePageLanguage' ) ? |
173 | $this->defaultLanguage->getCode() : |
174 | 'local' |
175 | ); |
176 | } |
177 | |
178 | public function setFirstMarkerProperties( ?string $fallbackText, stdClass $properties ): void { |
179 | $this->fallbackText = $fallbackText; |
180 | |
181 | if ( $this->config->get( 'KartographerUseMarkerStyle' ) && |
182 | isset( $properties->{'marker-color'} ) && |
183 | // JsonSchema already validates this value for us, however this regex will also fail |
184 | // if the color is invalid |
185 | preg_match( '/^#?((?:[\da-f]{3}){1,2})$/i', $properties->{'marker-color'}, $m ) |
186 | ) { |
187 | // Simplestyle allows colors "with or without the # prefix". Enforce it here. |
188 | $this->firstMarkerColor = '#' . $m[1]; |
189 | } |
190 | } |
191 | |
192 | public function getTextWithFallback(): ?string { |
193 | return $this->text ?? $this->fallbackText; |
194 | } |
195 | |
196 | } |