6 * @subpackage Controller
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)
14 * contact controller for Addressbook
16 * @package Addressbook
17 * @subpackage Controller
19 class Addressbook_Controller_Contact extends Tinebase_Controller_Record_Abstract
22 * set geo data for contacts
26 protected $_setGeoDataForContacts = FALSE;
31 * don't use the constructor. use the singleton
33 private function __construct()
35 $this->_applicationName = 'Addressbook';
36 $this->_modelName = 'Addressbook_Model_Contact';
37 $this->_backend = Addressbook_Backend_Factory::factory(Addressbook_Backend_Factory::SQL);
38 $this->_purgeRecords = FALSE;
39 $this->_resolveCustomFields = TRUE;
40 $this->_updateMultipleValidateEachRecord = TRUE;
41 $this->_duplicateCheckFields = Addressbook_Config::getInstance()->get(Addressbook_Config::CONTACT_DUP_FIELDS, array(
42 array('n_given', 'n_family', 'org_name'),
45 $this->_useRecordPaths = true;
47 // fields used for private and company address
48 $this->_addressFields = array('locality', 'postalcode', 'street', 'countryname');
50 $this->_setGeoDataForContacts = Tinebase_Config::getInstance()->get(Tinebase_Config::MAPPANEL, TRUE);
51 if (! $this->_setGeoDataForContacts) {
52 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Mappanel/geoext/nominatim disabled with config option.');
57 * don't clone. Use the singleton.
60 private function __clone()
65 * holds the instance of the singleton
67 * @var Addressbook_Controller_Contact
69 private static $_instance = NULL;
72 * the singleton pattern
74 * @return Addressbook_Controller_Contact
76 public static function getInstance()
78 if (self::$_instance === NULL) {
79 self::$_instance = new Addressbook_Controller_Contact();
82 return self::$_instance;
86 * gets binary contactImage
88 * @param int $_contactId
91 public function getImage($_contactId) {
92 // ensure user has rights to see image
93 $this->get($_contactId);
95 $image = $this->_backend->getImage($_contactId);
100 * returns the default addressbook
102 * @return Tinebase_Model_Container
104 public function getDefaultAddressbook()
106 return Tinebase_Container::getInstance()->getDefaultContainer($this->_applicationName, NULL, Addressbook_Preference::DEFAULTADDRESSBOOK);
110 * you can define default filters here
112 * @param Tinebase_Model_Filter_FilterGroup $_filter
114 protected function _addDefaultFilter(Tinebase_Model_Filter_FilterGroup $_filter = NULL)
116 if (! $_filter->isFilterSet('showDisabled')) {
117 $disabledFilter = $_filter->createFilter('showDisabled', 'equals', FALSE);
118 $disabledFilter->setIsImplicit(TRUE);
119 $_filter->addFilter($disabledFilter);
124 * fetch one contact identified by $_userId
126 * @param int $_userId
127 * @param boolean $_ignoreACL don't check acl grants
128 * @return Addressbook_Model_Contact
129 * @throws Addressbook_Exception_AccessDenied if user has no read grant
130 * @throws Addressbook_Exception_NotFound if contact is hidden from addressbook
132 * @todo this is almost always called with ignoreACL = TRUE because contacts can be hidden from addressbook.
133 * is this the way we want that?
135 public function getContactByUserId($_userId, $_ignoreACL = FALSE)
137 if (empty($_userId)) {
138 throw new Tinebase_Exception_InvalidArgument('Empty user id');
141 $contact = $this->_backend->getByUserId($_userId);
143 if ($_ignoreACL === FALSE) {
144 if (empty($contact->container_id)) {
145 throw new Addressbook_Exception_NotFound('Contact is hidden from addressbook (container id is empty).');
147 if (! Tinebase_Core::getUser()->hasGrant($contact->container_id, Tinebase_Model_Grants::GRANT_READ)) {
148 throw new Addressbook_Exception_AccessDenied('Read access to contact denied.');
152 if ($this->_resolveCustomFields && $contact->has('customfields')) {
153 Tinebase_CustomField::getInstance()->resolveRecordCustomFields($contact);
160 * can be called to activate/deactivate if geodata should be set for contacts (ignoring the config setting)
162 * @param boolean optional
165 public function setGeoDataForContacts()
167 $value = (func_num_args() === 1) ? (bool) func_get_arg(0) : NULL;
168 return $this->_setBooleanMemberVar('_setGeoDataForContacts', $value);
172 * gets profile portion of the requested user
174 * @param string $_userId
175 * @return Addressbook_Model_Contact
177 public function getUserProfile($_userId)
179 Tinebase_UserProfile::getInstance()->checkRight($_userId);
181 $contact = $this->getContactByUserId($_userId, TRUE);
182 $userProfile = Tinebase_UserProfile::getInstance()->doProfileCleanup($contact);
188 * update multiple records in an iteration
189 * @see Tinebase_Record_Iterator / self::updateMultiple()
191 * @param Tinebase_Record_RecordSet $_records
192 * @param array $_data
194 * Overwrites Tinebase_Controller_Record_Abstract::processUpdateMultipleIteration: jpegphoto is set to null, so no deletion of photos on multipleUpdate happens
195 * @TODO: Can be removed when "0000284: modlog of contact images / move images to vfs" is resolved.
198 public function processUpdateMultipleIteration($_records, $_data)
200 if (count($_records) === 0) {
204 foreach ($_records as $currentRecord) {
205 $oldRecordArray = $currentRecord->toArray();
206 $data = array_merge($oldRecordArray, $_data);
208 if ($this->_newRelations || $this->_removeRelations) {
209 $data['relations'] = $this->_iterateRelations($currentRecord);
213 $record = new $this->_modelName($data);
214 $record->__set('jpegphoto', NULL);
215 $updatedRecord = $this->update($record, FALSE);
217 $this->_updateMultipleResult['results']->addRecord($updatedRecord);
218 $this->_updateMultipleResult['totalcount'] ++;
220 } catch (Tinebase_Exception_Record_Validation $e) {
222 $this->_updateMultipleResult['exceptions']->addRecord(new Tinebase_Model_UpdateMultipleException(array(
223 'id' => $currentRecord->getId(),
225 'record' => $currentRecord,
226 'code' => $e->getCode(),
227 'message' => $e->getMessage()
229 $this->_updateMultipleResult['failcount'] ++;
235 * update profile portion of given contact
237 * @param Addressbook_Model_Contact $_userProfile
238 * @return Addressbook_Model_Contact
240 * @todo think about adding $_ignoreACL to generic update() to simplify this
242 public function updateUserProfile($_userProfile)
244 Tinebase_UserProfile::getInstance()->checkRight($_userProfile->account_id);
246 $doContainerACLChecks = $this->doContainerACLChecks(FALSE);
248 $contact = $this->getContactByUserId($_userProfile->account_id, true);
250 // we need to unset the jpegphoto because update() expects the image data and we only have a boolean value here
251 unset($contact->jpegphoto);
253 $userProfile = Tinebase_UserProfile::getInstance()->mergeProfileInfo($contact, $_userProfile);
255 $contact = $this->update($userProfile, FALSE);
257 $userProfile = Tinebase_UserProfile::getInstance()->doProfileCleanup($contact);
259 $this->doContainerACLChecks($doContainerACLChecks);
265 * inspect update of one record (after update)
267 * @param Tinebase_Record_Interface $updatedRecord the just updated record
268 * @param Tinebase_Record_Interface $record the update record
269 * @param Tinebase_Record_Interface $currentRecord the current record (before update)
272 protected function _inspectAfterUpdate($updatedRecord, $record, $currentRecord)
274 if ($updatedRecord->type === Addressbook_Model_Contact::CONTACTTYPE_USER) {
275 Tinebase_User::getInstance()->updateContact($updatedRecord);
281 * - don't delete if it belongs to an user account
283 * @param Tinebase_Record_Interface $_record
284 * @throws Addressbook_Exception_AccessDenied
286 protected function _deleteRecord(Tinebase_Record_Interface $_record)
288 if (!empty($_record->account_id)) {
289 throw new Addressbook_Exception_AccessDenied('It is not allowed to delete a contact linked to an user account!');
292 parent::_deleteRecord($_record);
296 * inspect creation of one record
298 * @param Tinebase_Record_Interface $_record
301 protected function _inspectBeforeCreate(Tinebase_Record_Interface $_record)
303 $this->_setGeoData($_record);
305 if (isset($_record->type) && $_record->type == Addressbook_Model_Contact::CONTACTTYPE_USER) {
306 throw new Addressbook_Exception_InvalidArgument('can not add contact of type user');
311 * inspect update of one record
313 * @param Tinebase_Record_Interface $_record the update record
314 * @param Tinebase_Record_Interface $_oldRecord the current persistent record
317 * @todo remove system note for updated jpegphoto when images are modlogged (@see 0000284: modlog of contact images / move images to vfs)
319 protected function _inspectBeforeUpdate($_record, $_oldRecord)
321 // do update of geo data only if one of address field changed
322 $addressDataChanged = FALSE;
323 foreach ($this->_addressFields as $field) {
325 ($_record->{'adr_one_' . $field} != $_oldRecord->{'adr_one_' . $field}) ||
326 ($_record->{'adr_two_' . $field} != $_oldRecord->{'adr_two_' . $field})
328 $addressDataChanged = TRUE;
333 if ($addressDataChanged) {
334 $this->_setGeoData($_record);
337 if (isset($_record->jpegphoto) && ! empty($_record->jpegphoto)) {
338 // add system note when jpegphoto gets updated
339 $translate = $translate = Tinebase_Translation::getTranslation('Addressbook');
340 $noteMessage = $translate->_('Uploaded new contact image.');
341 $traceException = new Exception($noteMessage);
342 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
343 . ' ' . $traceException);
344 Tinebase_Notes::getInstance()->addSystemNote($_record, Tinebase_Core::getUser(), Tinebase_Model_Note::SYSTEM_NOTE_NAME_CHANGED, $noteMessage);
347 if (isset($_oldRecord->type) && $_oldRecord->type == Addressbook_Model_Contact::CONTACTTYPE_USER) {
348 $_record->type = Addressbook_Model_Contact::CONTACTTYPE_USER;
353 * set geodata for given address of record
355 * @param string $_address (addressbook prefix - adr_one_ or adr_two_)
356 * @param Addressbook_Model_Contact $_record
357 * @param array $_ommitFields do not submit these fields to nominatim
360 protected function _setGeoDataForAddress($_address, Addressbook_Model_Contact $_record, $_ommitFields = array())
363 empty($_record->{$_address . 'locality'}) &&
364 empty($_record->{$_address . 'postalcode'}) &&
365 empty($_record->{$_address . 'street'}) &&
366 empty($_record->{$_address . 'countryname'})
368 $_record->{$_address . 'lon'} = NULL;
369 $_record->{$_address . 'lat'} = NULL;
374 $nominatim = new Zend_Service_Nominatim();
376 if (! empty($_record->{$_address . 'locality'})) {
377 $nominatim->setVillage($_record->{$_address . 'locality'});
380 if (! empty($_record->{$_address . 'postalcode'}) && ! in_array($_address . 'postalcode', $_ommitFields)) {
381 $nominatim->setPostcode($_record->{$_address . 'postalcode'});
384 if (! empty($_record->{$_address . 'street'})) {
385 $nominatim->setStreet($_record->{$_address . 'street'});
388 if (! empty($_record->{$_address . 'countryname'})) {
390 $country = Zend_Locale::getTranslation($_record->{$_address . 'countryname'}, 'Country', $_record->{$_address . 'countryname'});
391 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
392 . ($_address == 'adr_one_' ? ' Company address' : ' Private address') . ' country ' . $country);
393 $nominatim->setCountry($country);
394 } catch (Zend_Locale_Exception $zle) {
395 Tinebase_Exception::log($zle, true);
400 $places = $nominatim->search();
402 if (count($places) > 0) {
403 $place = $places->current();
404 $this->_applyNominatimPlaceToRecord($_address, $_record, $place);
407 if (! in_array($_address . 'postalcode', $_ommitFields)) {
408 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
409 ($_address == 'adr_one_' ? ' Company address' : ' Private address') . ' could not find place - try it again without postalcode.');
411 $this->_setGeoDataForAddress($_address, $_record, array($_address . 'postalcode'));
415 Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ .
416 ' ' . ($_address == 'adr_one_' ? 'Company address' : 'Private address') . ' Could not find place.');
417 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
418 ' ' . $_record->{$_address . 'street'} . ', ' . $_record->{$_address . 'postalcode'} . ', ' . $_record->{$_address . 'locality'} . ', ' . $_record->{$_address . 'countryname'});
420 $_record->{$_address . 'lon'} = NULL;
421 $_record->{$_address . 'lat'} = NULL;
423 } catch (Exception $e) {
424 Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' ' . $e->getMessage());
426 // the address has changed, the old values for lon/lat can not be valid anymore
427 $_record->{$_address . 'lon'} = NULL;
428 $_record->{$_address . 'lat'} = NULL;
433 * _applyNominatimPlaceToRecord
435 * @param string $address
436 * @param Addressbook_Model_Contact $record
437 * @param Zend_Service_Nominatim_Result $place
439 protected function _applyNominatimPlaceToRecord($address, $record, $place)
441 if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__ .
442 ' Place: ' . var_export($place, true));
444 $record->{$address . 'lon'} = $place->lon;
445 $record->{$address . 'lat'} = $place->lat;
447 if (empty($record->{$address . 'countryname'}) && ! empty($place->country_code)) {
448 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
449 . ' Updating record countryname from Nominatim: ' . $place->country_code);
450 $record->{$address . 'countryname'} = $place->country_code;
453 if (empty($record->{$address . 'postalcode'}) && ! empty($place->postcode)) {
454 $this->_applyNominatimPostcode($address, $record, $place->postcode);
457 if (empty($record->{$address . 'locality'}) && ! empty($place->city)) {
458 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
459 . ' Updating record locality from Nominatim: ' . $place->city);
460 $record->{$address . 'locality'} = $place->city;
463 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ .
464 ($address == 'adr_one_' ? ' Company' : ' Private') . ' Place found: lon/lat ' . $record->{$address . 'lon'} . ' / ' . $record->{$address . 'lat'});
468 * _applyNominatimPostcode
470 * @param string $address
471 * @param Addressbook_Model_Contact $record
472 * @param string $postcode
474 protected function _applyNominatimPostcode($address, $record, $postcode)
476 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
477 . ' Got postalcode from Nominatim: ' . $postcode);
479 // @see 0009424: missing postalcode prevents saving of contact
480 if (strpos($postcode, ',') !== false) {
481 $postcodes = explode(',', $postcode);
482 $postcode = $postcodes[0];
483 if (preg_match('/^[0-9]+$/',$postcode)) {
484 // find the similar numbers to create a postcode with placeholders ('x')
485 foreach ($postcodes as $code) {
486 for ($i = 0; $i < strlen($postcode); $i++) {
487 if ($code[$i] !== $postcode[$i]) {
495 if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
496 . ' Updating record postalcode from Nominatim: ' . $postcode);
498 $record->{$address . 'postalcode'} = $postcode;
502 * set geodata of record
504 * @param Addressbook_Model_Contact $_record
507 protected function _setGeoData(Addressbook_Model_Contact $_record)
509 if (! $this->_setGeoDataForContacts) {
513 $this->_setGeoDataForAddress('adr_one_', $_record);
514 $this->_setGeoDataForAddress('adr_two_', $_record);
518 * get number from street (and remove it)
520 * @param string $_street
523 protected function _splitNumberAndStreet(&$_street)
525 $pattern = '([0-9]+)';
526 preg_match('/ ' . $pattern . '$/', $_street, $matches);
528 if (empty($matches)) {
529 // look at the beginning
530 preg_match('/^' . $pattern . ' /', $_street, $matches);
533 if ((isset($matches[1]) || array_key_exists(1, $matches))) {
534 $result = $matches[1];
535 $_street = str_replace($matches[0], '', $_street);
544 * get contact information from string by parsing it using predefined rules
546 * @param string $_address
547 * @return array with Addressbook_Model_Contact + array of unrecognized tokens
549 public function parseAddressData($_address)
551 $converter = new Addressbook_Convert_Contact_String();
554 'contact' => $converter->toTine20Model($_address),
555 'unrecognizedTokens' => $converter->getUnrecognizedTokens(),
562 * generates path for the contact
564 * - we add to the path:
565 * - lists contact is member of
566 * - we add list role memberships
568 * @param Tinebase_Record_Abstract $record
569 * @return Tinebase_Record_RecordSet
571 public function generatePathForRecord($record)
573 $result = new Tinebase_Record_RecordSet('Tinebase_Model_Path');
575 // fetch all groups and role memberships and add to path
576 $listIds = Addressbook_Controller_List::getInstance()->getMemberships($record);
577 foreach ($listIds as $listId) {
578 $list = Addressbook_Controller_List::getInstance()->get($listId);
579 $listPaths = $this->_getPathsOfRecord($list);
580 if (count($listPaths) === 0) {
582 $listPaths->addRecord(new Tinebase_Model_Path(array(
583 'path' => $this->_getPathPart($list),
584 'shadow_path' => '/' . $list->getId(),
585 'record_id' => $list->getId(),
586 'creation_time' => Tinebase_DateTime::now(),
590 foreach ($listPaths as $listPath) {
591 if (count($list->memberroles) > 0) {
592 foreach ($list->memberroles as $role) {
593 $rolePath = clone($listPath);
594 if ($role->contact_id === $record->getId()) {
595 $role = Addressbook_Controller_ListRole::getInstance()->get($role->list_role_id);
596 $rolePath->path .= $this->_getPathPart($role);
597 $rolePath->shadow_path .= '/' . $role->getId();
598 $rolePath->record_id = $role->getId();
599 $result->addRecord($rolePath);
603 $result->addRecord($listPath);
608 foreach ($result as $listPath) {
609 $listPath->path .= $this->_getPathPart($record);
610 $listPath->shadow_path .= '/' . $record->getId();
611 $listPath->record_id = $record->getId();