n_given might be missing from VCARD, too
[tine20] / tine20 / Addressbook / Convert / Contact / VCard / Abstract.php
1 <?php
2 /**
3  * Tine 2.0
4  *
5  * @package     Addressbook
6  * @subpackage  Convert
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  * @copyright   Copyright (c) 2011-2013 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * abstract class to convert a single contact (repeating with exceptions) to/from VCARD
14  *
15  * @package     Addressbook
16  * @subpackage  Convert
17  */
18 abstract class Addressbook_Convert_Contact_VCard_Abstract implements Tinebase_Convert_Interface
19 {
20     /**
21      * use servers modlogProperties instead of given DTSTAMP & SEQUENCE
22      * use this if the concurrency checks are done differntly like in CardDAV 
23      * where the etag is checked
24      */
25     const OPTION_USE_SERVER_MODLOG = 'useServerModlog';
26     
27     /**
28      * the version string
29      * 
30      * @var string
31      */
32     protected $_version;
33     
34     /**
35      * @param  string  $_version  the version of the client
36      */
37     public function __construct($_version = null)
38     {
39         $this->_version = $_version;
40     }
41     
42     /**
43      * returns VObject of input data
44      * 
45      * @param   mixed  $blob
46      * @return  \Sabre\VObject\Component\VCard
47      */
48     public static function getVObject($blob)
49     {
50         if ($blob instanceof \Sabre\VObject\Component\VCard) {
51             return $blob;
52         }
53         
54         if (is_resource($blob)) {
55             $blob = stream_get_contents($blob);
56         }
57         
58         return \Sabre\VObject\Reader::read($blob);
59     }
60  
61     /**
62      * converts vcard to Addressbook_Model_Contact
63      * 
64      * @param  \Sabre\VObject\Component|stream|string  $blob       the vcard to parse
65      * @param  Tinebase_Record_Abstract                $_record    update existing contact
66      * @param  array                                   $options    array of options
67      * @return Addressbook_Model_Contact
68      */
69     public function toTine20Model($blob, Tinebase_Record_Abstract $_record = null, $options = array())
70     {
71         $vcard = self::getVObject($blob);
72         
73         if ($_record instanceof Addressbook_Model_Contact) {
74             $contact = $_record;
75         } else {
76             $contact = new Addressbook_Model_Contact(null, false);
77         }
78         
79         $data = $this->_emptyArray;
80         
81         foreach ($vcard->children() as $property) {
82             switch ($property->name) {
83                 case 'VERSION':
84                 case 'PRODID':
85                 case 'UID':
86                     // do nothing
87                     break;
88                     
89                 case 'ADR':
90                     $type = null;
91                     
92                     foreach ($property['TYPE'] as $typeProperty) {
93                         $typeProperty = strtolower($typeProperty);
94                         
95                         if (in_array($typeProperty, array('home','work'))) {
96                             $type = $typeProperty;
97                             break;
98                         }
99                     }
100                     
101                     $parts = $property->getParts();
102                     
103                     if ($type == 'home') {
104                         // home address
105                         $data['adr_two_street2']     = $parts[1];
106                         $data['adr_two_street']      = $parts[2];
107                         $data['adr_two_locality']    = $parts[3];
108                         $data['adr_two_region']      = $parts[4];
109                         $data['adr_two_postalcode']  = $parts[5];
110                         $data['adr_two_countryname'] = $parts[6];
111                     } elseif ($type == 'work') {
112                         // work address
113                         $data['adr_one_street2']     = $parts[1];
114                         $data['adr_one_street']      = $parts[2];
115                         $data['adr_one_locality']    = $parts[3];
116                         $data['adr_one_region']      = $parts[4];
117                         $data['adr_one_postalcode']  = $parts[5];
118                         $data['adr_one_countryname'] = $parts[6];
119                     }
120                     break;
121                     
122                 case 'CATEGORIES':
123                     $tags = Tinebase_Model_Tag::resolveTagNameToTag($property->getParts(), 'Addressbook');
124                     if (! isset($data['tags'])) {
125                         $data['tags'] = $tags;
126                     } else {
127                         $data['tags']->merge($tags);
128                     }
129                     break;
130                     
131                 case 'EMAIL':
132                     $this->_toTine20ModelParseEmail($data, $property, $vcard);
133                     break;
134                     
135                 case 'FN':
136                     $data['n_fn'] = $property->getValue();
137                     break;
138                     
139                 case 'N':
140                     $parts = $property->getParts();
141                     
142                     $data['n_family'] = $parts[0];
143                     $data['n_given']  = isset($parts[1]) ? $parts[1] : null;
144                     $data['n_middle'] = isset($parts[2]) ? $parts[2] : null;
145                     $data['n_prefix'] = isset($parts[3]) ? $parts[3] : null;
146                     $data['n_suffix'] = isset($parts[4]) ? $parts[4] : null;
147                     break;
148                     
149                 case 'NOTE':
150                     $data['note'] = $property->getValue();
151                     break;
152                     
153                 case 'ORG':
154                     $parts = $property->getParts();
155                     
156                     $data['org_name'] = $parts[0];
157                     $data['org_unit'] = isset($parts[1]) ? $parts[1] : null;
158                     break;
159                     
160                 case 'PHOTO':
161                     $jpegphoto = $property->getValue();
162                     break;
163                     
164                 case 'TEL':
165                     $this->_toTine20ModelParseTel($data, $property);
166                     break;
167                     
168                 case 'URL':
169                     switch (strtoupper($property['TYPE'])) {
170                         case 'HOME':
171                             $data['url_home'] = $property->getValue();
172                             break;
173                             
174                         case 'WORK':
175                         default:
176                             $data['url'] = $property->getValue();
177                             break;
178                     }
179                     break;
180                     
181                 case 'TITLE':
182                     $data['title'] = $property->getValue();
183                     break;
184
185                 case 'BDAY':
186                     $this->_toTine20ModelParseBday($data, $property);
187                     break;
188                     
189                 default:
190                     if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
191                         Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' cardData ' . $property->name);
192                     break;
193             }
194         }
195         
196         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
197             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' data ' . print_r($data, true));
198         
199         // Some email clients will only set a contact with FN (formatted name) without surname
200         if (empty($data['n_family']) && empty($data['org_name']) && (!empty($data['n_fn']))) {
201             if (strpos($data['n_fn'], ",") > 0) {
202                 list($lastname, $firstname) = explode(",", $data['n_fn'], 2);
203                 $data['n_family'] = trim($lastname);
204                 $data['n_given']  = trim($firstname);
205                 
206             } elseif (strpos($data['n_fn'], " ") > 0) {
207                 list($firstname, $lastname) = explode(" ", $data['n_fn'], 2);
208                 $data['n_family'] = trim($lastname);
209                 $data['n_given']  = trim($firstname);
210                 
211             } else {
212                 $data['n_family'] = empty($data['n_fn']) ? 'VCARD (imported)' : $data['n_fn'];
213             }
214         }
215         
216         $contact->setFromArray($data);
217
218         if (isset($jpegphoto)) {
219             $contact->setSmallContactImage($jpegphoto);
220         }
221         
222         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
223             Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' data ' . print_r($contact->toArray(), true));
224         
225         if (isset($options[self::OPTION_USE_SERVER_MODLOG]) && $options[self::OPTION_USE_SERVER_MODLOG] === true) {
226             $contact->creation_time = $_record->creation_time;
227             $contact->last_modified_time = $_record->last_modified_time;
228             $contact->seq = $_record->seq;
229         }
230         
231         return $contact;
232     }
233
234     /**
235      * converts Tinebase_Record_Abstract to external format
236      *
237      * @param  Tinebase_Record_Abstract  $record
238      * @return mixed
239      */ 
240     public function fromTine20Model(Tinebase_Record_Abstract $record)
241     {
242     }
243     
244     /**
245      * parse telephone
246      * 
247      * @param array $data
248      * @param \Sabre\VObject\Property $property
249      */
250     protected function _toTine20ModelParseTel(&$data, \Sabre\VObject\Property $property)
251     {
252         $telField = null;
253         
254         if (isset($property['TYPE'])) {
255             // comvert all TYPE's to lowercase and ignore voice and pref
256             $property['TYPE']->setParts(array_diff(
257                 array_map('strtolower', $property['TYPE']->getParts()), 
258                 array('voice', 'pref')
259             ));
260             
261             // CELL
262             if ($property['TYPE']->has('cell')) {
263                 if (count($property['TYPE']->getParts()) == 1 || $property['TYPE']->has('work')) {
264                     $telField = 'tel_cell';
265                 } elseif ($property['TYPE']->has('home')) {
266                     $telField = 'tel_cell_private';
267                 }
268                 
269             // PAGER
270             } elseif ($property['TYPE']->has('pager')) {
271                 $telField = 'tel_pager';
272                 
273             // FAX
274             } elseif ($property['TYPE']->has('fax')) {
275                 if (count($property['TYPE']->getParts()) == 1 || $property['TYPE']->has('work')) {
276                     $telField = 'tel_fax';
277                 } elseif ($property['TYPE']->has('home')) {
278                     $telField = 'tel_fax_home';
279                 }
280                 
281             // HOME
282             } elseif ($property['TYPE']->has('home')) {
283                 $telField = 'tel_home';
284                 
285             // WORK
286             } elseif ($property['TYPE']->has('work')) {
287                 $telField = 'tel_work';
288             }
289         } else {
290             $telField = 'work';
291         }
292         
293         if (!empty($telField)) {
294             $data[$telField] = $property->getValue();
295         }
296     }
297     
298     /**
299      * parse email address field
300      *
301      * @param  array                           $data      reference to tine20 data array
302      * @param  \Sabre\VObject\Property         $property  mail property
303      * @param  \Sabre\VObject\Component\VCard  $vcard     vcard object
304      */
305     protected function _toTine20ModelParseEmail(&$data, \Sabre\VObject\Property $property, \Sabre\VObject\Component\VCard $vcard)
306     {
307         $type = null;
308         
309         foreach ($property['TYPE'] as $typeProperty) {
310             if (strtolower($typeProperty) == 'home' || strtolower($typeProperty) == 'work') {
311                 $type = strtolower($typeProperty);
312                 break;
313             } elseif (strtolower($typeProperty) == 'internet') {
314                 $type = strtolower($typeProperty);
315             }
316         }
317         
318         switch ($type) {
319             case 'internet':
320                 if (empty($data['email'])) {
321                     // do not replace existing value
322                     $data['email'] = $property->getValue();
323                 }
324                 break;
325             
326             case 'home':
327                 $data['email_home'] = $property->getValue();
328                 break;
329             
330             case 'work':
331                 $data['email'] = $property->getValue();
332                 break;
333         }
334     }
335     
336     /**
337      * parse BIRTHDAY
338      * 
339      * @param array                    $data
340      * @param \Sabre\VObject\Property  $property
341      */
342     protected function _toTine20ModelParseBday(&$data, \Sabre\VObject\Property $property)
343     {
344         $tzone = new DateTimeZone(Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
345         $data['bday'] = new Tinebase_DateTime($property->getValue(), $tzone);
346         $data['bday']->setTimezone(new DateTimeZone('UTC'));
347     }
348     
349     /**
350      * add GEO data to VCard
351      * 
352      * @param  Tinebase_Record_Abstract  $record
353      * @param  \Sabre\VObject\Component  $card
354      */
355     protected function _fromTine20ModelAddGeoData(Tinebase_Record_Abstract $record, \Sabre\VObject\Component $card)
356     {
357         if ($record->adr_one_lat && $record->adr_one_lon) {
358             $card->add('GEO', array($record->adr_one_lat, $record->adr_one_lon));
359             
360         } elseif ($record->adr_two_lat && $record->adr_two_lon) {
361             $card->add('GEO', array($record->adr_two_lat, $record->adr_two_lon));
362         }
363     }
364     
365     /**
366      * add birthday data to VCard
367      * 
368      * @param  Tinebase_Record_Abstract  $record
369      * @param  \Sabre\VObject\Component  $card
370      */
371     protected function _fromTine20ModelAddBirthday(Tinebase_Record_Abstract $record, \Sabre\VObject\Component $card)
372     {
373         if ($record->bday instanceof Tinebase_DateTime) {
374             $date = clone $record->bday;
375             $date->setTimezone(Tinebase_Core::get(Tinebase_Core::USERTIMEZONE));
376             $date = $date->format('Y-m-d');
377             $card->add('BDAY', $date);
378         }
379     }
380     
381     /**
382      * parse categories from Tine20 model to VCard and attach it to VCard $card
383      *
384      * @param Tinebase_Record_Abstract &$_record
385      * @param Sabre\VObject\Component $card
386      */
387         protected function _fromTine20ModelAddCategories(Tinebase_Record_Abstract &$record, Sabre\VObject\Component $card)
388         {
389             if (!isset($record->tags)) {
390                 // If the record has not been populated yet with tags, let's try to get them all and update the record
391                 $record->tags = Tinebase_Tags::getInstance()->getTagsOfRecord($record);
392             }
393             if (isset($record->tags) && count($record->tags) > 0) {
394                 // we have some tags attached, so let's convert them and attach to the VCARD
395                 $card->add('CATEGORIES', (array) $record->tags->name);
396             }
397         }
398         
399     /**
400      * add photo data to VCard
401      * 
402      * @param  Addressbook_Model_Contact $record
403      * @param  \Sabre\VObject\Component  $card
404      */
405     protected function _fromTine20ModelAddPhoto(Addressbook_Model_Contact $record, \Sabre\VObject\Component $card)
406     {
407         if (! empty($record->jpegphoto)) {Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__);
408             try {
409                 $jpegData = $record->getSmallContactImage();
410                 $card->add('PHOTO',  $jpegData, array('TYPE' => 'JPEG', 'ENCODING' => 'b'));
411             } catch (Exception $e) {
412                 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) 
413                     Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . " Image for contact {$record->getId()} not found or invalid: {$e->getMessage()}");
414                 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) 
415                     Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . $e->getTraceAsString());
416             }
417         }
418     }
419     
420     /**
421      * initialize vcard object
422      * 
423      * @param  Tinebase_Record_Abstract  $record
424      * @return \Sabre\VObject\Component
425      */
426     protected function _fromTine20ModelRequiredFields(Tinebase_Record_Abstract $record)
427     {
428         $version = Tinebase_Application::getInstance()->getApplicationByName('Addressbook')->version;
429         
430         $card = new \Sabre\VObject\Component\VCard(array(
431             'VERSION' => '3.0',
432             'FN'      => $record->n_fileas,
433             'N'       => array($record->n_family, $record->n_given, $record->n_middle, $record->n_prefix, $record->n_suffix),
434             'PRODID'  => "-//tine20.com//Tine 2.0 Addressbook V$version//EN",
435             'UID'     => $record->getId(),
436             'ORG'     => array($record->org_name, $record->org_unit),
437             'TITLE'   => $record->title
438         ));
439         
440         return $card;
441     }
442 }