3 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
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.
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.
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
23 namespace MediaWiki\Api
;
25 use MediaWiki\Title\Title
;
26 use MediaWiki\Xml\Xml
;
27 use Wikimedia\ParamValidator\ParamValidator
;
30 * API XML output formatter
33 class ApiFormatXml
extends ApiFormatBase
{
36 private $mRootElemName = 'api';
38 public static $namespace = 'http://www.mediawiki.org/xml/api/';
40 private $mIncludeNamespace = false;
41 /** @var string|null */
42 private $mXslt = null;
44 public function getMimeType() {
48 public function setRootElement( $rootElemName ) {
49 $this->mRootElemName
= $rootElemName;
52 public function execute() {
53 $params = $this->extractRequestParams();
54 $this->mIncludeNamespace
= $params['includexmlnamespace'];
55 $this->mXslt
= $params['xslt'];
57 $this->printText( '<?xml version="1.0"?>' );
58 if ( $this->mXslt
!== null ) {
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
);
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
] ) {
76 $metadata[ApiResult
::META_TYPE
] = 'assoc';
81 'BC' => [ 'nobool', 'no*', 'nosub' ],
82 'Types' => [ 'ArmorKVP' => '_name' ],
86 static::recXmlPrint( $this->mRootElemName
,
88 $this->getIsHtml() ?
-2 : null
94 * This method takes an array and converts it to XML.
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
102 public static function recXmlPrint( $name, $value, $indent, $attributes = [] ) {
104 if ( $indent !== null ) {
105 if ( $name !== null ) {
108 $indstr = "\n" . str_repeat( ' ', $indent );
113 if ( is_object( $value ) ) {
114 $value = (array)$value;
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
]
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 )
128 $bcBools = $value[ApiResult
::META_BC_BOOLS
] ??
[];
129 $indexSubelements = isset( $value[ApiResult
::META_TYPE
] )
130 && $value[ApiResult
::META_TYPE
] !== 'array';
134 $indexedSubelements = [];
135 foreach ( $value as $k => $v ) {
136 if ( ApiResult
::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
141 if ( is_bool( $v ) && !in_array( $k, $bcBools, true ) ) {
142 $v = $v ?
'true' : 'false';
145 if ( $name !== null && $k === $contentKey ) {
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 )] = [
154 ApiResult
::META_CONTENT
=> 'content',
155 ApiResult
::META_TYPE
=> 'assoc',
157 } elseif ( is_bool( $oldv ) ) {
159 $attributes[self
::mangleName( $k, $preserveKeys )] = '';
161 } elseif ( $v !== null ) {
162 $attributes[self
::mangleName( $k, $preserveKeys )] = $v;
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',
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' ];
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 );
186 if ( $name !== null ) {
187 $retval .= $indstr . Xml
::element( $name, $attributes, null );
189 $retval .= static::recXmlPrint( null, $content, $indent );
190 if ( $name !== null ) {
191 $retval .= $indstr . Xml
::closeElement( $name );
194 } elseif ( !$indexedSubelements && !$subelements ) {
195 if ( $name !== null ) {
196 $retval .= $indstr . Xml
::element( $name, $attributes );
199 if ( $name !== null ) {
200 $retval .= $indstr . Xml
::element( $name, $attributes, null );
202 foreach ( $subelements as $k => $v ) {
203 $retval .= static::recXmlPrint( $k, $v, $indent );
205 foreach ( $indexedSubelements as $k => $v ) {
206 $retval .= static::recXmlPrint( $indexedTagName, $v, $indent,
207 $indexSubelements ?
[ '_idx' => $k ] : []
210 if ( $name !== null ) {
211 $retval .= $indstr . Xml
::closeElement( $name );
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 );
221 // @phan-suppress-next-line PhanTypeMismatchArgumentNullable name is check for null in other code
222 $retval .= $indstr . Xml
::element( $name, $attributes, $value );
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
235 private static function mangleName( $name, $preserveKeys = [] ) {
236 static $nsc = null, $nc = null;
238 if ( in_array( $name, $preserveKeys, true ) ) {
242 if ( $name === '' ) {
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
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}';
256 if ( preg_match( "/^[$nsc][$nc]*$/uS", $name ) ) {
260 return '_' . preg_replace_callback(
262 static function ( $m ) {
263 return sprintf( '.%X.', \UtfNormal\Utils
::utf8ToCodepoint( $m[0] ) );
265 str_replace( '.', '.2E.', $name )
269 protected function addXslt() {
270 $nt = Title
::newFromText( $this->mXslt
);
271 if ( $nt === null ||
!$nt->exists() ) {
272 $this->addWarning( 'apiwarn-invalidxmlstylesheet' );
276 if ( $nt->getNamespace() !== NS_MEDIAWIKI
) {
277 $this->addWarning( 'apiwarn-invalidxmlstylesheetns' );
281 if ( !str_ends_with( $nt->getText(), '.xsl' ) ) {
282 $this->addWarning( 'apiwarn-invalidxmlstylesheetext' );
286 $this->printText( '<?xml-stylesheet href="' .
287 htmlspecialchars( $nt->getLocalURL( 'action=raw' ) ) . '" type="text/xsl" ?>' );
290 public function getAllowedParams() {
291 return parent
::getAllowedParams() +
[
293 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-xml-param-xslt',
295 'includexmlnamespace' => [
296 ParamValidator
::PARAM_DEFAULT
=> false,
297 ApiBase
::PARAM_HELP_MSG
=> 'apihelp-xml-param-includexmlnamespace',
303 /** @deprecated class alias since 1.43 */
304 class_alias( ApiFormatXml
::class, 'ApiFormatXml' );