Aleksander Machniak
2014-09-15 83a64265a7d8865f0381e53a0cc47d6ef53217b6
commit | author | age
383379 1 <?php
AM 2
3 /*
4  +-----------------------------------------------------------------------+
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',
55         //'email_2_address'       => '', //@TODO
c66b60 56         //'email_2_type'          => '',
383379 57         //'email_3_address'       => '', //@TODO
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",
c66b60 189         //'email_2_address'   => "E-mail 2 Address",
AM 190         //'email_2_type'      => "E-mail 2 Type",
191         //'email_3_address'   => "E-mail 3 Address",
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',
305             ),
306         ),
307         'Phone' => array(
308             'Value' => array(
309                 'home'    => 'phone:home',
310                 'homefax' => 'phone:homefax',
311                 'main'    => 'phone:pref',
312                 'pager'   => 'phone:pager',
313                 'mobile'  => 'phone:cell',
314                 'work'    => 'phone:work',
315                 'workfax' => 'phone:workfax',
316             ),
317         ),
318         'Relation' => array(
319             'Value' => array(
320                 'spouse' => 'spouse',
321             ),
322         ),
323         'Website' => array(
324             'Value' => array(
325                 'profile'  => 'website:profile',
326                 'blog'     => 'website:blog',
327                 'homepage' => 'website:homepage',
328                 'work'     => 'website:work',
329             ),
330         ),
331         'Address' => array(
332             'Street' => array(
333                 'home' => 'street:home',
334                 'work' => 'street:work',
335             ),
336             'City' => array(
337                 'home' => 'locality:home',
338                 'work' => 'locality:work',
339             ),
340             'Region' => array(
341                 'home' => 'region:home',
342                 'work' => 'region:work',
343             ),
344             'Postal Code' => array(
345                 'home' => 'zipcode:home',
346                 'work' => 'zipcode:work',
347             ),
348             'Country' => array(
349                 'home' => 'country:home',
350                 'work' => 'country:work',
351             ),
352         ),
353         'Organization' => array(
354             'Name' => array(
355                 '' => 'organization',
356             ),
357             'Title' => array(
358                 '' => 'jobtitle',
359             ),
360             'Department' => array(
361                 '' => 'department',
362             ),
363         ),
364     );
365
366
92bd3a 367     protected $local_label_map = array();
5f1765 368     protected $vcards          = array();
AM 369     protected $map             = array();
370     protected $gmail_map       = array();
383379 371
AM 372
373     /**
374      * Class constructor
375      *
376      * @param string $lang File language
377      */
378     public function __construct($lang = 'en_US')
379     {
380         // Localize fields map
381         if ($lang && $lang != 'en_US') {
9be2f4 382             if (file_exists(RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc")) {
TB 383                 include RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc";
383379 384             }
AM 385
386             if (!empty($map)) {
92bd3a 387                 $this->local_label_map = array_merge($this->label_map, $map);
383379 388             }
AM 389         }
390
391         $this->label_map = array_flip($this->label_map);
92bd3a 392         $this->local_label_map = array_flip($this->local_label_map);
383379 393     }
AM 394
395     /**
396      *
397      */
398     public function import($csv)
399     {
400         // convert to UTF-8
5f1765 401         $head      = substr($csv, 0, 4096);
AM 402         $charset   = rcube_charset::detect($head, RCUBE_CHARSET);
403         $csv       = rcube_charset::convert($csv, $charset);
404         $csv       = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $csv); // also remove BOM
405         $head      = '';
406         $prev_line = false;
383379 407
5f1765 408         $this->map       = array();
AM 409         $this->gmail_map = array();
383379 410
AM 411         // Parse file
3725cf 412         foreach (preg_split("/[\r\n]+/", $csv) as $line) {
5f1765 413             if (!empty($prev_line)) {
AM 414                 $line = '"' . $line;
415             }
416
745d86 417             $elements = $this->parse_line($line);
5f1765 418
383379 419             if (empty($elements)) {
AM 420                 continue;
421             }
422
423             // Parse header
424             if (empty($this->map)) {
425                 $this->parse_header($elements);
426                 if (empty($this->map)) {
427                     break;
428                 }
429             }
430             // Parse data row
431             else {
5f1765 432                 // handle multiline elements (e.g. Gmail)
AM 433                 if (!empty($prev_line)) {
434                     $first = array_shift($elements);
435
436                     if ($first[0] == '"') {
437                         $prev_line[count($prev_line)-1] = '"' . $prev_line[count($prev_line)-1] . "\n" . substr($first, 1);
438                     }
439                     else {
440                         $prev_line[count($prev_line)-1] .= "\n" . $first;
441                     }
442
443                     $elements = array_merge($prev_line, $elements);
444                 }
445
446                 $last_element = $elements[count($elements)-1];
447                 if ($last_element[0] == '"') {
448                     $elements[count($elements)-1] = substr($last_element, 1);
449                     $prev_line = $elements;
450                     continue;
451                 }
383379 452                 $this->csv_to_vcard($elements);
5f1765 453                 $prev_line = false;
383379 454             }
AM 455         }
456     }
457
458     /**
459      * @return array rcube_vcard List of vcards
460      */
461     public function export()
462     {
463         return $this->vcards;
464     }
465
466     /**
745d86 467      * Parse CSV file line
AM 468      */
469     protected function parse_line($line)
470     {
471         $line = trim($line);
472         if (empty($line)) {
473             return null;
474         }
475
476         $fields = rcube_utils::explode_quoted_string(',', $line);
477
478         // remove quotes if needed
479         if (!empty($fields)) {
480             foreach ($fields as $idx => $value) {
481                 if (($len = strlen($value)) > 1 && $value[0] == '"' && $value[$len-1] == '"') {
482                     // remove surrounding quotes
483                     $value = substr($value, 1, -1);
484                     // replace doubled quotes inside the string with single quote
485                     $value = str_replace('""', '"', $value);
486
487                     $fields[$idx] = $value;
488                 }
489             }
490         }
491
492         return $fields;
493     }
494
495     /**
383379 496      * Parse CSV header line, detect fields mapping
AM 497      */
498     protected function parse_header($elements)
92bd3a 499     {
AM 500         $map1 = array();
501         $map2 = array();
502         $size = count($elements);
503
504         // check English labels
505         for ($i = 0; $i < $size; $i++) {
383379 506             $label = $this->label_map[$elements[$i]];
AM 507             if ($label && !empty($this->csv2vcard_map[$label])) {
92bd3a 508                 $map1[$i] = $this->csv2vcard_map[$label];
383379 509             }
AM 510         }
5f1765 511
92bd3a 512         // check localized labels
AM 513         if (!empty($this->local_label_map)) {
514             for ($i = 0; $i < $size; $i++) {
515                 $label = $this->local_label_map[$elements[$i]];
0b0cae 516
AM 517                 // special localization label
518                 if ($label && $label[0] == '_') {
519                     $label = substr($label, 1);
520                 }
521
92bd3a 522                 if ($label && !empty($this->csv2vcard_map[$label])) {
AM 523                     $map2[$i] = $this->csv2vcard_map[$label];
524                 }
525             }
526         }
527
528         $this->map = count($map1) >= count($map2) ? $map1 : $map2;
5f1765 529
AM 530         // support special Gmail format
531         foreach ($this->gmail_label_map as $key => $items) {
532             $num = 1;
533             while (($_key = "$key $num - Type") && ($found = array_search($_key, $elements)) !== false) {
534                 $this->gmail_map["$key:$num"] = array('_key' => $key, '_idx' => $found);
535                 foreach (array_keys($items) as $item_key) {
536                     $_key = "$key $num - $item_key";
537                     if (($found = array_search($_key, $elements)) !== false) {
538                         $this->gmail_map["$key:$num"][$item_key] = $found;
539                     }
540                 }
541
542                 $num++;
543             }
544         }
383379 545     }
AM 546
547     /**
548      * Convert CSV data row to vCard
549      */
550     protected function csv_to_vcard($data)
551     {
552         $contact = array();
553         foreach ($this->map as $idx => $name) {
554             $value = $data[$idx];
555             if ($value !== null && $value !== '') {
556                 $contact[$name] = $value;
557             }
558         }
559
5f1765 560         // Gmail format support
AM 561         foreach ($this->gmail_map as $idx => $item) {
562             $type = preg_replace('/[^a-z]/', '', strtolower($data[$item['_idx']]));
563             $key  = $item['_key'];
564
565             unset($item['_idx']);
566             unset($item['_key']);
567
568             foreach ($item as $item_key => $item_idx) {
569                 $value = $data[$item_idx];
570                 if ($value !== null && $value !== '' && ($data_idx = $this->gmail_label_map[$key][$item_key][$type])) {
571                     $contact[$data_idx] = $value;
572                 }
573             }
574         }
575
383379 576         if (empty($contact)) {
AM 577             return;
578         }
579
580         // Handle special values
581         if (!empty($contact['birthday-d']) && !empty($contact['birthday-m']) && !empty($contact['birthday-y'])) {
582             $contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d'];
583         }
584
5983ee 585         if (!empty($contact['groups'])) {
5f1765 586             // categories/groups separator in vCard is ',' not ';'
5983ee 587             $contact['groups'] = str_replace(';', ',', $contact['groups']);
5f1765 588
AM 589             // remove "* " added by GMail
590             if (!empty($this->gmail_map)) {
591                 $contact['groups'] = str_replace('* ', '', $contact['groups']);
592             }
5983ee 593         }
AM 594
609483 595         // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
c66b60 596         foreach (array('birthday', 'anniversary') as $key) {
609483 597             if (!empty($contact[$key])) {
AM 598                 $date = preg_replace('/[0[:^word:]]/', '', $contact[$key]);
599                 if (empty($date)) {
600                     unset($contact[$key]);
601                 }
c66b60 602             }
AM 603         }
604
605         if (!empty($contact['gender']) && ($gender = strtolower($contact['gender']))) {
606             if (!in_array($gender, array('male', 'female'))) {
607                 unset($contact['gender']);
608             }
609         }
610
acf851 611         // Convert address(es) to rcube_vcard data
AM 612         foreach ($contact as $idx => $value) {
613             $name = explode(':', $idx);
614             if (in_array($name[0], array('street', 'locality', 'region', 'zipcode', 'country'))) {
615                 $contact['address:'.$name[1]][$name[0]] = $value;
616                 unset($contact[$idx]);
617             }
618         }
619
383379 620         // Create vcard object
AM 621         $vcard = new rcube_vcard();
622         foreach ($contact as $name => $value) {
623             $name = explode(':', $name);
624             $vcard->set($name[0], $value, $name[1]);
625         }
626
627         // add to the list
628         $this->vcards[] = $vcard;
629     }
630 }