Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
96.05% |
146 / 152 |
|
71.43% |
5 / 7 |
CRAP | |
0.00% |
0 / 1 |
ApiFormatXml | |
96.69% |
146 / 151 |
|
71.43% |
5 / 7 |
61 | |
0.00% |
0 / 1 |
getMimeType | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setRootElement | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
100.00% |
26 / 26 |
|
100.00% |
1 / 1 |
7 | |||
recXmlPrint | |
95.18% |
79 / 83 |
|
0.00% |
0 / 1 |
41 | |||
mangleName | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
5 | |||
addXslt | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
getAllowedParams | |
100.00% |
9 / 9 |
|
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 MediaWiki\Title\Title; |
26 | use MediaWiki\Xml\Xml; |
27 | use Wikimedia\ParamValidator\ParamValidator; |
28 | |
29 | /** |
30 | * API XML output formatter |
31 | * @ingroup API |
32 | */ |
33 | class 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 */ |
304 | class_alias( ApiFormatXml::class, 'ApiFormatXml' ); |