1<?php
2
3namespace Sabre\VObject;
4
5/**
6 * Property
7 *
8 * A property is always in a KEY:VALUE structure, and may optionally contain
9 * parameters.
10 *
11 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
12 * @author Evert Pot (http://evertpot.com/)
13 * @license http://sabre.io/license/ Modified BSD License
14 */
15abstract class Property extends Node {
16
17    /**
18     * Property name.
19     *
20     * This will contain a string such as DTSTART, SUMMARY, FN.
21     *
22     * @var string
23     */
24    public $name;
25
26    /**
27     * Property group.
28     *
29     * This is only used in vcards
30     *
31     * @var string
32     */
33    public $group;
34
35    /**
36     * List of parameters
37     *
38     * @var array
39     */
40    public $parameters = array();
41
42    /**
43     * Current value
44     *
45     * @var mixed
46     */
47    protected $value;
48
49    /**
50     * In case this is a multi-value property. This string will be used as a
51     * delimiter.
52     *
53     * @var string|null
54     */
55    public $delimiter = ';';
56
57    /**
58     * Creates the generic property.
59     *
60     * Parameters must be specified in key=>value syntax.
61     *
62     * @param Component $root The root document
63     * @param string $name
64     * @param string|array|null $value
65     * @param array $parameters List of parameters
66     * @param string $group The vcard property group
67     * @return void
68     */
69    function __construct(Component $root, $name, $value = null, array $parameters = array(), $group = null) {
70
71        $this->name = $name;
72        $this->group = $group;
73
74        $this->root = $root;
75
76        foreach($parameters as $k=>$v) {
77            $this->add($k, $v);
78        }
79
80        if (!is_null($value)) {
81            $this->setValue($value);
82        }
83
84    }
85
86    /**
87     * Updates the current value.
88     *
89     * This may be either a single, or multiple strings in an array.
90     *
91     * @param string|array $value
92     * @return void
93     */
94    function setValue($value) {
95
96        $this->value = $value;
97
98    }
99
100    /**
101     * Returns the current value.
102     *
103     * This method will always return a singular value. If this was a
104     * multi-value object, some decision will be made first on how to represent
105     * it as a string.
106     *
107     * To get the correct multi-value version, use getParts.
108     *
109     * @return string
110     */
111    function getValue() {
112
113        if (is_array($this->value)) {
114            if (count($this->value)==0) {
115                return null;
116            } elseif (count($this->value)===1) {
117                return $this->value[0];
118            } else {
119                return $this->getRawMimeDirValue($this->value);
120            }
121        } else {
122            return $this->value;
123        }
124
125    }
126
127    /**
128     * Sets a multi-valued property.
129     *
130     * @param array $parts
131     * @return void
132     */
133    function setParts(array $parts) {
134
135        $this->value = $parts;
136
137    }
138
139    /**
140     * Returns a multi-valued property.
141     *
142     * This method always returns an array, if there was only a single value,
143     * it will still be wrapped in an array.
144     *
145     * @return array
146     */
147    function getParts() {
148
149        if (is_null($this->value)) {
150            return array();
151        } elseif (is_array($this->value)) {
152            return $this->value;
153        } else {
154            return array($this->value);
155        }
156
157    }
158
159    /**
160     * Adds a new parameter, and returns the new item.
161     *
162     * If a parameter with same name already existed, the values will be
163     * combined.
164     * If nameless parameter is added, we try to guess it's name.
165     *
166     * @param string $name
167     * @param string|null|array $value
168     * @return Node
169     */
170    function add($name, $value = null) {
171        $noName = false;
172        if ($name === null) {
173            $name = Parameter::guessParameterNameByValue($value);
174            $noName = true;
175        }
176
177        if (isset($this->parameters[strtoupper($name)])) {
178            $this->parameters[strtoupper($name)]->addValue($value);
179        }
180        else {
181            $param = new Parameter($this->root, $name, $value);
182            $param->noName = $noName;
183            $this->parameters[$param->name] = $param;
184        }
185    }
186
187    /**
188     * Returns an iterable list of children
189     *
190     * @return array
191     */
192    function parameters() {
193
194        return $this->parameters;
195
196    }
197
198    /**
199     * Returns the type of value.
200     *
201     * This corresponds to the VALUE= parameter. Every property also has a
202     * 'default' valueType.
203     *
204     * @return string
205     */
206    abstract function getValueType();
207
208    /**
209     * Sets a raw value coming from a mimedir (iCalendar/vCard) file.
210     *
211     * This has been 'unfolded', so only 1 line will be passed. Unescaping is
212     * not yet done, but parameters are not included.
213     *
214     * @param string $val
215     * @return void
216     */
217    abstract function setRawMimeDirValue($val);
218
219    /**
220     * Returns a raw mime-dir representation of the value.
221     *
222     * @return string
223     */
224    abstract function getRawMimeDirValue();
225
226    /**
227     * Turns the object back into a serialized blob.
228     *
229     * @return string
230     */
231    function serialize() {
232
233        $str = $this->name;
234        if ($this->group) $str = $this->group . '.' . $this->name;
235
236        foreach($this->parameters as $param) {
237
238            $str.=';' . $param->serialize();
239
240        }
241
242        $str.=':' . $this->getRawMimeDirValue();
243
244        $out = '';
245        while(strlen($str)>0) {
246            if (strlen($str)>75) {
247                $out.= mb_strcut($str,0,75,'utf-8') . "\r\n";
248                $str = ' ' . mb_strcut($str,75,strlen($str),'utf-8');
249            } else {
250                $out.=$str . "\r\n";
251                $str='';
252                break;
253            }
254        }
255
256        return $out;
257
258    }
259
260    /**
261     * Returns the value, in the format it should be encoded for json.
262     *
263     * This method must always return an array.
264     *
265     * @return array
266     */
267    function getJsonValue() {
268
269        return $this->getParts();
270
271    }
272
273    /**
274     * Sets the json value, as it would appear in a jCard or jCal object.
275     *
276     * The value must always be an array.
277     *
278     * @param array $value
279     * @return void
280     */
281    function setJsonValue(array $value) {
282
283        if (count($value)===1) {
284            $this->setValue(reset($value));
285        } else {
286            $this->setValue($value);
287        }
288
289    }
290
291    /**
292     * This method returns an array, with the representation as it should be
293     * encoded in json. This is used to create jCard or jCal documents.
294     *
295     * @return array
296     */
297    function jsonSerialize() {
298
299        $parameters = array();
300
301        foreach($this->parameters as $parameter) {
302            if ($parameter->name === 'VALUE') {
303                continue;
304            }
305            $parameters[strtolower($parameter->name)] = $parameter->jsonSerialize();
306        }
307        // In jCard, we need to encode the property-group as a separate 'group'
308        // parameter.
309        if ($this->group) {
310            $parameters['group'] = $this->group;
311        }
312
313        return array_merge(
314            array(
315                strtolower($this->name),
316                (object)$parameters,
317                strtolower($this->getValueType()),
318            ),
319            $this->getJsonValue()
320        );
321    }
322
323
324    /**
325     * Called when this object is being cast to a string.
326     *
327     * If the property only had a single value, you will get just that. In the
328     * case the property had multiple values, the contents will be escaped and
329     * combined with ,.
330     *
331     * @return string
332     */
333    function __toString() {
334
335        return (string)$this->getValue();
336
337    }
338
339    /* ArrayAccess interface {{{ */
340
341    /**
342     * Checks if an array element exists
343     *
344     * @param mixed $name
345     * @return bool
346     */
347    function offsetExists($name) {
348
349        if (is_int($name)) return parent::offsetExists($name);
350
351        $name = strtoupper($name);
352
353        foreach($this->parameters as $parameter) {
354            if ($parameter->name == $name) return true;
355        }
356        return false;
357
358    }
359
360    /**
361     * Returns a parameter.
362     *
363     * If the parameter does not exist, null is returned.
364     *
365     * @param string $name
366     * @return Node
367     */
368    function offsetGet($name) {
369
370        if (is_int($name)) return parent::offsetGet($name);
371        $name = strtoupper($name);
372
373        if (!isset($this->parameters[$name])) {
374            return null;
375        }
376
377        return $this->parameters[$name];
378
379    }
380
381    /**
382     * Creates a new parameter
383     *
384     * @param string $name
385     * @param mixed $value
386     * @return void
387     */
388    function offsetSet($name, $value) {
389
390        if (is_int($name)) {
391            parent::offsetSet($name, $value);
392            // @codeCoverageIgnoreStart
393            // This will never be reached, because an exception is always
394            // thrown.
395            return;
396            // @codeCoverageIgnoreEnd
397        }
398
399        $param = new Parameter($this->root, $name, $value);
400        $this->parameters[$param->name] = $param;
401
402    }
403
404    /**
405     * Removes one or more parameters with the specified name
406     *
407     * @param string $name
408     * @return void
409     */
410    function offsetUnset($name) {
411
412        if (is_int($name)) {
413            parent::offsetUnset($name);
414            // @codeCoverageIgnoreStart
415            // This will never be reached, because an exception is always
416            // thrown.
417            return;
418            // @codeCoverageIgnoreEnd
419        }
420
421        unset($this->parameters[strtoupper($name)]);
422
423    }
424    /* }}} */
425
426    /**
427     * This method is automatically called when the object is cloned.
428     * Specifically, this will ensure all child elements are also cloned.
429     *
430     * @return void
431     */
432    function __clone() {
433
434        foreach($this->parameters as $key=>$child) {
435            $this->parameters[$key] = clone $child;
436            $this->parameters[$key]->parent = $this;
437        }
438
439    }
440
441    /**
442     * Validates the node for correctness.
443     *
444     * The following options are supported:
445     *   - Node::REPAIR - If something is broken, and automatic repair may
446     *                    be attempted.
447     *
448     * An array is returned with warnings.
449     *
450     * Every item in the array has the following properties:
451     *    * level - (number between 1 and 3 with severity information)
452     *    * message - (human readable message)
453     *    * node - (reference to the offending node)
454     *
455     * @param int $options
456     * @return array
457     */
458    function validate($options = 0) {
459
460        $warnings = array();
461
462        // Checking if our value is UTF-8
463        if (!StringUtil::isUTF8($this->getRawMimeDirValue())) {
464
465            $oldValue = $this->getRawMimeDirValue();
466            $level = 3;
467            if ($options & self::REPAIR) {
468                $newValue = StringUtil::convertToUTF8($oldValue);
469                if (true || StringUtil::isUTF8($newValue)) {
470                    $this->setRawMimeDirValue($newValue);
471                    $level = 1;
472                }
473
474            }
475
476
477            if (preg_match('%([\x00-\x08\x0B-\x0C\x0E-\x1F\x7F])%', $oldValue, $matches)) {
478                $message = 'Property contained a control character (0x' . bin2hex($matches[1]) . ')';
479            } else {
480                $message = 'Property is not valid UTF-8! ' . $oldValue;
481            }
482
483            $warnings[] = array(
484                'level' => $level,
485                'message' => $message,
486                'node' => $this,
487            );
488        }
489
490        // Checking if the propertyname does not contain any invalid bytes.
491        if (!preg_match('/^([A-Z0-9-]+)$/', $this->name)) {
492            $warnings[] = array(
493                'level' => 1,
494                'message' => 'The propertyname: ' . $this->name . ' contains invalid characters. Only A-Z, 0-9 and - are allowed',
495                'node' => $this,
496            );
497            if ($options & self::REPAIR) {
498                // Uppercasing and converting underscores to dashes.
499                $this->name = strtoupper(
500                    str_replace('_', '-', $this->name)
501                );
502                // Removing every other invalid character
503                $this->name = preg_replace('/([^A-Z0-9-])/u', '', $this->name);
504
505            }
506
507        }
508
509        if ($encoding = $this->offsetGet('ENCODING')) {
510
511            if ($this->root->getDocumentType()===Document::VCARD40) {
512                $warnings[] = array(
513                    'level' => 1,
514                    'message' => 'ENCODING parameter is not valid in vCard 4.',
515                    'node' => $this
516                );
517            } else {
518
519                $encoding = (string)$encoding;
520
521                $allowedEncoding = array();
522
523                switch($this->root->getDocumentType()) {
524                    case Document::ICALENDAR20 :
525                        $allowedEncoding = array('8BIT', 'BASE64');
526                        break;
527                    case Document::VCARD21 :
528                        $allowedEncoding = array('QUOTED-PRINTABLE', 'BASE64', '8BIT');
529                        break;
530                    case Document::VCARD30 :
531                        $allowedEncoding = array('B');
532                        break;
533
534                }
535                if ($allowedEncoding && !in_array(strtoupper($encoding), $allowedEncoding)) {
536                    $warnings[] = array(
537                        'level' => 1,
538                        'message' => 'ENCODING=' . strtoupper($encoding) . ' is not valid for this document type.',
539                        'node' => $this
540                    );
541                }
542            }
543
544        }
545
546        // Validating inner parameters
547        foreach($this->parameters as $param) {
548            $warnings = array_merge($warnings, $param->validate($options));
549        }
550
551        return $warnings;
552
553    }
554
555}
556