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