1<?php
2
3namespace Sabre\VObject;
4
5use ArrayIterator;
6use Sabre\Xml;
7
8/**
9 * VObject Parameter.
10 *
11 * This class represents a parameter. A parameter is always tied to a property.
12 * In the case of:
13 *   DTSTART;VALUE=DATE:20101108
14 * VALUE=DATE would be the parameter name and value.
15 *
16 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
17 * @author Evert Pot (http://evertpot.com/)
18 * @license http://sabre.io/license/ Modified BSD License
19 */
20class Parameter extends Node
21{
22    /**
23     * Parameter name.
24     *
25     * @var string
26     */
27    public $name;
28
29    /**
30     * vCard 2.1 allows parameters to be encoded without a name.
31     *
32     * We can deduce the parameter name based on its value.
33     *
34     * @var bool
35     */
36    public $noName = false;
37
38    /**
39     * Parameter value.
40     *
41     * @var string
42     */
43    protected $value;
44
45    /**
46     * Sets up the object.
47     *
48     * It's recommended to use the create:: factory method instead.
49     *
50     * @param string $name
51     * @param string $value
52     */
53    public function __construct(Document $root, $name, $value = null)
54    {
55        $this->name = strtoupper($name);
56        $this->root = $root;
57        if (is_null($name)) {
58            $this->noName = true;
59            $this->name = static::guessParameterNameByValue($value);
60        }
61
62        // If guessParameterNameByValue() returns an empty string
63        // above, we're actually dealing with a parameter that has no value.
64        // In that case we have to move the value to the name.
65        if ('' === $this->name) {
66            $this->noName = false;
67            $this->name = strtoupper($value);
68        } else {
69            $this->setValue($value);
70        }
71    }
72
73    /**
74     * Try to guess property name by value, can be used for vCard 2.1 nameless parameters.
75     *
76     * Figuring out what the name should have been. Note that a ton of
77     * these are rather silly in 2014 and would probably rarely be
78     * used, but we like to be complete.
79     *
80     * @param string $value
81     *
82     * @return string
83     */
84    public static function guessParameterNameByValue($value)
85    {
86        switch (strtoupper($value)) {
87            // Encodings
88            case '7-BIT':
89            case 'QUOTED-PRINTABLE':
90            case 'BASE64':
91                $name = 'ENCODING';
92                break;
93
94            // Common types
95            case 'WORK':
96            case 'HOME':
97            case 'PREF':
98
99            // Delivery Label Type
100            case 'DOM':
101            case 'INTL':
102            case 'POSTAL':
103            case 'PARCEL':
104
105            // Telephone types
106            case 'VOICE':
107            case 'FAX':
108            case 'MSG':
109            case 'CELL':
110            case 'PAGER':
111            case 'BBS':
112            case 'MODEM':
113            case 'CAR':
114            case 'ISDN':
115            case 'VIDEO':
116
117            // EMAIL types (lol)
118            case 'AOL':
119            case 'APPLELINK':
120            case 'ATTMAIL':
121            case 'CIS':
122            case 'EWORLD':
123            case 'INTERNET':
124            case 'IBMMAIL':
125            case 'MCIMAIL':
126            case 'POWERSHARE':
127            case 'PRODIGY':
128            case 'TLX':
129            case 'X400':
130
131            // Photo / Logo format types
132            case 'GIF':
133            case 'CGM':
134            case 'WMF':
135            case 'BMP':
136            case 'DIB':
137            case 'PICT':
138            case 'TIFF':
139            case 'PDF':
140            case 'PS':
141            case 'JPEG':
142            case 'MPEG':
143            case 'MPEG2':
144            case 'AVI':
145            case 'QTIME':
146
147            // Sound Digital Audio Type
148            case 'WAVE':
149            case 'PCM':
150            case 'AIFF':
151
152            // Key types
153            case 'X509':
154            case 'PGP':
155                $name = 'TYPE';
156                break;
157
158            // Value types
159            case 'INLINE':
160            case 'URL':
161            case 'CONTENT-ID':
162            case 'CID':
163                $name = 'VALUE';
164                break;
165
166            default:
167                $name = '';
168        }
169
170        return $name;
171    }
172
173    /**
174     * Updates the current value.
175     *
176     * This may be either a single, or multiple strings in an array.
177     *
178     * @param string|array $value
179     */
180    public function setValue($value)
181    {
182        $this->value = $value;
183    }
184
185    /**
186     * Returns the current value.
187     *
188     * This method will always return a string, or null. If there were multiple
189     * values, it will automatically concatenate them (separated by comma).
190     *
191     * @return string|null
192     */
193    public function getValue()
194    {
195        if (is_array($this->value)) {
196            return implode(',', $this->value);
197        } else {
198            return $this->value;
199        }
200    }
201
202    /**
203     * Sets multiple values for this parameter.
204     *
205     * @param array $value
206     */
207    public function setParts(array $value)
208    {
209        $this->value = $value;
210    }
211
212    /**
213     * Returns all values for this parameter.
214     *
215     * If there were no values, an empty array will be returned.
216     *
217     * @return array
218     */
219    public function getParts()
220    {
221        if (is_array($this->value)) {
222            return $this->value;
223        } elseif (is_null($this->value)) {
224            return [];
225        } else {
226            return [$this->value];
227        }
228    }
229
230    /**
231     * Adds a value to this parameter.
232     *
233     * If the argument is specified as an array, all items will be added to the
234     * parameter value list.
235     *
236     * @param string|array $part
237     */
238    public function addValue($part)
239    {
240        if (is_null($this->value)) {
241            $this->value = $part;
242        } else {
243            $this->value = array_merge((array) $this->value, (array) $part);
244        }
245    }
246
247    /**
248     * Checks if this parameter contains the specified value.
249     *
250     * This is a case-insensitive match. It makes sense to call this for for
251     * instance the TYPE parameter, to see if it contains a keyword such as
252     * 'WORK' or 'FAX'.
253     *
254     * @param string $value
255     *
256     * @return bool
257     */
258    public function has($value)
259    {
260        return in_array(
261            strtolower($value),
262            array_map('strtolower', (array) $this->value)
263        );
264    }
265
266    /**
267     * Turns the object back into a serialized blob.
268     *
269     * @return string
270     */
271    public function serialize()
272    {
273        $value = $this->getParts();
274
275        if (0 === count($value)) {
276            return $this->name.'=';
277        }
278
279        if (Document::VCARD21 === $this->root->getDocumentType() && $this->noName) {
280            return implode(';', $value);
281        }
282
283        return $this->name.'='.array_reduce(
284            $value,
285            function ($out, $item) {
286                if (!is_null($out)) {
287                    $out .= ',';
288                }
289
290                // If there's no special characters in the string, we'll use the simple
291                // format.
292                //
293                // The list of special characters is defined as:
294                //
295                // Any character except CONTROL, DQUOTE, ";", ":", ","
296                //
297                // by the iCalendar spec:
298                // https://tools.ietf.org/html/rfc5545#section-3.1
299                //
300                // And we add ^ to that because of:
301                // https://tools.ietf.org/html/rfc6868
302                //
303                // But we've found that iCal (7.0, shipped with OSX 10.9)
304                // severaly trips on + characters not being quoted, so we
305                // added + as well.
306                if (!preg_match('#(?: [\n":;\^,\+] )#x', $item)) {
307                    return $out.$item;
308                } else {
309                    // Enclosing in double-quotes, and using RFC6868 for encoding any
310                    // special characters
311                    $out .= '"'.strtr(
312                        $item,
313                        [
314                            '^' => '^^',
315                            "\n" => '^n',
316                            '"' => '^\'',
317                        ]
318                    ).'"';
319
320                    return $out;
321                }
322            }
323        );
324    }
325
326    /**
327     * This method returns an array, with the representation as it should be
328     * encoded in JSON. This is used to create jCard or jCal documents.
329     *
330     * @return array
331     */
332    public function jsonSerialize()
333    {
334        return $this->value;
335    }
336
337    /**
338     * This method serializes the data into XML. This is used to create xCard or
339     * xCal documents.
340     *
341     * @param Xml\Writer $writer XML writer
342     */
343    public function xmlSerialize(Xml\Writer $writer)
344    {
345        foreach (explode(',', $this->value) as $value) {
346            $writer->writeElement('text', $value);
347        }
348    }
349
350    /**
351     * Called when this object is being cast to a string.
352     *
353     * @return string
354     */
355    public function __toString()
356    {
357        return (string) $this->getValue();
358    }
359
360    /**
361     * Returns the iterator for this object.
362     *
363     * @return ElementList
364     */
365    public function getIterator()
366    {
367        if (!is_null($this->iterator)) {
368            return $this->iterator;
369        }
370
371        return $this->iterator = new ArrayIterator((array) $this->value);
372    }
373}
374