Aleksander Machniak
2013-10-14 5a2d2a6f75b115183459997cc2aa787d9a085fe8
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',
50         'categories'            => 'categories',
51         //'children'              => '',
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'          => '',
383379 59         'email_address'         => 'email:main',
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',
383379 149     );
AM 150
151     /**
152      * CSV label to text mapping for English
153      *
154      * @var array
155      */
156     protected $label_map = array(
157         // MS Outlook 2010
158         'anniversary'       => "Anniversary",
159         'assistants_name'   => "Assistant's Name",
160         'assistants_phone'  => "Assistant's Phone",
161         'birthday'          => "Birthday",
162         'business_city'     => "Business City",
163         'business_countryregion' => "Business Country/Region",
164         'business_fax'      => "Business Fax",
165         'business_phone'    => "Business Phone",
166         'business_phone_2'  => "Business Phone 2",
167         'business_postal_code' => "Business Postal Code",
168         'business_state'    => "Business State",
169         'business_street'   => "Business Street",
170         //'business_street_2' => "Business Street 2",
171         //'business_street_3' => "Business Street 3",
172         'car_phone'         => "Car Phone",
173         'categories'        => "Categories",
174         //'children'          => "Children",
175         'company'           => "Company",
176         //'company_main_phone' => "Company Main Phone",
177         'department'        => "Department",
178         //'directory_server'  => "Directory Server",
c66b60 179         //'email_2_address'   => "E-mail 2 Address",
AM 180         //'email_2_type'      => "E-mail 2 Type",
181         //'email_3_address'   => "E-mail 3 Address",
182         //'email_3_type'      => "E-mail 3 Type",
383379 183         'email_address'     => "E-mail Address",
c66b60 184         //'email_type'        => "E-mail Type",
383379 185         'first_name'        => "First Name",
AM 186         'gender'            => "Gender",
187         'home_city'         => "Home City",
188         'home_countryregion' => "Home Country/Region",
189         'home_fax'          => "Home Fax",
190         'home_phone'        => "Home Phone",
191         'home_phone_2'      => "Home Phone 2",
192         'home_postal_code'  => "Home Postal Code",
193         'home_state'        => "Home State",
194         'home_street'       => "Home Street",
195         //'home_street_2'     => "Home Street 2",
196         //'home_street_3'     => "Home Street 3",
197         //'initials'          => "Initials",
198         //'isdn'              => "ISDN",
199         'job_title'         => "Job Title",
200         //'keywords'          => "Keywords",
201         //'language'          => "Language",
202         'last_name'         => "Last Name",
203         //'location'          => "Location",
204         'managers_name'     => "Manager's Name",
205         'middle_name'       => "Middle Name",
206         //'mileage'           => "Mileage",
207         'mobile_phone'      => "Mobile Phone",
208         'notes'             => "Notes",
209         //'office_location'   => "Office Location",
210         'other_city'        => "Other City",
211         'other_countryregion' => "Other Country/Region",
212         'other_fax'         => "Other Fax",
213         'other_phone'       => "Other Phone",
214         'other_postal_code' => "Other Postal Code",
215         'other_state'       => "Other State",
216         'other_street'      => "Other Street",
217         //'other_street_2'    => "Other Street 2",
218         //'other_street_3'    => "Other Street 3",
219         'pager'             => "Pager",
220         'primary_phone'     => "Primary Phone",
221         //'profession'        => "Profession",
222         //'radio_phone'       => "Radio Phone",
223         'spouse'            => "Spouse",
224         'suffix'            => "Suffix",
225         'title'             => "Title",
226         'web_page'          => "Web Page",
227
228         // Thunderbird
229         'birth_day'         => "Birth Day",
230         'birth_month'       => "Birth Month",
231         'birth_year'        => "Birth Year",
232         'display_name'      => "Display Name",
233         'fax_number'        => "Fax Number",
234         'home_address'      => "Home Address",
235         //'home_address_2'    => "Home Address 2",
236         'home_country'      => "Home Country",
237         'home_zipcode'      => "Home ZipCode",
238         'mobile_number'     => "Mobile Number",
239         'nickname'          => "Nickname",
240         'organization'      => "Organization",
241         'pager_number'      => "Pager Namber",
242         'primary_email'     => "Primary Email",
243         'secondary_email'   => "Secondary Email",
244         'web_page_1'        => "Web Page 1",
245         'web_page_2'        => "Web Page 2",
246         'work_phone'        => "Work Phone",
247         'work_address'      => "Work Address",
248         //'work_address_2'    => "Work Address 2",
38c19a 249         'work_city'         => "Work City",
383379 250         'work_country'      => "Work Country",
38c19a 251         'work_state'        => "Work State",
383379 252         'work_zipcode'      => "Work ZipCode",
609483 253
AM 254         // Atmail
255         'date_of_birth'     => "Date of Birth",
256         'email'             => "Email",
257         //'email_2'         => "Email2",
258         //'email_3'         => "Email3",
259         //'email_4'         => "Email4",
260         //'email_5'         => "Email5",
261         'home_mobile'       => "Home Mobile",
262         'home_zip'          => "Home Zip",
263         'info'              => "Info",
264         'user_photo'        => "User Photo",
265         'url'               => "URL",
266         'work_company'      => "Work Company",
267         'work_dept'         => "Work Dept",
268         'work_fax'          => "Work Fax",
269         'work_mobile'       => "Work Mobile",
270         'work_title'        => "Work Title",
271         'work_zip'          => "Work Zip",
027208 272         'groups'            => "Group",
383379 273     );
AM 274
92bd3a 275     protected $local_label_map = array();
383379 276     protected $vcards = array();
92bd3a 277     protected $map = array();
383379 278
AM 279
280     /**
281      * Class constructor
282      *
283      * @param string $lang File language
284      */
285     public function __construct($lang = 'en_US')
286     {
287         // Localize fields map
288         if ($lang && $lang != 'en_US') {
9be2f4 289             if (file_exists(RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc")) {
TB 290                 include RCUBE_LOCALIZATION_DIR . "$lang/csv2vcard.inc";
383379 291             }
AM 292
293             if (!empty($map)) {
92bd3a 294                 $this->local_label_map = array_merge($this->label_map, $map);
383379 295             }
AM 296         }
297
298         $this->label_map = array_flip($this->label_map);
92bd3a 299         $this->local_label_map = array_flip($this->local_label_map);
383379 300     }
AM 301
302     /**
303      *
304      */
305     public function import($csv)
306     {
307         // convert to UTF-8
308         $head     = substr($csv, 0, 4096);
a92beb 309         $charset  = rcube_charset::detect($head, RCUBE_CHARSET);
383379 310         $csv      = rcube_charset::convert($csv, $charset);
AM 311         $head     = '';
312
313         $this->map = array();
314
315         // Parse file
3725cf 316         foreach (preg_split("/[\r\n]+/", $csv) as $line) {
745d86 317             $elements = $this->parse_line($line);
383379 318             if (empty($elements)) {
AM 319                 continue;
320             }
321
322             // Parse header
323             if (empty($this->map)) {
324                 $this->parse_header($elements);
325                 if (empty($this->map)) {
326                     break;
327                 }
328             }
329             // Parse data row
330             else {
331                 $this->csv_to_vcard($elements);
332             }
333         }
334     }
335
336     /**
337      * @return array rcube_vcard List of vcards
338      */
339     public function export()
340     {
341         return $this->vcards;
342     }
343
344     /**
745d86 345      * Parse CSV file line
AM 346      */
347     protected function parse_line($line)
348     {
349         $line = trim($line);
350         if (empty($line)) {
351             return null;
352         }
353
354         $fields = rcube_utils::explode_quoted_string(',', $line);
355
356         // remove quotes if needed
357         if (!empty($fields)) {
358             foreach ($fields as $idx => $value) {
359                 if (($len = strlen($value)) > 1 && $value[0] == '"' && $value[$len-1] == '"') {
360                     // remove surrounding quotes
361                     $value = substr($value, 1, -1);
362                     // replace doubled quotes inside the string with single quote
363                     $value = str_replace('""', '"', $value);
364
365                     $fields[$idx] = $value;
366                 }
367             }
368         }
369
370         return $fields;
371     }
372
373     /**
383379 374      * Parse CSV header line, detect fields mapping
AM 375      */
376     protected function parse_header($elements)
92bd3a 377     {
AM 378         $map1 = array();
379         $map2 = array();
380         $size = count($elements);
381
382         // check English labels
383         for ($i = 0; $i < $size; $i++) {
383379 384             $label = $this->label_map[$elements[$i]];
AM 385             if ($label && !empty($this->csv2vcard_map[$label])) {
92bd3a 386                 $map1[$i] = $this->csv2vcard_map[$label];
383379 387             }
AM 388         }
92bd3a 389         // check localized labels
AM 390         if (!empty($this->local_label_map)) {
391             for ($i = 0; $i < $size; $i++) {
392                 $label = $this->local_label_map[$elements[$i]];
0b0cae 393
AM 394                 // special localization label
395                 if ($label && $label[0] == '_') {
396                     $label = substr($label, 1);
397                 }
398
92bd3a 399                 if ($label && !empty($this->csv2vcard_map[$label])) {
AM 400                     $map2[$i] = $this->csv2vcard_map[$label];
401                 }
402             }
403         }
404
405         $this->map = count($map1) >= count($map2) ? $map1 : $map2;
383379 406     }
AM 407
408     /**
409      * Convert CSV data row to vCard
410      */
411     protected function csv_to_vcard($data)
412     {
413         $contact = array();
414         foreach ($this->map as $idx => $name) {
415             $value = $data[$idx];
416             if ($value !== null && $value !== '') {
417                 $contact[$name] = $value;
418             }
419         }
420
421         if (empty($contact)) {
422             return;
423         }
424
425         // Handle special values
426         if (!empty($contact['birthday-d']) && !empty($contact['birthday-m']) && !empty($contact['birthday-y'])) {
427             $contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d'];
428         }
429
609483 430         // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
c66b60 431         foreach (array('birthday', 'anniversary') as $key) {
609483 432             if (!empty($contact[$key])) {
AM 433                 $date = preg_replace('/[0[:^word:]]/', '', $contact[$key]);
434                 if (empty($date)) {
435                     unset($contact[$key]);
436                 }
c66b60 437             }
AM 438         }
439
440         if (!empty($contact['gender']) && ($gender = strtolower($contact['gender']))) {
441             if (!in_array($gender, array('male', 'female'))) {
442                 unset($contact['gender']);
443             }
444         }
445
acf851 446         // Convert address(es) to rcube_vcard data
AM 447         foreach ($contact as $idx => $value) {
448             $name = explode(':', $idx);
449             if (in_array($name[0], array('street', 'locality', 'region', 'zipcode', 'country'))) {
450                 $contact['address:'.$name[1]][$name[0]] = $value;
451                 unset($contact[$idx]);
452             }
453         }
454
383379 455         // Create vcard object
AM 456         $vcard = new rcube_vcard();
457         foreach ($contact as $name => $value) {
458             $name = explode(':', $name);
459             $vcard->set($name[0], $value, $name[1]);
460         }
461
462         // add to the list
463         $this->vcards[] = $vcard;
464     }
465 }