Fixed spacing
[lhc/web/wiklou.git] / includes / api / ApiFormatXml.php
1 <?php
2 /**
3 *
4 *
5 * Created on Sep 19, 2006
6 *
7 * Copyright © 2006 Yuri Astrakhan "<Firstname><Lastname>@gmail.com"
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 * http://www.gnu.org/copyleft/gpl.html
23 *
24 * @file
25 */
26
27 /**
28 * API XML output formatter
29 * @ingroup API
30 */
31 class 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 /**
43 * @deprecated since 1.25
44 */
45 public function getNeedsRawData() {
46 return true;
47 }
48
49 public function setRootElement( $rootElemName ) {
50 $this->mRootElemName = $rootElemName;
51 }
52
53 public function execute() {
54 $params = $this->extractRequestParams();
55 $this->mIncludeNamespace = $params['includexmlnamespace'];
56 $this->mXslt = $params['xslt'];
57
58 $this->printText( '<?xml version="1.0"?>' );
59 if ( !is_null( $this->mXslt ) ) {
60 $this->addXslt();
61 }
62
63 $result = $this->getResult();
64 if ( $this->mIncludeNamespace && $result->getResultData( 'xmlns' ) === null ) {
65 // If the result data already contains an 'xmlns' namespace added
66 // for custom XML output types, it will override the one for the
67 // generic API results.
68 // This allows API output of other XML types like Atom, RSS, RSD.
69 $result->addValue( null, 'xmlns', self::$namespace, ApiResult::NO_SIZE_CHECK );
70 }
71 $data = $result->getResultData( null, array(
72 'Custom' => function ( &$data, &$metadata ) {
73 if ( isset( $metadata[ApiResult::META_TYPE] ) ) {
74 // We want to use non-BC for BCassoc to force outputting of _idx.
75 switch ( $metadata[ApiResult::META_TYPE] ) {
76 case 'BCassoc':
77 $metadata[ApiResult::META_TYPE] = 'assoc';
78 break;
79 }
80 }
81 },
82 'BC' => array( 'nobool', 'no*', 'nosub' ),
83 'Types' => array( 'ArmorKVP' => '_name' ),
84 ) );
85
86 $this->printText(
87 static::recXmlPrint( $this->mRootElemName,
88 $data,
89 $this->getIsHtml() ? -2 : null
90 )
91 );
92 }
93
94 /**
95 * This method takes an array and converts it to XML.
96 *
97 * @param string|null $name Tag name
98 * @param mixed $value Tag value (attributes/content/subelements)
99 * @param int|null $indent Indentation
100 * @param array $attributes Additional attributes
101 * @return string
102 */
103 public static function recXmlPrint( $name, $value, $indent, $attributes = array() ) {
104 $retval = '';
105 if ( $indent !== null ) {
106 if ( $name !== null ) {
107 $indent += 2;
108 }
109 $indstr = "\n" . str_repeat( ' ', $indent );
110 } else {
111 $indstr = '';
112 }
113
114 if ( is_object( $value ) ) {
115 $value = (array)$value;
116 }
117 if ( is_array( $value ) ) {
118 $contentKey = isset( $value[ApiResult::META_CONTENT] )
119 ? $value[ApiResult::META_CONTENT]
120 : '*';
121 $subelementKeys = isset( $value[ApiResult::META_SUBELEMENTS] )
122 ? $value[ApiResult::META_SUBELEMENTS]
123 : array();
124 if ( isset( $value[ApiResult::META_BC_SUBELEMENTS] ) ) {
125 $subelementKeys = array_merge(
126 $subelementKeys, $value[ApiResult::META_BC_SUBELEMENTS]
127 );
128 }
129 $preserveKeys = isset( $value[ApiResult::META_PRESERVE_KEYS] )
130 ? $value[ApiResult::META_PRESERVE_KEYS]
131 : array();
132 $indexedTagName = isset( $value[ApiResult::META_INDEXED_TAG_NAME] )
133 ? self::mangleName( $value[ApiResult::META_INDEXED_TAG_NAME], $preserveKeys )
134 : '_v';
135 $bcBools = isset( $value[ApiResult::META_BC_BOOLS] )
136 ? $value[ApiResult::META_BC_BOOLS]
137 : array();
138 $indexSubelements = isset( $value[ApiResult::META_TYPE] )
139 ? $value[ApiResult::META_TYPE] !== 'array'
140 : false;
141
142 $content = null;
143 $subelements = array();
144 $indexedSubelements = array();
145 foreach ( $value as $k => $v ) {
146 if ( ApiResult::isMetadataKey( $k ) && !in_array( $k, $preserveKeys, true ) ) {
147 continue;
148 }
149
150 $oldv = $v;
151 if ( is_bool( $v ) && !in_array( $k, $bcBools, true ) ) {
152 $v = $v ? 'true' : 'false';
153 }
154
155 if ( $name !== null && $k === $contentKey ) {
156 $content = $v;
157 } elseif ( is_int( $k ) ) {
158 $indexedSubelements[$k] = $v;
159 } elseif ( is_array( $v ) || is_object( $v ) ) {
160 $subelements[self::mangleName( $k, $preserveKeys )] = $v;
161 } elseif ( in_array( $k, $subelementKeys, true ) || $name === null ) {
162 $subelements[self::mangleName( $k, $preserveKeys )] = array(
163 'content' => $v,
164 ApiResult::META_CONTENT => 'content',
165 ApiResult::META_TYPE => 'assoc',
166 );
167 } elseif ( is_bool( $oldv ) ) {
168 if ( $oldv ) {
169 $attributes[self::mangleName( $k, $preserveKeys )] = '';
170 }
171 } elseif ( $v !== null ) {
172 $attributes[self::mangleName( $k, $preserveKeys )] = $v;
173 }
174 }
175
176 if ( $content !== null ) {
177 if ( $subelements || $indexedSubelements ) {
178 $subelements[self::mangleName( $contentKey, $preserveKeys )] = array(
179 'content' => $content,
180 ApiResult::META_CONTENT => 'content',
181 ApiResult::META_TYPE => 'assoc',
182 );
183 $content = null;
184 } elseif ( is_scalar( $content ) ) {
185 // Add xml:space="preserve" to the element so XML parsers
186 // will leave whitespace in the content alone
187 $attributes += array( 'xml:space' => 'preserve' );
188 }
189 }
190
191 if ( $content !== null ) {
192 if ( is_scalar( $content ) ) {
193 $retval .= $indstr . Xml::element( $name, $attributes, $content );
194 } else {
195 if ( $name !== null ) {
196 $retval .= $indstr . Xml::element( $name, $attributes, null );
197 }
198 $retval .= static::recXmlPrint( null, $content, $indent );
199 if ( $name !== null ) {
200 $retval .= $indstr . Xml::closeElement( $name );
201 }
202 }
203 } elseif ( !$indexedSubelements && !$subelements ) {
204 if ( $name !== null ) {
205 $retval .= $indstr . Xml::element( $name, $attributes );
206 }
207 } else {
208 if ( $name !== null ) {
209 $retval .= $indstr . Xml::element( $name, $attributes, null );
210 }
211 foreach ( $subelements as $k => $v ) {
212 $retval .= static::recXmlPrint( $k, $v, $indent );
213 }
214 foreach ( $indexedSubelements as $k => $v ) {
215 $retval .= static::recXmlPrint( $indexedTagName, $v, $indent,
216 $indexSubelements ? array( '_idx' => $k ) : array()
217 );
218 }
219 if ( $name !== null ) {
220 $retval .= $indstr . Xml::closeElement( $name );
221 }
222 }
223 } else {
224 // to make sure null value doesn't produce unclosed element,
225 // which is what Xml::element( $name, null, null ) returns
226 if ( $value === null ) {
227 $retval .= $indstr . Xml::element( $name, $attributes );
228 } else {
229 $retval .= $indstr . Xml::element( $name, $attributes, $value );
230 }
231 }
232
233 return $retval;
234 }
235
236 /**
237 * Mangle XML-invalid names to be valid in XML
238 * @param string $name
239 * @param array $preserveKeys Names to not mangle
240 * @return string Mangled name
241 */
242 private static function mangleName( $name, $preserveKeys = array() ) {
243 static $nsc = null, $nc = null;
244
245 if ( in_array( $name, $preserveKeys, true ) ) {
246 return $name;
247 }
248
249 if ( $name === '' ) {
250 return '_';
251 }
252
253 if ( $nsc === null ) {
254 // Note we omit ':' from $nsc and $nc because it's reserved for XML
255 // namespacing, and we omit '_' from $nsc (but not $nc) because we
256 // reserve it.
257 $nsc = 'A-Za-z\x{C0}-\x{D6}\x{D8}-\x{F6}\x{F8}-\x{2FF}\x{370}-\x{37D}\x{37F}-\x{1FFF}' .
258 '\x{200C}-\x{200D}\x{2070}-\x{218F}\x{2C00}-\x{2FEF}\x{3001}-\x{D7FF}' .
259 '\x{F900}-\x{FDCF}\x{FDF0}-\x{FFFD}\x{10000}-\x{EFFFF}';
260 $nc = $nsc . '_\-.0-9\x{B7}\x{300}-\x{36F}\x{203F}-\x{2040}';
261 }
262
263 if ( preg_match( "/^[$nsc][$nc]*$/uS", $name ) ) {
264 return $name;
265 }
266
267 return '_' . preg_replace_callback(
268 "/[^$nc]/uS",
269 function ( $m ) {
270 return sprintf( '.%X.', utf8ToCodepoint( $m[0] ) );
271 },
272 str_replace( '.', '.2E.', $name )
273 );
274 }
275
276 function addXslt() {
277 $nt = Title::newFromText( $this->mXslt );
278 if ( is_null( $nt ) || !$nt->exists() ) {
279 $this->setWarning( 'Invalid or non-existent stylesheet specified' );
280
281 return;
282 }
283 if ( $nt->getNamespace() != NS_MEDIAWIKI ) {
284 $this->setWarning( 'Stylesheet should be in the MediaWiki namespace.' );
285
286 return;
287 }
288 if ( substr( $nt->getText(), -4 ) !== '.xsl' ) {
289 $this->setWarning( 'Stylesheet should have .xsl extension.' );
290
291 return;
292 }
293 $this->printText( '<?xml-stylesheet href="' .
294 htmlspecialchars( $nt->getLocalURL( 'action=raw' ) ) . '" type="text/xsl" ?>' );
295 }
296
297 public function getAllowedParams() {
298 return array(
299 'xslt' => array(
300 ApiBase::PARAM_HELP_MSG => 'apihelp-xml-param-xslt',
301 ),
302 'includexmlnamespace' => array(
303 ApiBase::PARAM_DFLT => false,
304 ApiBase::PARAM_HELP_MSG => 'apihelp-xml-param-includexmlnamespace',
305 ),
306 );
307 }
308 }