Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.29% covered (success)
99.29%
139 / 140
100.00% covered (success)
100.00%
18 / 18
CRAP
100.00% covered (success)
100.00%
1 / 1
ApiFormatBase
100.00% covered (success)
100.00%
139 / 139
100.00% covered (success)
100.00%
18 / 18
47
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getMimeType
n/a
0 / 0
n/a
0 / 0
0
 getFilename
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getFormat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIsHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIsWrappedHtml
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 disable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isDisabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canPrintErrors
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 forceDefaultParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getParameterFromSettings
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 setHttpStatus
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 initPrinter
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
1 / 1
10
 closePrinter
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
1 / 1
13
 printText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBuffer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAllowedParams
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getExamplesMessages
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 getHelpUrls
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
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
23namespace MediaWiki\Api;
24
25use HttpStatus;
26use MediaWiki\Context\DerivativeContext;
27use MediaWiki\Html\Html;
28use MediaWiki\Json\FormatJson;
29use MediaWiki\MainConfigNames;
30use MediaWiki\MediaWikiServices;
31use MediaWiki\Output\OutputPage;
32use MediaWiki\SpecialPage\SpecialPage;
33use Wikimedia\ParamValidator\ParamValidator;
34
35/**
36 * This is the abstract base class for API formatters.
37 *
38 * @ingroup API
39 */
40abstract 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 */
408class_alias( ApiFormatBase::class, 'ApiFormatBase' );