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-2016 Metaways Infosystems GmbH (http://www.metaways.de)
13 * class to hold contact data
15 * @package Addressbook
17 * @property string account_id id of associated user
18 * @property string adr_one_countryname name of the country the contact lives in
19 * @property string adr_one_locality locality of the contact
20 * @property string adr_one_postalcode postalcode belonging to the locality
21 * @property string adr_one_region region the contact lives in
22 * @property string adr_one_street street where the contact lives
23 * @property string adr_one_street2 street2 where contact lives
24 * @property string adr_two_countryname second home/country where the contact lives
25 * @property string adr_two_locality second locality of the contact
26 * @property string adr_two_postalcode ostalcode belonging to second locality
27 * @property string adr_two_region second region the contact lives in
28 * @property string adr_two_street second street where the contact lives
29 * @property string adr_two_street2 second street2 where the contact lives
30 * @property string assistent name of the assistent of the contact
31 * @property datetime bday date of birth of contact
32 * @property integer container_id id of container
33 * @property string email the email address of the contact
34 * @property string email_home the private email address of the contact
35 * @property blob jpegphoto photo of the contact
36 * @property string n_family surname of the contact
37 * @property string n_fileas display surname, name
38 * @property string n_fn the full name
39 * @property string n_given forename of the contact
40 * @property string n_middle middle name of the contact
41 * @property string note notes of the contact
42 * @property string n_prefix
43 * @property string n_suffix
44 * @property string org_name name of the company the contact works at
45 * @property string org_unit
46 * @property string role type of role of the contact
47 * @property string tel_assistent phone number of the assistent
48 * @property string tel_car
49 * @property string tel_cell mobile phone number
50 * @property string tel_cell_private private mobile number
51 * @property string tel_fax number for calling the fax
52 * @property string tel_fax_home private fax number
53 * @property string tel_home telephone number of contact's home
54 * @property string tel_pager contact's pager number
55 * @property string tel_work contact's office phone number
56 * @property string title special title of the contact
57 * @property string type type of contact
58 * @property string url url of the contact
59 * @property string url_home private url of the contact
61 class Addressbook_Model_Contact extends Tinebase_Record_Abstract
64 * const to describe contact of current account id independent
68 const CURRENTCONTACT = 'currentContact';
71 * contact type: contact
75 const CONTACTTYPE_CONTACT = 'contact';
82 const CONTACTTYPE_USER = 'user';
85 * small contact photo size
89 const SMALL_PHOTO_SIZE = 36000;
92 * key in $_validators/$_properties array for the filed which
93 * represents the identifier
97 protected $_identifier = 'id';
100 * application the record belongs to
104 protected $_application = 'Addressbook';
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)
116 protected static $_resolveForeignIdFields = array(
117 'Tinebase_Model_User' => array('created_by', 'last_modified_by'),
118 'recursive' => array('attachments' => 'Tinebase_Model_Tree_Node'),
122 * list of zend inputfilter
124 * this filter get used when validating user generated content with Zend_Input_Filter
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'),
138 * list of zend validator
140 * this validators get used when validating user generated content with Zend_Input_Filter
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 => true),
179 'n_fn' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
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 'tel_assistent_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
200 'tel_car_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
201 'tel_cell_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
202 'tel_cell_private_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
203 'tel_fax_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
204 'tel_fax_home_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
205 'tel_home_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
206 'tel_pager_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
207 'tel_work_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
208 'tel_other_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
209 'tel_prefer_normalized' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
210 'tz' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
211 'geo' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
213 'created_by' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
214 'creation_time' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
215 'last_modified_by' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
216 'last_modified_time' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
217 'is_deleted' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
218 'deleted_time' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
219 'deleted_by' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
220 'seq' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 0),
221 // tine 2.0 generic fields
222 'tags' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
223 'notes' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
224 'relations' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
225 'attachments' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
226 'customfields' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => array()),
228 Zend_Filter_Input::ALLOW_EMPTY => true,
229 Zend_Filter_Input::DEFAULT_VALUE => self::CONTACTTYPE_CONTACT,
230 array('InArray', array(self::CONTACTTYPE_USER, self::CONTACTTYPE_CONTACT)),
232 'paths' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
236 * name of fields containing datetime or or an array of datetime information
238 * @var array list of datetime fields
240 protected $_datetimeFields = array(
243 'last_modified_time',
248 * name of fields that should be omited from modlog
250 * @var array list of modlog omit fields
252 protected $_modlogOmitFields = array(
257 * list of telephone country codes
259 * source of country codes:
260 * $json = json_decode(file_get_contents('https://raw.github.com/mledoze/countries/master/countries.json'));
261 * foreach($json as $val) { foreach($val->callingCode as $cc) $data['+'.$cc] = true;}
263 * echo 'array(\'' . join('\',\'', array_keys($data)) . '\');';
265 * @var array list of telephone country codes
267 protected static $countryCodes = array('+1','+7','+20','+27','+30','+31','+32','+33','+34','+36','+39','+40','+41','+43','+44','+45','+46','+47','+48','+49','+51','+52','+53','+54','+55','+56','+57','+58','+60','+61','+62','+63','+64','+65','+66','+76','+77','+81','+82','+84','+86','+90','+91','+92','+93','+94','+95','+98','+211','+212','+213','+216','+218','+220','+221','+222','+223','+224','+225','+226','+227','+228','+229','+230','+231','+232','+233','+234','+235','+236','+237','+238','+239','+240','+241','+242','+243','+244','+245','+246','+248','+249','+250','+251','+252','+253','+254','+255','+256','+257','+258','+260','+261','+262','+263','+264','+265','+266','+267','+268','+269','+291','+297','+298','+299','+350','+351','+352','+353','+354','+355','+356','+357','+358','+359','+370','+371','+372','+373','+374','+375','+376','+377','+378','+379','+380','+381','+382','+383','+385','+386','+387','+389','+420','+421','+423','+500','+501','+502','+503','+504','+505','+506','+507','+508','+509','+590','+591','+592','+593','+594','+595','+596','+597','+598','+670','+672','+673','+674','+675','+676','+677','+678','+679','+680','+681','+682','+683','+685','+686','+687','+688','+689','+690','+691','+692','+850','+852','+853','+855','+856','+880','+886','+960','+961','+962','+963','+964','+965','+966','+967','+968','+970','+971','+972','+973','+974','+975','+976','+977','+992','+993','+994','+995','+996','+998','+1242','+1246','+1264','+1268','+1284','+1340','+1345','+1441','+1473','+1649','+1664','+1670','+1671','+1684','+1721','+1758','+1767','+1784','+1787','+1809','+1829','+1849','+1868','+1869','+1876','+1939','+4779','+5999','+3906698');
270 * overwrite constructor to add more filters
272 * @param mixed $_data
273 * @param bool $_bypassFilters
274 * @param mixed $_convertDates
276 public function __construct($_data = NULL, $_bypassFilters = false, $_convertDates = true)
278 // set geofields to NULL if empty
279 $geoFields = array('adr_one_lon', 'adr_one_lat', 'adr_two_lon', 'adr_two_lat');
280 foreach ($geoFields as $geoField) {
281 $this->_filters[$geoField] = new Zend_Filter_Empty(NULL);
284 parent::__construct($_data, $_bypassFilters, $_convertDates);
288 * returns prefered email address of given contact
292 public function getPreferedEmailAddress()
294 // prefer work mail over private mail till we have prefs for this
295 return $this->email ? $this->email : $this->email_home;
300 * @see Tinebase/Record/Tinebase_Record_Abstract#setFromArray($_data)
302 public function setFromArray(array $_data)
304 $_data = $this->_resolveAutoValues($_data);
305 parent::setFromArray($_data);
309 * Resolves the auto values n_fn and n_fileas
310 * @param array $_data
311 * @return array $_data
313 protected function _resolveAutoValues(array $_data) {
314 if (! (isset($_data['org_name']) || array_key_exists('org_name', $_data))) {
315 $_data['org_name'] = '';
318 // try to guess name from n_fileas
320 if (empty($_data['org_name']) && empty($_data['n_family'])) {
321 if (! empty($_data['n_fileas'])) {
322 $names = preg_split('/\s*,\s*/', $_data['n_fileas']);
323 $_data['n_family'] = $names[0];
324 if (empty($_data['n_given'])&& isset($names[1])) {
325 $_data['n_given'] = $names[1];
330 // always update fileas and fn
331 $_data['n_fileas'] = (!empty($_data['n_family'])) ? $_data['n_family']
332 : ((! empty($_data['org_name'])) ? $_data['org_name']
333 : ((isset($_data['n_fileas'])) ? $_data['n_fileas'] : ''));
335 if (!empty($_data['n_given'])) {
336 $_data['n_fileas'] .= ', ' . $_data['n_given'];
339 $_data['n_fn'] = (!empty($_data['n_family'])) ? $_data['n_family']
340 : ((! empty($_data['org_name'])) ? $_data['org_name']
341 : ((isset($_data['n_fn'])) ? $_data['n_fn'] : ''));
343 if (!empty($_data['n_given'])) {
344 $_data['n_fn'] = $_data['n_given'] . ' ' . $_data['n_fn'];
350 * Overwrites the __set Method from Tinebase_Record_Abstract
351 * Also sets n_fn and n_fileas when org_name, n_given or n_family should be set
352 * @see Tinebase_Record_Abstract::__set()
354 public function __set($_name, $_value) {
358 $resolved = $this->_resolveAutoValues(array('n_given' => $_value, 'n_family' => $this->__get('n_family'), 'org_name' => $this->__get('org_name')));
359 parent::__set('n_fn', $resolved['n_fn']);
360 parent::__set('n_fileas', $resolved['n_fileas']);
363 $resolved = $this->_resolveAutoValues(array('n_family' => $_value, 'n_given' => $this->__get('n_given'), 'org_name' => $this->__get('org_name')));
364 parent::__set('n_fn', $resolved['n_fn']);
365 parent::__set('n_fileas', $resolved['n_fileas']);
368 $resolved = $this->_resolveAutoValues(array('org_name' => $_value, 'n_given' => $this->__get('n_given'), 'n_family' => $this->__get('n_family')));
369 parent::__set('n_fn', $resolved['n_fn']);
370 parent::__set('n_fileas', $resolved['n_fileas']);
373 // normalize telephone numbers
374 if (!empty($_value) && strpos($_name, 'tel_') === 0 && strpos($_name, '_normalized') === false) {
375 parent::__set($_name . '_normalized', static::normalizeTelephoneNoCountry($_value));
380 parent::__set($_name, $_value);
384 * normalizes telephone numbers and removes country part
385 * result will be of format 0xxxxxxxxx (only digits)
387 * @param string $telNumber
388 * @return string|null
390 public static function normalizeTelephoneNoCountry($telNumber)
392 $val = trim($telNumber);
394 // replace leading + with 00
395 if ($val[0] === '+') {
396 $val = '00' . mb_substr($val, 1);
399 // remove any non digit characters
400 $val = preg_replace('/\D+/u', '', $val);
402 // if not at least 5 digits, stop where
403 if (strlen($val) < 5)
407 if ($val[0] === '0' && $val[1] === '0') {
408 $val = '+' . mb_substr($val, 2);
411 // normalize to remove leading country codes and make the number start with 0
412 if ($val[0] === '+') {
413 $val = str_replace(static::$countryCodes, '0', $val);
414 } elseif($val[0] !== '0') {
418 // in case the country codes was not recognized...
419 if ($val[0] === '+') {
420 $val = '0' . mb_substr($val, 1);
427 * fills a contact from json data
429 * @param array $_data record data
432 * @todo timezone conversion for birthdays?
433 * @todo move this to Addressbook_Convert_Contact_Json
435 protected function _setFromJson(array &$_data)
437 $this->_setContactImage($_data);
440 // @todo is this still needed?
441 if (empty($_data['id'])) {
449 * @param array $_data
451 protected function _setContactImage(&$_data)
453 if (! isset($_data['jpegphoto']) || $_data['jpegphoto'] === '') {
457 $imageParams = Tinebase_ImageHelper::parseImageLink($_data['jpegphoto']);
458 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' image params:' . print_r($imageParams, TRUE));
459 if ($imageParams['isNewImage']) {
461 $_data['jpegphoto'] = Tinebase_ImageHelper::getImageData($imageParams);
462 } catch(Tinebase_Exception_UnexpectedValue $teuv) {
463 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not add contact image: ' . $teuv->getMessage());
464 unset($_data['jpegphoto']);
467 unset($_data['jpegphoto']);
472 * set small contact image
474 * @param $newPhotoBlob
477 public function setSmallContactImage($newPhotoBlob, $maxSize = self::SMALL_PHOTO_SIZE)
479 if ($this->getId()) {
481 $currentPhoto = Tinebase_Controller::getInstance()->getImage('Addressbook', $this->getId())->getBlob('image/jpeg', $maxSize);
482 } catch (Exception $e) {
487 if (isset($currentPhoto) && $currentPhoto == $newPhotoBlob) {
488 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->INFO(__METHOD__ . '::' . __LINE__
489 . " Photo did not change -> preserving current photo");
491 if (Tinebase_Core::isLogLevel(Zend_Log::INFO)) Tinebase_Core::getLogger()->INFO(__METHOD__ . '::' . __LINE__
492 . " Setting new contact photo (" . strlen($newPhotoBlob) . "KB)");
493 $this->jpegphoto = $newPhotoBlob;
498 * return small contact image for sync
503 * @throws Tinebase_Exception_InvalidArgument
504 * @throws Tinebase_Exception_NotFound
506 public function getSmallContactImage($maxSize = self::SMALL_PHOTO_SIZE)
508 $image = Tinebase_Controller::getInstance()->getImage('Addressbook', $this->getId());
509 return $image->getBlob('image/jpeg', $maxSize);
517 public function getTitle()