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