VCardConverter.php 16 KB

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