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