<?php
namespace Sabre\VObject\Component;
use Sabre\VObject;
use Sabre\Xml;
/**
* The VCard component.
*
* This component represents the BEGIN:VCARD and END:VCARD found in every
* vcard.
*
* @copyright Copyright (C) fruux GmbH (https://fruux.com/)
* @author Evert Pot (http://evertpot.com/)
* @license http://sabre.io/license/ Modified BSD License
*/
class VCard extends VObject\Document
{
/**
* The default name for this component.
*
* This should be 'VCALENDAR' or 'VCARD'.
*
* @var string
*/
public static $defaultName = 'VCARD';
/**
* Caching the version number.
*
* @var int
*/
private $version = null;
/**
* This is a list of components, and which classes they should map to.
*
* @var array
*/
public static $componentMap = [
'VCARD' => VCard::class,
];
/**
* List of value-types, and which classes they map to.
*
* @var array
*/
public static $valueMap = [
'BINARY' => VObject\Property\Binary::class,
'BOOLEAN' => VObject\Property\Boolean::class,
'CONTENT-ID' => VObject\Property\FlatText::class, // vCard 2.1 only
'DATE' => VObject\Property\VCard\Date::class,
'DATE-TIME' => VObject\Property\VCard\DateTime::class,
'DATE-AND-OR-TIME' => VObject\Property\VCard\DateAndOrTime::class, // vCard only
'FLOAT' => VObject\Property\FloatValue::class,
'INTEGER' => VObject\Property\IntegerValue::class,
'LANGUAGE-TAG' => VObject\Property\VCard\LanguageTag::class,
'PHONE-NUMBER' => VObject\Property\VCard\PhoneNumber::class, // vCard 3.0 only
'TIMESTAMP' => VObject\Property\VCard\TimeStamp::class,
'TEXT' => VObject\Property\Text::class,
'TIME' => VObject\Property\Time::class,
'UNKNOWN' => VObject\Property\Unknown::class, // jCard / jCal-only.
'URI' => VObject\Property\Uri::class,
'URL' => VObject\Property\Uri::class, // vCard 2.1 only
'UTC-OFFSET' => VObject\Property\UtcOffset::class,
];
/**
* List of properties, and which classes they map to.
*
* @var array
*/
public static $propertyMap = [
// vCard 2.1 properties and up
'N' => VObject\Property\Text::class,
'FN' => VObject\Property\FlatText::class,
'PHOTO' => VObject\Property\Binary::class,
'BDAY' => VObject\Property\VCard\DateAndOrTime::class,
'ADR' => VObject\Property\Text::class,
'LABEL' => VObject\Property\FlatText::class, // Removed in vCard 4.0
'TEL' => VObject\Property\FlatText::class,
'EMAIL' => VObject\Property\FlatText::class,
'MAILER' => VObject\Property\FlatText::class, // Removed in vCard 4.0
'GEO' => VObject\Property\FlatText::class,
'TITLE' => VObject\Property\FlatText::class,
'ROLE' => VObject\Property\FlatText::class,
'LOGO' => VObject\Property\Binary::class,
// 'AGENT' => 'Sabre\\VObject\\Property\\', // Todo: is an embedded vCard. Probably rare, so
// not supported at the moment
'ORG' => VObject\Property\Text::class,
'NOTE' => VObject\Property\FlatText::class,
'REV' => VObject\Property\VCard\TimeStamp::class,
'SOUND' => VObject\Property\FlatText::class,
'URL' => VObject\Property\Uri::class,
'UID' => VObject\Property\FlatText::class,
'VERSION' => VObject\Property\FlatText::class,
'KEY' => VObject\Property\FlatText::class,
'TZ' => VObject\Property\Text::class,
// vCard 3.0 properties
'CATEGORIES' => VObject\Property\Text::class,
'SORT-STRING' => VObject\Property\FlatText::class,
'PRODID' => VObject\Property\FlatText::class,
'NICKNAME' => VObject\Property\Text::class,
'CLASS' => VObject\Property\FlatText::class, // Removed in vCard 4.0
// rfc2739 properties
'FBURL' => VObject\Property\Uri::class,
'CAPURI' => VObject\Property\Uri::class,
'CALURI' => VObject\Property\Uri::class,
'CALADRURI' => VObject\Property\Uri::class,
// rfc4770 properties
'IMPP' => VObject\Property\Uri::class,
// vCard 4.0 properties
'SOURCE' => VObject\Property\Uri::class,
'XML' => VObject\Property\FlatText::class,
'ANNIVERSARY' => VObject\Property\VCard\DateAndOrTime::class,
'CLIENTPIDMAP' => VObject\Property\Text::class,
'LANG' => VObject\Property\VCard\LanguageTag::class,
'GENDER' => VObject\Property\Text::class,
'KIND' => VObject\Property\FlatText::class,
'MEMBER' => VObject\Property\Uri::class,
'RELATED' => VObject\Property\Uri::class,
// rfc6474 properties
'BIRTHPLACE' => VObject\Property\FlatText::class,
'DEATHPLACE' => VObject\Property\FlatText::class,
'DEATHDATE' => VObject\Property\VCard\DateAndOrTime::class,
// rfc6715 properties
'EXPERTISE' => VObject\Property\FlatText::class,
'HOBBY' => VObject\Property\FlatText::class,
'INTEREST' => VObject\Property\FlatText::class,
'ORG-DIRECTORY' => VObject\Property\FlatText::class,
];
/**
* Returns the current document type.
*
* @return int
*/
public function getDocumentType()
{
if (!$this->version) {
$version = (string) $this->VERSION;
switch ($version) {
case '2.1':
$this->version = self::VCARD21;
break;
case '3.0':
$this->version = self::VCARD30;
break;
case '4.0':
$this->version = self::VCARD40;
break;
default:
// We don't want to cache the version if it's unknown,
// because we might get a version property in a bit.
return self::UNKNOWN;
}
}
return $this->version;
}
/**
* Converts the document to a different vcard version.
*
* Use one of the VCARD constants for the target. This method will return
* a copy of the vcard in the new version.
*
* At the moment the only supported conversion is from 3.0 to 4.0.
*
* If input and output version are identical, a clone is returned.
*
* @param int $target
*
* @return VCard
*/
public function convert($target)
{
$converter = new VObject\VCardConverter();
return $converter->convert($this, $target);
}
/**
* VCards with version 2.1, 3.0 and 4.0 are found.
*
* If the VCARD doesn't know its version, 2.1 is assumed.
*/
const DEFAULT_VERSION = self::VCARD21;
/**
* Validates the node for correctness.
*
* The following options are supported:
* Node::REPAIR - May attempt to automatically repair the problem.
*
* This method returns an array with detected problems.
* Every element has the following properties:
*
* * level - problem level.
* * message - A human-readable string describing the issue.
* * node - A reference to the problematic node.
*
* The level means:
* 1 - The issue was repaired (only happens if REPAIR was turned on)
* 2 - An inconsequential issue
* 3 - A severe issue.
*
* @param int $options
*
* @return array
*/
public function validate($options = 0)
{
$warnings = [];
$versionMap = [
self::VCARD21 => '2.1',
self::VCARD30 => '3.0',
self::VCARD40 => '4.0',
];
$version = $this->select('VERSION');
if (1 === count($version)) {
$version = (string) $this->VERSION;
if ('2.1' !== $version && '3.0' !== $version && '4.0' !== $version) {
$warnings[] = [
'level' => 3,
'message' => 'Only vcard version 4.0 (RFC6350), version 3.0 (RFC2426) or version 2.1 (icm-vcard-2.1) are supported.',
'node' => $this,
];
if ($options & self::REPAIR) {
$this->VERSION = $versionMap[self::DEFAULT_VERSION];
}
}
if ('2.1' === $version && ($options & self::PROFILE_CARDDAV)) {
$warnings[] = [
'level' => 3,
'message' => 'CardDAV servers are not allowed to accept vCard 2.1.',
'node' => $this,
];
}
}
$uid = $this->select('UID');
if (0 === count($uid)) {
if ($options & self::PROFILE_CARDDAV) {
// Required for CardDAV
$warningLevel = 3;
$message = 'vCards on CardDAV servers MUST have a UID property.';
} else {
// Not required for regular vcards
$warningLevel = 2;
$message = 'Adding a UID to a vCard property is recommended.';
}
if ($options & self::REPAIR) {
$this->UID = VObject\UUIDUtil::getUUID();
$warningLevel = 1;
}
$warnings[] = [
'level' => $warningLevel,
'message' => $message,
'node' => $this,
];
}
$fn = $this->select('FN');
if (1 !== count($fn)) {
$repaired = false;
if (($options & self::REPAIR) && 0 === count($fn)) {
// We're going to try to see if we can use the contents of the
// N property.
if (isset($this->N)) {
$value = explode(';', (string) $this->N);
if (isset($value[1]) && $value[1]) {
$this->FN = $value[1].' '.$value[0];
} else {
$this->FN = $value[0];
}
$repaired = true;
// Otherwise, the ORG property may work
} elseif (isset($this->ORG)) {
$this->FN = (string) $this->ORG;
$repaired = true;
// Otherwise, the NICKNAME property may work
} elseif (isset($this->NICKNAME)) {
$this->FN = (string) $this->NICKNAME;
$repaired = true;
// Otherwise, the EMAIL property may work
} elseif (isset($this->EMAIL)) {
$this->FN = (string) $this->EMAIL;
$repaired = true;
}
}
$warnings[] = [
'level' => $repaired ? 1 : 3,
'message' => 'The FN property must appear in the VCARD component exactly 1 time',
'node' => $this,
];
}
return array_merge(
parent::validate($options),
$warnings
);
}
/**
* A simple list of validation rules.
*
* This is simply a list of properties, and how many times they either
* must or must not appear.
*
* Possible values per property:
* * 0 - Must not appear.
* * 1 - Must appear exactly once.
* * + - Must appear at least once.
* * * - Can appear any number of times.
* * ? - May appear, but not more than once.
*
* @var array
*/
public function getValidationRules()
{
return [
'ADR' => '*',
'ANNIVERSARY' => '?',
'BDAY' => '?',
'CALADRURI' => '*',
'CALURI' => '*',
'CATEGORIES' => '*',
'CLIENTPIDMAP' => '*',
'EMAIL' => '*',
'FBURL' => '*',
'IMPP' => '*',
'GENDER' => '?',
'GEO' => '*',
'KEY' => '*',
'KIND' => '?',
'LANG' => '*',
'LOGO' => '*',
'MEMBER' => '*',
'N' => '?',
'NICKNAME' => '*',
'NOTE' => '*',
'ORG' => '*',
'PHOTO' => '*',
'PRODID' => '?',
'RELATED' => '*',
'REV' => '?',
'ROLE' => '*',
'SOUND' => '*',
'SOURCE' => '*',
'TEL' => '*',
'TITLE' => '*',
'TZ' => '*',
'URL' => '*',
'VERSION' => '1',
'XML' => '*',
// FN is commented out, because it's already handled by the
// validate function, which may also try to repair it.
// 'FN' => '+',
'UID' => '?',
];
}
/**
* Returns a preferred field.
*
* VCards can indicate whether a field such as ADR, TEL or EMAIL is
* preferred by specifying TYPE=PREF (vcard 2.1, 3) or PREF=x (vcard 4, x
* being a number between 1 and 100).
*
* If neither of those parameters are specified, the first is returned, if
* a field with that name does not exist, null is returned.
*
* @param string $fieldName
*
* @return VObject\Property|null
*/
public function preferred($propertyName)
{
$preferred = null;
$lastPref = 101;
foreach ($this->select($propertyName) as $field) {
$pref = 101;
if (isset($field['TYPE']) && $field['TYPE']->has('PREF')) {
$pref = 1;
} elseif (isset($field['PREF'])) {
$pref = $field['PREF']->getValue();
}
if ($pref < $lastPref || is_null($preferred)) {
$preferred = $field;
$lastPref = $pref;
}
}
return $preferred;
}
/**
* Returns a property with a specific TYPE value (ADR, TEL, or EMAIL).
*
* This function will return null if the property does not exist. If there are
* multiple properties with the same TYPE value, only one will be returned.
*
* @param string $propertyName
* @param string $type
*
* @return VObject\Property|null
*/
public function getByType($propertyName, $type)
{
foreach ($this->select($propertyName) as $field) {
if (isset($field['TYPE']) && $field['TYPE']->has($type)) {
return $field;
}
}
}
/**
* This method should return a list of default property values.
*
* @return array
*/
protected function getDefaults()
{
return [
'VERSION' => '4.0',
'PRODID' => '-//Sabre//Sabre VObject '.VObject\Version::VERSION.'//EN',
'UID' => 'sabre-vobject-'.VObject\UUIDUtil::getUUID(),
];
}
/**
* This method returns an array, with the representation as it should be
* encoded in json. This is used to create jCard or jCal documents.
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
{
// A vcard does not have sub-components, so we're overriding this
// method to remove that array element.
$properties = [];
foreach ($this->children() as $child) {
$properties[] = $child->jsonSerialize();
}
return [
strtolower($this->name),
$properties,
];
}
/**
* This method serializes the data into XML. This is used to create xCard or
* xCal documents.
*
* @param Xml\Writer $writer XML writer
*/
public function xmlSerialize(Xml\Writer $writer)
{
$propertiesByGroup = [];
foreach ($this->children() as $property) {
$group = $property->group;
if (!isset($propertiesByGroup[$group])) {
$propertiesByGroup[$group] = [];
}
$propertiesByGroup[$group][] = $property;
}
$writer->startElement(strtolower($this->name));
foreach ($propertiesByGroup as $group => $properties) {
if (!empty($group)) {
$writer->startElement('group');
$writer->writeAttribute('name', strtolower($group));
}
foreach ($properties as $property) {
switch ($property->name) {
case 'VERSION':
break;
case 'XML':
$value = $property->getParts();
$fragment = new Xml\Element\XmlFragment($value[0]);
$writer->write($fragment);
break;
default:
$property->xmlSerialize($writer);
break;
}
}
if (!empty($group)) {
$writer->endElement();
}
}
$writer->endElement();
}
/**
* Returns the default class for a property name.
*
* @param string $propertyName
*
* @return string
*/
public function getClassNameForPropertyName($propertyName)
{
$className = parent::getClassNameForPropertyName($propertyName);
// In vCard 4, BINARY no longer exists, and we need URI instead.
if (VObject\Property\Binary::class == $className && self::VCARD40 === $this->getDocumentType()) {
return VObject\Property\Uri::class;
}
return $className;
}
}