Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
96.05% covered (success)
96.05%
146 / 152
71.43% covered (warning)
71.43%
5 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiFormatXml
96.69% covered (success)
96.69%
146 / 151
71.43% covered (warning)
71.43%
5 / 7
61
0.00% covered (danger)
0.00%
0 / 1
 getMimeType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setRootElement
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
7
 recXmlPrint
95.18% covered (success)
95.18%
79 / 83
0.00% covered (danger)
0.00%
0 / 1
41
 mangleName
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
5
 addXslt
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
5
 getAllowedParams
100.00% covered (success)
100.00%
9 / 9
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 MediaWiki\Title\Title;
26use MediaWiki\Xml\Xml;
27use Wikimedia\ParamValidator\ParamValidator;
28
29/**
30 * API XML output formatter
31 * @ingroup API
32 */
33class ApiFormatXml extends ApiFormatBase {
34
35    /** @var string */
36    private $mRootElemName = 'api';
37    /** @var string */
38    public static $namespace = 'http://www.mediawiki.org/xml/api/';
39    /** @var bool */
40    private $mIncludeNamespace = false;
41    /** @var string|null */
42    private $mXslt = null;
43
44    public function getMimeType() {
45        return 'text/xml';
46    }
47
48    public function setRootElement( $rootElemName ) {
49        $this->mRootElemName = $rootElemName;
50    }
51
52    public function execute() {
53        $params = $this->extractRequestParams();
54        $this->mIncludeNamespace = $params['includexmlnamespace'];
55        $this->mXslt = $params['xslt'];
56
57        $this->printText( '<?xml version="1.0"?>' );
58        if ( $this->mXslt !== null ) {
59            $this->addXslt();
60        }
61
62        $result = $this->getResult();
63        if ( $this->mIncludeNamespace && $result->getResultData( 'xmlns' ) === null ) {
64            // If the result data already contains an 'xmlns' namespace added
65            // for custom XML output types, it will override the one for the
66            // generic API results.
67            // This allows API output of other XML types like Atom, RSS, RSD.
68            $result->addValue( null, 'xmlns', self::$namespace, ApiResult::NO_SIZE_CHECK );
69        }
70        $data = $result->getResultData( null, [
71            'Custom' => static function ( &$data, &$metadata ) {
72                if ( isset( $metadata[ApiResult::META_TYPE] ) ) {
73                    // We want to use non-BC for BCassoc to force outputting of _idx.
74                    switch ( $metadata[ApiResult::META_TYPE] ) {
75                        case 'BCassoc':
76                            $metadata[ApiResult::META_TYPE] = 'assoc';
77                            break;
78                    }
79                }
80            },
81            'BC' => [ 'nobool', 'no*', 'nosub' ],
82            'Types' => [ 'ArmorKVP' => '_name' ],
83        ] );
84
85        $this->printText(
86            static::recXmlPrint( $this->mRootElemName,
87                $data,
88                $this->getIsHtml() ? -2 : null
89            )
90        );
91    }
92
93    /**
94     * This method takes an array and converts it to XML.
95     *
96     * @param string|null $name Tag name
97     * @param mixed $value Tag value (attributes/content/subelements)
98     * @param int|null $indent Indentation
99     * @param array $attributes Additional attributes
100     * @return string
101     */
102    public static function recXmlPrint( $name, $value, $indent, $attributes = [] ) {
103        $retval = '';
104        if ( $indent !== null ) {
105            if ( $name !== null ) {
106                $indent += 2;
107            }
108            $indstr = "\n" . str_repeat( ' ', $indent );
109        } else {
110            $indstr = '';
111        }
112
113        if ( is_object( $value ) ) {
114            $value = (array)$value;
115        }
116        if ( is_array( $value ) ) {
117            $contentKey = $value[ApiResult::META_CONTENT] ?? '*';
118            $subelementKeys = $value[ApiResult::META_SUBELEMENTS] ?? [];
119            if ( isset( $value[ApiResult::META_BC_SUBELEMENTS] ) ) {
120                $subelementKeys = array_merge(
121                    $subelementKeys, $value[ApiResult::META_BC_SUBELEMENTS]
122                );
123            }
124            $preserveKeys = $value[ApiResult::META_PRESERVE_KEYS] ?? [];
125            $indexedTagName = isset( $value[ApiResult::META_INDEXED_TAG_NAME] )
126                ? self::mangleName( $value[ApiResult::META_INDEXED_TAG_NAME], $preserveKeys )
127                : '_v';
128            $bcBools = $value[ApiResult::META_BC_BOOLS] ?? [];
129            $indexSubelements = isset( $value[ApiResult::META_TYPE] )
130                && $value[ApiResult::META_TYPE] !== 'array';
131
132            $content = null;
133            $subelements = [];
134            $indexedSubelements = [];
135            foreach ( $value as $k => $v ) {
136                if ( ApiResult::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
137                    continue;
138                }
139
140                $oldv = $v;
141                if ( is_bool( $v ) && !in_array( $k, $bcBools, true ) ) {
142                    $v = $v ? 'true' : 'false';
143                }
144
145                if ( $name !== null && $k === $contentKey ) {
146                    $content = $v;
147                } elseif ( is_int( $k ) ) {
148                    $indexedSubelements[$k] = $v;
149                } elseif ( is_array( $v ) || is_object( $v ) ) {
150                    $subelements[self::mangleName( $k, $preserveKeys )] = $v;
151                } elseif ( in_array( $k, $subelementKeys, true ) || $name === null ) {
152                    $subelements[self::mangleName( $k, $preserveKeys )] = [
153                        'content' => $v,
154                        ApiResult::META_CONTENT => 'content',
155                        ApiResult::META_TYPE => 'assoc',
156                    ];
157                } elseif ( is_bool( $oldv ) ) {
158                    if ( $oldv ) {
159                        $attributes[self::mangleName( $k, $preserveKeys )] = '';
160                    }
161                } elseif ( $v !== null ) {
162                    $attributes[self::mangleName( $k, $preserveKeys )] = $v;
163                }
164            }
165
166            if ( $content !== null ) {
167                if ( $subelements || $indexedSubelements ) {
168                    $subelements[self::mangleName( $contentKey, $preserveKeys )] = [
169                        'content' => $content,
170                        ApiResult::META_CONTENT => 'content',
171                        ApiResult::META_TYPE => 'assoc',
172                    ];
173                    $content = null;
174                } elseif ( is_scalar( $content ) ) {
175                    // Add xml:space="preserve" to the element so XML parsers
176                    // will leave whitespace in the content alone
177                    $attributes += [ 'xml:space' => 'preserve' ];
178                }
179            }
180
181            if ( $content !== null ) {
182                if ( is_scalar( $content ) ) {
183                    // @phan-suppress-next-line PhanTypeMismatchArgumentNullable name is check for null in other code
184                    $retval .= $indstr . Xml::element( $name, $attributes, $content );
185                } else {
186                    if ( $name !== null ) {
187                        $retval .= $indstr . Xml::element( $name, $attributes, null );
188                    }
189                    $retval .= static::recXmlPrint( null, $content, $indent );
190                    if ( $name !== null ) {
191                        $retval .= $indstr . Xml::closeElement( $name );
192                    }
193                }
194            } elseif ( !$indexedSubelements && !$subelements ) {
195                if ( $name !== null ) {
196                    $retval .= $indstr . Xml::element( $name, $attributes );
197                }
198            } else {
199                if ( $name !== null ) {
200                    $retval .= $indstr . Xml::element( $name, $attributes, null );
201                }
202                foreach ( $subelements as $k => $v ) {
203                    $retval .= static::recXmlPrint( $k, $v, $indent );
204                }
205                foreach ( $indexedSubelements as $k => $v ) {
206                    $retval .= static::recXmlPrint( $indexedTagName, $v, $indent,
207                        $indexSubelements ? [ '_idx' => $k ] : []
208                    );
209                }
210                if ( $name !== null ) {
211                    $retval .= $indstr . Xml::closeElement( $name );
212                }
213            }
214        } else {
215            // to make sure null value doesn't produce unclosed element,
216            // which is what Xml::element( $name, null, null ) returns
217            if ( $value === null ) {
218                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable name is check for null in other code
219                $retval .= $indstr . Xml::element( $name, $attributes );
220            } else {
221                // @phan-suppress-next-line PhanTypeMismatchArgumentNullable name is check for null in other code
222                $retval .= $indstr . Xml::element( $name, $attributes, $value );
223            }
224        }
225
226        return $retval;
227    }
228
229    /**
230     * Mangle XML-invalid names to be valid in XML
231     * @param string $name
232     * @param array $preserveKeys Names to not mangle
233     * @return string Mangled name
234     */
235    private static function mangleName( $name, $preserveKeys = [] ) {
236        static $nsc = null, $nc = null;
237
238        if ( in_array( $name, $preserveKeys, true ) ) {
239            return $name;
240        }
241
242        if ( $name === '' ) {
243            return '_';
244        }
245
246        if ( $nsc === null ) {
247            // Note we omit ':' from $nsc and $nc because it's reserved for XML
248            // namespacing, and we omit '_' from $nsc (but not $nc) because we
249            // reserve it.
250            $nsc = 'A-Za-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}' .
251                '\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}' .
252                '\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}';
253            $nc = $nsc . '_\-.0-9\x{B7}\x{300}-\x{36F}\x{203F}-\x{2040}';
254        }
255
256        if ( preg_match( "/^[$nsc][$nc]*$/uS", $name ) ) {
257            return $name;
258        }
259
260        return '_' . preg_replace_callback(
261            "/[^$nc]/uS",
262            static function ( $m ) {
263                return sprintf( '.%X.', \UtfNormal\Utils::utf8ToCodepoint( $m[0] ) );
264            },
265            str_replace( '.', '.2E.', $name )
266        );
267    }
268
269    protected function addXslt() {
270        $nt = Title::newFromText( $this->mXslt );
271        if ( $nt === null || !$nt->exists() ) {
272            $this->addWarning( 'apiwarn-invalidxmlstylesheet' );
273
274            return;
275        }
276        if ( $nt->getNamespace() !== NS_MEDIAWIKI ) {
277            $this->addWarning( 'apiwarn-invalidxmlstylesheetns' );
278
279            return;
280        }
281        if ( !str_ends_with( $nt->getText(), '.xsl' ) ) {
282            $this->addWarning( 'apiwarn-invalidxmlstylesheetext' );
283
284            return;
285        }
286        $this->printText( '<?xml-stylesheet href="' .
287            htmlspecialchars( $nt->getLocalURL( 'action=raw' ) ) . '" type="text/xsl" ?>' );
288    }
289
290    public function getAllowedParams() {
291        return parent::getAllowedParams() + [
292            'xslt' => [
293                ApiBase::PARAM_HELP_MSG => 'apihelp-xml-param-xslt',
294            ],
295            'includexmlnamespace' => [
296                ParamValidator::PARAM_DEFAULT => false,
297                ApiBase::PARAM_HELP_MSG => 'apihelp-xml-param-includexmlnamespace',
298            ],
299        ];
300    }
301}
302
303/** @deprecated class alias since 1.43 */
304class_alias( ApiFormatXml::class, 'ApiFormatXml' );