VCard.php 17 KB

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