1<?php
2
3namespace Sabre\VObject\Parser;
4
5use Sabre\VObject\Component\VCalendar;
6use Sabre\VObject\Component\VCard;
7use Sabre\VObject\Document;
8use Sabre\VObject\EofException;
9use Sabre\VObject\ParseException;
10use Sabre\VObject\Property\FlatText;
11use Sabre\VObject\Property\Text;
12
13/**
14 * Json Parser.
15 *
16 * This parser parses both the jCal and jCard formats.
17 *
18 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
19 * @author Evert Pot (http://evertpot.com/)
20 * @license http://sabre.io/license/ Modified BSD License
21 */
22class Json extends Parser
23{
24    /**
25     * The input data.
26     *
27     * @var array
28     */
29    protected $input;
30
31    /**
32     * Root component.
33     *
34     * @var Document
35     */
36    protected $root;
37
38    /**
39     * This method starts the parsing process.
40     *
41     * If the input was not supplied during construction, it's possible to pass
42     * it here instead.
43     *
44     * If either input or options are not supplied, the defaults will be used.
45     *
46     * @param resource|string|array|null $input
47     * @param int                        $options
48     *
49     * @return \Sabre\VObject\Document
50     */
51    public function parse($input = null, $options = 0)
52    {
53        if (!is_null($input)) {
54            $this->setInput($input);
55        }
56        if (is_null($this->input)) {
57            throw new EofException('End of input stream, or no input supplied');
58        }
59
60        if (0 !== $options) {
61            $this->options = $options;
62        }
63
64        switch ($this->input[0]) {
65            case 'vcalendar':
66                $this->root = new VCalendar([], false);
67                break;
68            case 'vcard':
69                $this->root = new VCard([], false);
70                break;
71            default:
72                throw new ParseException('The root component must either be a vcalendar, or a vcard');
73        }
74        foreach ($this->input[1] as $prop) {
75            $this->root->add($this->parseProperty($prop));
76        }
77        if (isset($this->input[2])) {
78            foreach ($this->input[2] as $comp) {
79                $this->root->add($this->parseComponent($comp));
80            }
81        }
82
83        // Resetting the input so we can throw an feof exception the next time.
84        $this->input = null;
85
86        return $this->root;
87    }
88
89    /**
90     * Parses a component.
91     *
92     * @return \Sabre\VObject\Component
93     */
94    public function parseComponent(array $jComp)
95    {
96        // We can remove $self from PHP 5.4 onward.
97        $self = $this;
98
99        $properties = array_map(
100            function ($jProp) use ($self) {
101                return $self->parseProperty($jProp);
102            },
103            $jComp[1]
104        );
105
106        if (isset($jComp[2])) {
107            $components = array_map(
108                function ($jComp) use ($self) {
109                    return $self->parseComponent($jComp);
110                },
111                $jComp[2]
112            );
113        } else {
114            $components = [];
115        }
116
117        return $this->root->createComponent(
118            $jComp[0],
119            array_merge($properties, $components),
120            $defaults = false
121        );
122    }
123
124    /**
125     * Parses properties.
126     *
127     * @return \Sabre\VObject\Property
128     */
129    public function parseProperty(array $jProp)
130    {
131        list(
132            $propertyName,
133            $parameters,
134            $valueType
135        ) = $jProp;
136
137        $propertyName = strtoupper($propertyName);
138
139        // This is the default class we would be using if we didn't know the
140        // value type. We're using this value later in this function.
141        $defaultPropertyClass = $this->root->getClassNameForPropertyName($propertyName);
142
143        $parameters = (array) $parameters;
144
145        $value = array_slice($jProp, 3);
146
147        $valueType = strtoupper($valueType);
148
149        if (isset($parameters['group'])) {
150            $propertyName = $parameters['group'].'.'.$propertyName;
151            unset($parameters['group']);
152        }
153
154        $prop = $this->root->createProperty($propertyName, null, $parameters, $valueType);
155        $prop->setJsonValue($value);
156
157        // We have to do something awkward here. FlatText as well as Text
158        // represents TEXT values. We have to normalize these here. In the
159        // future we can get rid of FlatText once we're allowed to break BC
160        // again.
161        if (FlatText::class === $defaultPropertyClass) {
162            $defaultPropertyClass = Text::class;
163        }
164
165        // If the value type we received (e.g.: TEXT) was not the default value
166        // type for the given property (e.g.: BDAY), we need to add a VALUE=
167        // parameter.
168        if ($defaultPropertyClass !== get_class($prop)) {
169            $prop['VALUE'] = $valueType;
170        }
171
172        return $prop;
173    }
174
175    /**
176     * Sets the input data.
177     *
178     * @param resource|string|array $input
179     */
180    public function setInput($input)
181    {
182        if (is_resource($input)) {
183            $input = stream_get_contents($input);
184        }
185        if (is_string($input)) {
186            $input = json_decode($input);
187        }
188        $this->input = $input;
189    }
190}
191