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