VCardConverter.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. <?php
  2. namespace Sabre\VObject;
  3. /**
  4. * This utility converts vcards from one version to another.
  5. *
  6. * @copyright Copyright (C) fruux GmbH (https://fruux.com/)
  7. * @author Evert Pot (http://evertpot.com/)
  8. * @license http://sabre.io/license/ Modified BSD License
  9. */
  10. class VCardConverter
  11. {
  12. /**
  13. * Converts a vCard object to a new version.
  14. *
  15. * targetVersion must be one of:
  16. * Document::VCARD21
  17. * Document::VCARD30
  18. * Document::VCARD40
  19. *
  20. * Currently only 3.0 and 4.0 as input and output versions.
  21. *
  22. * 2.1 has some minor support for the input version, it's incomplete at the
  23. * moment though.
  24. *
  25. * If input and output version are identical, a clone is returned.
  26. *
  27. * @param int $targetVersion
  28. */
  29. public function convert(Component\VCard $input, $targetVersion)
  30. {
  31. $inputVersion = $input->getDocumentType();
  32. if ($inputVersion === $targetVersion) {
  33. return clone $input;
  34. }
  35. if (!in_array($inputVersion, [Document::VCARD21, Document::VCARD30, Document::VCARD40])) {
  36. throw new \InvalidArgumentException('Only vCard 2.1, 3.0 and 4.0 are supported for the input data');
  37. }
  38. if (!in_array($targetVersion, [Document::VCARD30, Document::VCARD40])) {
  39. throw new \InvalidArgumentException('You can only use vCard 3.0 or 4.0 for the target version');
  40. }
  41. $newVersion = Document::VCARD40 === $targetVersion ? '4.0' : '3.0';
  42. $output = new Component\VCard([
  43. 'VERSION' => $newVersion,
  44. ]);
  45. // We might have generated a default UID. Remove it!
  46. unset($output->UID);
  47. foreach ($input->children() as $property) {
  48. $this->convertProperty($input, $output, $property, $targetVersion);
  49. }
  50. return $output;
  51. }
  52. /**
  53. * Handles conversion of a single property.
  54. *
  55. * @param int $targetVersion
  56. */
  57. protected function convertProperty(Component\VCard $input, Component\VCard $output, Property $property, $targetVersion)
  58. {
  59. // Skipping these, those are automatically added.
  60. if (in_array($property->name, ['VERSION', 'PRODID'])) {
  61. return;
  62. }
  63. $parameters = $property->parameters();
  64. $valueType = null;
  65. if (isset($parameters['VALUE'])) {
  66. $valueType = $parameters['VALUE']->getValue();
  67. unset($parameters['VALUE']);
  68. }
  69. if (!$valueType) {
  70. $valueType = $property->getValueType();
  71. }
  72. if (Document::VCARD30 !== $targetVersion && 'PHONE-NUMBER' === $valueType) {
  73. $valueType = null;
  74. }
  75. $newProperty = $output->createProperty(
  76. $property->name,
  77. $property->getParts(),
  78. [], // parameters will get added a bit later.
  79. $valueType
  80. );
  81. if (Document::VCARD30 === $targetVersion) {
  82. if ($property instanceof Property\Uri && in_array($property->name, ['PHOTO', 'LOGO', 'SOUND'])) {
  83. $newProperty = $this->convertUriToBinary($output, $newProperty);
  84. } elseif ($property instanceof Property\VCard\DateAndOrTime) {
  85. // In vCard 4, the birth year may be optional. This is not the
  86. // case for vCard 3. Apple has a workaround for this that
  87. // allows applications that support Apple's extension still
  88. // omit birthyears in vCard 3, but applications that do not
  89. // support this, will just use a random birthyear. We're
  90. // choosing 1604 for the birthyear, because that's what apple
  91. // uses.
  92. $parts = DateTimeParser::parseVCardDateTime($property->getValue());
  93. if (is_null($parts['year'])) {
  94. $newValue = '1604-'.$parts['month'].'-'.$parts['date'];
  95. $newProperty->setValue($newValue);
  96. $newProperty['X-APPLE-OMIT-YEAR'] = '1604';
  97. }
  98. if ('ANNIVERSARY' == $newProperty->name) {
  99. // Microsoft non-standard anniversary
  100. $newProperty->name = 'X-ANNIVERSARY';
  101. // We also need to add a new apple property for the same
  102. // purpose. This apple property needs a 'label' in the same
  103. // group, so we first need to find a groupname that doesn't
  104. // exist yet.
  105. $x = 1;
  106. while ($output->select('ITEM'.$x.'.')) {
  107. ++$x;
  108. }
  109. $output->add('ITEM'.$x.'.X-ABDATE', $newProperty->getValue(), ['VALUE' => 'DATE-AND-OR-TIME']);
  110. $output->add('ITEM'.$x.'.X-ABLABEL', '_$!<Anniversary>!$_');
  111. }
  112. } elseif ('KIND' === $property->name) {
  113. switch (strtolower($property->getValue())) {
  114. case 'org':
  115. // vCard 3.0 does not have an equivalent to KIND:ORG,
  116. // but apple has an extension that means the same
  117. // thing.
  118. $newProperty = $output->createProperty('X-ABSHOWAS', 'COMPANY');
  119. break;
  120. case 'individual':
  121. // Individual is implicit, so we skip it.
  122. return;
  123. case 'group':
  124. // OS X addressbook property
  125. $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-KIND', 'GROUP');
  126. break;
  127. }
  128. } elseif ('MEMBER' === $property->name) {
  129. $newProperty = $output->createProperty('X-ADDRESSBOOKSERVER-MEMBER', $property->getValue());
  130. }
  131. } elseif (Document::VCARD40 === $targetVersion) {
  132. // These properties were removed in vCard 4.0
  133. if (in_array($property->name, ['NAME', 'MAILER', 'LABEL', 'CLASS'])) {
  134. return;
  135. }
  136. if ($property instanceof Property\Binary) {
  137. $newProperty = $this->convertBinaryToUri($output, $newProperty, $parameters);
  138. } elseif ($property instanceof Property\VCard\DateAndOrTime && isset($parameters['X-APPLE-OMIT-YEAR'])) {
  139. // If a property such as BDAY contained 'X-APPLE-OMIT-YEAR',
  140. // then we're stripping the year from the vcard 4 value.
  141. $parts = DateTimeParser::parseVCardDateTime($property->getValue());
  142. if ($parts['year'] === $property['X-APPLE-OMIT-YEAR']->getValue()) {
  143. $newValue = '--'.$parts['month'].'-'.$parts['date'];
  144. $newProperty->setValue($newValue);
  145. }
  146. // Regardless if the year matched or not, we do need to strip
  147. // X-APPLE-OMIT-YEAR.
  148. unset($parameters['X-APPLE-OMIT-YEAR']);
  149. }
  150. switch ($property->name) {
  151. case 'X-ABSHOWAS':
  152. if ('COMPANY' === strtoupper($property->getValue())) {
  153. $newProperty = $output->createProperty('KIND', 'ORG');
  154. }
  155. break;
  156. case 'X-ADDRESSBOOKSERVER-KIND':
  157. if ('GROUP' === strtoupper($property->getValue())) {
  158. $newProperty = $output->createProperty('KIND', 'GROUP');
  159. }
  160. break;
  161. case 'X-ADDRESSBOOKSERVER-MEMBER':
  162. $newProperty = $output->createProperty('MEMBER', $property->getValue());
  163. break;
  164. case 'X-ANNIVERSARY':
  165. $newProperty->name = 'ANNIVERSARY';
  166. // If we already have an anniversary property with the same
  167. // value, ignore.
  168. foreach ($output->select('ANNIVERSARY') as $anniversary) {
  169. if ($anniversary->getValue() === $newProperty->getValue()) {
  170. return;
  171. }
  172. }
  173. break;
  174. case 'X-ABDATE':
  175. // Find out what the label was, if it exists.
  176. if (!$property->group) {
  177. break;
  178. }
  179. $label = $input->{$property->group.'.X-ABLABEL'};
  180. // We only support converting anniversaries.
  181. if (!$label || '_$!<Anniversary>!$_' !== $label->getValue()) {
  182. break;
  183. }
  184. // If we already have an anniversary property with the same
  185. // value, ignore.
  186. foreach ($output->select('ANNIVERSARY') as $anniversary) {
  187. if ($anniversary->getValue() === $newProperty->getValue()) {
  188. return;
  189. }
  190. }
  191. $newProperty->name = 'ANNIVERSARY';
  192. break;
  193. // Apple's per-property label system.
  194. case 'X-ABLABEL':
  195. if ('_$!<Anniversary>!$_' === $newProperty->getValue()) {
  196. // We can safely remove these, as they are converted to
  197. // ANNIVERSARY properties.
  198. return;
  199. }
  200. break;
  201. }
  202. }
  203. // set property group
  204. $newProperty->group = $property->group;
  205. if (Document::VCARD40 === $targetVersion) {
  206. $this->convertParameters40($newProperty, $parameters);
  207. } else {
  208. $this->convertParameters30($newProperty, $parameters);
  209. }
  210. // Lastly, we need to see if there's a need for a VALUE parameter.
  211. //
  212. // We can do that by instantiating a empty property with that name, and
  213. // seeing if the default valueType is identical to the current one.
  214. $tempProperty = $output->createProperty($newProperty->name);
  215. if ($tempProperty->getValueType() !== $newProperty->getValueType()) {
  216. $newProperty['VALUE'] = $newProperty->getValueType();
  217. }
  218. $output->add($newProperty);
  219. }
  220. /**
  221. * Converts a BINARY property to a URI property.
  222. *
  223. * vCard 4.0 no longer supports BINARY properties.
  224. *
  225. * @param Property\Uri $property the input property
  226. * @param $parameters list of parameters that will eventually be added to
  227. * the new property
  228. *
  229. * @return Property\Uri
  230. */
  231. protected function convertBinaryToUri(Component\VCard $output, Property\Binary $newProperty, array &$parameters)
  232. {
  233. $value = $newProperty->getValue();
  234. $newProperty = $output->createProperty(
  235. $newProperty->name,
  236. null, // no value
  237. [], // no parameters yet
  238. 'URI' // Forcing the BINARY type
  239. );
  240. $mimeType = 'application/octet-stream';
  241. // See if we can find a better mimetype.
  242. if (isset($parameters['TYPE'])) {
  243. $newTypes = [];
  244. foreach ($parameters['TYPE']->getParts() as $typePart) {
  245. if (in_array(
  246. strtoupper($typePart),
  247. ['JPEG', 'PNG', 'GIF']
  248. )) {
  249. $mimeType = 'image/'.strtolower($typePart);
  250. } else {
  251. $newTypes[] = $typePart;
  252. }
  253. }
  254. // If there were any parameters we're not converting to a
  255. // mime-type, we need to keep them.
  256. if ($newTypes) {
  257. $parameters['TYPE']->setParts($newTypes);
  258. } else {
  259. unset($parameters['TYPE']);
  260. }
  261. }
  262. $newProperty->setValue('data:'.$mimeType.';base64,'.base64_encode($value));
  263. return $newProperty;
  264. }
  265. /**
  266. * Converts a URI property to a BINARY property.
  267. *
  268. * In vCard 4.0 attachments are encoded as data: uri. Even though these may
  269. * be valid in vCard 3.0 as well, we should convert those to BINARY if
  270. * possible, to improve compatibility.
  271. *
  272. * @param Property\Uri $property the input property
  273. *
  274. * @return Property\Binary|null
  275. */
  276. protected function convertUriToBinary(Component\VCard $output, Property\Uri $newProperty)
  277. {
  278. $value = $newProperty->getValue();
  279. // Only converting data: uris
  280. if ('data:' !== substr($value, 0, 5)) {
  281. return $newProperty;
  282. }
  283. $newProperty = $output->createProperty(
  284. $newProperty->name,
  285. null, // no value
  286. [], // no parameters yet
  287. 'BINARY'
  288. );
  289. $mimeType = substr($value, 5, strpos($value, ',') - 5);
  290. if (strpos($mimeType, ';')) {
  291. $mimeType = substr($mimeType, 0, strpos($mimeType, ';'));
  292. $newProperty->setValue(base64_decode(substr($value, strpos($value, ',') + 1)));
  293. } else {
  294. $newProperty->setValue(substr($value, strpos($value, ',') + 1));
  295. }
  296. unset($value);
  297. $newProperty['ENCODING'] = 'b';
  298. switch ($mimeType) {
  299. case 'image/jpeg':
  300. $newProperty['TYPE'] = 'JPEG';
  301. break;
  302. case 'image/png':
  303. $newProperty['TYPE'] = 'PNG';
  304. break;
  305. case 'image/gif':
  306. $newProperty['TYPE'] = 'GIF';
  307. break;
  308. }
  309. return $newProperty;
  310. }
  311. /**
  312. * Adds parameters to a new property for vCard 4.0.
  313. */
  314. protected function convertParameters40(Property $newProperty, array $parameters)
  315. {
  316. // Adding all parameters.
  317. foreach ($parameters as $param) {
  318. // vCard 2.1 allowed parameters with no name
  319. if ($param->noName) {
  320. $param->noName = false;
  321. }
  322. switch ($param->name) {
  323. // We need to see if there's any TYPE=PREF, because in vCard 4
  324. // that's now PREF=1.
  325. case 'TYPE':
  326. foreach ($param->getParts() as $paramPart) {
  327. if ('PREF' === strtoupper($paramPart)) {
  328. $newProperty->add('PREF', '1');
  329. } else {
  330. $newProperty->add($param->name, $paramPart);
  331. }
  332. }
  333. break;
  334. // These no longer exist in vCard 4
  335. case 'ENCODING':
  336. case 'CHARSET':
  337. break;
  338. default:
  339. $newProperty->add($param->name, $param->getParts());
  340. break;
  341. }
  342. }
  343. }
  344. /**
  345. * Adds parameters to a new property for vCard 3.0.
  346. */
  347. protected function convertParameters30(Property $newProperty, array $parameters)
  348. {
  349. // Adding all parameters.
  350. foreach ($parameters as $param) {
  351. // vCard 2.1 allowed parameters with no name
  352. if ($param->noName) {
  353. $param->noName = false;
  354. }
  355. switch ($param->name) {
  356. case 'ENCODING':
  357. // This value only existed in vCard 2.1, and should be
  358. // removed for anything else.
  359. if ('QUOTED-PRINTABLE' !== strtoupper($param->getValue())) {
  360. $newProperty->add($param->name, $param->getParts());
  361. }
  362. break;
  363. /*
  364. * Converting PREF=1 to TYPE=PREF.
  365. *
  366. * Any other PREF numbers we'll drop.
  367. */
  368. case 'PREF':
  369. if ('1' == $param->getValue()) {
  370. $newProperty->add('TYPE', 'PREF');
  371. }
  372. break;
  373. default:
  374. $newProperty->add($param->name, $param->getParts());
  375. break;
  376. }
  377. }
  378. }
  379. }