1<?php
2
3namespace Sabre\VObject\Component;
4
5use Sabre\VObject;
6use Sabre\Xml;
7
8/**
9 * The VCard component.
10 *
11 * This component represents the BEGIN:VCARD and END:VCARD found in every
12 * vcard.
13 *
14 * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
15 * @author Evert Pot (http://evertpot.com/)
16 * @license http://sabre.io/license/ Modified BSD License
17 */
18class VCard extends VObject\Document {
19
20    /**
21     * The default name for this component.
22     *
23     * This should be 'VCALENDAR' or 'VCARD'.
24     *
25     * @var string
26     */
27    static $defaultName = 'VCARD';
28
29    /**
30     * Caching the version number.
31     *
32     * @var int
33     */
34    private $version = null;
35
36    /**
37     * This is a list of components, and which classes they should map to.
38     *
39     * @var array
40     */
41    static $componentMap = [
42        'VCARD' => 'Sabre\\VObject\\Component\\VCard',
43    ];
44
45    /**
46     * List of value-types, and which classes they map to.
47     *
48     * @var array
49     */
50    static $valueMap = [
51        'BINARY'           => 'Sabre\\VObject\\Property\\Binary',
52        'BOOLEAN'          => 'Sabre\\VObject\\Property\\Boolean',
53        'CONTENT-ID'       => 'Sabre\\VObject\\Property\\FlatText',   // vCard 2.1 only
54        'DATE'             => 'Sabre\\VObject\\Property\\VCard\\Date',
55        'DATE-TIME'        => 'Sabre\\VObject\\Property\\VCard\\DateTime',
56        'DATE-AND-OR-TIME' => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime', // vCard only
57        'FLOAT'            => 'Sabre\\VObject\\Property\\FloatValue',
58        'INTEGER'          => 'Sabre\\VObject\\Property\\IntegerValue',
59        'LANGUAGE-TAG'     => 'Sabre\\VObject\\Property\\VCard\\LanguageTag',
60        'TIMESTAMP'        => 'Sabre\\VObject\\Property\\VCard\\TimeStamp',
61        'TEXT'             => 'Sabre\\VObject\\Property\\Text',
62        'TIME'             => 'Sabre\\VObject\\Property\\Time',
63        'UNKNOWN'          => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only.
64        'URI'              => 'Sabre\\VObject\\Property\\Uri',
65        'URL'              => 'Sabre\\VObject\\Property\\Uri', // vCard 2.1 only
66        'UTC-OFFSET'       => 'Sabre\\VObject\\Property\\UtcOffset',
67    ];
68
69    /**
70     * List of properties, and which classes they map to.
71     *
72     * @var array
73     */
74    static $propertyMap = [
75
76        // vCard 2.1 properties and up
77        'N'      => 'Sabre\\VObject\\Property\\Text',
78        'FN'     => 'Sabre\\VObject\\Property\\FlatText',
79        'PHOTO'  => 'Sabre\\VObject\\Property\\Binary',
80        'BDAY'   => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime',
81        'ADR'    => 'Sabre\\VObject\\Property\\Text',
82        'LABEL'  => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0
83        'TEL'    => 'Sabre\\VObject\\Property\\FlatText',
84        'EMAIL'  => 'Sabre\\VObject\\Property\\FlatText',
85        'MAILER' => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0
86        'GEO'    => 'Sabre\\VObject\\Property\\FlatText',
87        'TITLE'  => 'Sabre\\VObject\\Property\\FlatText',
88        'ROLE'   => 'Sabre\\VObject\\Property\\FlatText',
89        'LOGO'   => 'Sabre\\VObject\\Property\\Binary',
90        // 'AGENT'   => 'Sabre\\VObject\\Property\\',      // Todo: is an embedded vCard. Probably rare, so
91                                 // not supported at the moment
92        'ORG'     => 'Sabre\\VObject\\Property\\Text',
93        'NOTE'    => 'Sabre\\VObject\\Property\\FlatText',
94        'REV'     => 'Sabre\\VObject\\Property\\VCard\\TimeStamp',
95        'SOUND'   => 'Sabre\\VObject\\Property\\FlatText',
96        'URL'     => 'Sabre\\VObject\\Property\\Uri',
97        'UID'     => 'Sabre\\VObject\\Property\\FlatText',
98        'VERSION' => 'Sabre\\VObject\\Property\\FlatText',
99        'KEY'     => 'Sabre\\VObject\\Property\\FlatText',
100        'TZ'      => 'Sabre\\VObject\\Property\\Text',
101
102        // vCard 3.0 properties
103        'CATEGORIES'  => 'Sabre\\VObject\\Property\\Text',
104        'SORT-STRING' => 'Sabre\\VObject\\Property\\FlatText',
105        'PRODID'      => 'Sabre\\VObject\\Property\\FlatText',
106        'NICKNAME'    => 'Sabre\\VObject\\Property\\Text',
107        'CLASS'       => 'Sabre\\VObject\\Property\\FlatText', // Removed in vCard 4.0
108
109        // rfc2739 properties
110        'FBURL'     => 'Sabre\\VObject\\Property\\Uri',
111        'CAPURI'    => 'Sabre\\VObject\\Property\\Uri',
112        'CALURI'    => 'Sabre\\VObject\\Property\\Uri',
113        'CALADRURI' => 'Sabre\\VObject\\Property\\Uri',
114
115        // rfc4770 properties
116        'IMPP' => 'Sabre\\VObject\\Property\\Uri',
117
118        // vCard 4.0 properties
119        'SOURCE'       => 'Sabre\\VObject\\Property\\Uri',
120        'XML'          => 'Sabre\\VObject\\Property\\FlatText',
121        'ANNIVERSARY'  => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime',
122        'CLIENTPIDMAP' => 'Sabre\\VObject\\Property\\Text',
123        'LANG'         => 'Sabre\\VObject\\Property\\VCard\\LanguageTag',
124        'GENDER'       => 'Sabre\\VObject\\Property\\Text',
125        'KIND'         => 'Sabre\\VObject\\Property\\FlatText',
126        'MEMBER'       => 'Sabre\\VObject\\Property\\Uri',
127        'RELATED'      => 'Sabre\\VObject\\Property\\Uri',
128
129        // rfc6474 properties
130        'BIRTHPLACE' => 'Sabre\\VObject\\Property\\FlatText',
131        'DEATHPLACE' => 'Sabre\\VObject\\Property\\FlatText',
132        'DEATHDATE'  => 'Sabre\\VObject\\Property\\VCard\\DateAndOrTime',
133
134        // rfc6715 properties
135        'EXPERTISE'     => 'Sabre\\VObject\\Property\\FlatText',
136        'HOBBY'         => 'Sabre\\VObject\\Property\\FlatText',
137        'INTEREST'      => 'Sabre\\VObject\\Property\\FlatText',
138        'ORG-DIRECTORY' => 'Sabre\\VObject\\Property\\FlatText',
139
140    ];
141
142    /**
143     * Returns the current document type.
144     *
145     * @return int
146     */
147    function getDocumentType() {
148
149        if (!$this->version) {
150
151            $version = (string)$this->VERSION;
152
153            switch ($version) {
154                case '2.1' :
155                    $this->version = self::VCARD21;
156                    break;
157                case '3.0' :
158                    $this->version = self::VCARD30;
159                    break;
160                case '4.0' :
161                    $this->version = self::VCARD40;
162                    break;
163                default :
164                    // We don't want to cache the version if it's unknown,
165                    // because we might get a version property in a bit.
166                    return self::UNKNOWN;
167            }
168        }
169
170        return $this->version;
171
172    }
173
174    /**
175     * Converts the document to a different vcard version.
176     *
177     * Use one of the VCARD constants for the target. This method will return
178     * a copy of the vcard in the new version.
179     *
180     * At the moment the only supported conversion is from 3.0 to 4.0.
181     *
182     * If input and output version are identical, a clone is returned.
183     *
184     * @param int $target
185     *
186     * @return VCard
187     */
188    function convert($target) {
189
190        $converter = new VObject\VCardConverter();
191        return $converter->convert($this, $target);
192
193    }
194
195    /**
196     * VCards with version 2.1, 3.0 and 4.0 are found.
197     *
198     * If the VCARD doesn't know its version, 2.1 is assumed.
199     */
200    const DEFAULT_VERSION = self::VCARD21;
201
202    /**
203     * Validates the node for correctness.
204     *
205     * The following options are supported:
206     *   Node::REPAIR - May attempt to automatically repair the problem.
207     *
208     * This method returns an array with detected problems.
209     * Every element has the following properties:
210     *
211     *  * level - problem level.
212     *  * message - A human-readable string describing the issue.
213     *  * node - A reference to the problematic node.
214     *
215     * The level means:
216     *   1 - The issue was repaired (only happens if REPAIR was turned on)
217     *   2 - An inconsequential issue
218     *   3 - A severe issue.
219     *
220     * @param int $options
221     *
222     * @return array
223     */
224    function validate($options = 0) {
225
226        $warnings = [];
227
228        $versionMap = [
229            self::VCARD21 => '2.1',
230            self::VCARD30 => '3.0',
231            self::VCARD40 => '4.0',
232        ];
233
234        $version = $this->select('VERSION');
235        if (count($version) === 1) {
236            $version = (string)$this->VERSION;
237            if ($version !== '2.1' && $version !== '3.0' && $version !== '4.0') {
238                $warnings[] = [
239                    'level'   => 3,
240                    'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
241                    'node'    => $this,
242                ];
243                if ($options & self::REPAIR) {
244                    $this->VERSION = $versionMap[self::DEFAULT_VERSION];
245                }
246            }
247            if ($version === '2.1' && ($options & self::PROFILE_CARDDAV)) {
248                $warnings[] = [
249                    'level'   => 3,
250                    'message' => 'CardDAV servers are not allowed to accept vCard 2.1.',
251                    'node'    => $this,
252                ];
253            }
254
255        }
256        $uid = $this->select('UID');
257        if (count($uid) === 0) {
258            if ($options & self::PROFILE_CARDDAV) {
259                // Required for CardDAV
260                $warningLevel = 3;
261                $message = 'vCards on CardDAV servers MUST have a UID property.';
262            } else {
263                // Not required for regular vcards
264                $warningLevel = 2;
265                $message = 'Adding a UID to a vCard property is recommended.';
266            }
267            if ($options & self::REPAIR) {
268                $this->UID = VObject\UUIDUtil::getUUID();
269                $warningLevel = 1;
270            }
271            $warnings[] = [
272                'level'   => $warningLevel,
273                'message' => $message,
274                'node'    => $this,
275            ];
276        }
277
278        $fn = $this->select('FN');
279        if (count($fn) !== 1) {
280
281            $repaired = false;
282            if (($options & self::REPAIR) && count($fn) === 0) {
283                // We're going to try to see if we can use the contents of the
284                // N property.
285                if (isset($this->N)) {
286                    $value = explode(';', (string)$this->N);
287                    if (isset($value[1]) && $value[1]) {
288                        $this->FN = $value[1] . ' ' . $value[0];
289                    } else {
290                        $this->FN = $value[0];
291                    }
292                    $repaired = true;
293
294                // Otherwise, the ORG property may work
295                } elseif (isset($this->ORG)) {
296                    $this->FN = (string)$this->ORG;
297                    $repaired = true;
298                }
299
300            }
301            $warnings[] = [
302                'level'   => $repaired ? 1 : 3,
303                'message' => 'The FN property must appear in the VCARD component exactly 1 time',
304                'node'    => $this,
305            ];
306        }
307
308        return array_merge(
309            parent::validate($options),
310            $warnings
311        );
312
313    }
314
315    /**
316     * A simple list of validation rules.
317     *
318     * This is simply a list of properties, and how many times they either
319     * must or must not appear.
320     *
321     * Possible values per property:
322     *   * 0 - Must not appear.
323     *   * 1 - Must appear exactly once.
324     *   * + - Must appear at least once.
325     *   * * - Can appear any number of times.
326     *   * ? - May appear, but not more than once.
327     *
328     * @var array
329     */
330    function getValidationRules() {
331
332        return [
333            'ADR'          => '*',
334            'ANNIVERSARY'  => '?',
335            'BDAY'         => '?',
336            'CALADRURI'    => '*',
337            'CALURI'       => '*',
338            'CATEGORIES'   => '*',
339            'CLIENTPIDMAP' => '*',
340            'EMAIL'        => '*',
341            'FBURL'        => '*',
342            'IMPP'         => '*',
343            'GENDER'       => '?',
344            'GEO'          => '*',
345            'KEY'          => '*',
346            'KIND'         => '?',
347            'LANG'         => '*',
348            'LOGO'         => '*',
349            'MEMBER'       => '*',
350            'N'            => '?',
351            'NICKNAME'     => '*',
352            'NOTE'         => '*',
353            'ORG'          => '*',
354            'PHOTO'        => '*',
355            'PRODID'       => '?',
356            'RELATED'      => '*',
357            'REV'          => '?',
358            'ROLE'         => '*',
359            'SOUND'        => '*',
360            'SOURCE'       => '*',
361            'TEL'          => '*',
362            'TITLE'        => '*',
363            'TZ'           => '*',
364            'URL'          => '*',
365            'VERSION'      => '1',
366            'XML'          => '*',
367
368            // FN is commented out, because it's already handled by the
369            // validate function, which may also try to repair it.
370            // 'FN'           => '+',
371            'UID' => '?',
372        ];
373
374    }
375
376    /**
377     * Returns a preferred field.
378     *
379     * VCards can indicate wether a field such as ADR, TEL or EMAIL is
380     * preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x
381     * being a number between 1 and 100).
382     *
383     * If neither of those parameters are specified, the first is returned, if
384     * a field with that name does not exist, null is returned.
385     *
386     * @param string $fieldName
387     *
388     * @return VObject\Property|null
389     */
390    function preferred($propertyName) {
391
392        $preferred = null;
393        $lastPref = 101;
394        foreach ($this->select($propertyName) as $field) {
395
396            $pref = 101;
397            if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) {
398                $pref = 1;
399            } elseif (isset($field['PREF'])) {
400                $pref = $field['PREF']->getValue();
401            }
402
403            if ($pref < $lastPref || is_null($preferred)) {
404                $preferred = $field;
405                $lastPref = $pref;
406            }
407
408        }
409        return $preferred;
410
411    }
412
413    /**
414     * Returns a property with a specific TYPE value (ADR, TEL, or EMAIL).
415     *
416     * This function will return null if the property does not exist. If there are
417     * multiple properties with the same TYPE value, only one will be returned.
418     *
419     * @param string $propertyName
420     * @param string $type
421     *
422     * @return VObject\Property|null
423     */
424    function getByType($propertyName, $type) {
425        foreach ($this->select($propertyName) as $field) {
426            if (isset($field['TYPE']) && $field['TYPE']->has($type)) {
427                return $field;
428            }
429        }
430    }
431
432    /**
433     * This method should return a list of default property values.
434     *
435     * @return array
436     */
437    protected function getDefaults() {
438
439        return [
440            'VERSION' => '4.0',
441            'PRODID'  => '-//Sabre//Sabre VObject ' . VObject\Version::VERSION . '//EN',
442            'UID'     => 'sabre-vobject-' . VObject\UUIDUtil::getUUID(),
443        ];
444
445    }
446
447    /**
448     * This method returns an array, with the representation as it should be
449     * encoded in json. This is used to create jCard or jCal documents.
450     *
451     * @return array
452     */
453    function jsonSerialize() {
454
455        // A vcard does not have sub-components, so we're overriding this
456        // method to remove that array element.
457        $properties = [];
458
459        foreach ($this->children() as $child) {
460            $properties[] = $child->jsonSerialize();
461        }
462
463        return [
464            strtolower($this->name),
465            $properties,
466        ];
467
468    }
469
470    /**
471     * This method serializes the data into XML. This is used to create xCard or
472     * xCal documents.
473     *
474     * @param Xml\Writer $writer  XML writer.
475     *
476     * @return void
477     */
478    function xmlSerialize(Xml\Writer $writer) {
479
480        $propertiesByGroup = [];
481
482        foreach ($this->children() as $property) {
483
484            $group = $property->group;
485
486            if (!isset($propertiesByGroup[$group])) {
487                $propertiesByGroup[$group] = [];
488            }
489
490            $propertiesByGroup[$group][] = $property;
491
492        }
493
494        $writer->startElement(strtolower($this->name));
495
496        foreach ($propertiesByGroup as $group => $properties) {
497
498            if (!empty($group)) {
499
500                $writer->startElement('group');
501                $writer->writeAttribute('name', strtolower($group));
502
503            }
504
505            foreach ($properties as $property) {
506                switch ($property->name) {
507
508                    case 'VERSION':
509                        continue;
510
511                    case 'XML':
512                        $value = $property->getParts();
513                        $fragment = new Xml\Element\XmlFragment($value[0]);
514                        $writer->write($fragment);
515                        break;
516
517                    default:
518                        $property->xmlSerialize($writer);
519                        break;
520
521                }
522            }
523
524            if (!empty($group)) {
525                $writer->endElement();
526            }
527
528        }
529
530        $writer->endElement();
531
532    }
533
534    /**
535     * Returns the default class for a property name.
536     *
537     * @param string $propertyName
538     *
539     * @return string
540     */
541    function getClassNameForPropertyName($propertyName) {
542
543        $className = parent::getClassNameForPropertyName($propertyName);
544
545        // In vCard 4, BINARY no longer exists, and we need URI instead.
546        if ($className == 'Sabre\\VObject\\Property\\Binary' && $this->getDocumentType() === self::VCARD40) {
547            return 'Sabre\\VObject\\Property\\Uri';
548        }
549        return $className;
550
551    }
552
553}
554