b6371cf3d5b1848dc7ccfe19de85827643b5749f
[tine20] / tine20 / Addressbook / Model / Contact.php
1 <?php
2 /**
3  * Tine 2.0
4  * 
5  * @package     Addressbook
6  * @subpackage  Model
7  * @license     http://www.gnu.org/licenses/agpl.html AGPL Version 3
8  * @author      Lars Kneschke <l.kneschke@metaways.de>
9  * @copyright   Copyright (c) 2007-2012 Metaways Infosystems GmbH (http://www.metaways.de)
10  */
11
12 /**
13  * class to hold contact data
14  * 
15  * @package     Addressbook
16  * @subpackage  Model
17  * @property    account_id                 id of associated user
18  * @property    adr_one_countryname        name of the country the contact lives in
19  * @property    adr_one_locality           locality of the contact
20  * @property    adr_one_postalcode         postalcode belonging to the locality
21  * @property    adr_one_region             region the contact lives in
22  * @property    adr_one_street             street where the contact lives
23  * @property    adr_one_street2            street2 where contact lives
24  * @property    adr_two_countryname        second home/country where the contact lives
25  * @property    adr_two_locality           second locality of the contact
26  * @property    adr_two_postalcode         ostalcode belonging to second locality
27  * @property    adr_two_region             second region the contact lives in
28  * @property    adr_two_street             second street where the contact lives
29  * @property    adr_two_street2            second street2 where the contact lives
30  * @property    assistent                  name of the assistent of the contact
31  * @property    bday                       date of birth of contact
32  * @property    container_id               id of container
33  * @property    email                      the email address of the contact
34  * @property    email_home                 the private email address of the contact
35  * @property    jpegphoto                  photo of the contact
36  * @property    n_family                   surname of the contact
37  * @property    n_fileas                   display surname, name
38  * @property    n_fn                       the full name
39  * @property    n_given                    forename of the contact
40  * @property    n_middle                   middle name of the contact
41  * @property    note                       notes of the contact    
42  * @property    n_prefix
43  * @property    n_suffix
44  * @property    org_name                   name of the company the contact works at
45  * @property    org_unit
46  * @property    role                       type of role of the contact  
47  * @property    tel_assistent              phone number of the assistent
48  * @property    tel_car
49  * @property    tel_cell                   mobile phone number
50  * @property    tel_cell_private           private mobile number
51  * @property    tel_fax                    number for calling the fax
52  * @property    tel_fax_home               private fax number
53  * @property    tel_home                   telephone number of contact's home
54  * @property    tel_pager                  contact's pager number
55  * @property    tel_work                   contact's office phone number
56  * @property    title                      special title of the contact
57  * @property    type                       type of contact
58  * @property    url                        url of the contact
59  * @property    url_home                   private url of the contact
60  */
61 class Addressbook_Model_Contact extends Tinebase_Record_Abstract
62 {
63     /**
64      * const to describe contact of current account id independent
65      * 
66      * @var string
67      */
68     const CURRENTCONTACT = 'currentContact';
69     
70     /**
71      * contact type: contact
72      * 
73      * @var string
74      */
75     const CONTACTTYPE_CONTACT = 'contact';
76     
77     /**
78      * contact type: user
79      * 
80      * @var string
81      */
82     const CONTACTTYPE_USER = 'user';
83
84     /**
85      * small contact photo size
86      *
87      * @var integer
88      */
89     const SMALL_PHOTO_SIZE = 36000;
90     
91     /**
92      * key in $_validators/$_properties array for the filed which 
93      * represents the identifier
94      * 
95      * @var string
96      */
97     protected $_identifier = 'id';
98     
99     /**
100      * application the record belongs to
101      *
102      * @var string
103      */
104     protected $_application = 'Addressbook';
105     
106     /**
107      * if foreign Id fields should be resolved on search and get from json
108      * should have this format: 
109      *     array('Calendar_Model_Contact' => 'contact_id', ...)
110      * or for more fields:
111      *     array('Calendar_Model_Contact' => array('contact_id', 'customer_id), ...)
112      * (e.g. resolves contact_id with the corresponding Model)
113      * 
114      * @var array
115      */
116     protected static $_resolveForeignIdFields = array(
117         'Tinebase_Model_User' => array('created_by', 'last_modified_by'),
118         'recursive'           => array('attachments' => 'Tinebase_Model_Tree_Node'),
119     );
120     
121     /**
122      * list of zend inputfilter
123      * 
124      * this filter get used when validating user generated content with Zend_Input_Filter
125      *
126      * @var array
127      */
128     protected $_filters = array(
129         'adr_one_countryname'   => array('StringTrim', 'StringToUpper'),
130         'adr_two_countryname'   => array('StringTrim', 'StringToUpper'),
131         'email'                 => array('StringTrim', 'StringToLower'),
132         'email_home'            => array('StringTrim', 'StringToLower'),
133         'url'                   => array('StringTrim'),
134         'url_home'              => array('StringTrim'),
135     );
136
137     /**
138      * list of zend validator
139      * 
140      * this validators get used when validating user generated content with Zend_Input_Filter
141      *
142      * @var array
143      */
144     protected $_validators = array(
145         'adr_one_countryname'   => array(Zend_Filter_Input::ALLOW_EMPTY => true),
146         'adr_one_locality'      => array(Zend_Filter_Input::ALLOW_EMPTY => true),
147         'adr_one_postalcode'    => array(Zend_Filter_Input::ALLOW_EMPTY => true),
148         'adr_one_region'        => array(Zend_Filter_Input::ALLOW_EMPTY => true),
149         'adr_one_street'        => array(Zend_Filter_Input::ALLOW_EMPTY => true),
150         'adr_one_street2'       => array(Zend_Filter_Input::ALLOW_EMPTY => true),
151         'adr_one_lon'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
152         'adr_one_lat'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
153         'adr_two_countryname'   => array(Zend_Filter_Input::ALLOW_EMPTY => true),
154         'adr_two_locality'      => array(Zend_Filter_Input::ALLOW_EMPTY => true),
155         'adr_two_postalcode'    => array(Zend_Filter_Input::ALLOW_EMPTY => true),
156         'adr_two_region'        => array(Zend_Filter_Input::ALLOW_EMPTY => true),
157         'adr_two_street'        => array(Zend_Filter_Input::ALLOW_EMPTY => true),
158         'adr_two_street2'       => array(Zend_Filter_Input::ALLOW_EMPTY => true),
159         'adr_two_lon'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
160         'adr_two_lat'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
161         'assistent'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
162         'bday'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
163         'calendar_uri'          => array(Zend_Filter_Input::ALLOW_EMPTY => true),
164         'email'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true),
165         'email_home'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
166         'jpegphoto'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
167         'freebusy_uri'          => array(Zend_Filter_Input::ALLOW_EMPTY => true),
168         'id'                    => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
169         'account_id'            => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => NULL),
170         'note'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
171         'container_id'          => array(Zend_Filter_Input::ALLOW_EMPTY => true),
172         'role'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
173         'salutation'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
174         'title'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true),
175         'url'                   => array(Zend_Filter_Input::ALLOW_EMPTY => true),
176         'url_home'              => array(Zend_Filter_Input::ALLOW_EMPTY => true),
177         'n_family'              => array(Zend_Filter_Input::ALLOW_EMPTY => true),
178         'n_fileas'              => array(Zend_Filter_Input::ALLOW_EMPTY => false, 'presence'=>'required'),
179         'n_fn'                  => array(Zend_Filter_Input::ALLOW_EMPTY => false, 'presence'=>'required'),
180         'n_given'               => array(Zend_Filter_Input::ALLOW_EMPTY => true),
181         'n_middle'              => array(Zend_Filter_Input::ALLOW_EMPTY => true),
182         'n_prefix'              => array(Zend_Filter_Input::ALLOW_EMPTY => true),
183         'n_suffix'              => array(Zend_Filter_Input::ALLOW_EMPTY => true),
184         'org_name'              => array(Zend_Filter_Input::ALLOW_EMPTY => true),
185         'org_unit'              => array(Zend_Filter_Input::ALLOW_EMPTY => true),
186         'pubkey'                => array(Zend_Filter_Input::ALLOW_EMPTY => true),
187         'room'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
188         'tel_assistent'         => array(Zend_Filter_Input::ALLOW_EMPTY => true),
189         'tel_car'               => array(Zend_Filter_Input::ALLOW_EMPTY => true),
190         'tel_cell'              => array(Zend_Filter_Input::ALLOW_EMPTY => true),
191         'tel_cell_private'      => array(Zend_Filter_Input::ALLOW_EMPTY => true),
192         'tel_fax'               => array(Zend_Filter_Input::ALLOW_EMPTY => true),
193         'tel_fax_home'          => array(Zend_Filter_Input::ALLOW_EMPTY => true),
194         'tel_home'              => array(Zend_Filter_Input::ALLOW_EMPTY => true),
195         'tel_pager'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
196         'tel_work'              => array(Zend_Filter_Input::ALLOW_EMPTY => true),
197         'tel_other'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
198         'tel_prefer'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
199         'tz'                    => array(Zend_Filter_Input::ALLOW_EMPTY => true),
200         'geo'                   => array(Zend_Filter_Input::ALLOW_EMPTY => true),
201     // modlog fields
202         'created_by'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
203         'creation_time'         => array(Zend_Filter_Input::ALLOW_EMPTY => true),
204         'last_modified_by'      => array(Zend_Filter_Input::ALLOW_EMPTY => true),
205         'last_modified_time'    => array(Zend_Filter_Input::ALLOW_EMPTY => true),
206         'is_deleted'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
207         'deleted_time'          => array(Zend_Filter_Input::ALLOW_EMPTY => true),
208         'deleted_by'            => array(Zend_Filter_Input::ALLOW_EMPTY => true),
209         'seq'                   => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 0),
210     // tine 2.0 generic fields
211         'tags'                  => array(Zend_Filter_Input::ALLOW_EMPTY => true),
212         'notes'                 => array(Zend_Filter_Input::ALLOW_EMPTY => true),
213         'relations'             => array(Zend_Filter_Input::ALLOW_EMPTY => true),
214         'attachments'           => array(Zend_Filter_Input::ALLOW_EMPTY => true),
215         'customfields'          => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => array()),
216         'type'                  => array(
217             Zend_Filter_Input::ALLOW_EMPTY => true,
218             Zend_Filter_Input::DEFAULT_VALUE => self::CONTACTTYPE_CONTACT,
219             array('InArray', array(self::CONTACTTYPE_USER, self::CONTACTTYPE_CONTACT)),
220         )
221     );
222     
223     /**
224      * name of fields containing datetime or or an array of datetime information
225      *
226      * @var array list of datetime fields
227      */
228     protected $_datetimeFields = array(
229         'bday',
230         'creation_time',
231         'last_modified_time',
232         'deleted_time'
233     );
234     
235     /**
236     * name of fields that should be omited from modlog
237     *
238     * @var array list of modlog omit fields
239     */
240     protected $_modlogOmitFields = array(
241         'jpegphoto',
242     );
243     
244     /**
245     * overwrite constructor to add more filters
246     *
247     * @param mixed $_data
248     * @param bool $_bypassFilters
249     * @param mixed $_convertDates
250     * @return void
251     */
252     public function __construct($_data = NULL, $_bypassFilters = false, $_convertDates = true)
253     {
254         // set geofields to NULL if empty
255         $geoFields = array('adr_one_lon', 'adr_one_lat', 'adr_two_lon', 'adr_two_lat');
256         foreach ($geoFields as $geoField) {
257             $this->_filters[$geoField]        = new Zend_Filter_Empty(NULL);
258         }
259     
260         return parent::__construct($_data, $_bypassFilters, $_convertDates);
261     }
262     
263     /**
264      * returns prefered email address of given contact
265      * 
266      * @return string
267      */
268     public function getPreferedEmailAddress()
269     {
270         // prefer work mail over private mail till we have prefs for this
271         return $this->email ? $this->email : $this->email_home;
272     }
273     
274     /**
275      * (non-PHPdoc)
276      * @see Tinebase/Record/Tinebase_Record_Abstract#setFromArray($_data)
277      */
278     public function setFromArray(array $_data)
279     {
280         $_data = $this->_resolveAutoValues($_data);
281         parent::setFromArray($_data);
282     }
283     
284     /**
285      * Resolves the auto values n_fn and n_fileas
286      * @param array $_data
287      * @return array $_data
288      */
289     protected function _resolveAutoValues(array $_data) {
290         if (! (isset($_data['org_name']) || array_key_exists('org_name', $_data))) {
291             $_data['org_name'] = '';
292         }
293         
294         // always update fileas and fn
295         $_data['n_fileas'] = (!empty($_data['n_family'])) ? $_data['n_family']
296             : ((! empty($_data['org_name'])) ? $_data['org_name']
297             : ((isset($_data['n_fileas'])) ? $_data['n_fileas'] : ''));
298         
299         if (!empty($_data['n_given'])) {
300             $_data['n_fileas'] .= ', ' . $_data['n_given'];
301             
302             // to change n_fileas to "ngiven nfamily" use this line instead of the above
303             //$_data['n_fileas'] = $_data['n_given'] . ' ' . $_data['n_fileas'];
304         }
305         
306         $_data['n_fn'] = (!empty($_data['n_family'])) ? $_data['n_family']
307             : ((! empty($_data['org_name'])) ? $_data['org_name']
308             : ((isset($_data['n_fn'])) ? $_data['n_fn'] : ''));
309         
310         if (!empty($_data['n_given'])) {
311             $_data['n_fn'] = $_data['n_given'] . ' ' . $_data['n_fn'];
312         }
313         return $_data;
314     }
315     
316     /**
317      * Overwrites the __set Method from Tinebase_Record_Abstract
318      * Also sets n_fn and n_fileas when org_name, n_given or n_family should be set
319      * @see Tinebase_Record_Abstract::__set()
320      */
321     public function __set($_name, $_value) {
322         
323         switch ($_name) {
324             case 'n_given':
325                 $resolved = $this->_resolveAutoValues(array('n_given' => $_value, 'n_family' => $this->__get('n_family'), 'org_name' => $this->__get('org_name')));
326                 parent::__set('n_fn', $resolved['n_fn']);
327                 parent::__set('n_fileas', $resolved['n_fileas']);
328                 break;
329             case 'n_family':
330                 $resolved = $this->_resolveAutoValues(array('n_family' => $_value, 'n_given' => $this->__get('n_given'), 'org_name' => $this->__get('org_name')));
331                 parent::__set('n_fn', $resolved['n_fn']);
332                 parent::__set('n_fileas', $resolved['n_fileas']);
333                 break;
334             case 'org_name':
335                 $resolved = $this->_resolveAutoValues(array('org_name' => $_value, 'n_given' => $this->__get('n_given'), 'n_family' => $this->__get('n_family')));
336                 parent::__set('n_fn', $resolved['n_fn']);
337                 parent::__set('n_fileas', $resolved['n_fileas']);
338         }
339         
340         parent::__set($_name, $_value);
341     }
342
343     /**
344      * additional validation
345      *
346      * @param $_throwExceptionOnInvalidData
347      * @return bool
348      * @throws Tinebase_Exception_Record_Validation
349      * @see Tinebase_Record_Abstract::isValid()
350      */
351     function isValid($_throwExceptionOnInvalidData = false) {
352         
353         if ((!$this->__get('org_name')) && (!$this->__get('n_family'))) {
354             array_push($this->_validationErrors, array('id' => 'org_name', 'msg' => 'either "org_name" or "n_family" must be given!'));
355             array_push($this->_validationErrors, array('id' => 'n_family', 'msg' => 'either "org_name" or "n_family" must be given!'));
356             
357             $valid = false;
358         } else {
359             $valid = true;
360         }
361         
362         $parentException = false;
363         $parentValid = false;
364         
365         try {
366             $parentValid = parent::isValid($_throwExceptionOnInvalidData);
367         } catch (Tinebase_Exception_Record_Validation $e) {
368             $parentException = $e;
369         }
370         
371         if ($_throwExceptionOnInvalidData && (!$valid || !$parentValid)) {
372             
373             if(!$valid) {
374                 $message = 'either "org_name" or "n_family" must be given!';
375             }
376             
377             if($parentException) $message .= ', ' . $parentException->getMessage();
378             $e = new Tinebase_Exception_Record_Validation($message);
379             if(!$valid) Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__ . ":\n" . print_r($this->_validationErrors,true). $e);
380             throw $e;
381         }
382         
383         return $parentValid && $valid;
384     }    
385
386     /**
387      * fills a contact from json data
388      *
389      * @param array $_data record data
390      * @return void
391      * 
392      * @todo timezone conversion for birthdays?
393      * @todo move this to Addressbook_Convert_Contact_Json
394      */
395     protected function _setFromJson(array &$_data)
396     {
397         $this->_setContactImage($_data);
398         
399         // unset if empty
400         // @todo is this still needed?
401         if (empty($_data['id'])) {
402             unset($_data['id']);
403         }
404     }
405     
406     /**
407      * set contact image
408      * 
409      * @param array $_data
410      */
411     protected function _setContactImage(&$_data)
412     {
413         if (! isset($_data['jpegphoto']) || $_data['jpegphoto'] === '') {
414             return;
415         }
416         
417         $imageParams = Tinebase_ImageHelper::parseImageLink($_data['jpegphoto']);
418         if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' image params:' . print_r($imageParams, TRUE));
419         if ($imageParams['isNewImage']) {
420             try {
421                 $_data['jpegphoto'] = Tinebase_ImageHelper::getImageData($imageParams);
422             } catch(Tinebase_Exception_UnexpectedValue $teuv) {
423                 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not add contact image: ' . $teuv->getMessage());
424                 unset($_data['jpegphoto']);
425             }
426         } else {
427             unset($_data['jpegphoto']);
428         }
429     }
430
431     /**
432      * set small contact image
433      *
434      * @param $newPhotoBlob
435      */
436     public function setSmallContactImage($newPhotoBlob)
437     {
438         if ($this->getId()) {
439             try {
440                 $currentPhoto = Tinebase_Controller::getInstance()->getImage('Addressbook', $this->getId())->getBlob('image/jpeg', self::SMALL_PHOTO_SIZE);
441             } catch (Exception $e) {
442                 // no current photo
443             }
444         }
445
446         if (isset($currentPhoto) && $currentPhoto == $newPhotoBlob) {
447             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->INFO(__METHOD__ . '::' . __LINE__
448                 . " Photo did not change -> preserving current photo");
449         } else {
450             if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->INFO(__METHOD__ . '::' . __LINE__
451                 . " Setting new contact photo (" . strlen($newPhotoBlob) . "KB)");
452             $this->jpegphoto = $newPhotoBlob;
453         }
454     }
455
456     /**
457      * return small contact image for sync
458      *
459      * @return string
460      * @throws Tinebase_Exception_InvalidArgument
461      * @throws Tinebase_Exception_NotFound
462      */
463     public function getSmallContactImage()
464     {
465         $image = Tinebase_Controller::getInstance()->getImage('Addressbook', $this->getId());
466         return $image->getBlob('image/jpeg', self::SMALL_PHOTO_SIZE);
467     }
468 }