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