1<?php
2
3namespace Sabre\VObject\Parser;
4
5use Sabre\VObject\Component;
6use Sabre\VObject\Component\VCalendar;
7use Sabre\VObject\Component\VCard;
8use Sabre\VObject\EofException;
9use Sabre\VObject\ParseException;
10use Sabre\Xml as SabreXml;
11
12/**
13 * XML Parser.
14 *
15 * This parser parses both the xCal and xCard formats.
16 *
17 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
18 * @author Ivan Enderlin
19 * @license http://sabre.io/license/ Modified BSD License
20 */
21class XML extends Parser {
22
23    const XCAL_NAMESPACE = 'urn:ietf:params:xml:ns:icalendar-2.0';
24    const XCARD_NAMESPACE = 'urn:ietf:params:xml:ns:vcard-4.0';
25
26    /**
27     * The input data.
28     *
29     * @var array
30     */
31    protected $input;
32
33    /**
34     * A pointer/reference to the input.
35     *
36     * @var array
37     */
38    private $pointer;
39
40    /**
41     * Document, root component.
42     *
43     * @var Sabre\VObject\Document
44     */
45    protected $root;
46
47    /**
48     * Creates the parser.
49     *
50     * Optionally, it's possible to parse the input stream here.
51     *
52     * @param mixed $input
53     * @param int $options Any parser options (OPTION constants).
54     *
55     * @return void
56     */
57    function __construct($input = null, $options = 0) {
58
59        if (0 === $options) {
60            $options = parent::OPTION_FORGIVING;
61        }
62
63        parent::__construct($input, $options);
64
65    }
66
67    /**
68     * Parse xCal or xCard.
69     *
70     * @param resource|string $input
71     * @param int $options
72     *
73     * @throws \Exception
74     *
75     * @return Sabre\VObject\Document
76     */
77    function parse($input = null, $options = 0) {
78
79        if (!is_null($input)) {
80            $this->setInput($input);
81        }
82
83        if (0 !== $options) {
84            $this->options = $options;
85        }
86
87        if (is_null($this->input)) {
88            throw new EofException('End of input stream, or no input supplied');
89        }
90
91        switch ($this->input['name']) {
92
93            case '{' . self::XCAL_NAMESPACE . '}icalendar':
94                $this->root = new VCalendar([], false);
95                $this->pointer = &$this->input['value'][0];
96                $this->parseVCalendarComponents($this->root);
97                break;
98
99            case '{' . self::XCARD_NAMESPACE . '}vcards':
100                foreach ($this->input['value'] as &$vCard) {
101
102                    $this->root = new VCard(['version' => '4.0'], false);
103                    $this->pointer = &$vCard;
104                    $this->parseVCardComponents($this->root);
105
106                    // We just parse the first <vcard /> element.
107                    break;
108
109                }
110                break;
111
112            default:
113                throw new ParseException('Unsupported XML standard');
114
115        }
116
117        return $this->root;
118    }
119
120    /**
121     * Parse a xCalendar component.
122     *
123     * @param Component $parentComponent
124     *
125     * @return void
126     */
127    protected function parseVCalendarComponents(Component $parentComponent) {
128
129        foreach ($this->pointer['value'] ?: [] as $children) {
130
131            switch (static::getTagName($children['name'])) {
132
133                case 'properties':
134                    $this->pointer = &$children['value'];
135                    $this->parseProperties($parentComponent);
136                    break;
137
138                case 'components':
139                    $this->pointer = &$children;
140                    $this->parseComponent($parentComponent);
141                    break;
142            }
143        }
144
145    }
146
147    /**
148     * Parse a xCard component.
149     *
150     * @param Component $parentComponent
151     *
152     * @return void
153     */
154    protected function parseVCardComponents(Component $parentComponent) {
155
156        $this->pointer = &$this->pointer['value'];
157        $this->parseProperties($parentComponent);
158
159    }
160
161    /**
162     * Parse xCalendar and xCard properties.
163     *
164     * @param Component $parentComponent
165     * @param string  $propertyNamePrefix
166     *
167     * @return void
168     */
169    protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '') {
170
171        foreach ($this->pointer ?: [] as $xmlProperty) {
172
173            list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']);
174
175            $propertyName = $tagName;
176            $propertyValue = [];
177            $propertyParameters = [];
178            $propertyType = 'text';
179
180            // A property which is not part of the standard.
181            if ($namespace !== self::XCAL_NAMESPACE
182                && $namespace !== self::XCARD_NAMESPACE) {
183
184                $propertyName = 'xml';
185                $value = '<' . $tagName . ' xmlns="' . $namespace . '"';
186
187                foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) {
188                    $value .= ' ' . $attributeName . '="' . str_replace('"', '\"', $attributeValue) . '"';
189                }
190
191                $value .= '>' . $xmlProperty['value'] . '</' . $tagName . '>';
192
193                $propertyValue = [$value];
194
195                $this->createProperty(
196                    $parentComponent,
197                    $propertyName,
198                    $propertyParameters,
199                    $propertyType,
200                    $propertyValue
201                );
202
203                continue;
204            }
205
206            // xCard group.
207            if ($propertyName === 'group') {
208
209                if (!isset($xmlProperty['attributes']['name'])) {
210                    continue;
211                }
212
213                $this->pointer = &$xmlProperty['value'];
214                $this->parseProperties(
215                    $parentComponent,
216                    strtoupper($xmlProperty['attributes']['name']) . '.'
217                );
218
219                continue;
220
221            }
222
223            // Collect parameters.
224            foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) {
225
226                if (!is_array($xmlPropertyChild)
227                    || 'parameters' !== static::getTagName($xmlPropertyChild['name']))
228                    continue;
229
230                $xmlParameters = $xmlPropertyChild['value'];
231
232                foreach ($xmlParameters as $xmlParameter) {
233
234                    $propertyParameterValues = [];
235
236                    foreach ($xmlParameter['value'] as $xmlParameterValues) {
237                        $propertyParameterValues[] = $xmlParameterValues['value'];
238                    }
239
240                    $propertyParameters[static::getTagName($xmlParameter['name'])]
241                        = implode(',', $propertyParameterValues);
242
243                }
244
245                array_splice($xmlProperty['value'], $i, 1);
246
247            }
248
249            $propertyNameExtended = ($this->root instanceof VCalendar
250                                      ? 'xcal'
251                                      : 'xcard') . ':' . $propertyName;
252
253            switch ($propertyNameExtended) {
254
255                case 'xcal:geo':
256                    $propertyType = 'float';
257                    $propertyValue['latitude'] = 0;
258                    $propertyValue['longitude'] = 0;
259
260                    foreach ($xmlProperty['value'] as $xmlRequestChild) {
261                        $propertyValue[static::getTagName($xmlRequestChild['name'])]
262                            = $xmlRequestChild['value'];
263                    }
264                    break;
265
266                case 'xcal:request-status':
267                    $propertyType = 'text';
268
269                    foreach ($xmlProperty['value'] as $xmlRequestChild) {
270                        $propertyValue[static::getTagName($xmlRequestChild['name'])]
271                            = $xmlRequestChild['value'];
272                    }
273                    break;
274
275                case 'xcal:freebusy':
276                    $propertyType = 'freebusy';
277                    // We don't break because we only want to set
278                    // another property type.
279
280                case 'xcal:categories':
281                case 'xcal:resources':
282                case 'xcal:exdate':
283                    foreach ($xmlProperty['value'] as $specialChild) {
284                        $propertyValue[static::getTagName($specialChild['name'])]
285                            = $specialChild['value'];
286                    }
287                    break;
288
289                case 'xcal:rdate':
290                    $propertyType = 'date-time';
291
292                    foreach ($xmlProperty['value'] as $specialChild) {
293
294                        $tagName = static::getTagName($specialChild['name']);
295
296                        if ('period' === $tagName) {
297
298                            $propertyParameters['value'] = 'PERIOD';
299                            $propertyValue[] = implode('/', $specialChild['value']);
300
301                        }
302                        else {
303                            $propertyValue[] = $specialChild['value'];
304                        }
305                    }
306                    break;
307
308                default:
309                    $propertyType = static::getTagName($xmlProperty['value'][0]['name']);
310
311                    foreach ($xmlProperty['value'] as $value) {
312                        $propertyValue[] = $value['value'];
313                    }
314
315                    if ('date' === $propertyType) {
316                        $propertyParameters['value'] = 'DATE';
317                    }
318                    break;
319            }
320
321            $this->createProperty(
322                $parentComponent,
323                $propertyNamePrefix . $propertyName,
324                $propertyParameters,
325                $propertyType,
326                $propertyValue
327            );
328
329        }
330
331    }
332
333    /**
334     * Parse a component.
335     *
336     * @param Component $parentComponent
337     *
338     * @return void
339     */
340    protected function parseComponent(Component $parentComponent) {
341
342        $components = $this->pointer['value'] ?: [];
343
344        foreach ($components as $component) {
345
346            $componentName = static::getTagName($component['name']);
347            $currentComponent = $this->root->createComponent(
348                $componentName,
349                null,
350                false
351            );
352
353            $this->pointer = &$component;
354            $this->parseVCalendarComponents($currentComponent);
355
356            $parentComponent->add($currentComponent);
357
358        }
359
360    }
361
362    /**
363     * Create a property.
364     *
365     * @param Component $parentComponent
366     * @param string $name
367     * @param array $parameters
368     * @param string $type
369     * @param mixed $value
370     *
371     * @return void
372     */
373    protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value) {
374
375        $property = $this->root->createProperty(
376            $name,
377            null,
378            $parameters,
379            $type
380        );
381        $parentComponent->add($property);
382        $property->setXmlValue($value);
383
384    }
385
386    /**
387     * Sets the input data.
388     *
389     * @param resource|string $input
390     *
391     * @return void
392     */
393    function setInput($input) {
394
395        if (is_resource($input)) {
396            $input = stream_get_contents($input);
397        }
398
399        if (is_string($input)) {
400
401            $reader = new SabreXml\Reader();
402            $reader->elementMap['{' . self::XCAL_NAMESPACE . '}period']
403                = 'Sabre\VObject\Parser\XML\Element\KeyValue';
404            $reader->elementMap['{' . self::XCAL_NAMESPACE . '}recur']
405                = 'Sabre\VObject\Parser\XML\Element\KeyValue';
406            $reader->xml($input);
407            $input = $reader->parse();
408
409        }
410
411        $this->input = $input;
412
413    }
414
415    /**
416     * Get tag name from a Clark notation.
417     *
418     * @param string $clarkedTagName
419     *
420     * @return string
421     */
422    protected static function getTagName($clarkedTagName) {
423
424        list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName);
425        return $tagName;
426
427    }
428}
429