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