Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
51.09% |
47 / 92 |
|
41.67% |
5 / 12 |
CRAP | |
0.00% |
0 / 1 |
StringMatcher | |
51.09% |
47 / 92 |
|
41.67% |
5 / 12 |
119.75 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
init | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 | |||
getValidKeyChars | |
27.27% |
3 / 11 |
|
0.00% |
0 / 1 |
3.54 | |||
setConf | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
matches | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
mangle | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
mangleList | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
mangleArray | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
unmangle | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
3 | |||
unmangleList | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
unmangleArray | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getExtraSchema | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | declare( strict_types = 1 ); |
3 | |
4 | namespace MediaWiki\Extension\Translate\MessageProcessing; |
5 | |
6 | use MediaWiki\Extension\Translate\MessageGroupConfiguration\MetaYamlSchemaExtender; |
7 | use MediaWiki\Title\Title; |
8 | |
9 | /** |
10 | * The versatile default implementation of StringMangler interface. |
11 | * It supports exact matches and patterns with any-wildcard (*). |
12 | * All matching strings are prefixed with the same prefix. |
13 | * |
14 | * @author Niklas Laxström |
15 | * @license GPL-2.0-or-later |
16 | */ |
17 | class StringMatcher implements StringMangler, MetaYamlSchemaExtender { |
18 | /** @var string Prefix for mangled message keys */ |
19 | protected string $sPrefix = ''; |
20 | /** @var string[] Exact message keys */ |
21 | protected array $aExact = []; |
22 | /** @var int[] Patterns of type foo* */ |
23 | protected array $aPrefix = []; |
24 | /** @var string[] Patterns that contain wildcard anywhere else than in the end */ |
25 | protected array $aRegex = []; |
26 | |
27 | public function __construct( string $prefix = '', array $patterns = [] ) { |
28 | $this->sPrefix = $prefix; |
29 | $this->init( $patterns ); |
30 | } |
31 | |
32 | /** |
33 | * Preprocesses the patterns. |
34 | * |
35 | * They are split into exact keys, prefix matches and pattern matches to |
36 | * speed up matching process. |
37 | * |
38 | * @param string[] $strings Key patterns. |
39 | */ |
40 | protected function init( array $strings ): void { |
41 | foreach ( $strings as $string ) { |
42 | $pos = strpos( $string, '*' ); |
43 | if ( $pos === false ) { |
44 | $this->aExact[] = $string; |
45 | } elseif ( $pos + 1 === strlen( $string ) ) { |
46 | $prefix = substr( $string, 0, -1 ); |
47 | $this->aPrefix[$prefix] = strlen( $prefix ); |
48 | } else { |
49 | $string = str_replace( '\\*', '.+', preg_quote( $string, '/' ) ); |
50 | $this->aRegex[] = "/^$string$/"; |
51 | } |
52 | } |
53 | } |
54 | |
55 | protected static function getValidKeyChars(): string { |
56 | static $valid = null; |
57 | if ( $valid === null ) { |
58 | $valid = strtr( |
59 | Title::legalChars(), |
60 | [ |
61 | '=' => '', // equals sign, which is itself usef for escaping |
62 | '&' => '', // ampersand, for entities |
63 | '%' => '', // percent sign, which is used in URL encoding |
64 | ] |
65 | ); |
66 | } |
67 | |
68 | return $valid; |
69 | } |
70 | |
71 | /** @inheritDoc */ |
72 | public function setConf( array $conf ): void { |
73 | $this->sPrefix = $conf['prefix']; |
74 | $this->init( $conf['patterns'] ); |
75 | } |
76 | |
77 | /** @inheritDoc */ |
78 | public function matches( string $key ): bool { |
79 | if ( in_array( $key, $this->aExact ) ) { |
80 | return true; |
81 | } |
82 | |
83 | foreach ( $this->aPrefix as $prefix => $len ) { |
84 | if ( strncmp( $key, $prefix, $len ) === 0 ) { |
85 | return true; |
86 | } |
87 | } |
88 | |
89 | foreach ( $this->aRegex as $regex ) { |
90 | if ( preg_match( $regex, $key ) ) { |
91 | return true; |
92 | } |
93 | } |
94 | |
95 | return false; |
96 | } |
97 | |
98 | /** @inheritDoc */ |
99 | public function mangle( string $key ): string { |
100 | if ( $this->matches( $key ) ) { |
101 | $key = $this->sPrefix . $key; |
102 | } |
103 | |
104 | $escaper = static function ( $match ) { |
105 | $esc = ''; |
106 | foreach ( str_split( $match[0] ) as $c ) { |
107 | $esc .= '=' . sprintf( '%02X', ord( $c ) ); |
108 | } |
109 | return $esc; |
110 | }; |
111 | |
112 | // Apply a "quoted-printable"-like escaping |
113 | $valid = self::getValidKeyChars(); |
114 | $key = preg_replace_callback( "/[^$valid]/", $escaper, $key ); |
115 | // Additional limitations in MediaWiki, see MediaWikiTitleCodec::splitTitleString |
116 | $key = preg_replace_callback( '/(~~~|^[ _]|[ _]$|[ _]{2,}|^:)/', $escaper, $key ); |
117 | // TODO: length check + truncation |
118 | // TODO: forbid path travels |
119 | |
120 | return $key; |
121 | } |
122 | |
123 | /** @inheritDoc */ |
124 | public function mangleList( array $list ): array { |
125 | return array_map( [ $this, 'mangle' ], $list ); |
126 | } |
127 | |
128 | /** @inheritDoc */ |
129 | public function mangleArray( array $array ): array { |
130 | $out = []; |
131 | foreach ( $array as $key => $value ) { |
132 | $out[$this->mangle( (string)$key )] = $value; |
133 | } |
134 | |
135 | return $out; |
136 | } |
137 | |
138 | /** @inheritDoc */ |
139 | public function unmangle( string $key ): string { |
140 | // Unescape the "quoted-printable"-like escaping, |
141 | // which is applied in mangle |
142 | $unescapedString = preg_replace_callback( |
143 | '/=([A-F0-9]{2})/', |
144 | static function ( $match ) { |
145 | return chr( hexdec( $match[1] ) ); |
146 | }, |
147 | $key |
148 | ); |
149 | |
150 | if ( strncmp( $unescapedString, $this->sPrefix, strlen( $this->sPrefix ) ) === 0 ) { |
151 | $unmangled = substr( $unescapedString, strlen( $this->sPrefix ) ); |
152 | |
153 | // Check if this string should be mangled / un-mangled to begin with |
154 | if ( $this->matches( $unmangled ) ) { |
155 | return $unmangled; |
156 | } |
157 | } |
158 | return $unescapedString; |
159 | } |
160 | |
161 | /** @inheritDoc */ |
162 | public function unmangleList( array $list ): array { |
163 | foreach ( $list as $index => $key ) { |
164 | $list[$index] = $this->unmangle( $key ); |
165 | } |
166 | |
167 | return $list; |
168 | } |
169 | |
170 | /** @inheritDoc */ |
171 | public function unmangleArray( array $array ): array { |
172 | $out = []; |
173 | foreach ( $array as $key => $value ) { |
174 | $out[$this->unmangle( $key )] = $value; |
175 | } |
176 | |
177 | return $out; |
178 | } |
179 | |
180 | /** @inheritDoc */ |
181 | public static function getExtraSchema(): array { |
182 | $schema = [ |
183 | 'root' => [ |
184 | '_type' => 'array', |
185 | '_children' => [ |
186 | 'MANGLER' => [ |
187 | '_type' => 'array', |
188 | '_children' => [ |
189 | 'prefix' => [ |
190 | '_type' => 'text', |
191 | '_not_empty' => true, |
192 | ], |
193 | 'patterns' => [ |
194 | '_type' => 'array', |
195 | '_required' => true, |
196 | '_ignore_extra_keys' => true, |
197 | '_children' => [], |
198 | ], |
199 | ], |
200 | ], |
201 | ], |
202 | ], |
203 | ]; |
204 | |
205 | return $schema; |
206 | } |
207 | } |