You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

rcube_csv2vcard.php 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663
  1. <?php
  2. /**
  3. +-----------------------------------------------------------------------+
  4. | This file is part of the Roundcube Webmail client |
  5. | Copyright (C) 2008-2012, The Roundcube Dev Team |
  6. | |
  7. | Licensed under the GNU General Public License version 3 or |
  8. | any later version with exceptions for skins & plugins. |
  9. | See the README file for a full license statement. |
  10. | |
  11. | PURPOSE: |
  12. | CSV to vCard data conversion |
  13. +-----------------------------------------------------------------------+
  14. | Author: Aleksander Machniak <alec@alec.pl> |
  15. +-----------------------------------------------------------------------+
  16. */
  17. /**
  18. * CSV to vCard data converter
  19. *
  20. * @package Framework
  21. * @subpackage Addressbook
  22. * @author Aleksander Machniak <alec@alec.pl>
  23. */
  24. class rcube_csv2vcard
  25. {
  26. /**
  27. * CSV to vCard fields mapping
  28. *
  29. * @var array
  30. */
  31. protected $csv2vcard_map = array(
  32. // MS Outlook 2010
  33. 'anniversary' => 'anniversary',
  34. 'assistants_name' => 'assistant',
  35. 'assistants_phone' => 'phone:assistant',
  36. 'birthday' => 'birthday',
  37. 'business_city' => 'locality:work',
  38. 'business_countryregion' => 'country:work',
  39. 'business_fax' => 'phone:work,fax',
  40. 'business_phone' => 'phone:work',
  41. 'business_phone_2' => 'phone:work2',
  42. 'business_postal_code' => 'zipcode:work',
  43. 'business_state' => 'region:work',
  44. 'business_street' => 'street:work',
  45. //'business_street_2' => '',
  46. //'business_street_3' => '',
  47. 'car_phone' => 'phone:car',
  48. 'categories' => 'groups',
  49. //'children' => '',
  50. 'company' => 'organization',
  51. //'company_main_phone' => '',
  52. 'department' => 'department',
  53. 'email_2_address' => 'email:other',
  54. //'email_2_type' => '',
  55. 'email_3_address' => 'email:other',
  56. //'email_3_type' => '',
  57. 'email_address' => 'email:pref',
  58. //'email_type' => '',
  59. 'first_name' => 'firstname',
  60. 'gender' => 'gender',
  61. 'home_city' => 'locality:home',
  62. 'home_countryregion' => 'country:home',
  63. 'home_fax' => 'phone:home,fax',
  64. 'home_phone' => 'phone:home',
  65. 'home_phone_2' => 'phone:home2',
  66. 'home_postal_code' => 'zipcode:home',
  67. 'home_state' => 'region:home',
  68. 'home_street' => 'street:home',
  69. //'home_street_2' => '',
  70. //'home_street_3' => '',
  71. //'initials' => '',
  72. //'isdn' => '',
  73. 'job_title' => 'jobtitle',
  74. //'keywords' => '',
  75. //'language' => '',
  76. 'last_name' => 'surname',
  77. //'location' => '',
  78. 'managers_name' => 'manager',
  79. 'middle_name' => 'middlename',
  80. //'mileage' => '',
  81. 'mobile_phone' => 'phone:cell',
  82. 'notes' => 'notes',
  83. //'office_location' => '',
  84. 'other_city' => 'locality:other',
  85. 'other_countryregion' => 'country:other',
  86. 'other_fax' => 'phone:other,fax',
  87. 'other_phone' => 'phone:other',
  88. 'other_postal_code' => 'zipcode:other',
  89. 'other_state' => 'region:other',
  90. 'other_street' => 'street:other',
  91. //'other_street_2' => '',
  92. //'other_street_3' => '',
  93. 'pager' => 'phone:pager',
  94. 'primary_phone' => 'phone:pref',
  95. //'profession' => '',
  96. //'radio_phone' => '',
  97. 'spouse' => 'spouse',
  98. 'suffix' => 'suffix',
  99. 'title' => 'title',
  100. 'web_page' => 'website:homepage',
  101. // Thunderbird
  102. 'birth_day' => 'birthday-d',
  103. 'birth_month' => 'birthday-m',
  104. 'birth_year' => 'birthday-y',
  105. 'display_name' => 'displayname',
  106. 'fax_number' => 'phone:fax',
  107. 'home_address' => 'street:home',
  108. //'home_address_2' => '',
  109. 'home_country' => 'country:home',
  110. 'home_zipcode' => 'zipcode:home',
  111. 'mobile_number' => 'phone:cell',
  112. 'nickname' => 'nickname',
  113. 'organization' => 'organization',
  114. 'pager_number' => 'phone:pager',
  115. 'primary_email' => 'email:pref',
  116. 'secondary_email' => 'email:other',
  117. 'web_page_1' => 'website:homepage',
  118. 'web_page_2' => 'website:other',
  119. 'work_phone' => 'phone:work',
  120. 'work_address' => 'street:work',
  121. //'work_address_2' => '',
  122. 'work_country' => 'country:work',
  123. 'work_zipcode' => 'zipcode:work',
  124. 'last' => 'surname',
  125. 'first' => 'firstname',
  126. 'work_city' => 'locality:work',
  127. 'work_state' => 'region:work',
  128. 'home_city_short' => 'locality:home',
  129. 'home_state_short' => 'region:home',
  130. // Atmail
  131. 'date_of_birth' => 'birthday',
  132. 'email' => 'email:pref',
  133. 'home_mobile' => 'phone:cell',
  134. 'home_zip' => 'zipcode:home',
  135. 'info' => 'notes',
  136. 'user_photo' => 'photo',
  137. 'url' => 'website:homepage',
  138. 'work_company' => 'organization',
  139. 'work_dept' => 'departament',
  140. 'work_fax' => 'phone:work,fax',
  141. 'work_mobile' => 'phone:work,cell',
  142. 'work_title' => 'jobtitle',
  143. 'work_zip' => 'zipcode:work',
  144. 'group' => 'groups',
  145. // GMail
  146. 'groups' => 'groups',
  147. 'group_membership' => 'groups',
  148. 'given_name' => 'firstname',
  149. 'additional_name' => 'middlename',
  150. 'family_name' => 'surname',
  151. 'name' => 'displayname',
  152. 'name_prefix' => 'prefix',
  153. 'name_suffix' => 'suffix',
  154. );
  155. /**
  156. * CSV label to text mapping for English
  157. *
  158. * @var array
  159. */
  160. protected $label_map = array(
  161. // MS Outlook 2010
  162. 'anniversary' => "Anniversary",
  163. 'assistants_name' => "Assistant's Name",
  164. 'assistants_phone' => "Assistant's Phone",
  165. 'birthday' => "Birthday",
  166. 'business_city' => "Business City",
  167. 'business_countryregion' => "Business Country/Region",
  168. 'business_fax' => "Business Fax",
  169. 'business_phone' => "Business Phone",
  170. 'business_phone_2' => "Business Phone 2",
  171. 'business_postal_code' => "Business Postal Code",
  172. 'business_state' => "Business State",
  173. 'business_street' => "Business Street",
  174. //'business_street_2' => "Business Street 2",
  175. //'business_street_3' => "Business Street 3",
  176. 'car_phone' => "Car Phone",
  177. 'categories' => "Categories",
  178. //'children' => "Children",
  179. 'company' => "Company",
  180. //'company_main_phone' => "Company Main Phone",
  181. 'department' => "Department",
  182. //'directory_server' => "Directory Server",
  183. 'email_2_address' => "E-mail 2 Address",
  184. //'email_2_type' => "E-mail 2 Type",
  185. 'email_3_address' => "E-mail 3 Address",
  186. //'email_3_type' => "E-mail 3 Type",
  187. 'email_address' => "E-mail Address",
  188. //'email_type' => "E-mail Type",
  189. 'first_name' => "First Name",
  190. 'gender' => "Gender",
  191. 'home_city' => "Home City",
  192. 'home_countryregion' => "Home Country/Region",
  193. 'home_fax' => "Home Fax",
  194. 'home_phone' => "Home Phone",
  195. 'home_phone_2' => "Home Phone 2",
  196. 'home_postal_code' => "Home Postal Code",
  197. 'home_state' => "Home State",
  198. 'home_street' => "Home Street",
  199. //'home_street_2' => "Home Street 2",
  200. //'home_street_3' => "Home Street 3",
  201. //'initials' => "Initials",
  202. //'isdn' => "ISDN",
  203. 'job_title' => "Job Title",
  204. //'keywords' => "Keywords",
  205. //'language' => "Language",
  206. 'last_name' => "Last Name",
  207. //'location' => "Location",
  208. 'managers_name' => "Manager's Name",
  209. 'middle_name' => "Middle Name",
  210. //'mileage' => "Mileage",
  211. 'mobile_phone' => "Mobile Phone",
  212. 'notes' => "Notes",
  213. //'office_location' => "Office Location",
  214. 'other_city' => "Other City",
  215. 'other_countryregion' => "Other Country/Region",
  216. 'other_fax' => "Other Fax",
  217. 'other_phone' => "Other Phone",
  218. 'other_postal_code' => "Other Postal Code",
  219. 'other_state' => "Other State",
  220. 'other_street' => "Other Street",
  221. //'other_street_2' => "Other Street 2",
  222. //'other_street_3' => "Other Street 3",
  223. 'pager' => "Pager",
  224. 'primary_phone' => "Primary Phone",
  225. //'profession' => "Profession",
  226. //'radio_phone' => "Radio Phone",
  227. 'spouse' => "Spouse",
  228. 'suffix' => "Suffix",
  229. 'title' => "Title",
  230. 'web_page' => "Web Page",
  231. // Thunderbird
  232. 'birth_day' => "Birth Day",
  233. 'birth_month' => "Birth Month",
  234. 'birth_year' => "Birth Year",
  235. 'display_name' => "Display Name",
  236. 'fax_number' => "Fax Number",
  237. 'home_address' => "Home Address",
  238. //'home_address_2' => "Home Address 2",
  239. 'home_country' => "Home Country",
  240. 'home_zipcode' => "Home ZipCode",
  241. 'mobile_number' => "Mobile Number",
  242. 'nickname' => "Nickname",
  243. 'organization' => "Organization",
  244. 'pager_number' => "Pager Namber",
  245. 'primary_email' => "Primary Email",
  246. 'secondary_email' => "Secondary Email",
  247. 'web_page_1' => "Web Page 1",
  248. 'web_page_2' => "Web Page 2",
  249. 'work_phone' => "Work Phone",
  250. 'work_address' => "Work Address",
  251. //'work_address_2' => "Work Address 2",
  252. 'work_city' => "Work City",
  253. 'work_country' => "Work Country",
  254. 'work_state' => "Work State",
  255. 'work_zipcode' => "Work ZipCode",
  256. // Atmail
  257. 'date_of_birth' => "Date of Birth",
  258. 'email' => "Email",
  259. //'email_2' => "Email2",
  260. //'email_3' => "Email3",
  261. //'email_4' => "Email4",
  262. //'email_5' => "Email5",
  263. 'home_mobile' => "Home Mobile",
  264. 'home_zip' => "Home Zip",
  265. 'info' => "Info",
  266. 'user_photo' => "User Photo",
  267. 'url' => "URL",
  268. 'work_company' => "Work Company",
  269. 'work_dept' => "Work Dept",
  270. 'work_fax' => "Work Fax",
  271. 'work_mobile' => "Work Mobile",
  272. 'work_title' => "Work Title",
  273. 'work_zip' => "Work Zip",
  274. 'group' => "Group",
  275. // GMail
  276. 'groups' => "Groups",
  277. 'group_membership' => "Group Membership",
  278. 'given_name' => "Given Name",
  279. 'additional_name' => "Additional Name",
  280. 'family_name' => "Family Name",
  281. 'name' => "Name",
  282. 'name_prefix' => "Name Prefix",
  283. 'name_suffix' => "Name Suffix",
  284. );
  285. /**
  286. * Special fields map for GMail format
  287. *
  288. * @var array
  289. */
  290. protected $gmail_label_map = array(
  291. 'E-mail' => array(
  292. 'Value' => array(
  293. 'home' => 'email:home',
  294. 'work' => 'email:work',
  295. '*' => 'email:other',
  296. ),
  297. ),
  298. 'Phone' => array(
  299. 'Value' => array(
  300. 'home' => 'phone:home',
  301. 'homefax' => 'phone:homefax',
  302. 'main' => 'phone:pref',
  303. 'pager' => 'phone:pager',
  304. 'mobile' => 'phone:cell',
  305. 'work' => 'phone:work',
  306. 'workfax' => 'phone:workfax',
  307. ),
  308. ),
  309. 'Relation' => array(
  310. 'Value' => array(
  311. 'spouse' => 'spouse',
  312. ),
  313. ),
  314. 'Website' => array(
  315. 'Value' => array(
  316. 'profile' => 'website:profile',
  317. 'blog' => 'website:blog',
  318. 'homepage' => 'website:homepage',
  319. 'work' => 'website:work',
  320. ),
  321. ),
  322. 'Address' => array(
  323. 'Street' => array(
  324. 'home' => 'street:home',
  325. 'work' => 'street:work',
  326. ),
  327. 'City' => array(
  328. 'home' => 'locality:home',
  329. 'work' => 'locality:work',
  330. ),
  331. 'Region' => array(
  332. 'home' => 'region:home',
  333. 'work' => 'region:work',
  334. ),
  335. 'Postal Code' => array(
  336. 'home' => 'zipcode:home',
  337. 'work' => 'zipcode:work',
  338. ),
  339. 'Country' => array(
  340. 'home' => 'country:home',
  341. 'work' => 'country:work',
  342. ),
  343. ),
  344. 'Organization' => array(
  345. 'Name' => array(
  346. '' => 'organization',
  347. ),
  348. 'Title' => array(
  349. '' => 'jobtitle',
  350. ),
  351. 'Department' => array(
  352. '' => 'department',
  353. ),
  354. ),
  355. );
  356. protected $local_label_map = array();
  357. protected $vcards = array();
  358. protected $map = array();
  359. protected $gmail_map = array();
  360. /**
  361. * Class constructor
  362. *
  363. * @param string $lang File language
  364. */
  365. public function __construct($lang = 'en_US')
  366. {
  367. // Localize fields map
  368. if ($lang && $lang != 'en_US') {
  369. if (file_exists(RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc")) {
  370. include RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc";
  371. }
  372. if (!empty($map)) {
  373. $this->local_label_map = array_merge($this->label_map, $map);
  374. }
  375. }
  376. $this->label_map = array_flip($this->label_map);
  377. $this->local_label_map = array_flip($this->local_label_map);
  378. }
  379. /**
  380. * Import contacts from CSV file
  381. *
  382. * @param string $csv Content of the CSV file
  383. */
  384. public function import($csv)
  385. {
  386. // convert to UTF-8
  387. $head = substr($csv, 0, 4096);
  388. $charset = rcube_charset::detect($head, RCUBE_CHARSET);
  389. $csv = rcube_charset::convert($csv, $charset);
  390. $csv = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $csv); // also remove BOM
  391. $head = '';
  392. $prev_line = false;
  393. $this->map = array();
  394. $this->gmail_map = array();
  395. // Parse file
  396. foreach (preg_split("/[\r\n]+/", $csv) as $line) {
  397. if (!empty($prev_line)) {
  398. $line = '"' . $line;
  399. }
  400. $elements = $this->parse_line($line);
  401. if (empty($elements)) {
  402. continue;
  403. }
  404. // Parse header
  405. if (empty($this->map)) {
  406. $this->parse_header($elements);
  407. if (empty($this->map)) {
  408. break;
  409. }
  410. }
  411. // Parse data row
  412. else {
  413. // handle multiline elements (e.g. Gmail)
  414. if (!empty($prev_line)) {
  415. $first = array_shift($elements);
  416. if ($first[0] == '"') {
  417. $prev_line[count($prev_line)-1] = '"' . $prev_line[count($prev_line)-1] . "\n" . substr($first, 1);
  418. }
  419. else {
  420. $prev_line[count($prev_line)-1] .= "\n" . $first;
  421. }
  422. $elements = array_merge($prev_line, $elements);
  423. }
  424. $last_element = $elements[count($elements)-1];
  425. if ($last_element[0] == '"') {
  426. $elements[count($elements)-1] = substr($last_element, 1);
  427. $prev_line = $elements;
  428. continue;
  429. }
  430. $this->csv_to_vcard($elements);
  431. $prev_line = false;
  432. }
  433. }
  434. }
  435. /**
  436. * Export vCards
  437. *
  438. * @return array rcube_vcard List of vcards
  439. */
  440. public function export()
  441. {
  442. return $this->vcards;
  443. }
  444. /**
  445. * Parse CSV file line
  446. */
  447. protected function parse_line($line)
  448. {
  449. $line = trim($line);
  450. if (empty($line)) {
  451. return null;
  452. }
  453. $fields = rcube_utils::explode_quoted_string(',', $line);
  454. // remove quotes if needed
  455. if (!empty($fields)) {
  456. foreach ($fields as $idx => $value) {
  457. if (($len = strlen($value)) > 1 && $value[0] == '"' && $value[$len-1] == '"') {
  458. // remove surrounding quotes
  459. $value = substr($value, 1, -1);
  460. // replace doubled quotes inside the string with single quote
  461. $value = str_replace('""', '"', $value);
  462. $fields[$idx] = $value;
  463. }
  464. }
  465. }
  466. return $fields;
  467. }
  468. /**
  469. * Parse CSV header line, detect fields mapping
  470. */
  471. protected function parse_header($elements)
  472. {
  473. $map1 = array();
  474. $map2 = array();
  475. $size = count($elements);
  476. // check English labels
  477. for ($i = 0; $i < $size; $i++) {
  478. $label = $this->label_map[$elements[$i]];
  479. if ($label && !empty($this->csv2vcard_map[$label])) {
  480. $map1[$i] = $this->csv2vcard_map[$label];
  481. }
  482. }
  483. // check localized labels
  484. if (!empty($this->local_label_map)) {
  485. for ($i = 0; $i < $size; $i++) {
  486. $label = $this->local_label_map[$elements[$i]];
  487. // special localization label
  488. if ($label && $label[0] == '_') {
  489. $label = substr($label, 1);
  490. }
  491. if ($label && !empty($this->csv2vcard_map[$label])) {
  492. $map2[$i] = $this->csv2vcard_map[$label];
  493. }
  494. }
  495. }
  496. $this->map = count($map1) >= count($map2) ? $map1 : $map2;
  497. // support special Gmail format
  498. foreach ($this->gmail_label_map as $key => $items) {
  499. $num = 1;
  500. while (($_key = "$key $num - Type") && ($found = array_search($_key, $elements)) !== false) {
  501. $this->gmail_map["$key:$num"] = array('_key' => $key, '_idx' => $found);
  502. foreach (array_keys($items) as $item_key) {
  503. $_key = "$key $num - $item_key";
  504. if (($found = array_search($_key, $elements)) !== false) {
  505. $this->gmail_map["$key:$num"][$item_key] = $found;
  506. }
  507. }
  508. $num++;
  509. }
  510. }
  511. }
  512. /**
  513. * Convert CSV data row to vCard
  514. */
  515. protected function csv_to_vcard($data)
  516. {
  517. $contact = array();
  518. foreach ($this->map as $idx => $name) {
  519. $value = $data[$idx];
  520. if ($value !== null && $value !== '') {
  521. if (!empty($contact[$name])) {
  522. $contact[$name] = (array) $contact[$name];
  523. $contact[$name][] = $value;
  524. }
  525. else {
  526. $contact[$name] = $value;
  527. }
  528. }
  529. }
  530. // Gmail format support
  531. foreach ($this->gmail_map as $idx => $item) {
  532. $type = preg_replace('/[^a-z]/', '', strtolower($data[$item['_idx']]));
  533. $key = $item['_key'];
  534. unset($item['_idx']);
  535. unset($item['_key']);
  536. foreach ($item as $item_key => $item_idx) {
  537. $value = $data[$item_idx];
  538. if ($value !== null && $value !== '') {
  539. foreach (array($type, '*') as $_type) {
  540. if ($data_idx = $this->gmail_label_map[$key][$item_key][$_type]) {
  541. $value = explode(' ::: ', $value);
  542. if (!empty($contact[$data_idx])) {
  543. $contact[$data_idx] = array_merge((array) $contact[$data_idx], $value);
  544. }
  545. else {
  546. $contact[$data_idx] = $value;
  547. }
  548. break;
  549. }
  550. }
  551. }
  552. }
  553. }
  554. if (empty($contact)) {
  555. return;
  556. }
  557. // Handle special values
  558. if (!empty($contact['birthday-d']) && !empty($contact['birthday-m']) && !empty($contact['birthday-y'])) {
  559. $contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d'];
  560. }
  561. if (!empty($contact['groups'])) {
  562. // categories/groups separator in vCard is ',' not ';'
  563. $contact['groups'] = str_replace(',', '', $contact['groups']);
  564. $contact['groups'] = str_replace(';', ',', $contact['groups']);
  565. if (!empty($this->gmail_map)) {
  566. // remove "* " added by GMail
  567. $contact['groups'] = str_replace('* ', '', $contact['groups']);
  568. // replace strange delimiter
  569. $contact['groups'] = str_replace(' ::: ', ',', $contact['groups']);
  570. }
  571. }
  572. // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
  573. foreach (array('birthday', 'anniversary') as $key) {
  574. if (!empty($contact[$key])) {
  575. $date = preg_replace('/[0[:^word:]]/', '', $contact[$key]);
  576. if (empty($date)) {
  577. unset($contact[$key]);
  578. }
  579. }
  580. }
  581. if (!empty($contact['gender']) && ($gender = strtolower($contact['gender']))) {
  582. if (!in_array($gender, array('male', 'female'))) {
  583. unset($contact['gender']);
  584. }
  585. }
  586. // Convert address(es) to rcube_vcard data
  587. foreach ($contact as $idx => $value) {
  588. $name = explode(':', $idx);
  589. if (in_array($name[0], array('street', 'locality', 'region', 'zipcode', 'country'))) {
  590. $contact['address:'.$name[1]][$name[0]] = $value;
  591. unset($contact[$idx]);
  592. }
  593. }
  594. // Create vcard object
  595. $vcard = new rcube_vcard();
  596. foreach ($contact as $name => $value) {
  597. $name = explode(':', $name);
  598. if (is_array($value) && $name[0] != 'address') {
  599. foreach ((array) $value as $val) {
  600. $vcard->set($name[0], $val, $name[1]);
  601. }
  602. }
  603. else {
  604. $vcard->set($name[0], $value, $name[1]);
  605. }
  606. }
  607. // add to the list
  608. $this->vcards[] = $vcard;
  609. }
  610. }