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