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    public 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    public 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    public 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        'PHONE-NUMBER' => 'Sabre\\VObject\\Property\\VCard\\PhoneNumber', // vCard 3.0 only
61        'TIMESTAMP' => 'Sabre\\VObject\\Property\\VCard\\TimeStamp',
62        'TEXT' => 'Sabre\\VObject\\Property\\Text',
63        'TIME' => 'Sabre\\VObject\\Property\\Time',
64        'UNKNOWN' => 'Sabre\\VObject\\Property\\Unknown', // jCard / jCal-only.
65        'URI' => 'Sabre\\VObject\\Property\\Uri',
66        'URL' => 'Sabre\\VObject\\Property\\Uri', // vCard 2.1 only
67        'UTC-OFFSET' => 'Sabre\\VObject\\Property\\UtcOffset',
68    ];
69
70    /**
71     * List of properties, and which classes they map to.
72     *
73     * @var array
74     */
75    public static $propertyMap = [
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     * Returns the current document type.
143     *
144     * @return int
145     */
146    public function getDocumentType()
147    {
148        if (!$this->version) {
149            $version = (string) $this->VERSION;
150
151            switch ($version) {
152                case '2.1':
153                    $this->version = self::VCARD21;
154                    break;
155                case '3.0':
156                    $this->version = self::VCARD30;
157                    break;
158                case '4.0':
159                    $this->version = self::VCARD40;
160                    break;
161                default:
162                    // We don't want to cache the version if it's unknown,
163                    // because we might get a version property in a bit.
164                    return self::UNKNOWN;
165            }
166        }
167
168        return $this->version;
169    }
170
171    /**
172     * Converts the document to a different vcard version.
173     *
174     * Use one of the VCARD constants for the target. This method will return
175     * a copy of the vcard in the new version.
176     *
177     * At the moment the only supported conversion is from 3.0 to 4.0.
178     *
179     * If input and output version are identical, a clone is returned.
180     *
181     * @param int $target
182     *
183     * @return VCard
184     */
185    public function convert($target)
186    {
187        $converter = new VObject\VCardConverter();
188
189        return $converter->convert($this, $target);
190    }
191
192    /**
193     * VCards with version 2.1, 3.0 and 4.0 are found.
194     *
195     * If the VCARD doesn't know its version, 2.1 is assumed.
196     */
197    const DEFAULT_VERSION = self::VCARD21;
198
199    /**
200     * Validates the node for correctness.
201     *
202     * The following options are supported:
203     *   Node::REPAIR - May attempt to automatically repair the problem.
204     *
205     * This method returns an array with detected problems.
206     * Every element has the following properties:
207     *
208     *  * level - problem level.
209     *  * message - A human-readable string describing the issue.
210     *  * node - A reference to the problematic node.
211     *
212     * The level means:
213     *   1 - The issue was repaired (only happens if REPAIR was turned on)
214     *   2 - An inconsequential issue
215     *   3 - A severe issue.
216     *
217     * @param int $options
218     *
219     * @return array
220     */
221    public function validate($options = 0)
222    {
223        $warnings = [];
224
225        $versionMap = [
226            self::VCARD21 => '2.1',
227            self::VCARD30 => '3.0',
228            self::VCARD40 => '4.0',
229        ];
230
231        $version = $this->select('VERSION');
232        if (1 === count($version)) {
233            $version = (string) $this->VERSION;
234            if ('2.1' !== $version && '3.0' !== $version && '4.0' !== $version) {
235                $warnings[] = [
236                    'level' => 3,
237                    'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
238                    'node' => $this,
239                ];
240                if ($options & self::REPAIR) {
241                    $this->VERSION = $versionMap[self::DEFAULT_VERSION];
242                }
243            }
244            if ('2.1' === $version && ($options & self::PROFILE_CARDDAV)) {
245                $warnings[] = [
246                    'level' => 3,
247                    'message' => 'CardDAV servers are not allowed to accept vCard 2.1.',
248                    'node' => $this,
249                ];
250            }
251        }
252        $uid = $this->select('UID');
253        if (0 === count($uid)) {
254            if ($options & self::PROFILE_CARDDAV) {
255                // Required for CardDAV
256                $warningLevel = 3;
257                $message = 'vCards on CardDAV servers MUST have a UID property.';
258            } else {
259                // Not required for regular vcards
260                $warningLevel = 2;
261                $message = 'Adding a UID to a vCard property is recommended.';
262            }
263            if ($options & self::REPAIR) {
264                $this->UID = VObject\UUIDUtil::getUUID();
265                $warningLevel = 1;
266            }
267            $warnings[] = [
268                'level' => $warningLevel,
269                'message' => $message,
270                'node' => $this,
271            ];
272        }
273
274        $fn = $this->select('FN');
275        if (1 !== count($fn)) {
276            $repaired = false;
277            if (($options & self::REPAIR) && 0 === count($fn)) {
278                // We're going to try to see if we can use the contents of the
279                // N property.
280                if (isset($this->N)) {
281                    $value = explode(';', (string) $this->N);
282                    if (isset($value[1]) && $value[1]) {
283                        $this->FN = $value[1].' '.$value[0];
284                    } else {
285                        $this->FN = $value[0];
286                    }
287                    $repaired = true;
288
289                // Otherwise, the ORG property may work
290                } elseif (isset($this->ORG)) {
291                    $this->FN = (string) $this->ORG;
292                    $repaired = true;
293
294                // Otherwise, the EMAIL property may work
295                } elseif (isset($this->EMAIL)) {
296                    $this->FN = (string) $this->EMAIL;
297                    $repaired = true;
298                }
299            }
300            $warnings[] = [
301                'level' => $repaired ? 1 : 3,
302                'message' => 'The FN property must appear in the VCARD component exactly 1 time',
303                'node' => $this,
304            ];
305        }
306
307        return array_merge(
308            parent::validate($options),
309            $warnings
310        );
311    }
312
313    /**
314     * A simple list of validation rules.
315     *
316     * This is simply a list of properties, and how many times they either
317     * must or must not appear.
318     *
319     * Possible values per property:
320     *   * 0 - Must not appear.
321     *   * 1 - Must appear exactly once.
322     *   * + - Must appear at least once.
323     *   * * - Can appear any number of times.
324     *   * ? - May appear, but not more than once.
325     *
326     * @var array
327     */
328    public function getValidationRules()
329    {
330        return [
331            'ADR' => '*',
332            'ANNIVERSARY' => '?',
333            'BDAY' => '?',
334            'CALADRURI' => '*',
335            'CALURI' => '*',
336            'CATEGORIES' => '*',
337            'CLIENTPIDMAP' => '*',
338            'EMAIL' => '*',
339            'FBURL' => '*',
340            'IMPP' => '*',
341            'GENDER' => '?',
342            'GEO' => '*',
343            'KEY' => '*',
344            'KIND' => '?',
345            'LANG' => '*',
346            'LOGO' => '*',
347            'MEMBER' => '*',
348            'N' => '?',
349            'NICKNAME' => '*',
350            'NOTE' => '*',
351            'ORG' => '*',
352            'PHOTO' => '*',
353            'PRODID' => '?',
354            'RELATED' => '*',
355            'REV' => '?',
356            'ROLE' => '*',
357            'SOUND' => '*',
358            'SOURCE' => '*',
359            'TEL' => '*',
360            'TITLE' => '*',
361            'TZ' => '*',
362            'URL' => '*',
363            'VERSION' => '1',
364            'XML' => '*',
365
366            // FN is commented out, because it's already handled by the
367            // validate function, which may also try to repair it.
368            // 'FN'           => '+',
369            'UID' => '?',
370        ];
371    }
372
373    /**
374     * Returns a preferred field.
375     *
376     * VCards can indicate wether a field such as ADR, TEL or EMAIL is
377     * preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x
378     * being a number between 1 and 100).
379     *
380     * If neither of those parameters are specified, the first is returned, if
381     * a field with that name does not exist, null is returned.
382     *
383     * @param string $fieldName
384     *
385     * @return VObject\Property|null
386     */
387    public function preferred($propertyName)
388    {
389        $preferred = null;
390        $lastPref = 101;
391        foreach ($this->select($propertyName) as $field) {
392            $pref = 101;
393            if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) {
394                $pref = 1;
395            } elseif (isset($field['PREF'])) {
396                $pref = $field['PREF']->getValue();
397            }
398
399            if ($pref < $lastPref || is_null($preferred)) {
400                $preferred = $field;
401                $lastPref = $pref;
402            }
403        }
404
405        return $preferred;
406    }
407
408    /**
409     * Returns a property with a specific TYPE value (ADR, TEL, or EMAIL).
410     *
411     * This function will return null if the property does not exist. If there are
412     * multiple properties with the same TYPE value, only one will be returned.
413     *
414     * @param string $propertyName
415     * @param string $type
416     *
417     * @return VObject\Property|null
418     */
419    public function getByType($propertyName, $type)
420    {
421        foreach ($this->select($propertyName) as $field) {
422            if (isset($field['TYPE']) && $field['TYPE']->has($type)) {
423                return $field;
424            }
425        }
426    }
427
428    /**
429     * This method should return a list of default property values.
430     *
431     * @return array
432     */
433    protected function getDefaults()
434    {
435        return [
436            'VERSION' => '4.0',
437            'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN',
438            'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
439        ];
440    }
441
442    /**
443     * This method returns an array, with the representation as it should be
444     * encoded in json. This is used to create jCard or jCal documents.
445     *
446     * @return array
447     */
448    public function jsonSerialize()
449    {
450        // A vcard does not have sub-components, so we're overriding this
451        // method to remove that array element.
452        $properties = [];
453
454        foreach ($this->children() as $child) {
455            $properties[] = $child->jsonSerialize();
456        }
457
458        return [
459            strtolower($this->name),
460            $properties,
461        ];
462    }
463
464    /**
465     * This method serializes the data into XML. This is used to create xCard or
466     * xCal documents.
467     *
468     * @param Xml\Writer $writer XML writer
469     */
470    public function xmlSerialize(Xml\Writer $writer)
471    {
472        $propertiesByGroup = [];
473
474        foreach ($this->children() as $property) {
475            $group = $property->group;
476
477            if (!isset($propertiesByGroup[$group])) {
478                $propertiesByGroup[$group] = [];
479            }
480
481            $propertiesByGroup[$group][] = $property;
482        }
483
484        $writer->startElement(strtolower($this->name));
485
486        foreach ($propertiesByGroup as $group => $properties) {
487            if (!empty($group)) {
488                $writer->startElement('group');
489                $writer->writeAttribute('name', strtolower($group));
490            }
491
492            foreach ($properties as $property) {
493                switch ($property->name) {
494                    case 'VERSION':
495                        break;
496
497                    case 'XML':
498                        $value = $property->getParts();
499                        $fragment = new Xml\Element\XmlFragment($value[0]);
500                        $writer->write($fragment);
501                        break;
502
503                    default:
504                        $property->xmlSerialize($writer);
505                        break;
506                }
507            }
508
509            if (!empty($group)) {
510                $writer->endElement();
511            }
512        }
513
514        $writer->endElement();
515    }
516
517    /**
518     * Returns the default class for a property name.
519     *
520     * @param string $propertyName
521     *
522     * @return string
523     */
524    public function getClassNameForPropertyName($propertyName)
525    {
526        $className = parent::getClassNameForPropertyName($propertyName);
527
528        // In vCard 4, BINARY no longer exists, and we need URI instead.
529        if ('Sabre\\VObject\\Property\\Binary' == $className && self::VCARD40 === $this->getDocumentType()) {
530            return 'Sabre\\VObject\\Property\\Uri';
531        }
532
533        return $className;
534    }
535}
536