Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
99.29% |
139 / 140 |
|
100.00% |
18 / 18 |
CRAP | |
100.00% |
1 / 1 |
ApiFormatBase | |
100.00% |
139 / 139 |
|
100.00% |
18 / 18 |
47 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getMimeType | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
getFilename | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
getFormat | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIsHtml | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIsWrappedHtml | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
disable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isDisabled | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
canPrintErrors | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
forceDefaultParams | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getParameterFromSettings | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
setHttpStatus | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
initPrinter | |
100.00% |
28 / 28 |
|
100.00% |
1 / 1 |
10 | |||
closePrinter | |
100.00% |
65 / 65 |
|
100.00% |
1 / 1 |
13 | |||
printText | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBuffer | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getAllowedParams | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getExamplesMessages | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
getHelpUrls | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com" |
4 | * |
5 | * This program is free software; you can redistribute it and/or modify |
6 | * it under the terms of the GNU General Public License as published by |
7 | * the Free Software Foundation; either version 2 of the License, or |
8 | * (at your option) any later version. |
9 | * |
10 | * This program is distributed in the hope that it will be useful, |
11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
13 | * GNU General Public License for more details. |
14 | * |
15 | * You should have received a copy of the GNU General Public License along |
16 | * with this program; if not, write to the Free Software Foundation, Inc., |
17 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
18 | * http://www.gnu.org/copyleft/gpl.html |
19 | * |
20 | * @file |
21 | */ |
22 | |
23 | namespace MediaWiki\Api; |
24 | |
25 | use HttpStatus; |
26 | use MediaWiki\Context\DerivativeContext; |
27 | use MediaWiki\Html\Html; |
28 | use MediaWiki\Json\FormatJson; |
29 | use MediaWiki\MainConfigNames; |
30 | use MediaWiki\MediaWikiServices; |
31 | use MediaWiki\Output\OutputPage; |
32 | use MediaWiki\SpecialPage\SpecialPage; |
33 | use Wikimedia\ParamValidator\ParamValidator; |
34 | |
35 | /** |
36 | * This is the abstract base class for API formatters. |
37 | * |
38 | * @ingroup API |
39 | */ |
40 | abstract class ApiFormatBase extends ApiBase { |
41 | private bool $mIsHtml; |
42 | private string $mFormat; |
43 | private string $mBuffer = ''; |
44 | private bool $mDisabled = false; |
45 | /** @var bool */ |
46 | private $mIsWrappedHtml = false; |
47 | /** @var int|false */ |
48 | private $mHttpStatus = false; |
49 | /** @var bool */ |
50 | protected $mForceDefaultParams = false; |
51 | |
52 | /** |
53 | * If $format ends with 'fm', pretty-print the output in HTML. |
54 | * |
55 | * @param ApiMain $main |
56 | * @param string $format Format name |
57 | */ |
58 | public function __construct( ApiMain $main, string $format ) { |
59 | parent::__construct( $main, $format ); |
60 | |
61 | $this->mIsHtml = str_ends_with( $format, 'fm' ); |
62 | if ( $this->mIsHtml ) { |
63 | $this->mFormat = substr( $format, 0, -2 ); // remove ending 'fm' |
64 | $this->mIsWrappedHtml = $this->getMain()->getCheck( 'wrappedhtml' ); |
65 | } else { |
66 | $this->mFormat = $format; |
67 | } |
68 | $this->mFormat = strtoupper( $this->mFormat ); |
69 | } |
70 | |
71 | /** |
72 | * Overriding class returns the MIME type that should be sent to the client. |
73 | * |
74 | * When getIsHtml() returns true, the return value here is used for syntax |
75 | * highlighting, but the client sees text/html. |
76 | * |
77 | * @return string|null |
78 | */ |
79 | abstract public function getMimeType(); |
80 | |
81 | /** |
82 | * Return a filename for this module's output. |
83 | * |
84 | * @note If $this->getIsWrappedHtml() || $this->getIsHtml(), you'll very |
85 | * likely want to fall back to this class's version. |
86 | * @since 1.27 |
87 | * @return string Generally, this should be "api-result.$ext" |
88 | */ |
89 | public function getFilename() { |
90 | if ( $this->getIsWrappedHtml() ) { |
91 | return 'api-result-wrapped.json'; |
92 | } |
93 | |
94 | if ( $this->getIsHtml() ) { |
95 | return 'api-result.html'; |
96 | } |
97 | |
98 | $mimeAnalyzer = MediaWikiServices::getInstance()->getMimeAnalyzer(); |
99 | $ext = $mimeAnalyzer->getExtensionFromMimeTypeOrNull( $this->getMimeType() ) |
100 | ?? strtolower( $this->mFormat ); |
101 | return "api-result.$ext"; |
102 | } |
103 | |
104 | /** |
105 | * Get the internal format name |
106 | * |
107 | * @return string |
108 | */ |
109 | public function getFormat() { |
110 | return $this->mFormat; |
111 | } |
112 | |
113 | /** |
114 | * Returns true when the HTML pretty-printer should be used. |
115 | * The default implementation assumes that formats ending with 'fm' should be formatted in HTML. |
116 | * |
117 | * @return bool |
118 | */ |
119 | public function getIsHtml() { |
120 | return $this->mIsHtml; |
121 | } |
122 | |
123 | /** |
124 | * Returns true when the special-wrapped mode is enabled. |
125 | * |
126 | * @since 1.27 |
127 | * @return bool |
128 | */ |
129 | protected function getIsWrappedHtml() { |
130 | return $this->mIsWrappedHtml; |
131 | } |
132 | |
133 | /** |
134 | * Disable the formatter. |
135 | * |
136 | * This causes calls to initPrinter() and closePrinter() to be ignored. |
137 | */ |
138 | public function disable() { |
139 | $this->mDisabled = true; |
140 | } |
141 | |
142 | /** |
143 | * Whether the printer is disabled. |
144 | * |
145 | * @return bool |
146 | */ |
147 | public function isDisabled() { |
148 | return $this->mDisabled; |
149 | } |
150 | |
151 | /** |
152 | * Whether this formatter can handle printing API errors. |
153 | * |
154 | * If this returns false, then when API errors occur, the default printer will be instantiated. |
155 | * @since 1.23 |
156 | * @return bool |
157 | */ |
158 | public function canPrintErrors() { |
159 | return true; |
160 | } |
161 | |
162 | /** |
163 | * Ignore request parameters, force a default. |
164 | * |
165 | * Used as a fallback if errors are being thrown. |
166 | * |
167 | * @since 1.26 |
168 | */ |
169 | public function forceDefaultParams() { |
170 | $this->mForceDefaultParams = true; |
171 | } |
172 | |
173 | /** |
174 | * Overridden to honor $this->forceDefaultParams(), if applicable |
175 | * @inheritDoc |
176 | * @since 1.26 |
177 | */ |
178 | protected function getParameterFromSettings( $paramName, $paramSettings, $parseLimit ) { |
179 | if ( !$this->mForceDefaultParams ) { |
180 | return parent::getParameterFromSettings( $paramName, $paramSettings, $parseLimit ); |
181 | } |
182 | |
183 | if ( !is_array( $paramSettings ) ) { |
184 | return $paramSettings; |
185 | } |
186 | |
187 | return $paramSettings[ParamValidator::PARAM_DEFAULT] ?? null; |
188 | } |
189 | |
190 | /** |
191 | * Set the HTTP status code to be used for the response |
192 | * @since 1.29 |
193 | * @param int $code |
194 | */ |
195 | public function setHttpStatus( $code ) { |
196 | if ( $this->mDisabled ) { |
197 | return; |
198 | } |
199 | |
200 | if ( $this->getIsHtml() ) { |
201 | $this->mHttpStatus = $code; |
202 | } else { |
203 | $this->getMain()->getRequest()->response()->statusHeader( $code ); |
204 | } |
205 | } |
206 | |
207 | /** |
208 | * Initialize the printer function and prepare the output headers. |
209 | * @param bool $unused Always false since 1.25 |
210 | */ |
211 | public function initPrinter( $unused = false ) { |
212 | if ( $this->mDisabled ) { |
213 | return; |
214 | } |
215 | |
216 | if ( $this->getIsHtml() && $this->getMain()->getCacheMode() === 'public' ) { |
217 | // The HTML may contain user secrets! T354045 |
218 | $this->getMain()->setCacheMode( 'anon-public-user-private' ); |
219 | } |
220 | |
221 | $mime = $this->getIsWrappedHtml() |
222 | ? 'text/mediawiki-api-prettyprint-wrapped' |
223 | : ( $this->getIsHtml() ? 'text/html' : $this->getMimeType() ); |
224 | |
225 | // Some printers (ex. Feed) do their own header settings, |
226 | // in which case $mime will be set to null |
227 | if ( $mime === null ) { |
228 | return; // skip any initialization |
229 | } |
230 | |
231 | $this->getMain()->getRequest()->response()->header( "Content-Type: $mime; charset=utf-8" ); |
232 | |
233 | // Set X-Frame-Options API results (T41180) |
234 | $apiFrameOptions = $this->getConfig()->get( MainConfigNames::ApiFrameOptions ); |
235 | if ( $apiFrameOptions ) { |
236 | $this->getMain()->getRequest()->response()->header( "X-Frame-Options: $apiFrameOptions" ); |
237 | } |
238 | |
239 | // Set a Content-Disposition header so something downloading an API |
240 | // response uses a halfway-sensible filename (T128209). |
241 | $header = 'Content-Disposition: inline'; |
242 | $filename = $this->getFilename(); |
243 | $compatFilename = mb_convert_encoding( $filename, 'ISO-8859-1' ); |
244 | if ( preg_match( '/^[0-9a-zA-Z!#$%&\'*+\-.^_`|~]+$/', $compatFilename ) ) { |
245 | $header .= '; filename=' . $compatFilename; |
246 | } else { |
247 | $header .= '; filename="' |
248 | . preg_replace( '/([\0-\x1f"\x5c\x7f])/', '\\\\$1', $compatFilename ) . '"'; |
249 | } |
250 | if ( $compatFilename !== $filename ) { |
251 | $value = "UTF-8''" . rawurlencode( $filename ); |
252 | // rawurlencode() encodes more characters than RFC 5987 specifies. Unescape the ones it allows. |
253 | $value = strtr( $value, [ |
254 | '%21' => '!', '%23' => '#', '%24' => '$', '%26' => '&', '%2B' => '+', '%5E' => '^', |
255 | '%60' => '`', '%7C' => '|', |
256 | ] ); |
257 | $header .= '; filename*=' . $value; |
258 | } |
259 | $this->getMain()->getRequest()->response()->header( $header ); |
260 | } |
261 | |
262 | /** |
263 | * Finish printing and output buffered data. |
264 | */ |
265 | public function closePrinter() { |
266 | if ( $this->mDisabled ) { |
267 | return; |
268 | } |
269 | |
270 | $mime = $this->getMimeType(); |
271 | if ( $this->getIsHtml() && $mime !== null ) { |
272 | $format = $this->getFormat(); |
273 | $lcformat = strtolower( $format ); |
274 | $result = $this->getBuffer(); |
275 | |
276 | $context = new DerivativeContext( $this->getMain() ); |
277 | $skinFactory = MediaWikiServices::getInstance()->getSkinFactory(); |
278 | $context->setSkin( $skinFactory->makeSkin( 'apioutput' ) ); |
279 | $context->setTitle( SpecialPage::getTitleFor( 'ApiHelp' ) ); |
280 | $out = new OutputPage( $context ); |
281 | $context->setOutput( $out ); |
282 | |
283 | $out->setRobotPolicy( 'noindex,nofollow' ); |
284 | $out->addModuleStyles( 'mediawiki.apipretty' ); |
285 | $out->setPageTitleMsg( $context->msg( 'api-format-title' ) ); |
286 | |
287 | if ( !$this->getIsWrappedHtml() ) { |
288 | // When the format without suffix 'fm' is defined, there is a non-html version |
289 | if ( $this->getMain()->getModuleManager()->isDefined( $lcformat, 'format' ) ) { |
290 | if ( !$this->getRequest()->wasPosted() ) { |
291 | $nonHtmlUrl = strtok( $this->getRequest()->getFullRequestURL(), '?' ) |
292 | . '?' . $this->getRequest()->appendQueryValue( 'format', $lcformat ); |
293 | $msg = $context->msg( 'api-format-prettyprint-header-hyperlinked' ) |
294 | ->params( $format, $lcformat, $nonHtmlUrl ); |
295 | } else { |
296 | $msg = $context->msg( 'api-format-prettyprint-header' )->params( $format, $lcformat ); |
297 | } |
298 | } else { |
299 | $msg = $context->msg( 'api-format-prettyprint-header-only-html' )->params( $format ); |
300 | } |
301 | |
302 | $header = $msg->parseAsBlock(); |
303 | $out->addHTML( |
304 | Html::rawElement( 'div', [ 'class' => 'api-pretty-header' ], |
305 | ApiHelp::fixHelpLinks( $header ) |
306 | ) |
307 | ); |
308 | |
309 | if ( $this->mHttpStatus && $this->mHttpStatus !== 200 ) { |
310 | $out->addHTML( |
311 | Html::rawElement( 'div', [ 'class' => [ 'api-pretty-header', 'api-pretty-status' ] ], |
312 | $this->msg( |
313 | 'api-format-prettyprint-status', |
314 | $this->mHttpStatus, |
315 | HttpStatus::getMessage( $this->mHttpStatus ) |
316 | )->parse() |
317 | ) |
318 | ); |
319 | } |
320 | } |
321 | |
322 | if ( $this->getHookRunner()->onApiFormatHighlight( $context, $result, $mime, $format ) ) { |
323 | $out->addHTML( |
324 | Html::element( 'pre', [ 'class' => 'api-pretty-content' ], $result ) |
325 | ); |
326 | } |
327 | |
328 | if ( $this->getIsWrappedHtml() ) { |
329 | // This is a special output mode mainly intended for ApiSandbox use |
330 | $time = $this->getMain()->getRequest()->getElapsedTime(); |
331 | echo FormatJson::encode( |
332 | [ |
333 | 'status' => (int)( $this->mHttpStatus ?: 200 ), |
334 | 'statustext' => HttpStatus::getMessage( $this->mHttpStatus ?: 200 ), |
335 | 'html' => $out->getHTML(), |
336 | 'modules' => array_values( array_unique( array_merge( |
337 | $out->getModules(), |
338 | $out->getModuleStyles() |
339 | ) ) ), |
340 | 'continue' => $this->getResult()->getResultData( 'continue' ), |
341 | 'time' => round( $time * 1000 ), |
342 | ], |
343 | false, FormatJson::ALL_OK |
344 | ); |
345 | } else { |
346 | // API handles its own clickjacking protection. |
347 | // Note: $wgBreakFrames will still override $wgApiFrameOptions for format mode. |
348 | $out->getMetadata()->setPreventClickjacking( false ); |
349 | $out->output(); |
350 | } |
351 | } else { |
352 | // For non-HTML output, clear all errors that might have been |
353 | // displayed if display_errors=On |
354 | ob_clean(); |
355 | |
356 | echo $this->getBuffer(); |
357 | } |
358 | } |
359 | |
360 | /** |
361 | * Append text to the output buffer. |
362 | * |
363 | * @param string $text |
364 | */ |
365 | public function printText( $text ) { |
366 | $this->mBuffer .= $text; |
367 | } |
368 | |
369 | /** |
370 | * Get the contents of the buffer. |
371 | * |
372 | * @return string |
373 | */ |
374 | public function getBuffer() { |
375 | return $this->mBuffer; |
376 | } |
377 | |
378 | public function getAllowedParams() { |
379 | $ret = []; |
380 | if ( $this->getIsHtml() ) { |
381 | $ret['wrappedhtml'] = [ |
382 | ParamValidator::PARAM_DEFAULT => false, |
383 | ApiBase::PARAM_HELP_MSG => 'apihelp-format-param-wrappedhtml', |
384 | ]; |
385 | } |
386 | return $ret; |
387 | } |
388 | |
389 | protected function getExamplesMessages() { |
390 | return [ |
391 | 'action=query&meta=siteinfo&siprop=namespaces&format=' . $this->getModuleName() |
392 | => [ 'apihelp-format-example-generic', $this->getFormat() ] |
393 | ]; |
394 | } |
395 | |
396 | public function getHelpUrls() { |
397 | return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Data_formats'; |
398 | } |
399 | |
400 | } |
401 | |
402 | /** |
403 | * For really cool vim folding this needs to be at the end: |
404 | * vim: foldmarker=@{,@} foldmethod=marker |
405 | */ |
406 | |
407 | /** @deprecated class alias since 1.43 */ |
408 | class_alias( ApiFormatBase::class, 'ApiFormatBase' ); |