Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.30% |
130 / 135 |
|
70.00% |
7 / 10 |
CRAP | |
0.00% |
0 / 1 |
StatusFormatter | |
96.30% |
130 / 135 |
|
70.00% |
7 / 10 |
48 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
cleanParams | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getWikiText | |
96.67% |
29 / 30 |
|
0.00% |
0 / 1 |
9 | |||
getMessage | |
100.00% |
34 / 34 |
|
100.00% |
1 / 1 |
8 | |||
getPsr3MessageAndContext | |
100.00% |
24 / 24 |
|
100.00% |
1 / 1 |
11 | |||
getErrorMessage | |
87.50% |
21 / 24 |
|
0.00% |
0 / 1 |
10.20 | |||
getHTML | |
83.33% |
5 / 6 |
|
0.00% |
0 / 1 |
2.02 | |||
getErrorMessageArray | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
msg | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
msgInLang | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Status; |
22 | |
23 | use ApiMessage; |
24 | use ApiRawMessage; |
25 | use Language; |
26 | use MediaWiki\Language\RawMessage; |
27 | use MediaWiki\Message\Message; |
28 | use MediaWiki\Parser\ParserOutput; |
29 | use MediaWiki\StubObject\StubUserLang; |
30 | use MessageCache; |
31 | use MessageLocalizer; |
32 | use MessageSpecifier; |
33 | use StatusValue; |
34 | use UnexpectedValueException; |
35 | |
36 | /** |
37 | * Formatter for StatusValue objects. |
38 | * |
39 | * @since 1.42 |
40 | * |
41 | * @see StatusValue |
42 | */ |
43 | class StatusFormatter { |
44 | |
45 | private MessageLocalizer $messageLocalizer; |
46 | private MessageCache $messageCache; |
47 | |
48 | public function __construct( MessageLocalizer $messageLocalizer, MessageCache $messageCache ) { |
49 | $this->messageLocalizer = $messageLocalizer; |
50 | $this->messageCache = $messageCache; |
51 | } |
52 | |
53 | /** |
54 | * @param array $params |
55 | * @param array $options |
56 | * |
57 | * @return array |
58 | */ |
59 | private function cleanParams( array $params, array $options = [] ) { |
60 | $cleanCallback = $options['cleanCallback'] ?? null; |
61 | |
62 | if ( !$cleanCallback ) { |
63 | return $params; |
64 | } |
65 | $cleanParams = []; |
66 | foreach ( $params as $i => $param ) { |
67 | $cleanParams[$i] = call_user_func( $cleanCallback, $param ); |
68 | } |
69 | return $cleanParams; |
70 | } |
71 | |
72 | /** |
73 | * Get the error list as a wikitext formatted list |
74 | * |
75 | * @param StatusValue $status |
76 | * @param array $options An array of options, supporting the following keys: |
77 | * - 'shortContext' (string|false|null) A short enclosing context message name, to |
78 | * be used when there is a single error |
79 | * - 'longContext' (string|false|null) A long enclosing context message name, for a list |
80 | * - 'lang' (string|Language|StubUserLang|null) Language to use for processing messages |
81 | * - 'cleanCallback' (callable|null) A callback for sanitizing parameter values |
82 | * |
83 | * @return string |
84 | */ |
85 | public function getWikiText( StatusValue $status, array $options = [] ) { |
86 | $shortContext = $options['shortContext'] ?? null; |
87 | $longContext = $options['longContext'] ?? null; |
88 | $lang = $options['lang'] ?? null; |
89 | |
90 | $rawErrors = $status->getErrors(); |
91 | if ( count( $rawErrors ) === 0 ) { |
92 | if ( $status->isOK() ) { |
93 | $status->fatal( |
94 | 'internalerror_info', |
95 | __METHOD__ . " called for a good result, this is incorrect\n" |
96 | ); |
97 | } else { |
98 | $status->fatal( |
99 | 'internalerror_info', |
100 | __METHOD__ . ": Invalid result object: no error text but not OK\n" |
101 | ); |
102 | } |
103 | $rawErrors = $status->getErrors(); // just added a fatal |
104 | } |
105 | |
106 | if ( count( $rawErrors ) === 1 ) { |
107 | $s = $this->getErrorMessage( $rawErrors[0], $options )->plain(); |
108 | if ( $shortContext ) { |
109 | $s = $this->msgInLang( $shortContext, $lang, $s )->plain(); |
110 | } elseif ( $longContext ) { |
111 | $s = $this->msgInLang( $longContext, $lang, "* $s\n" )->plain(); |
112 | } |
113 | } else { |
114 | $errors = $this->getErrorMessageArray( $rawErrors, $options ); |
115 | foreach ( $errors as &$error ) { |
116 | $error = $error->plain(); |
117 | } |
118 | $s = '* ' . implode( "\n* ", $errors ) . "\n"; |
119 | if ( $longContext ) { |
120 | $s = $this->msgInLang( $longContext, $lang, $s )->plain(); |
121 | } elseif ( $shortContext ) { |
122 | $s = $this->msgInLang( $shortContext, $lang, "\n$s\n" )->plain(); |
123 | } |
124 | } |
125 | return $s; |
126 | } |
127 | |
128 | /** |
129 | * Get a bullet list of the errors as a Message object. |
130 | * |
131 | * $shortContext and $longContext can be used to wrap the error list in some text. |
132 | * $shortContext will be preferred when there is a single error; $longContext will be |
133 | * preferred when there are multiple ones. In either case, $1 will be replaced with |
134 | * the list of errors. |
135 | * |
136 | * $shortContext is assumed to use $1 as an inline parameter: if there is a single item, |
137 | * it will not be made into a list; if there are multiple items, newlines will be inserted |
138 | * around the list. |
139 | * $longContext is assumed to use $1 as a standalone parameter; it will always receive a list. |
140 | * |
141 | * If both parameters are missing, and there is only one error, no bullet will be added. |
142 | * |
143 | * @param StatusValue $status |
144 | * @param array $options An array of options, supporting the following keys: |
145 | * - 'shortContext' (string|false|null) A short enclosing context message name, to |
146 | * be used when there is a single error |
147 | * - 'longContext' (string|false|null) A long enclosing context message name, for a list |
148 | * - 'lang' (string|Language|StubUserLang|null) Language to use for processing messages |
149 | * - 'cleanCallback' (callable|null) A callback for sanitizing parameter values |
150 | * |
151 | * @return Message |
152 | */ |
153 | public function getMessage( StatusValue $status, array $options = [] ) { |
154 | $shortContext = $options['shortContext'] ?? null; |
155 | $longContext = $options['longContext'] ?? null; |
156 | $lang = $options['lang'] ?? null; |
157 | |
158 | $rawErrors = $status->getErrors(); |
159 | if ( count( $rawErrors ) === 0 ) { |
160 | if ( $status->isOK() ) { |
161 | $status->fatal( |
162 | 'internalerror_info', |
163 | __METHOD__ . " called for a good result, this is incorrect\n" |
164 | ); |
165 | } else { |
166 | $status->fatal( |
167 | 'internalerror_info', |
168 | __METHOD__ . ": Invalid result object: no error text but not OK\n" |
169 | ); |
170 | } |
171 | $rawErrors = $status->getErrors(); // just added a fatal |
172 | } |
173 | if ( count( $rawErrors ) === 1 ) { |
174 | $s = $this->getErrorMessage( $rawErrors[0], $options ); |
175 | if ( $shortContext ) { |
176 | $s = $this->msgInLang( $shortContext, $lang, $s ); |
177 | } elseif ( $longContext ) { |
178 | $wrapper = new RawMessage( "* \$1\n" ); |
179 | $wrapper->params( $s )->parse(); |
180 | $s = $this->msgInLang( $longContext, $lang, $wrapper ); |
181 | } |
182 | } else { |
183 | $msgs = $this->getErrorMessageArray( $rawErrors, $options ); |
184 | $msgCount = count( $msgs ); |
185 | |
186 | $s = new RawMessage( '* $' . implode( "\n* \$", range( 1, $msgCount ) ) ); |
187 | $s->params( $msgs )->parse(); |
188 | |
189 | if ( $longContext ) { |
190 | $s = $this->msgInLang( $longContext, $lang, $s ); |
191 | } elseif ( $shortContext ) { |
192 | $wrapper = new RawMessage( "\n\$1\n", [ $s ] ); |
193 | $wrapper->parse(); |
194 | $s = $this->msgInLang( $shortContext, $lang, $wrapper ); |
195 | } |
196 | } |
197 | |
198 | return $s; |
199 | } |
200 | |
201 | /** |
202 | * Try to convert the status to a PSR-3 friendly format. The output will be similar to |
203 | * getWikiText( false, false, 'en' ), but message parameters will be extracted into the |
204 | * context array with parameter names 'parameter1' etc. when possible. |
205 | * |
206 | * @return array A pair of (message, context) suitable for passing to a PSR-3 logger. |
207 | * @phan-return array{0:string,1:(int|float|string)[]} |
208 | */ |
209 | public function getPsr3MessageAndContext( StatusValue $status ): array { |
210 | $options = [ 'lang' => 'en' ]; |
211 | $errors = $status->getErrors(); |
212 | |
213 | if ( count( $errors ) === 1 ) { |
214 | // identical to getMessage( false, false, 'en' ) when there's just one error |
215 | $message = $this->getErrorMessage( $errors[0], [ 'lang' => 'en' ] ); |
216 | |
217 | $text = null; |
218 | if ( in_array( get_class( $message ), [ Message::class, ApiMessage::class ], true ) ) { |
219 | // Fall back to getWikiText for rawmessage, which is just a placeholder for non-translated text. |
220 | // Turning the entire message into a context parameter wouldn't be useful. |
221 | if ( $message->getKey() === 'rawmessage' ) { |
222 | return [ $this->getWikiText( $status, $options ), [] ]; |
223 | } |
224 | // $1,$2... will be left as-is when no parameters are provided. |
225 | $text = $this->msgInLang( $message->getKey(), 'en' )->plain(); |
226 | } elseif ( in_array( get_class( $message ), [ RawMessage::class, ApiRawMessage::class ], true ) ) { |
227 | $text = $message->getKey(); |
228 | } else { |
229 | // Unknown Message subclass, we can't be sure how it marks parameters. Fall back to getWikiText. |
230 | return [ $this->getWikiText( $status, $options ), [] ]; |
231 | } |
232 | |
233 | $context = []; |
234 | $i = 1; |
235 | foreach ( $message->getParams() as $param ) { |
236 | if ( is_array( $param ) && count( $param ) === 1 ) { |
237 | // probably Message::numParam() or similar |
238 | $param = reset( $param ); |
239 | } |
240 | if ( is_int( $param ) || is_float( $param ) || is_string( $param ) ) { |
241 | $context["parameter$i"] = $param; |
242 | } else { |
243 | // Parameter is not of a safe type, fall back to getWikiText. |
244 | return [ $this->getWikiText( $status, $options ), [] ]; |
245 | } |
246 | |
247 | $text = str_replace( "\$$i", "{parameter$i}", $text ); |
248 | |
249 | $i++; |
250 | } |
251 | |
252 | return [ $text, $context ]; |
253 | } |
254 | // Parameters cannot be easily extracted, fall back to getWikiText, |
255 | return [ $this->getWikiText( $status, $options ), [] ]; |
256 | } |
257 | |
258 | /** |
259 | * Return the message for a single error |
260 | * |
261 | * The code string can be used a message key with per-language versions. |
262 | * If $error is an array, the "params" field is a list of parameters for the message. |
263 | * |
264 | * @param array|string $error Code string or (key: code string, params: string[]) map |
265 | * @param array $options |
266 | * @return Message |
267 | */ |
268 | private function getErrorMessage( $error, array $options = [] ) { |
269 | $lang = $options['lang'] ?? null; |
270 | |
271 | if ( is_array( $error ) ) { |
272 | if ( isset( $error['message'] ) && $error['message'] instanceof Message ) { |
273 | // Apply context from MessageLocalizer even if we have a Message object already |
274 | $msg = $this->msg( $error['message'] ); |
275 | } elseif ( isset( $error['message'] ) && isset( $error['params'] ) ) { |
276 | $msg = $this->msg( |
277 | $error['message'], |
278 | array_map( static function ( $param ) { |
279 | return is_string( $param ) ? wfEscapeWikiText( $param ) : $param; |
280 | }, $this->cleanParams( $error['params'], $options ) ) |
281 | ); |
282 | } else { |
283 | $msgName = array_shift( $error ); |
284 | $msg = $this->msg( |
285 | $msgName, |
286 | array_map( static function ( $param ) { |
287 | return is_string( $param ) ? wfEscapeWikiText( $param ) : $param; |
288 | }, $this->cleanParams( $error, $options ) ) |
289 | ); |
290 | } |
291 | } elseif ( is_string( $error ) ) { |
292 | $msg = $this->msg( $error ); |
293 | } else { |
294 | throw new UnexpectedValueException( 'Got ' . get_class( $error ) . ' for key.' ); |
295 | } |
296 | |
297 | if ( $lang ) { |
298 | $msg->inLanguage( $lang ); |
299 | } |
300 | return $msg; |
301 | } |
302 | |
303 | /** |
304 | * Get the error message as HTML. This is done by parsing the wikitext error message |
305 | * |
306 | * @param StatusValue $status |
307 | * @param array $options An array of options, supporting the following keys: |
308 | * - 'shortContext' (string|false|null) A short enclosing context message name, to |
309 | * be used when there is a single error |
310 | * - 'longContext' (string|false|null) A long enclosing context message name, for a list |
311 | * - 'lang' (string|Language|StubUserLang|null) Language to use for processing messages |
312 | * - 'cleanCallback' (callable|null) A callback for sanitizing parameter values |
313 | * |
314 | * @return string |
315 | */ |
316 | public function getHTML( StatusValue $status, array $options = [] ) { |
317 | $lang = $options['lang'] ?? null; |
318 | |
319 | $text = $this->getWikiText( $status, $options ); |
320 | $out = $this->messageCache->parse( $text, null, true, true, $lang ); |
321 | |
322 | return $out instanceof ParserOutput |
323 | ? $out->getText( [ 'enableSectionEditLinks' => false ] ) |
324 | : $out; |
325 | } |
326 | |
327 | /** |
328 | * Return an array with a Message object for each error. |
329 | * |
330 | * @param array $errors |
331 | * @param array $options |
332 | * |
333 | * @return Message[] |
334 | */ |
335 | private function getErrorMessageArray( $errors, array $options = [] ) { |
336 | return array_map( function ( $e ) use ( $options ) { |
337 | return $this->getErrorMessage( $e, $options ); |
338 | }, $errors ); |
339 | } |
340 | |
341 | /** |
342 | * @param string|MessageSpecifier $key |
343 | * @param string|string[] ...$params |
344 | * @return Message |
345 | */ |
346 | private function msg( $key, ...$params ): Message { |
347 | return $this->messageLocalizer->msg( $key, ...$params ); |
348 | } |
349 | |
350 | /** |
351 | * @param string|MessageSpecifier $key |
352 | * @param string|Language|StubUserLang|null $lang |
353 | * @param mixed ...$params |
354 | * @return Message |
355 | */ |
356 | private function msgInLang( $key, $lang, ...$params ): Message { |
357 | $msg = $this->msg( $key, ...$params ); |
358 | if ( $lang ) { |
359 | $msg->inLanguage( $lang ); |
360 | } |
361 | return $msg; |
362 | } |
363 | } |