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    public function __construct($input = null, $options = 0)
56    {
57        if (0 === $options) {
58            $options = parent::OPTION_FORGIVING;
59        }
60
61        parent::__construct($input, $options);
62    }
63
64    /**
65     * Parse xCal or xCard.
66     *
67     * @param resource|string $input
68     * @param int             $options
69     *
70     * @throws \Exception
71     *
72     * @return \Sabre\VObject\Document
73     */
74    public function parse($input = null, $options = 0)
75    {
76        if (!is_null($input)) {
77            $this->setInput($input);
78        }
79
80        if (0 !== $options) {
81            $this->options = $options;
82        }
83
84        if (is_null($this->input)) {
85            throw new EofException('End of input stream, or no input supplied');
86        }
87
88        switch ($this->input['name']) {
89            case '{'.self::XCAL_NAMESPACE.'}icalendar':
90                $this->root = new VCalendar([], false);
91                $this->pointer = &$this->input['value'][0];
92                $this->parseVCalendarComponents($this->root);
93                break;
94
95            case '{'.self::XCARD_NAMESPACE.'}vcards':
96                foreach ($this->input['value'] as &$vCard) {
97                    $this->root = new VCard(['version' => '4.0'], false);
98                    $this->pointer = &$vCard;
99                    $this->parseVCardComponents($this->root);
100
101                    // We just parse the first <vcard /> element.
102                    break;
103                }
104                break;
105
106            default:
107                throw new ParseException('Unsupported XML standard');
108        }
109
110        return $this->root;
111    }
112
113    /**
114     * Parse a xCalendar component.
115     *
116     * @param Component $parentComponent
117     */
118    protected function parseVCalendarComponents(Component $parentComponent)
119    {
120        foreach ($this->pointer['value'] ?: [] as $children) {
121            switch (static::getTagName($children['name'])) {
122                case 'properties':
123                    $this->pointer = &$children['value'];
124                    $this->parseProperties($parentComponent);
125                    break;
126
127                case 'components':
128                    $this->pointer = &$children;
129                    $this->parseComponent($parentComponent);
130                    break;
131            }
132        }
133    }
134
135    /**
136     * Parse a xCard component.
137     *
138     * @param Component $parentComponent
139     */
140    protected function parseVCardComponents(Component $parentComponent)
141    {
142        $this->pointer = &$this->pointer['value'];
143        $this->parseProperties($parentComponent);
144    }
145
146    /**
147     * Parse xCalendar and xCard properties.
148     *
149     * @param Component $parentComponent
150     * @param string    $propertyNamePrefix
151     */
152    protected function parseProperties(Component $parentComponent, $propertyNamePrefix = '')
153    {
154        foreach ($this->pointer ?: [] as $xmlProperty) {
155            list($namespace, $tagName) = SabreXml\Service::parseClarkNotation($xmlProperty['name']);
156
157            $propertyName = $tagName;
158            $propertyValue = [];
159            $propertyParameters = [];
160            $propertyType = 'text';
161
162            // A property which is not part of the standard.
163            if (self::XCAL_NAMESPACE !== $namespace
164                && self::XCARD_NAMESPACE !== $namespace) {
165                $propertyName = 'xml';
166                $value = '<'.$tagName.' xmlns="'.$namespace.'"';
167
168                foreach ($xmlProperty['attributes'] as $attributeName => $attributeValue) {
169                    $value .= ' '.$attributeName.'="'.str_replace('"', '\"', $attributeValue).'"';
170                }
171
172                $value .= '>'.$xmlProperty['value'].'</'.$tagName.'>';
173
174                $propertyValue = [$value];
175
176                $this->createProperty(
177                    $parentComponent,
178                    $propertyName,
179                    $propertyParameters,
180                    $propertyType,
181                    $propertyValue
182                );
183
184                continue;
185            }
186
187            // xCard group.
188            if ('group' === $propertyName) {
189                if (!isset($xmlProperty['attributes']['name'])) {
190                    continue;
191                }
192
193                $this->pointer = &$xmlProperty['value'];
194                $this->parseProperties(
195                    $parentComponent,
196                    strtoupper($xmlProperty['attributes']['name']).'.'
197                );
198
199                continue;
200            }
201
202            // Collect parameters.
203            foreach ($xmlProperty['value'] as $i => $xmlPropertyChild) {
204                if (!is_array($xmlPropertyChild)
205                    || 'parameters' !== static::getTagName($xmlPropertyChild['name'])) {
206                    continue;
207                }
208
209                $xmlParameters = $xmlPropertyChild['value'];
210
211                foreach ($xmlParameters as $xmlParameter) {
212                    $propertyParameterValues = [];
213
214                    foreach ($xmlParameter['value'] as $xmlParameterValues) {
215                        $propertyParameterValues[] = $xmlParameterValues['value'];
216                    }
217
218                    $propertyParameters[static::getTagName($xmlParameter['name'])]
219                        = implode(',', $propertyParameterValues);
220                }
221
222                array_splice($xmlProperty['value'], $i, 1);
223            }
224
225            $propertyNameExtended = ($this->root instanceof VCalendar
226                                      ? 'xcal'
227                                      : 'xcard').':'.$propertyName;
228
229            switch ($propertyNameExtended) {
230                case 'xcal:geo':
231                    $propertyType = 'float';
232                    $propertyValue['latitude'] = 0;
233                    $propertyValue['longitude'] = 0;
234
235                    foreach ($xmlProperty['value'] as $xmlRequestChild) {
236                        $propertyValue[static::getTagName($xmlRequestChild['name'])]
237                            = $xmlRequestChild['value'];
238                    }
239                    break;
240
241                case 'xcal:request-status':
242                    $propertyType = 'text';
243
244                    foreach ($xmlProperty['value'] as $xmlRequestChild) {
245                        $propertyValue[static::getTagName($xmlRequestChild['name'])]
246                            = $xmlRequestChild['value'];
247                    }
248                    break;
249
250                case 'xcal:freebusy':
251                    $propertyType = 'freebusy';
252                    // We don't break because we only want to set
253                    // another property type.
254
255                    // no break
256                case 'xcal:categories':
257                case 'xcal:resources':
258                case 'xcal:exdate':
259                    foreach ($xmlProperty['value'] as $specialChild) {
260                        $propertyValue[static::getTagName($specialChild['name'])]
261                            = $specialChild['value'];
262                    }
263                    break;
264
265                case 'xcal:rdate':
266                    $propertyType = 'date-time';
267
268                    foreach ($xmlProperty['value'] as $specialChild) {
269                        $tagName = static::getTagName($specialChild['name']);
270
271                        if ('period' === $tagName) {
272                            $propertyParameters['value'] = 'PERIOD';
273                            $propertyValue[] = implode('/', $specialChild['value']);
274                        } else {
275                            $propertyValue[] = $specialChild['value'];
276                        }
277                    }
278                    break;
279
280                default:
281                    $propertyType = static::getTagName($xmlProperty['value'][0]['name']);
282
283                    foreach ($xmlProperty['value'] as $value) {
284                        $propertyValue[] = $value['value'];
285                    }
286
287                    if ('date' === $propertyType) {
288                        $propertyParameters['value'] = 'DATE';
289                    }
290                    break;
291            }
292
293            $this->createProperty(
294                $parentComponent,
295                $propertyNamePrefix.$propertyName,
296                $propertyParameters,
297                $propertyType,
298                $propertyValue
299            );
300        }
301    }
302
303    /**
304     * Parse a component.
305     *
306     * @param Component $parentComponent
307     */
308    protected function parseComponent(Component $parentComponent)
309    {
310        $components = $this->pointer['value'] ?: [];
311
312        foreach ($components as $component) {
313            $componentName = static::getTagName($component['name']);
314            $currentComponent = $this->root->createComponent(
315                $componentName,
316                null,
317                false
318            );
319
320            $this->pointer = &$component;
321            $this->parseVCalendarComponents($currentComponent);
322
323            $parentComponent->add($currentComponent);
324        }
325    }
326
327    /**
328     * Create a property.
329     *
330     * @param Component $parentComponent
331     * @param string    $name
332     * @param array     $parameters
333     * @param string    $type
334     * @param mixed     $value
335     */
336    protected function createProperty(Component $parentComponent, $name, $parameters, $type, $value)
337    {
338        $property = $this->root->createProperty(
339            $name,
340            null,
341            $parameters,
342            $type
343        );
344        $parentComponent->add($property);
345        $property->setXmlValue($value);
346    }
347
348    /**
349     * Sets the input data.
350     *
351     * @param resource|string $input
352     */
353    public function setInput($input)
354    {
355        if (is_resource($input)) {
356            $input = stream_get_contents($input);
357        }
358
359        if (is_string($input)) {
360            $reader = new SabreXml\Reader();
361            $reader->elementMap['{'.self::XCAL_NAMESPACE.'}period']
362                = 'Sabre\VObject\Parser\XML\Element\KeyValue';
363            $reader->elementMap['{'.self::XCAL_NAMESPACE.'}recur']
364                = 'Sabre\VObject\Parser\XML\Element\KeyValue';
365            $reader->xml($input);
366            $input = $reader->parse();
367        }
368
369        $this->input = $input;
370    }
371
372    /**
373     * Get tag name from a Clark notation.
374     *
375     * @param string $clarkedTagName
376     *
377     * @return string
378     */
379    protected static function getTagName($clarkedTagName)
380    {
381        list(, $tagName) = SabreXml\Service::parseClarkNotation($clarkedTagName);
382
383        return $tagName;
384    }
385}
386