1<?php
2
3namespace Sabre\Xml;
4
5use XMLWriter;
6use InvalidArgumentException;
7
8/**
9 * The XML Writer class.
10 *
11 * This class works exactly as PHP's built-in XMLWriter, with a few additions.
12 *
13 * Namespaces can be registered beforehand, globally. When the first element is
14 * written, namespaces will automatically be declared.
15 *
16 * The writeAttribute, startElement and writeElement can now take a
17 * clark-notation element name (example: {http://www.w3.org/2005/Atom}link).
18 *
19 * If, when writing the namespace is a known one a prefix will automatically be
20 * selected, otherwise a random prefix will be generated.
21 *
22 * Instead of standard string values, the writer can take Element classes (as
23 * defined by this library) to delegate the serialization.
24 *
25 * The write() method can take array structures to quickly write out simple xml
26 * trees.
27 *
28 * @copyright Copyright (C) 2009-2015 fruux GmbH (https://fruux.com/).
29 * @author Evert Pot (http://evertpot.com/)
30 * @license http://sabre.io/license/ Modified BSD License
31 */
32class Writer extends XMLWriter {
33
34    use ContextStackTrait;
35
36    /**
37     * Any namespace that the writer is asked to write, will be added here.
38     *
39     * Any of these elements will get a new namespace definition *every single
40     * time* they are used, but this array allows the writer to make sure that
41     * the prefixes are consistent anyway.
42     *
43     * @var array
44     */
45    protected $adhocNamespaces = [];
46
47    /**
48     * When the first element is written, this flag is set to true.
49     *
50     * This ensures that the namespaces in the namespaces map are only written
51     * once.
52     *
53     * @var bool
54     */
55    protected $namespacesWritten = false;
56
57    /**
58     * Writes a value to the output stream.
59     *
60     * The following values are supported:
61     *   1. Scalar values will be written as-is, as text.
62     *   2. Null values will be skipped (resulting in a short xml tag).
63     *   3. If a value is an instance of an Element class, writing will be
64     *      delegated to the object.
65     *   4. If a value is an array, two formats are supported.
66     *
67     *  Array format 1:
68     *  [
69     *    "{namespace}name1" => "..",
70     *    "{namespace}name2" => "..",
71     *  ]
72     *
73     *  One element will be created for each key in this array. The values of
74     *  this array support any format this method supports (this method is
75     *  called recursively).
76     *
77     *  Array format 2:
78     *
79     *  [
80     *    [
81     *      "name" => "{namespace}name1"
82     *      "value" => "..",
83     *      "attributes" => [
84     *          "attr" => "attribute value",
85     *      ]
86     *    ],
87     *    [
88     *      "name" => "{namespace}name1"
89     *      "value" => "..",
90     *      "attributes" => [
91     *          "attr" => "attribute value",
92     *      ]
93     *    ]
94     * ]
95     *
96     * @param mixed $value
97     * @return void
98     */
99    function write($value) {
100
101        if (is_scalar($value)) {
102            $this->text($value);
103        } elseif ($value instanceof XmlSerializable) {
104            $value->xmlSerialize($this);
105        } elseif (is_null($value)) {
106            // noop
107        } elseif (is_array($value)) {
108
109            reset($value);
110            foreach ($value as $name => $item) {
111
112                if (is_int($name)) {
113
114                    // This item has a numeric index. We expect to be an array with a name and a value.
115                    if (!is_array($item) || !array_key_exists('name', $item) || !array_key_exists('value', $item)) {
116                        throw new InvalidArgumentException('When passing an array to ->write with numeric indices, every item must be an array containing the "name" and "value" key');
117                    }
118
119                    $attributes = isset($item['attributes']) ? $item['attributes'] : [];
120                    $name = $item['name'];
121                    $item = $item['value'];
122
123                } elseif (is_array($item) && array_key_exists('value', $item)) {
124
125                    // This item has a text index. We expect to be an array with a value and optional attributes.
126                    $attributes = isset($item['attributes']) ? $item['attributes'] : [];
127                    $item = $item['value'];
128
129                } else {
130                    // If it's an array with text-indices, we expect every item's
131                    // key to be an xml element name in clark notation.
132                    // No attributes can be passed.
133                    $attributes = [];
134                }
135
136                $this->startElement($name);
137                $this->writeAttributes($attributes);
138                $this->write($item);
139                $this->endElement();
140
141            }
142
143        } elseif (is_object($value)) {
144
145            throw new InvalidArgumentException('The writer cannot serialize objects of type: ' . get_class($value));
146
147        }
148
149    }
150
151    /**
152     * Starts an element.
153     *
154     * @param string $name
155     * @return bool
156     */
157    function startElement($name) {
158
159        if ($name[0] === '{') {
160
161            list($namespace, $localName) =
162                Service::parseClarkNotation($name);
163
164            if (array_key_exists($namespace, $this->namespaceMap)) {
165                $result = $this->startElementNS($this->namespaceMap[$namespace], $localName, null);
166            } else {
167
168                // An empty namespace means it's the global namespace. This is
169                // allowed, but it mustn't get a prefix.
170                if ($namespace === "") {
171                    $result = $this->startElement($localName);
172                    $this->writeAttribute('xmlns', '');
173                } else {
174                    if (!isset($this->adhocNamespaces[$namespace])) {
175                        $this->adhocNamespaces[$namespace] = 'x' . (count($this->adhocNamespaces) + 1);
176                    }
177                    $result = $this->startElementNS($this->adhocNamespaces[$namespace], $localName, $namespace);
178                }
179            }
180
181        } else {
182            $result = parent::startElement($name);
183        }
184
185        if (!$this->namespacesWritten) {
186
187            foreach ($this->namespaceMap as $namespace => $prefix) {
188                $this->writeAttribute(($prefix ? 'xmlns:' . $prefix : 'xmlns'), $namespace);
189            }
190            $this->namespacesWritten = true;
191
192        }
193
194        return $result;
195
196    }
197
198    /**
199     * Write a full element tag.
200     *
201     * This method automatically closes the element as well.
202     *
203     * @param string $name
204     * @param string $content
205     * @return bool
206     */
207    function writeElement($name, $content = null) {
208
209        $this->startElement($name);
210        if (!is_null($content)) {
211            $this->write($content);
212        }
213        $this->endElement();
214
215    }
216
217    /**
218     * Writes a list of attributes.
219     *
220     * Attributes are specified as a key->value array.
221     *
222     * The key is an attribute name. If the key is a 'localName', the current
223     * xml namespace is assumed. If it's a 'clark notation key', this namespace
224     * will be used instead.
225     *
226     * @param array $attributes
227     * @return void
228     */
229    function writeAttributes(array $attributes) {
230
231        foreach ($attributes as $name => $value) {
232            $this->writeAttribute($name, $value);
233        }
234
235    }
236
237    /**
238     * Writes a new attribute.
239     *
240     * The name may be specified in clark-notation.
241     *
242     * Returns true when successful.
243     *
244     * @param string $name
245     * @param string $value
246     * @return bool
247     */
248    function writeAttribute($name, $value) {
249
250        if ($name[0] === '{') {
251
252            list(
253                $namespace,
254                $localName
255            ) = Service::parseClarkNotation($name);
256
257            if (array_key_exists($namespace, $this->namespaceMap)) {
258                // It's an attribute with a namespace we know
259                $this->writeAttribute(
260                    $this->namespaceMap[$namespace] . ':' . $localName,
261                    $value
262                );
263            } else {
264
265                // We don't know the namespace, we must add it in-line
266                if (!isset($this->adhocNamespaces[$namespace])) {
267                    $this->adhocNamespaces[$namespace] = 'x' . (count($this->adhocNamespaces) + 1);
268                }
269                $this->writeAttributeNS(
270                    $this->adhocNamespaces[$namespace],
271                    $localName,
272                    $namespace,
273                    $value
274                );
275
276            }
277
278        } else {
279            return parent::writeAttribute($name, $value);
280        }
281
282    }
283
284}
285