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