From 7ada2354d3ed9b2f275c387ecc003c6b5b60f93a Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Sun, 6 Mar 2016 13:45:15 +0000 Subject: [PATCH] move addresbook_bo to Api\Contacts, ldap to Api\Ldap, ldapserverinfo to Api\Ldap\ServerInfo, bo_tracking to Api\Storage\Tracking, historylog to Api\Storage\History, Api\Customfields to Api\Storage\Customfields --- addressbook/inc/class.addressbook_bo.inc.php | 2394 +--------------- .../inc/class.addressbook_contactform.inc.php | 17 +- .../inc/class.addressbook_groupdav.inc.php | 8 +- ...ss.addressbook_import_contacts_csv.inc.php | 31 +- addressbook/inc/class.addressbook_so.inc.php | 1118 +------- addressbook/inc/class.addressbook_ui.inc.php | 10 +- admin/inc/class.customfields.inc.php | 121 +- api/src/Config.php | 6 +- api/src/Contacts.php | 2402 +++++++++++++++++ .../src/Contacts/Ads.php | 51 +- .../src/Contacts/Ldap.php | 75 +- .../src/Contacts/Sql.php | 101 +- api/src/Contacts/Storage.php | 1138 ++++++++ .../src/Contacts/Tracking.php | 44 +- .../src/Contacts/Univention.php | 9 +- api/src/Ldap.php | 263 ++ api/src/Ldap/ServerInfo.php | 243 ++ api/src/Storage.php | 2 +- api/src/{ => Storage}/Customfields.php | 30 +- api/src/Storage/History.php | 288 ++ api/src/Storage/Tracking.php | 1220 +++++++++ etemplate/inc/class.bo_tracking.inc.php | 1178 +------- setup/inc/class.setup_cmd_ldap.inc.php | 66 +- 23 files changed, 5858 insertions(+), 4957 deletions(-) create mode 100755 api/src/Contacts.php rename addressbook/inc/class.addressbook_ads.inc.php => api/src/Contacts/Ads.php (78%) rename addressbook/inc/class.addressbook_ldap.inc.php => api/src/Contacts/Ldap.php (95%) rename addressbook/inc/class.addressbook_sql.inc.php => api/src/Contacts/Sql.php (88%) create mode 100755 api/src/Contacts/Storage.php rename addressbook/inc/class.addressbook_tracking.inc.php => api/src/Contacts/Tracking.php (84%) rename addressbook/inc/class.addressbook_univention.inc.php => api/src/Contacts/Univention.php (85%) create mode 100644 api/src/Ldap.php create mode 100644 api/src/Ldap/ServerInfo.php rename api/src/{ => Storage}/Customfields.php (94%) create mode 100644 api/src/Storage/History.php create mode 100644 api/src/Storage/Tracking.php diff --git a/addressbook/inc/class.addressbook_bo.inc.php b/addressbook/inc/class.addressbook_bo.inc.php index 765a875207..eedeabce29 100755 --- a/addressbook/inc/class.addressbook_bo.inc.php +++ b/addressbook/inc/class.addressbook_bo.inc.php @@ -1,2403 +1,27 @@ * @author Ralf Becker * @author Joerg Lehrke * @package addressbook - * @copyright (c) 2005-15 by Ralf Becker + * @copyright (c) 2005-16 by Ralf Becker * @copyright (c) 2005/6 by Cornelius Weiss * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ +use EGroupware\Api; + /** - * General business object of the adressbook + * Business object for addressbook + * + * Currently this only contains PGP stuff, which needs to be called via Ajax */ -class addressbook_bo extends addressbook_so +class addressbook_bo extends Api\Contacts { - /** - * @var int $now_su actual user (!) time - */ - var $now_su; - - /** - * @var array $timestamps timestamps - */ - var $timestamps = array('modified','created'); - - /** - * @var array $fileas_types - */ - var $fileas_types = array( - 'org_name: n_family, n_given', - 'org_name: n_family, n_prefix', - 'org_name: n_given n_family', - 'org_name: n_fn', - 'org_name, org_unit: n_family, n_given', - 'org_name, adr_one_locality: n_family, n_given', - 'org_name, org_unit, adr_one_locality: n_family, n_given', - 'n_family, n_given: org_name', - 'n_family, n_given (org_name)', - 'n_family, n_prefix: org_name', - 'n_given n_family: org_name', - 'n_prefix n_family: org_name', - 'n_fn: org_name', - 'org_name', - 'org_name - org_unit', - 'n_given n_family', - 'n_prefix n_family', - 'n_family, n_given', - 'n_family, n_prefix', - 'n_fn', - ); - - /** - * @var array $org_fields fields belonging to the (virtual) organisation entry - */ - var $org_fields = array( - 'org_name', - 'org_unit', - 'adr_one_street', - 'adr_one_street2', - 'adr_one_locality', - 'adr_one_region', - 'adr_one_postalcode', - 'adr_one_countryname', - 'adr_one_countrycode', - 'label', - 'tel_work', - 'tel_fax', - 'tel_assistent', - 'assistent', - 'email', - 'url', - 'tz', - ); - - /** - * Which fields is a (non-admin) user allowed to edit in his own account - * - * @var array - */ - var $own_account_acl; - - /** - * @var double $org_common_factor minimum percentage of the contacts with identical values to construct the "common" (virtual) org-entry - */ - var $org_common_factor = 0.6; - - var $contact_fields = array(); - var $business_contact_fields = array(); - var $home_contact_fields = array(); - - /** - * Set Logging - * - * @var boolean - */ - var $log = false; - var $logfile = '/tmp/log-addressbook_bo'; - - /** - * Number and message of last error or false if no error, atm. only used for saving - * - * @var string/boolean - */ - var $error; - /** - * Addressbook preferences of the user - * - * @var array - */ - var $prefs; - /** - * Default addressbook for new contacts, if no addressbook is specified (user preference) - * - * @var int - */ - var $default_addressbook; - /** - * Default addressbook is the private one - * - * @var boolean - */ - var $default_private; - /** - * Use a separate private addressbook (former private flag), for contacts not shareable via regular read acl - * - * @var boolean - */ - var $private_addressbook = false; - /** - * Categories object - * - * @var categories - */ - var $categories; - - /** - * Tracking changes - * - * @var addressbook_tracking - */ - protected $tracking; - - /** - * Keep deleted addresses, or really delete them - * Set in Admin -> Addressbook -> Site Configuration - * ''=really delete, 'history'=keep, only admins delete, 'userpurge'=keep, users delete - * - * @var string - */ - protected $delete_history = ''; - - /** - * Constructor - * - * @param string $contact_app='addressbook' used for acl->get_grants() - * @param egw_db $db=null - */ - function __construct($contact_app='addressbook',egw_db $db=null) - { - parent::__construct($contact_app,$db); - if ($this->log) - { - $this->logfile = $GLOBALS['egw_info']['server']['temp_dir'].'/log-addressbook_bo'; - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($contact_app)\n", 3 ,$this->logfile); - } - - $this->now_su = egw_time::to('now','ts'); - - $this->prefs =& $GLOBALS['egw_info']['user']['preferences']['addressbook']; - // get the default addressbook from the users prefs - $this->default_addressbook = $GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'] ? - (int)$GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'] : $this->user; - $this->default_private = substr($GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'],-1) == 'p'; - if ($this->default_addressbook > 0 && $this->default_addressbook != $this->user && - ($this->default_private || - $this->default_addressbook == (int)$GLOBALS['egw']->preferences->forced['addressbook']['add_default'] || - $this->default_addressbook == (int)$GLOBALS['egw']->preferences->default['addressbook']['add_default'])) - { - $this->default_addressbook = $this->user; // admin set a default or forced pref for personal addressbook - } - $this->private_addressbook = self::private_addressbook($this->contact_repository == 'sql', $this->prefs); - - $this->contact_fields = array( - 'id' => lang('Contact ID'), - 'tid' => lang('Type'), - 'owner' => lang('Addressbook'), - 'private' => lang('private'), - 'cat_id' => lang('Category'), - 'n_prefix' => lang('prefix'), - 'n_given' => lang('first name'), - 'n_middle' => lang('middle name'), - 'n_family' => lang('last name'), - 'n_suffix' => lang('suffix'), - 'n_fn' => lang('full name'), - 'n_fileas' => lang('own sorting'), - 'bday' => lang('birthday'), - 'org_name' => lang('Organisation'), - 'org_unit' => lang('Department'), - 'title' => lang('title'), - 'role' => lang('role'), - 'assistent' => lang('Assistent'), - 'room' => lang('Room'), - 'adr_one_street' => lang('business street'), - 'adr_one_street2' => lang('business address line 2'), - 'adr_one_locality' => lang('business city'), - 'adr_one_region' => lang('business state'), - 'adr_one_postalcode' => lang('business zip code'), - 'adr_one_countryname' => lang('business country'), - 'adr_one_countrycode' => lang('business country code'), - 'label' => lang('label'), - 'adr_two_street' => lang('street (private)'), - 'adr_two_street2' => lang('address line 2 (private)'), - 'adr_two_locality' => lang('city (private)'), - 'adr_two_region' => lang('state (private)'), - 'adr_two_postalcode' => lang('zip code (private)'), - 'adr_two_countryname' => lang('country (private)'), - 'adr_two_countrycode' => lang('country code (private)'), - 'tel_work' => lang('work phone'), - 'tel_cell' => lang('mobile phone'), - 'tel_fax' => lang('business fax'), - 'tel_assistent' => lang('assistent phone'), - 'tel_car' => lang('car phone'), - 'tel_pager' => lang('pager'), - 'tel_home' => lang('home phone'), - 'tel_fax_home' => lang('fax (private)'), - 'tel_cell_private' => lang('mobile phone (private)'), - 'tel_other' => lang('other phone'), - 'tel_prefer' => lang('preferred phone'), - 'email' => lang('business email'), - 'email_home' => lang('email (private)'), - 'url' => lang('url (business)'), - 'url_home' => lang('url (private)'), - 'freebusy_uri' => lang('Freebusy URI'), - 'calendar_uri' => lang('Calendar URI'), - 'note' => lang('note'), - 'tz' => lang('time zone'), - 'geo' => lang('geo'), - 'pubkey' => lang('public key'), - 'created' => lang('created'), - 'creator' => lang('created by'), - 'modified' => lang('last modified'), - 'modifier' => lang('last modified by'), - 'jpegphoto' => lang('photo'), - 'account_id' => lang('Account ID'), - ); - $this->business_contact_fields = array( - 'org_name' => lang('Company'), - 'org_unit' => lang('Department'), - 'title' => lang('Title'), - 'role' => lang('Role'), - 'n_prefix' => lang('prefix'), - 'n_given' => lang('first name'), - 'n_middle' => lang('middle name'), - 'n_family' => lang('last name'), - 'n_suffix' => lang('suffix'), - 'adr_one_street' => lang('street').' ('.lang('business').')', - 'adr_one_street2' => lang('address line 2').' ('.lang('business').')', - 'adr_one_locality' => lang('city').' ('.lang('business').')', - 'adr_one_region' => lang('state').' ('.lang('business').')', - 'adr_one_postalcode' => lang('zip code').' ('.lang('business').')', - 'adr_one_countryname' => lang('country').' ('.lang('business').')', - ); - $this->home_contact_fields = array( - 'org_name' => lang('Company'), - 'org_unit' => lang('Department'), - 'title' => lang('Title'), - 'role' => lang('Role'), - 'n_prefix' => lang('prefix'), - 'n_given' => lang('first name'), - 'n_middle' => lang('middle name'), - 'n_family' => lang('last name'), - 'n_suffix' => lang('suffix'), - 'adr_two_street' => lang('street').' ('.lang('business').')', - 'adr_two_street2' => lang('address line 2').' ('.lang('business').')', - 'adr_two_locality' => lang('city').' ('.lang('business').')', - 'adr_two_region' => lang('state').' ('.lang('business').')', - 'adr_two_postalcode' => lang('zip code').' ('.lang('business').')', - 'adr_two_countryname' => lang('country').' ('.lang('business').')', - ); - //_debug_array($this->contact_fields); - $this->own_account_acl = $GLOBALS['egw_info']['server']['own_account_acl']; - if (!is_array($this->own_account_acl)) $this->own_account_acl = json_php_unserialize($this->own_account_acl, true); - // we have only one acl (n_fn) for the whole name, as not all backends store every part in an own field - if ($this->own_account_acl && in_array('n_fn',$this->own_account_acl)) - { - $this->own_account_acl = array_merge($this->own_account_acl,array('n_prefix','n_given','n_middle','n_family','n_suffix')); - } - if ($GLOBALS['egw_info']['server']['org_fileds_to_update']) - { - $this->org_fields = $GLOBALS['egw_info']['server']['org_fileds_to_update']; - if (!is_array($this->org_fields)) $this->org_fields = unserialize($this->org_fields); - - // Set country code if country name is selected - $supported_fields = $this->get_fields('supported',null,0); - if(in_array('adr_one_countrycode', $supported_fields) && in_array('adr_one_countryname',$this->org_fields)) - { - $this->org_fields[] = 'adr_one_countrycode'; - } - if(in_array('adr_two_countrycode', $supported_fields) && in_array('adr_two_countryname',$this->org_fields)) - { - $this->org_fields[] = 'adr_two_countrycode'; - } - } - $this->categories = new categories($this->user,'addressbook'); - - $this->delete_history = $GLOBALS['egw_info']['server']['history']; - } - - /** - * Do we use a private addressbook (in comparison to a personal one) - * - * Used to set $this->private_addressbook for current user. - * - * @param string $contact_repository - * @param array $prefs addressbook preferences - * @return boolean - */ - public static function private_addressbook($contact_repository, array $prefs=null) - { - return $contact_repository == 'sql' && $prefs['private_addressbook']; - } - - /** - * Get the availible addressbooks of the user - * - * @param int $required=EGW_ACL_READ required rights on the addressbook or multiple rights or'ed together, - * to return only addressbooks fullfilling all the given rights - * @param string $extra_label first label if given (already translated) - * @param int $user=null account_id or null for current user - * @return array with owner => label pairs - */ - function get_addressbooks($required=EGW_ACL_READ,$extra_label=null,$user=null) - { - //echo "uicontacts::get_addressbooks($required,$include_all) grants="; _debug_array($this->grants); - - if (is_null($user)) - { - $user = $this->user; - $preferences = $GLOBALS['egw_info']['user']['preferences']; - $grants = $this->grants; - } - else - { - $prefs_obj = new preferences($user); - $preferences = $prefs_obj->read_repository(); - $grants = $this->get_grants($user, 'addressbook', $preferences); - } - - $addressbooks = $to_sort = array(); - if ($extra_label) $addressbooks[''] = $extra_label; - $addressbooks[$user] = lang('Personal'); - // add all group addressbooks the user has the necessary rights too - foreach($grants as $uid => $rights) - { - if (($rights & $required) == $required && $GLOBALS['egw']->accounts->get_type($uid) == 'g') - { - $to_sort[$uid] = lang('Group %1',$GLOBALS['egw']->accounts->id2name($uid)); - } - } - if ($to_sort) - { - asort($to_sort); - $addressbooks += $to_sort; - } - if ($required != EGW_ACL_ADD && // do NOT allow to set accounts as default addressbook (AB can add accounts) - !$preferences['addressbook']['hide_accounts'] && ( - ($grants[0] & $required) == $required || - $preferences['common']['account_selection'] == 'groupmembers' && - $this->account_repository != 'ldap' && ($required & EGW_ACL_READ))) - { - $addressbooks[0] = lang('Accounts'); - } - // add all other user addressbooks the user has the necessary rights too - $to_sort = array(); - foreach($grants as $uid => $rights) - { - if ($uid != $user && ($rights & $required) == $required && $GLOBALS['egw']->accounts->get_type($uid) == 'u') - { - $to_sort[$uid] = common::grab_owner_name($uid); - } - } - if ($to_sort) - { - asort($to_sort); - $addressbooks += $to_sort; - } - if ($user > 0 && self::private_addressbook($this->contact_repository, $preferences['addressbook'])) - { - $addressbooks[$user.'p'] = lang('Private'); - } - //echo "

".__METHOD__."($required,'$extra_label')"; _debug_array($addressbooks); - return $addressbooks; - } - - /** - * calculate the file_as string from the contact and the file_as type - * - * @param array $contact - * @param string $type=null file_as type, default null to read it from the contact, unknown/not set type default to the first one - * @param boolean $update=false If true, reads the old record for any not set fields - * @return string - */ - function fileas($contact,$type=null, $isUpdate=false) - { - if (is_null($type)) $type = $contact['fileas_type']; - if (!$type) $type = $this->prefs['fileas_default'] ? $this->prefs['fileas_default'] : $this->fileas_types[0]; - - if (strpos($type,'n_fn') !== false) $contact['n_fn'] = $this->fullname($contact); - - if($isUpdate) - { - $fileas_fields = array('n_prefix','n_given','n_middle','n_family','n_suffix','n_fn','org_name','org_unit','adr_one_locality'); - $old = null; - foreach($fileas_fields as $field) - { - if(!isset($contact[$field])) - { - if(is_null($old)) $old = $this->read($contact['id']); - $contact[$field] = $old[$field]; - } - } - unset($old); - } - - $fileas = str_replace(array('n_prefix','n_given','n_middle','n_family','n_suffix','n_fn','org_name','org_unit','adr_one_locality'), - array($contact['n_prefix'],$contact['n_given'],$contact['n_middle'],$contact['n_family'],$contact['n_suffix'], - $contact['n_fn'],$contact['org_name'],$contact['org_unit'],$contact['adr_one_locality']),$type); - - // removing empty delimiters, caused by empty contact fields - $fileas = str_replace(array(', , : ',', : ',': , ',', , ',': : ',' ()'),array(': ',': ',': ',', ',': ',''),$fileas); - while ($fileas[0] == ':' || $fileas[0] == ',') $fileas = substr($fileas,2); - while (substr($fileas,-2) == ': ' || substr($fileas,-2) == ', ') $fileas = substr($fileas,0,-2); - - //echo "

bocontacts::fileas(,$type)='$fileas'

\n"; - return $fileas; - } - - /** - * determine the file_as type from the file_as string and the contact - * - * @param array $contact - * @param string $type=null file_as type, default null to read it from the contact, unknown/not set type default to the first one - * @return string - */ - function fileas_type($contact,$file_as=null) - { - if (is_null($file_as)) $file_as = $contact['n_fileas']; - - if ($file_as) - { - foreach($this->fileas_types as $type) - { - if ($this->fileas($contact,$type) == $file_as) - { - return $type; - } - } - } - return $this->prefs['fileas_default'] ? $this->prefs['fileas_default'] : $this->fileas_types[0]; - } - - /** - * get selectbox options for the customfields - * - * @param array $field=null - * @return array with options: - */ - public static function cf_options() - { - $cf_fields = config::get_customfields('addressbook',TRUE); - foreach ($cf_fields as $key => $value ) - { - $options[$key]= $value['label']; - } - return $options; - } - - /** - * get selectbox options for the fileas types with translated labels, or real content - * - * @param array $contact=null real content to use, default none - * @return array with options: fileas type => label pairs - */ - function fileas_options($contact=null) - { - $labels = array( - 'n_prefix' => lang('prefix'), - 'n_given' => lang('first name'), - 'n_middle' => lang('middle name'), - 'n_family' => lang('last name'), - 'n_suffix' => lang('suffix'), - 'n_fn' => lang('full name'), - 'org_name' => lang('company'), - 'org_unit' => lang('department'), - 'adr_one_locality' => lang('city'), - ); - foreach($labels as $name => $label) - { - if ($contact[$name]) $labels[$name] = $contact[$name]; - } - foreach($this->fileas_types as $fileas_type) - { - $options[$fileas_type] = $this->fileas($labels,$fileas_type); - } - return $options; - } - - /** - * Set n_fileas (and n_fn) in contacts of all users (called by Admin >> Addressbook >> Site configuration (Admin only) - * - * If $all all fileas fields will be set, if !$all only empty ones - * - * @param string $fileas_type '' or type of $this->fileas_types - * @param int $all=false update all contacts or only ones with empty values - * @param int &$errors=null on return number of errors - * @return int|boolean number of contacts updated, false for wrong fileas type - */ - function set_all_fileas($fileas_type,$all=false,&$errors=null,$ignore_acl=false) - { - if ($fileas_type != '' && !in_array($fileas_type, $this->fileas_types)) - { - return false; - } - if ($ignore_acl) - { - unset($this->somain->grants); // to NOT limit search to contacts readable by current user - } - // to be able to work on huge contact repositories we read the contacts in chunks of 100 - for($n = $updated = $errors = 0; ($contacts = parent::search($all ? array() : array( - 'n_fileas IS NULL', - "n_fileas=''", - 'n_fn IS NULL', - "n_fn=''", - ),false,'','','',false,'OR',array($n*100,100))); ++$n) - { - foreach($contacts as $contact) - { - $old_fn = $contact['n_fn']; - $old_fileas = $contact['n_fileas']; - $contact['n_fn'] = $this->fullname($contact); - // only update fileas if type is given AND (all should be updated or n_fileas is empty) - if ($fileas_type && ($all || empty($contact['n_fileas']))) - { - $contact['n_fileas'] = $this->fileas($contact,$fileas_type); - } - if ($old_fileas != $contact['n_fileas'] || $old_fn != $contact['n_fn']) - { - //echo "

('$old_fileas' != '{$contact['n_fileas']}' || '$old_fn' != '{$contact['n_fn']}')=".array2string($old_fileas != $contact['n_fileas'] || $old_fn != $contact['n_fn'])."

\n"; - // only specify/write updated fields plus "keys" - $contact = array_intersect_key($contact,array( - 'id' => true, - 'owner' => true, - 'private' => true, - 'account_id' => true, - 'uid' => true, - )+($old_fileas != $contact['n_fileas'] ? array('n_fileas' => true) : array())+($old_fn != $contact['n_fn'] ? array('n_fn' => true) : array())); - if ($this->save($contact,$ignore_acl)) - { - $updated++; - } - else - { - $errors++; - } - } - } - } - return $updated; - } - - /** - * Cleanup all contacts db fields of all users (called by Admin >> Addressbook >> Site configuration (Admin only) - * - * Cleanup means to truncate all unnecessary chars like whitespaces or tabs, - * remove unneeded carriage returns or set empty fields to NULL - * - * @param int &$errors=null on return number of errors - * @return int|boolean number of contacts updated - */ - function set_all_cleanup(&$errors=null,$ignore_acl=false) - { - if ($ignore_acl) - { - unset($this->somain->grants); // to NOT limit search to contacts readable by current user - } - - // fields that must not be touched - $fields_exclude = array( - 'id' => true, - 'tid' => true, - 'owner' => true, - 'private' => true, - 'created' => true, - 'creator' => true, - 'modified' => true, - 'modifier' => true, - 'account_id' => true, - 'etag' => true, - 'uid' => true, - 'freebusy_uri' => true, - 'calendar_uri' => true, - 'photo' => true, - ); - - // to be able to work on huge contact repositories we read the contacts in chunks of 100 - for($n = $updated = $errors = 0; ($contacts = parent::search(array(),false,'','','',false,'OR',array($n*100,100))); ++$n) - { - foreach($contacts as $contact) - { - $fields_to_update = array(); - foreach($contact as $field_name => $field_value) - { - if($fields_exclude[$field_name] === true) continue; // dont touch specified field - - if (is_string($field_value) && $field_name != 'pubkey' && $field_name != 'jpegphoto') - { - // check if field has to be trimmed - if (strlen($field_value) != strlen(trim($field_value))) - { - $fields_to_update[$field_name] = $field_value = trim($field_value); - } - // check if field contains a carriage return - exclude notes - if ($field_name != 'note' && strpos($field_value,"\x0D\x0A") !== false) - { - $fields_to_update[$field_name] = $field_value = str_replace("\x0D\x0A"," ",$field_value);; - } - } - // check if a field contains an empty string - if (is_string($field_value) && strlen($field_value) == 0) - { - $fields_to_update[$field_name] = $field_value = null; - } - // check for valid birthday date - if ($field_name == 'bday' && $field_value != null && - !preg_match('/^(18|19|20|21|22)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/',$field_value)) - { - $fields_to_update[$field_name] = $field_value = null; - } - } - - if(count($fields_to_update) > 0) - { - $contact_to_save = array( - 'id' => $contact['id'], - 'owner' => $contact['owner'], - 'private' => $contact['private'], - 'account_id' => $contact['account_id'], - 'uid' => $contact['uid']) + $fields_to_update; - - if ($this->save($contact_to_save,$ignore_acl)) - { - $updated++; - } - else - { - $errors++; - } - } - } - } - return $updated; - } - - /** - * get full name from the name-parts - * - * @param array $contact - * @return string full name - */ - function fullname($contact) - { - if (empty($contact['n_family']) && empty($contact['n_given'])) { - $cpart = array('org_name'); - } else { - $cpart = array('n_prefix','n_given','n_middle','n_family','n_suffix'); - } - $parts = array(); - foreach($cpart as $n) - { - if ($contact[$n]) $parts[] = $contact[$n]; - } - return implode(' ',$parts); - } - - /** - * changes the data from the db-format to your work-format - * - * it gets called everytime when data is read from the db - * This function needs to be reimplemented in the derived class - * - * @param array $data - * @param $date_format='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format - * - * @return array updated data - */ - function db2data($data, $date_format='ts') - { - static $fb_url = false; - - // convert timestamps from server-time in the db to user-time - foreach ($this->timestamps as $name) - { - if (isset($data[$name])) - { - $data[$name] = egw_time::server2user($data[$name], $date_format); - } - } - $data['photo'] = $this->photo_src($data['id'],$data['jpegphoto'],'',$data['etag']); - - // set freebusy_uri for accounts - if (!$data['freebusy_uri'] && !$data['owner'] && $data['account_id'] && !is_object($GLOBALS['egw_setup'])) - { - if ($fb_url || @is_dir(EGW_SERVER_ROOT.'/calendar/inc')) - { - $fb_url = true; - $user = isset($data['account_lid']) ? $data['account_lid'] : $GLOBALS['egw']->accounts->id2name($data['account_id']); - $data['freebusy_uri'] = calendar_bo::freebusy_url($user); - } - } - return $data; - } - - /** - * src for photo: returns array with linkparams if jpeg exists or the $default image-name if not - * @param int $id contact_id - * @param boolean $jpeg=false jpeg exists or not - * @param string $default='' image-name to use if !$jpeg, eg. 'template' - * @param string $etag=null etag to set in url to allow caching with Expires header - * @return string/array - */ - function photo_src($id,$jpeg,$default='',$etag=null) - { - //error_log(__METHOD__."($id, ..., etag=$etag) ". function_backtrace()); - return $jpeg ? array( - 'menuaction' => 'addressbook.addressbook_ui.photo', - 'contact_id' => $id, - )+(isset($etag) ? array( - 'etag' => $etag, - ) : array()) : $default; - } - - /** - * changes the data from your work-format to the db-format - * - * It gets called everytime when data gets writen into db or on keys for db-searches - * this needs to be reimplemented in the derived class - * - * @param array $data - * @param $date_format='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format - * - * @return array upated data - */ - function data2db($data, $date_format='ts') - { - // convert timestamps from user-time to server-time in the db - foreach ($this->timestamps as $name) - { - if (isset($data[$name])) - { - $data[$name] = egw_time::user2server($data[$name], $date_format); - } - } - return $data; - } - - /** - * deletes contact in db - * - * @param mixed &$contact contact array with key id or (array of) id(s) - * @param boolean $deny_account_delete=true if true never allow to delete accounts - * @param int $check_etag=null - * @return boolean|int true on success or false on failiure, 0 if etag does not match - */ - function delete($contact,$deny_account_delete=true,$check_etag=null) - { - if (is_array($contact) && isset($contact['id'])) - { - $contact = array($contact); - } - elseif (!is_array($contact)) - { - $contact = array($contact); - } - foreach($contact as $c) - { - $id = is_array($c) ? $c['id'] : $c; - - $ok = false; - if ($this->check_perms(EGW_ACL_DELETE,$c,$deny_account_delete)) - { - if (!($old = $this->read($id))) return false; - // check if we only mark contacts as deleted, or really delete them - // already marked as deleted item and accounts are always really deleted - // we cant mark accounts as deleted, as no such thing exists for accounts! - if ($old['owner'] && $this->delete_history != '' && $old['tid'] != addressbook_so::DELETED_TYPE) - { - $delete = $old; - $delete['tid'] = addressbook_so::DELETED_TYPE; - if ($check_etag) $delete['etag'] = $check_etag; - if (($ok = $this->save($delete))) $ok = true; // we have to return true or false - egw_link::unlink(0,'addressbook',$id,'','','',true); - } - elseif (($ok = parent::delete($id,$check_etag))) - { - egw_link::unlink(0,'addressbook',$id); - } - - // Don't notify of final purge - if ($ok && $old['tid'] != addressbook_so::DELETED_TYPE) - { - if (!isset($this->tracking)) $this->tracking = new addressbook_tracking($this); - $this->tracking->track(array('id' => $id), array('id' => $id), null, true); - } - } - else - { - break; - } - } - //error_log(__METHOD__.'('.array2string($contact).', deny_account_delete='.array2string($deny_account_delete).', check_etag='.array2string($check_etag).' returning '.array2string($ok)); - return $ok; - } - - /** - * saves contact to db - * - * @param array &$contact contact array from etemplate::exec - * @param boolean $ignore_acl=false should the acl be checked or not - * @return int/string/boolean id on success, false on failure, the error-message is in $this->error - */ - function save(&$contact,$ignore_acl=false) - { - // remember if we add or update a entry - if (($isUpdate = $contact['id'])) - { - if (!isset($contact['owner']) || !isset($contact['private'])) // owner/private not set on update, eg. SyncML - { - if (($old = $this->read($contact['id']))) // --> try reading the old entry and set it from there - { - if(!isset($contact['owner'])) - { - $contact['owner'] = $old['owner']; - } - if(!isset($contact['private'])) - { - $contact['private'] = $old['private']; - } - } - else // entry not found --> create a new one - { - $isUpdate = $contact['id'] = null; - } - } - } - else - { - // if no owner/addressbook set use the setting of the add_default prefs (if set, otherwise the users personal addressbook) - if (!isset($contact['owner'])) $contact['owner'] = $this->default_addressbook; - if (!isset($contact['private'])) $contact['private'] = (int)$this->default_private; - // do NOT allow to create new accounts via addressbook, they are broken without an account_id - if (!$contact['owner'] && empty($contact['account_id'])) - { - $contact['owner'] = $this->default_addressbook ? $this->default_addressbook : $this->user; - } - // allow admins to import contacts with creator / created date set - if (!$contact['creator'] || !$this->is_admin($contact)) $contact['creator'] = $this->user; - if (!$contact['created'] || !$this->is_admin($contact)) $contact['created'] = $this->now_su; - - if (!$contact['tid']) $contact['tid'] = 'n'; - } - // ensure accounts and group addressbooks are never private! - if ($contact['owner'] <= 0) - { - $contact['private'] = 0; - } - if(!$ignore_acl && !$this->check_perms($isUpdate ? EGW_ACL_EDIT : EGW_ACL_ADD,$contact)) - { - $this->error = 'access denied'; - return false; - } - // resize image to 60px width - if (!empty($contact['jpegphoto'])) - { - $contact['jpegphoto'] = $this->resize_photo($contact['jpegphoto']); - } - // convert categories - if (is_array($contact['cat_id'])) - { - $contact['cat_id'] = implode(',',$contact['cat_id']); - } - - // Update country codes - foreach(array('adr_one_', 'adr_two_') as $c_prefix) { - if($contact[$c_prefix.'countryname'] && !$contact[$c_prefix.'countrycode'] && - $code = $GLOBALS['egw']->country->country_code($contact[$c_prefix.'countryname'])) - { - if(strlen($code) == 2) - { - $contact[$c_prefix.'countrycode'] = $code; - } - else - { - $contact[$c_prefix.'countrycode'] = null; - } - } - if($contact[$c_prefix.'countrycode'] != null) - { - $contact[$c_prefix.'countryname'] = null; - } - } - - // last modified - $contact['modifier'] = $this->user; - $contact['modified'] = $this->now_su; - // set full name and fileas from the content - if (!isset($contact['n_fn'])) - { - $contact['n_fn'] = $this->fullname($contact); - } - if (isset($contact['org_name'])) $contact['n_fileas'] = $this->fileas($contact, null, false); - - // Get old record for tracking changes - if (!isset($old) && $isUpdate) - { - $old = $this->read($contact['id']); - } - $to_write = $contact; - // (non-admin) user editing his own account, make sure he does not change fields he is not allowed to (eg. via SyncML or xmlrpc) - if (!$ignore_acl && !$contact['owner'] && !($this->is_admin($contact) || $this->allow_account_edit())) - { - foreach($contact as $field => $value) - { - if (!in_array($field,$this->own_account_acl) && !in_array($field,array('id','owner','account_id','modified','modifier'))) - { - // user is not allowed to change that - if ($old) - { - $to_write[$field] = $contact[$field] = $old[$field]; - } - else - { - unset($to_write[$field]); - } - } - } - } - - // IF THE OLD ENTRY IS A ACCOUNT, dont allow to change the owner/location - // maybe we need that for id and account_id as well. - if (is_array($old) && (!isset($old['owner']) || empty($old['owner']))) - { - if (isset($to_write['owner']) && !empty($to_write['owner'])) - { - error_log(__METHOD__.__LINE__." Trying to change account to owner:". $to_write['owner'].' Account affected:'.array2string($old).' Data send:'.array2string($to_write)); - unset($to_write['owner']); - } - } - - if(!($this->error = parent::save($to_write))) - { - $contact['id'] = $to_write['id']; - $contact['uid'] = $to_write['uid']; - $contact['etag'] = $to_write['etag']; - - // if contact is an account and account-relevant data got updated, handle it like account got updated - if ($contact['account_id'] && $isUpdate && - ($old['email'] != $contact['email'] || $old['n_family'] != $contact['n_family'] || $old['n_given'] != $contact['n_given'])) - { - // invalidate the cache of the accounts class - $GLOBALS['egw']->accounts->cache_invalidate($contact['account_id']); - // call edit-accout hook, to let other apps know about changed account (names or email) - $GLOBALS['hook_values'] = $GLOBALS['egw']->accounts->read($contact['account_id']); - $GLOBALS['egw']->hooks->process($GLOBALS['hook_values']+array( - 'location' => 'editaccount', - ),False,True); // called for every app now, not only enabled ones) - } - // notify interested apps about changes in the account-contact data - if (!$to_write['owner'] && $to_write['account_id'] && $isUpdate) - { - $to_write['location'] = 'editaccountcontact'; - $GLOBALS['egw']->hooks->process($to_write,False,True); // called for every app now, not only enabled ones)); - } - // Notify linked apps about changes in the contact data - egw_link::notify_update('addressbook', $contact['id'], $contact); - - // Check for restore of deleted contact, restore held links - if($old && $old['tid'] == addressbook_so::DELETED_TYPE && $contact['tid'] != addressbook_so::DELETED_TYPE) - { - egw_link::restore('addressbook', $contact['id']); - } - - // Record change history for sql - doesn't work for LDAP accounts - if(!$contact['account_id'] || $contact['account_id'] && $this->account_repository == 'sql') - { - $deleted = ($old['tid'] == addressbook_so::DELETED_TYPE || $contact['tid'] == addressbook_so::DELETED_TYPE); - if (!isset($this->tracking)) $this->tracking = new addressbook_tracking($this); - $this->tracking->track($to_write, $old ? $old : null, null, $deleted); - } - } - - return $this->error ? false : $contact['id']; - } - - /** - * Resizes photo to 60*80 pixel and returns it - * - * @param string|FILE $photo string with image or open filedescribtor - * @param int $dst_w=240 max width to resize to - * @return string with resized jpeg photo, null on error - */ - public static function resize_photo($photo,$dst_w=240) - { - if (is_resource($photo)) - { - $photo = stream_get_contents($photo); - } - if (empty($photo) || !($image = imagecreatefromstring($photo))) - { - error_log(__METHOD__."() invalid image!"); - return null; - } - $src_w = imagesx($image); - $src_h = imagesy($image); - //error_log(__METHOD__."() got image $src_w * $src_h, is_jpeg=".array2string(substr($photo,0,2) === "\377\330")); - - // if $photo is to width or not a jpeg image --> resize it - if ($src_w > $dst_w || cut_bytes($photo,0,2) !== "\377\330") - { - //error_log(__METHOD__."(,dst_w=$dst_w) src_w=$src_w, cut_bytes(photo,0,2)=".array2string(cut_bytes($photo,0,2)).' --> resizing'); - // scale the image to a width of 60 and a height according to the proportion of the source image - $resized = imagecreatetruecolor($dst_w,$dst_h = round($src_h * $dst_w / $src_w)); - imagecopyresized($resized,$image,0,0,0,0,$dst_w,$dst_h,$src_w,$src_h); - - ob_start(); - imagejpeg($resized,null,90); - $photo = ob_get_contents(); - ob_end_clean(); - - imagedestroy($resized); - //error_log(__METHOD__."() resized image $src_w*$src_h to $dst_w*$dst_h"); - } - //else error_log(__METHOD__."(,dst_w=$dst_w) src_w=$src_w, cut_bytes(photo,0,2)=".array2string(cut_bytes($photo,0,2)).' --> NOT resizing'); - - imagedestroy($image); - - return $photo; - } - - /** - * reads contacts matched by key and puts all cols in the data array - * - * @param int|string $contact_id - * @param boolean $ignore_acl =false true: no acl check - * @return array|boolean array with contact data, null if not found or false on no view perms - */ - function read($contact_id, $ignore_acl=false) - { - // get so_sql_cf to read private customfields too, if we ignore acl - if ($ignore_acl && is_a($this->somain, 'addressbook_sql')) - { - $cf_backup = (array)$this->somain->customfields; - $this->somain->customfields = egw_customfields::get('addressbook', true); - } - if (!($data = parent::read($contact_id))) - { - $data = null; // not found - } - elseif (!$ignore_acl && !$this->check_perms(EGW_ACL_READ,$data)) - { - $data = false; // no view perms - } - else - { - // determine the file-as type - $data['fileas_type'] = $this->fileas_type($data); - - // Update country name from code - if($data['adr_one_countrycode'] != null) { - $data['adr_one_countryname'] = $GLOBALS['egw']->country->get_full_name($data['adr_one_countrycode'], true); - } - if($data['adr_two_countrycode'] != null) { - $data['adr_two_countryname'] = $GLOBALS['egw']->country->get_full_name($data['adr_two_countrycode'], true); - } - } - if (isset($cf_backup)) - { - $this->somain->customfields = $cf_backup; - } - //error_log(__METHOD__.'('.array2string($contact_id).') returning '.array2string($data)); - return $data; - } - - /** - * Checks if the current user has the necessary ACL rights - * - * If the access of a contact is set to private, one need a private grant for a personal addressbook - * or the group membership for a group-addressbook - * - * @param int $needed necessary ACL right: EGW_ACL_{READ|EDIT|DELETE} - * @param mixed $contact contact as array or the contact-id - * @param boolean $deny_account_delete=false if true never allow to delete accounts - * @param int $user=null for which user to check, default current user - * @return boolean true permission granted, false for permission denied, null for contact does not exist - */ - function check_perms($needed,$contact,$deny_account_delete=false,$user=null) - { - if (!$user) $user = $this->user; - if ($user == $this->user) - { - $grants = $this->grants; - $memberships = $this->memberships; - } - else - { - $grants = $this->get_grants($user); - $memberships = $GLOBALS['egw']->accounts->memberships($user,true); - } - - if ((!is_array($contact) || !isset($contact['owner'])) && - !($contact = parent::read(is_array($contact) ? $contact['id'] : $contact))) - { - return null; - } - $owner = $contact['owner']; - - // allow the user to edit his own account - if (!$owner && $needed == EGW_ACL_EDIT && $contact['account_id'] == $user && $this->own_account_acl) - { - $access = true; - } - // dont allow to delete own account (as admin handels it too) - elseif (!$owner && $needed == EGW_ACL_DELETE && ($deny_account_delete || $contact['account_id'] == $user)) - { - $access = false; - } - // for reading accounts (owner == 0) and account_selection == groupmembers, check if current user and contact are groupmembers - elseif ($owner == 0 && $needed == EGW_ACL_READ && - $GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'groupmembers' && - !isset($GLOBALS['egw_info']['user']['apps']['admin'])) - { - $access = !!array_intersect($memberships,$GLOBALS['egw']->accounts->memberships($contact['account_id'],true)); - } - else - { - $access = ($grants[$owner] & $needed) && - (!$contact['private'] || ($grants[$owner] & EGW_ACL_PRIVATE) || in_array($owner,$memberships)); - } - //error_log(__METHOD__."($needed,$contact[id],$deny_account_delete,$user) returning ".array2string($access)); - return $access; - } - - /** - * Check access to the file store - * - * @param int|array $id id of entry or entry array - * @param int $check EGW_ACL_READ for read and EGW_ACL_EDIT for write or delete access - * @param string $rel_path=null currently not used in InfoLog - * @param int $user=null for which user to check, default current user - * @return boolean true if access is granted or false otherwise - */ - function file_access($id,$check,$rel_path=null,$user=null) - { - return $this->check_perms($check,$id,false,$user); - } - - /** - * Read (virtual) org-entry (values "common" for most contacts in the given org) - * - * @param string $org_id org_name:oooooo|||org_unit:uuuuuuuuu|||adr_one_locality:lllllll (org_unit and adr_one_locality are optional) - * @return array/boolean array with common org fields or false if org not found - */ - function read_org($org_id) - { - if (!$org_id) return false; - if (strpos($org_id,'*AND*')!== false) $org_id = str_replace('*AND*','&',$org_id); - $org = array(); - foreach(explode('|||',$org_id) as $part) - { - list($name,$value) = explode(':',$part,2); - $org[$name] = $value; - } - $csvs = array('cat_id'); // fields with comma-separated-values - - // split regular fields and custom fields - $custom_fields = $regular_fields = array(); - foreach($this->org_fields as $name) - { - if ($name[0] != '#') - { - $regular_fields[] = $name; - } - else - { - $custom_fields[] = $name = substr($name,1); - $regular_fields['id'] = 'id'; - if (substr($this->customfields[$name]['type'],0,6)=='select' && $this->customfields[$name]['rows'] || // multiselection - $this->customfields[$name]['type'] == 'radio') - { - $csvs[] = '#'.$name; - } - } - } - // read the regular fields - $contacts = parent::search('',$regular_fields,'','','',false,'AND',false,$org); - if (!$contacts) return false; - - // if we have custom fields, read and merge them in - if ($custom_fields) - { - foreach($contacts as $contact) - { - $ids[] = $contact['id']; - } - if (($cfs = $this->read_customfields($ids,$custom_fields))) - { - foreach ($contacts as &$contact) - { - $id = $contact['id']; - if (isset($cfs[$id])) - { - foreach($cfs[$id] as $name => $value) - { - $contact['#'.$name] = $value; - } - } - } - unset($contact); - } - } - - // create a statistic about the commonness of each fields values - $fields = array(); - foreach($contacts as $contact) - { - foreach($contact as $name => $value) - { - if (!in_array($name,$csvs)) - { - $fields[$name][$value]++; - } - else - { - // for comma separated fields, we have to use each single value - foreach(explode(',',$value) as $val) - { - $fields[$name][$val]++; - } - } - } - } - foreach($fields as $name => $values) - { - if (!in_array($name,$this->org_fields)) continue; - - arsort($values,SORT_NUMERIC); - list($value,$num) = each($values); - //echo "

$name: '$value' $num/".count($contacts)."=".($num / (double) count($contacts))." >= $this->org_common_factor = ".($num / (double) count($contacts) >= $this->org_common_factor ? 'true' : 'false')."

\n"; - if ($value && $num / (double) count($contacts) >= $this->org_common_factor) - { - if (!in_array($name,$csvs)) - { - $org[$name] = $value; - } - else - { - $org[$name] = array(); - foreach ($values as $value => $num) - { - if ($value && $num / (double) count($contacts) >= $this->org_common_factor) - { - $org[$name][] = $value; - } - } - $org[$name] = implode(',',$org[$name]); - } - } - } - return $org; - } - - /** - * Return all org-members with same content in one or more of the given fields (only org_fields are counting) - * - * @param string $org_name - * @param array $fields field-name => value pairs - * @return array with contacts - */ - function org_similar($org_name,$fields) - { - $criteria = array(); - foreach($this->org_fields as $name) - { - if (isset($fields[$name])) - { - if (empty($fields[$name])) - { - $criteria[] = "($name IS NULL OR $name='')"; - } - else - { - $criteria[$name] = $fields[$name]; - } - } - } - return parent::search($criteria,false,'n_family,n_given','','',false,'OR',false,array('org_name'=>$org_name)); - } - - /** - * Return the changed fields from two versions of a contact (not modified or modifier) - * - * @param array $from original/old version of the contact - * @param array $to changed/new version of the contact - * @param boolean $onld_org_fields=true check and return only org_fields, default true - * @return array with field-name => value from $from - */ - function changed_fields($from,$to,$only_org_fields=true) - { - // we only care about countryname, if contrycode is empty - foreach(array( - 'adr_one_countryname' => 'adr_one_countrycode', - 'adr_two_countryname' => 'adr_one_countrycode', - ) as $name => $code) - { - if (!empty($from[$code])) $from[$name] = ''; - if (!empty($to[$code])) $to[$name] = ''; - } - $changed = array(); - foreach($only_org_fields ? $this->org_fields : array_keys($this->contact_fields) as $name) - { - if (in_array($name,array('modified','modifier'))) // never count these - { - continue; - } - if ((string) $from[$name] != (string) $to[$name]) - { - $changed[$name] = $from[$name]; - } - } - return $changed; - } - - /** - * Change given fields in all members of the org with identical content in the field - * - * @param string $org_name - * @param array $from original/old version of the contact - * @param array $to changed/new version of the contact - * @param array $members=null org-members to change, default null --> function queries them itself - * @return array/boolean (changed-members,changed-fields,failed-members) or false if no org_fields changed or no (other) members matching that fields - */ - function change_org($org_name,$from,$to,$members=null) - { - if (!($changed = $this->changed_fields($from,$to,true))) return false; - - if (is_null($members) || !is_array($members)) - { - $members = $this->org_similar($org_name,$changed); - } - if (!$members) return false; - - $ids = array(); - foreach($members as $member) - { - $ids[] = $member['id']; - } - $customfields = $this->read_customfields($ids); - - $changed_members = $changed_fields = $failed_members = 0; - foreach($members as $member) - { - if (isset($customfields[$member['id']])) - { - foreach($this->customfields as $name => $data) - { - $member['#'.$name] = $customfields[$member['id']][$name]; - } - } - $fields = 0; - foreach($changed as $name => $value) - { - if ((string)$value == (string)$member[$name]) - { - $member[$name] = $to[$name]; - //echo "

$member[n_family], $member[n_given]: $name='{$to[$name]}'

\n"; - ++$fields; - } - } - if ($fields) - { - if (!$this->check_perms(EGW_ACL_EDIT,$member) || !$this->save($member)) - { - ++$failed_members; - } - else - { - ++$changed_members; - $changed_fields += $fields; - } - } - } - return array($changed_members,$changed_fields,$failed_members); - } - - /** - * get title for a contact identified by $contact - * - * Is called as hook to participate in the linking. The format is determined by the link_title preference. - * - * @param int/string/array $contact int/string id or array with contact - * @return string/boolean string with the title, null if contact does not exitst, false if no perms to view it - */ - function link_title($contact) - { - if (!is_array($contact) && $contact) - { - $contact = $this->read($contact); - } - if (!is_array($contact)) - { - return $contact; - } - $type = $this->prefs['link_title']; - if (!$type || $type === 'n_fileas') - { - if ($contact['n_fileas']) return $contact['n_fileas']; - $type = null; - } - $title = $this->fileas($contact,$type); - if ($this->prefs['link_title_cf'] && $contact['#'.$this->prefs['link_title_cf']]) - { - $title .= ' ' . $contact['#'.$this->prefs['link_title_cf']]; - } - return $title ; - } - - /** - * get title for multiple contacts identified by $ids - * - * Is called as hook to participate in the linking. The format is determined by the link_title preference. - * - * @param array $ids array with contact-id's - * @return array with titles, see link_title - */ - function link_titles(array $ids) - { - $titles = array(); - if (($contacts =& $this->search(array('contact_id' => $ids),false))) - { - $ids = array(); - foreach($contacts as $contact) - { - $ids[] = $contact['id']; - } - $cfs = $this->read_customfields($ids); - foreach($contacts as $contact) - { - $titles[$contact['id']] = $this->link_title($contact+(array)$cfs[$contact['id']]); - } - } - // we assume all not returned contacts are not readable for the user (as we report all deleted contacts to egw_link) - foreach($ids as $id) - { - if (!isset($titles[$id])) - { - $titles[$id] = false; - } - } - return $titles; - } - - /** - * query addressbook for contacts matching $pattern - * - * Is called as hook to participate in the linking - * - * @param string|array $pattern pattern to search, or an array with a 'search' key - * @param array $options Array of options for the search - * @return array with id - title pairs of the matching entries - */ - function link_query($pattern, Array &$options = array()) - { - $result = $criteria = array(); - $limit = false; - if ($pattern) - { - $criteria = is_array($pattern) ? $pattern['search'] : $pattern; - } - if($options['start'] || $options['num_rows']) - { - $limit = array($options['start'], $options['num_rows']); - } - $filter = (array)$options['filter']; - if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts']) $filter['account_id'] = null; - if (($contacts =& parent::search($criteria,false,'org_name,n_family,n_given,cat_id,contact_email','','%',false,'OR', $limit, $filter))) - { - $ids = array(); - foreach($contacts as $contact) - { - $ids[] = $contact['id']; - } - $cfs = $this->read_customfields($ids); - foreach($contacts as $contact) - { - $result[$contact['id']] = $this->link_title($contact+(array)$cfs[$contact['id']]); - // make sure to return a correctly quoted rfc822 address, if requested - if ($options['type'] === 'email') - { - $args = explode('@', $contact['email']); - $args[] = $result[$contact['id']]; - $result[$contact['id']] = call_user_func_array('imap_rfc822_write_address', $args); - } - // show category color - if ($contact['cat_id'] && ($color = etemplate::cats2color($contact['cat_id']))) - { - $result[$contact['id']] = array( - 'label' => $result[$contact['id']], - 'style.backgroundColor' => $color, - ); - } - } - } - $options['total'] = $this->total; - return $result; - } - - /** - * Query for subtype email (returns only contacts with email address set) - * - * @param string|array $pattern - * @param array $options - * @return Ambigous string > - */ - function link_query_email($pattern, Array &$options = array()) - { - if (isset($options['filter']) && !is_array($options['filter'])) - { - $options['filter'] = (array)$options['filter']; - } - // return only contacts with email set - $options['filter'][] = "contact_email ".$this->db->capabilities[egw_db::CAPABILITY_CASE_INSENSITIV_LIKE]." '%@%'"; - - // let link query know, to append email to list - $options['type'] = 'email'; - - return $this->link_query($pattern,$options); - } - - /** - * returns info about contacts for calender - * - * @param int/array $ids single contact-id or array of id's - * @return array - */ - function calendar_info($ids) - { - if (!$ids) return null; - - $data = array(); - foreach(!is_array($ids) ? array($ids) : $ids as $id) - { - if (!($contact = $this->read($id))) continue; - - $data[] = array( - 'res_id' => $id, - 'email' => $contact['email'] ? $contact['email'] : $contact['email_home'], - 'rights' => EGW_ACL_READ_FOR_PARTICIPANTS, - 'name' => $this->link_title($contact), - 'cn' => trim($contact['n_given'].' '.$contact['n_family']), - ); - } - //echo "

calendar_info(".print_r($ids,true).")="; _debug_array($data); - return $data; - } - - /** - * Read the next and last event of given contacts - * - * @param array $ids contact_id's - * @param boolean $extra_title=true if true, use a short date only title and put the full title as extra_title (tooltip) - * @return array - */ - function read_calendar($ids,$extra_title=true) - { - if (!$GLOBALS['egw_info']['user']['apps']['calendar']) return array(); - - $uids = array(); - foreach($ids as $id) - { - if (is_numeric($id)) $uids[] = 'c'.$id; - } - if (!$uids) return array(); - - $bocal = new calendar_bo(); - $events = $bocal->search(array( - 'users' => $uids, - 'enum_recuring' => true, - )); - if (!$events) return array(); - - //_debug_array($events); - $calendars = array(); - foreach($events as $event) - { - foreach($event['participants'] as $uid => $status) - { - if ($uid[0] != 'c' || ($status == 'R' && !$GLOBALS['egw_info']['user']['preferences']['calendar']['show_rejected'])) - { - continue; - } - $id = (int)substr($uid,1); - - if ($event['start'] < $this->now_su) // past event --> check for last event - { - if (!isset($calendars[$id]['last_event']) || $event['start'] > $calendars[$id]['last_event']) - { - $calendars[$id]['last_event'] = $event['start']; - $link = array( - 'id' => $event['id'], - 'app' => 'calendar', - 'title' => $bocal->link_title($event), - 'extra_args' => array( - 'date' => date('Ymd',$event['start']), - ), - ); - if ($extra_title) - { - $link['extra_title'] = $link['title']; - $link['title'] = date($GLOBALS['egw_info']['user']['preferences']['common']['dateformat'],$event['start']); - } - $calendars[$id]['last_link'] = $link; - } - } - else // future event --> check for next event - { - if (!isset($calendars[$id]['next_event']) || $event['start'] < $calendars[$id]['next_event']) - { - $calendars[$id]['next_event'] = $event['start']; - $link = array( - 'id' => $event['id'], - 'app' => 'calendar', - 'title' => $bocal->link_title($event), - 'extra_args' => array( - 'date' => date('Ymd',$event['start']), - ), - ); - if ($extra_title) - { - $link['extra_title'] = $link['title']; - $link['title'] = date($GLOBALS['egw_info']['user']['preferences']['common']['dateformat'],$event['start']); - } - $calendars[$id]['next_link'] = $link; - } - } - } - } - return $calendars; - } - - /** - * Called by delete-account hook, when an account get deleted --> deletes/moves the personal addressbook - * - * @param array $data - */ - function deleteaccount($data) - { - // delete/move personal addressbook - parent::deleteaccount($data); - } - - /** - * Called by delete_category hook, when a category gets deleted. - * Removes the category from addresses - */ - function delete_category($data) - { - // get all cats if you want to drop sub cats - $drop_subs = ($data['drop_subs'] && !$data['modify_subs']); - if($drop_subs) - { - $cats = new categories('', 'addressbook'); - $cat_ids = $cats->return_all_children($data['cat_id']); - } - else - { - $cat_ids = array($data['cat_id']); - } - - // Get addresses that use the category - @set_time_limit( 0 ); - foreach($cat_ids as $cat_id) - { - if (($ids = $this->search(array('cat_id' => $cat_id), false))) - { - foreach($ids as &$info) - { - $info['cat_id'] = implode(',',array_diff(explode(',',$info['cat_id']), $cat_ids)); - $this->save($info); - } - } - } - } - - /** - * Called by edit-account hook, when an account get edited --> not longer used - * - * This function is still there, to not give a fatal error, if the hook still exists. - * Can be removed after the next db-update, which also reloads the hooks. RalfBecker 2006/09/18 - * - * @param array $data - */ - function editaccount($data) - { - // just force a new registration of the addressbook hooks - include(EGW_INCLUDE_ROOT.'/addressbook/setup/setup.inc.php'); - $GLOBALS['egw']->hooks->register_hooks('addressbook',$setup_info['addressbook']['hooks']); - } - - /** - * Merges some given addresses into the first one and delete the others - * - * If one of the other addresses is an account, everything is merged into the account. - * If two accounts are in $ids, the function fails (returns false). - * - * @param array $ids contact-id's to merge - * @return int number of successful merged contacts, false on a fatal error (eg. cant merge two accounts) - */ - function merge($ids) - { - $this->error = false; - $account = null; - $custom_fields = config::get_customfields('addressbook', true); - $custom_field_list = $this->read_customfields($ids); - foreach(parent::search(array('id'=>$ids),false) as $contact) // $this->search calls the extended search from ui! - { - if ($contact['account_id']) - { - if (!is_null($account)) - { - echo $this->error = 'Can not merge more then one account!'; - return false; // we dont deal with two accounts! - } - $account = $contact; - continue; - } - // Add in custom fields - if (is_array($custom_field_list[$contact['id']])) $contact = array_merge($contact, $custom_field_list[$contact['id']]); - - $pos = array_search($contact['id'],$ids); - $contacts[$pos] = $contact; - } - if (!is_null($account)) // we found an account, so we merge the contacts into it - { - $target = $account; - unset($account); - } - else // we found no account, so we merge all but the first into the first - { - $target = $contacts[0]; - unset($contacts[0]); - } - if (!$this->check_perms(EGW_ACL_EDIT,$target)) - { - echo $this->error = 'No edit permission for the target contact!'; - return 0; - } - foreach($contacts as $contact) - { - foreach($contact as $name => $value) - { - if (!$value) continue; - - switch($name) - { - case 'id': - case 'tid': - case 'owner': - case 'private': - case 'etag'; - break; // ignored - - case 'cat_id': // cats are all merged together - if (!is_array($target['cat_id'])) $target['cat_id'] = $target['cat_id'] ? explode(',',$target['cat_id']) : array(); - $target['cat_id'] = array_unique(array_merge($target['cat_id'],is_array($value)?$value:explode(',',$value))); - break; - - default: - // Multi-select custom fields can also be merged - if($name[0] == '#') { - $c_name = substr($name, 1); - if($custom_fields[$c_name]['type'] == 'select' && $custom_fields[$c_name]['rows'] > 1) { - if (!is_array($target[$name])) $target[$name] = $target[$name] ? explode(',',$target[$name]) : array(); - $target[$name] = implode(',',array_unique(array_merge($target[$name],is_array($value)?$value:explode(',',$value)))); - } - } - if (!$target[$name]) $target[$name] = $value; - break; - } - } - } - if (!$this->save($target)) return 0; - - $success = 1; - foreach($contacts as $contact) - { - if (!$this->check_perms(EGW_ACL_DELETE,$contact)) - { - continue; - } - foreach(egw_link::get_links('addressbook',$contact['id']) as $data) - { - //_debug_array(array('function'=>__METHOD__,'line'=>__LINE__,'app'=>'addressbook','id'=>$contact['id'],'data:'=>$data,'target'=>$target['id'])); - // info_from and info_link_id (main link) - $newlinkID = egw_link::link('addressbook',$target['id'],$data['app'],$data['id'],$data['remark'],$target['owner']); - //_debug_array(array('newLinkID'=>$newlinkID)); - if ($newlinkID) - { - // update egw_infolog set info_link_id=$newlinkID where info_id=$data['id'] and info_link_id=$data['link_id'] - if ($data['app']=='infolog') - { - $this->db->update('egw_infolog',array( - 'info_link_id' => $newlinkID - ),array( - 'info_id' => $data['id'], - 'info_link_id' => $data['link_id'] - ),__LINE__,__FILE__,'infolog'); - } - unset($newlinkID); - } - } - if ($this->delete($contact['id'])) $success++; - } - return $success; - } - - /** - * Some caching for lists within request - * - * @var array - */ - private static $list_cache = array(); - - /** - * Check if user has required rights for a list or list-owner - * - * @param int $list - * @param int $required - * @param int $owner=null - * @return boolean - */ - function check_list($list,$required,$owner=null) - { - if ($list && ($list_data = $this->read_list($list))) - { - $owner = $list_data['list_owner']; - } - //error_log(__METHOD__."($list, $required, $owner) grants[$owner]=".$this->grants[$owner]." returning ".array2string(!!($this->grants[$owner] & $required))); - return !!($this->grants[$owner] & $required); - } - - /** - * Adds / updates a distribution list - * - * @param string|array $keys list-name or array with column-name => value pairs to specify the list - * @param int $owner user- or group-id - * @param array $contacts=array() contacts to add (only for not yet existing lists!) - * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name' - * @return int|boolean integer list_id or false on error - */ - function add_list($keys,$owner,$contacts=array(),array &$data=array()) - { - if (!$this->check_list(null,EGW_ACL_ADD|EGW_ACL_EDIT,$owner)) return false; - - try { - $ret = parent::add_list($keys,$owner,$contacts,$data); - if ($ret) unset(self::$list_cache[$ret]); - } - // catch sql error, as creating same name&owner list gives a sql error doublicate key - catch(egw_exception_db_invalid_sql $e) { - return false; - } - return $ret; - } - - /** - * Adds contacts to a distribution list - * - * @param int|array $contact contact_id(s) - * @param int $list list-id - * @param array $existing=null array of existing contact-id(s) of list, to not reread it, eg. array() - * @return false on error - */ - function add2list($contact,$list,array $existing=null) - { - if (!$this->check_list($list,EGW_ACL_EDIT)) return false; - - unset(self::$list_cache[$list]); - - return parent::add2list($contact,$list,$existing); - } - - /** - * Removes one contact from distribution list(s) - * - * @param int|array $contact contact_id(s) - * @param int $list list-id - * @return false on error - */ - function remove_from_list($contact,$list=null) - { - if ($list && !$this->check_list($list,EGW_ACL_EDIT)) return false; - - if ($list) - { - unset(self::$list_cache[$list]); - } - else - { - self::$list_cache = array(); - } - - return parent::remove_from_list($contact,$list); - } - - /** - * Deletes a distribution list (incl. it's members) - * - * @param int/array $list list_id(s) - * @return number of members deleted or false if list does not exist - */ - function delete_list($list) - { - if (!$this->check_list($list,EGW_ACL_DELETE)) return false; - - foreach((array)$list as $l) - { - unset(self::$list_cache[$l]); - } - - return parent::delete_list($list); - } - - /** - * Read data of a distribution list - * - * @param int $list list_id - * @return array of data or false if list does not exist - */ - function read_list($list) - { - if (isset(self::$list_cache[$list])) return self::$list_cache[$list]; - - return self::$list_cache[$list] = parent::read_list($list); - } - - /** - * Get the address-format of a country - * - * This is a good reference where I got nearly all information, thanks to mikaelarhelger-AT-gmail.com - * http://www.bitboost.com/ref/international-address-formats.html - * - * Mail me (RalfBecker-AT-outdoor-training.de) if you want your nation added or fixed. - * - * @param string $country - * @return string 'city_state_postcode' (eg. US) or 'postcode_city' (eg. DE) - */ - function addr_format_by_country($country) - { - $code = $GLOBALS['egw']->country->country_code($country); - - switch($code) - { - case 'AU': - case 'CA': - case 'GB': // not exactly right, postcode is in separate line - case 'HK': // not exactly right, they have no postcode - case 'IN': - case 'ID': - case 'IE': // not exactly right, they have no postcode - case 'JP': // not exactly right - case 'KR': - case 'LV': - case 'NZ': - case 'TW': - case 'SA': // not exactly right, postcode is in separate line - case 'SG': - case 'US': - $adr_format = 'city_state_postcode'; - break; - - case 'AR': - case 'AT': - case 'BE': - case 'CH': - case 'CZ': - case 'DK': - case 'EE': - case 'ES': - case 'FI': - case 'FR': - case 'DE': - case 'GL': - case 'IS': - case 'IL': - case 'IT': - case 'LT': - case 'LU': - case 'MY': - case 'MX': - case 'NL': - case 'NO': - case 'PL': - case 'PT': - case 'RO': - case 'RU': - case 'SE': - $adr_format = 'postcode_city'; - break; - - default: - $adr_format = $this->prefs['addr_format'] ? $this->prefs['addr_format'] : 'postcode_city'; - } - //echo "

bocontacts::addr_format_by_country('$country'='$code') = '$adr_format'

\n"; - return $adr_format; - } - - /** - * Find existing categories in database by name or add categories that do not exist yet - * currently used for vcard import - * - * @param array $catname_list names of the categories which should be found or added - * @param int $contact_id=null match against existing contact and expand the returned category ids - * by the ones the user normally does not see due to category permissions - used to preserve categories - * @return array category ids (found, added and preserved categories) - */ - function find_or_add_categories($catname_list, $contact_id=null) - { - if ($contact_id && $contact_id > 0 && ($old_contact = $this->read($contact_id))) - { - // preserve categories without users read access - $old_categories = explode(',',$old_contact['cat_id']); - $old_cats_preserve = array(); - if (is_array($old_categories) && count($old_categories) > 0) - { - foreach ($old_categories as $cat_id) - { - if (!$this->categories->check_perms(EGW_ACL_READ, $cat_id)) - { - $old_cats_preserve[] = $cat_id; - } - } - } - } - - $cat_id_list = array(); - foreach ((array)$catname_list as $cat_name) - { - $cat_name = trim($cat_name); - $cat_id = $this->categories->name2id($cat_name, 'X-'); - if (!$cat_id) - { - // some SyncML clients (mostly phones) add an X- to the category names - if (strncmp($cat_name, 'X-', 2) == 0) - { - $cat_name = substr($cat_name, 2); - } - $cat_id = $this->categories->add(array('name' => $cat_name, 'descr' => $cat_name, 'access' => 'private')); - } - - if ($cat_id) - { - $cat_id_list[] = $cat_id; - } - } - - if (is_array($old_cats_preserve) && count($old_cats_preserve) > 0) - { - $cat_id_list = array_merge($cat_id_list, $old_cats_preserve); - } - - if (count($cat_id_list) > 1) - { - $cat_id_list = array_unique($cat_id_list); - sort($cat_id_list, SORT_NUMERIC); - } - - //error_log(__METHOD__."(".array2string($catname_list).", $contact_id) returning ".array2string($cat_id_list)); - return $cat_id_list; - } - - function get_categories($cat_id_list) - { - if (!is_object($this->categories)) - { - $this->categories = new categories($this->user,'addressbook'); - } - - if (!is_array($cat_id_list)) - { - $cat_id_list = explode(',',$cat_id_list); - } - $cat_list = array(); - foreach($cat_id_list as $cat_id) - { - if ($cat_id && $this->categories->check_perms(EGW_ACL_READ, $cat_id) && - ($cat_name = $this->categories->id2name($cat_id)) && $cat_name != '--') - { - $cat_list[] = $cat_name; - } - } - - return $cat_list; - } - - function fixup_contact(&$contact) - { - if (empty($contact['n_fn'])) - { - $contact['n_fn'] = $this->fullname($contact); - } - - if (empty($contact['n_fileas'])) - { - $contact['n_fileas'] = $this->fileas($contact); - } - } - - /** - * Try to find a matching db entry - * - * @param array $contact the contact data we try to find - * @param boolean $relax=false if asked to relax, we only match against some key fields - * @return array od matching contact_ids - */ - function find_contact($contact, $relax=false) - { - $empty_addr_one = $empty_addr_two = true; - - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ - . '('. ($relax ? 'RELAX': 'EXACT') . ')[ContactData]:' - . array2string($contact) - . "\n", 3, $this->logfile); - } - - $matchingContacts = array(); - if ($contact['id'] && ($found = $this->read($contact['id']))) - { - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ - . '()[ContactID]: ' . $contact['id'] - . "\n", 3, $this->logfile); - } - // We only do a simple consistency check - if (!$relax || ((empty($found['n_family']) || $found['n_family'] == $contact['n_family']) - && (empty($found['n_given']) || $found['n_given'] == $contact['n_given']) - && (empty($found['org_name']) || $found['org_name'] == $contact['org_name']))) - { - return array($found['id']); - } - } - unset($contact['id']); - - if (!$relax && !empty($contact['uid'])) - { - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ - . '()[ContactUID]: ' . $contact['uid'] - . "\n", 3, $this->logfile); - } - // Try the given UID first - $criteria = array ('contact_uid' => $contact['uid']); - if (($foundContacts = parent::search($criteria))) - { - foreach ($foundContacts as $egwContact) - { - $matchingContacts[] = $egwContact['id']; - } - } - return $matchingContacts; - } - unset($contact['uid']); - - $columns_to_search = array('n_family', 'n_given', 'n_middle', 'n_prefix', 'n_suffix', - 'bday', 'org_name', 'org_unit', 'title', 'role', - 'email', 'email_home'); - $tolerance_fields = array('n_middle', 'n_prefix', 'n_suffix', - 'bday', 'org_unit', 'title', 'role', - 'email', 'email_home'); - $addr_one_fields = array('adr_one_street', 'adr_one_locality', - 'adr_one_region', 'adr_one_postalcode'); - $addr_two_fields = array('adr_two_street', 'adr_two_locality', - 'adr_two_region', 'adr_two_postalcode'); - - if (!empty($contact['owner'])) - { - $columns_to_search += array('owner'); - } - - $result = false; - - $criteria = array(); - - foreach ($columns_to_search as $field) - { - if ($relax && in_array($field, $tolerance_fields)) continue; - - if (empty($contact[$field])) - { - // Not every device supports all fields - if (!in_array($field, $tolerance_fields)) - { - $criteria[$field] = ''; - } - } - else - { - $criteria[$field] = $contact[$field]; - } - } - - if (!$relax) - { - // We use addresses only for strong matching - - foreach ($addr_one_fields as $field) - { - if (empty($contact[$field])) - { - $criteria[$field] = ''; - } - else - { - $empty_addr_one = false; - $criteria[$field] = $contact[$field]; - } - } - - foreach ($addr_two_fields as $field) - { - if (empty($contact[$field])) - { - $criteria[$field] = ''; - } - else - { - $empty_addr_two = false; - $criteria[$field] = $contact[$field]; - } - } - } - - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ - . '()[Addressbook FIND Step 1]: ' - . 'CRITERIA = ' . array2string($criteria) - . "\n", 3, $this->logfile); - } - - // first try full match - if (($foundContacts = parent::search($criteria, true, '', '', '', true))) - { - foreach ($foundContacts as $egwContact) - { - $matchingContacts[] = $egwContact['id']; - } - } - - // No need for more searches for relaxed matching - if ($relax || count($matchingContacts)) return $matchingContacts; - - - if (!$empty_addr_one && $empty_addr_two) - { - // try given address and ignore the second one in EGW - foreach ($addr_two_fields as $field) - { - unset($criteria[$field]); - } - - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ - . '()[Addressbook FIND Step 2]: ' - . 'CRITERIA = ' . array2string($criteria) - . "\n", 3, $this->logfile); - } - - if (($foundContacts = parent::search($criteria, true, '', '', '', true))) - { - foreach ($foundContacts as $egwContact) - { - $matchingContacts[] = $egwContact['id']; - } - } - else - { - // try address as home address -- some devices don't qualify addresses - foreach ($addr_two_fields as $key => $field) - { - $criteria[$field] = $criteria[$addr_one_fields[$key]]; - unset($criteria[$addr_one_fields[$key]]); - } - - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ - . '()[Addressbook FIND Step 3]: ' - . 'CRITERIA = ' . array2string($criteria) - . "\n", 3, $this->logfile); - } - - if (($foundContacts = parent::search($criteria, true, '', '', '', true))) - { - foreach ($foundContacts as $egwContact) - { - $matchingContacts[] = $egwContact['id']; - } - } - } - } - elseif (!$empty_addr_one && !$empty_addr_two) - { // try again after address swap - - foreach ($addr_one_fields as $key => $field) - { - $_temp = $criteria[$field]; - $criteria[$field] = $criteria[$addr_two_fields[$key]]; - $criteria[$addr_two_fields[$key]] = $_temp; - } - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ - . '()[Addressbook FIND Step 4]: ' - . 'CRITERIA = ' . array2string($criteria) - . "\n", 3, $this->logfile); - } - if (($foundContacts = parent::search($criteria, true, '', '', '', true))) - { - foreach ($foundContacts as $egwContact) - { - $matchingContacts[] = $egwContact['id']; - } - } - } - if ($this->log) - { - error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ - . '()[FOUND]: ' . array2string($matchingContacts) - . "\n", 3, $this->logfile); - } - return $matchingContacts; - } - - /** - * Get a ctag (collection tag) for one addressbook or all addressbooks readable by a user - * - * Currently implemented as maximum modification date (1 seconde granularity!) - * - * We have to include deleted entries, as otherwise the ctag will not change if an entry gets deleted! - * (Only works if tracking of deleted entries / history is switched on!) - * - * @param int|array $owner=null 0=accounts, null=all addressbooks or integer account_id of user or group - * @return string - */ - public function get_ctag($owner=null) - { - $filter = array('tid' => null); // tid=null --> use all entries incl. deleted (tid='D') - // show addressbook of a single user? - if (!is_null($owner)) $filter['owner'] = $owner; - - // should we hide the accounts addressbook - if (!$owner && $GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts']) - { - $filter['account_id'] = null; - } - $result = $this->search(array(),'contact_modified','contact_modified DESC','','',false,'AND',array(0,1),$filter); - - if (!$result || !isset($result[0]['modified'])) - { - $ctag = 'empty'; // ctag for empty addressbook - } - else - { - // need to convert modified time back to server-time (was converted to user-time by search) - // as we use it direct in server-queries eg. CardDAV sync-report and to be consistent with CalDAV - $ctag = egw_time::user2server($result[0]['modified']); - } - //error_log(__METHOD__.'('.array2string($owner).') returning '.array2string($ctag)); - return $ctag; - } - static public $pgp_key_regexp = '/-----BEGIN PGP PUBLIC KEY BLOCK-----.*-----END PGP PUBLIC KEY BLOCK-----\r?\n/s'; /** @@ -2529,7 +153,7 @@ class addressbook_bo extends addressbook_so } if ($update) { - config::save_value('own_account_acl', $this->own_account_acl, 'phpgwapi'); + Config::save_value('own_account_acl', $this->own_account_acl, 'phpgwapi'); } } $criteria = array(); diff --git a/addressbook/inc/class.addressbook_contactform.inc.php b/addressbook/inc/class.addressbook_contactform.inc.php index 933746286c..860af85663 100644 --- a/addressbook/inc/class.addressbook_contactform.inc.php +++ b/addressbook/inc/class.addressbook_contactform.inc.php @@ -5,11 +5,13 @@ * @link http://www.egroupware.org * @author Ralf Becker * @package addressbook - * @copyright (c) 2007-15 by Ralf Becker + * @copyright (c) 2007-16 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ +use EGroupware\Api; + /** * SiteMgr contact form for the addressbook * @@ -75,7 +77,7 @@ class addressbook_contactform elseif ($content['submitit']) { $submitted = true; - $contact = new addressbook_bo(); + $contact = new Api\Contacts(); if ($content['owner']) // save the contact in the addressbook { $content['private'] = 0; // in case default_private is set @@ -90,9 +92,9 @@ class addressbook_contactform // the anonymous user to have run rights for addressbook AND // edit rights for the addressbook used to store the new entry, // which is clearly not wanted securitywise - egw_vfs::$is_root = true; + Api\Vfs::$is_root = true; egw_link::link('addressbook',$id,egw_link::VFS_APPNAME,$value,$name); - egw_vfs::$is_root = false; + Api\Vfs::$is_root = false; } } @@ -108,8 +110,7 @@ class addressbook_contactform { if ($content['email_contactform']) { - require_once(EGW_INCLUDE_ROOT.'/addressbook/inc/class.addressbook_tracking.inc.php'); - $tracking = new addressbook_tracking($contact); + $tracking = new Api\Contacts\Tracking($contact); } if ($tracking->do_notifications($contact->data2db($content),null)) { @@ -141,7 +142,7 @@ class addressbook_contactform static $contact; if (is_null($contact)) { - $contact = new addressbook_bo(); + $contact = new Api\Contacts(); } $content['show']['custom'.$custom] = true; $content['customfield'][$custom] = $name; @@ -174,7 +175,7 @@ class addressbook_contactform if ($name[0] == '#') // custom field { static $contact; - if (is_null($contact)) $contact = new addressbook_bo(); + if (is_null($contact)) $contact = new Api\Contacts(); $content['show']['custom'.$custom] = true; $content['customfield'][$custom] = $name; $content['customlabel'][$custom] = $contact->customfields[substr($name,1)]['label']; diff --git a/addressbook/inc/class.addressbook_groupdav.inc.php b/addressbook/inc/class.addressbook_groupdav.inc.php index 8f4eee0d32..fb85d1c3eb 100644 --- a/addressbook/inc/class.addressbook_groupdav.inc.php +++ b/addressbook/inc/class.addressbook_groupdav.inc.php @@ -7,10 +7,12 @@ * @package addressbook * @subpackage groupdav * @author Ralf Becker - * @copyright (c) 2007-15 by Ralf Becker + * @copyright (c) 2007-16 by Ralf Becker * @version $Id$ */ +use EGroupware\Api; + /** * EGroupware: GroupDAV access: addressbook handler * @@ -953,7 +955,7 @@ class addressbook_groupdav extends groupdav_handler if (is_null($non_deleted_tids)) { $non_deleted_tids = $this->bo->content_types; - unset($non_deleted_tids[addressbook_so::DELETED_TYPE]); + unset($non_deleted_tids[Api\Contacts::DELETED_TYPE]); $non_deleted_tids = array_keys($non_deleted_tids); } $contact = $this->bo->read(array(self::$path_attr => $id, 'tid' => $non_deleted_tids)); @@ -1000,7 +1002,7 @@ class addressbook_groupdav extends groupdav_handler $contact = null; } - if ($contact && $contact['tid'] == addressbook_so::DELETED_TYPE) + if ($contact && $contact['tid'] == Api\Contacts::DELETED_TYPE) { $contact = null; // handle deleted events, as not existing (404 Not Found) } diff --git a/addressbook/inc/class.addressbook_import_contacts_csv.inc.php b/addressbook/inc/class.addressbook_import_contacts_csv.inc.php index 695db5181f..721ef633e5 100644 --- a/addressbook/inc/class.addressbook_import_contacts_csv.inc.php +++ b/addressbook/inc/class.addressbook_import_contacts_csv.inc.php @@ -1,15 +1,16 @@ * @copyright Cornelius Weiss - * @version $Id: $ + * @version $Id$ */ +use EGroupware\Api; /** * class import_csv for addressbook @@ -29,8 +30,10 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { private $bocontacts; /** - * For figuring out if a contact has changed - */ + * For figuring out if a contact has changed + * + * @var Api\Contacts\Tracking + */ protected $tracking; /** @@ -48,10 +51,10 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { public function init(importexport_definition &$_definition ) { // fetch the addressbook bo - $this->bocontacts = new addressbook_bo(); + $this->bocontacts = new Api\Contacts(); // Get the tracker for changes - $this->tracking = new addressbook_tracking($this->bocontacts); + $this->tracking = new Api\Contacts\Tracking($this->bocontacts); $this->lookups = array( 'tid' => array('n'=>'contact') @@ -138,10 +141,10 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { // Also handle categories in their own field $record_array = $record->get_record_array(); $more_categories = array(); - foreach($this->definition->plugin_options['field_mapping'] as $number => $field_name) { + foreach($this->definition->plugin_options['field_mapping'] as $field_name) { if(!array_key_exists($field_name, $record_array) || substr($field_name,0,3) != 'cat' || !$record->$field_name || $field_name == 'cat_id') continue; - list($cat, $cat_id) = explode('-', $field_name); + list(, $cat_id) = explode('-', $field_name); if(is_numeric($record->$field_name) && $record->$field_name != 1) { // Column has a single category ID $more_categories[] = $record->$field_name; @@ -175,7 +178,7 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { if($record_array[$condition['string']]) { $searchcondition = array( $condition['string'] => $record_array[$condition['string']]); // if we use account_id for the condition, we need to set the owner for filtering, as this - // enables addressbook_so to decide what backend is to be used + // enables Api\Contacts\Storage to decide what backend is to be used if ($condition['string']=='account_id') $searchcondition['owner']=0; $contacts = $this->bocontacts->search( //array( $condition['string'] => $record[$condition['string']],), @@ -204,7 +207,7 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { break; case 'equal': // Match on field - $result = $this->equal($record, $condition, $matches); + $result = $this->equal($record, $condition); if($result) { // Apply true action to any matching records found @@ -270,7 +273,8 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { $old = $this->bocontacts->read($_data['id']); // if we get countrycodes as countryname, try to translate them -> the rest should be handled by bo classes. foreach(array('adr_one_', 'adr_two_') as $c_prefix) { - if (strlen(trim($_data[$c_prefix.'countryname']))==2) $_data[$c_prefix.'countryname'] = $GLOBALS['egw']->country->get_full_name(trim($_data[$c_prefix.'countryname']),$translated=true); + if (strlen(trim($_data[$c_prefix.'countryname']))==2) + $_data[$c_prefix.'countryname'] = $GLOBALS['egw']->country->get_full_name(trim($_data[$c_prefix.'countryname']), true); } // Don't change a user account into a contact if($old['owner'] == 0) { @@ -288,7 +292,7 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { } else { //error_log(__METHOD__.__LINE__.array2string($changed).' Old:'.$old['adr_one_countryname'].' ('.$old['adr_one_countrycode'].') New:'.$_data['adr_one_countryname'].' ('.$_data['adr_one_countryname'].')'); } - + // Make sure n_fn gets updated unset($_data['n_fn']); @@ -320,7 +324,7 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { } default: throw new egw_exception('Unsupported action: '. $_action); - + } } @@ -412,4 +416,3 @@ class addressbook_import_contacts_csv extends importexport_basic_import_csv { return $this->results; } } // end of iface_export_plugin -?> diff --git a/addressbook/inc/class.addressbook_so.inc.php b/addressbook/inc/class.addressbook_so.inc.php index 2dc7ee7895..5e8549b35a 100755 --- a/addressbook/inc/class.addressbook_so.inc.php +++ b/addressbook/inc/class.addressbook_so.inc.php @@ -1,1127 +1,23 @@ * @author Ralf Becker * @package addressbook - * @copyright (c) 2005-12 by Ralf Becker + * @copyright (c) 2005-16 by Ralf Becker * @copyright (c) 2005/6 by Cornelius Weiss * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ +use EGroupware\Api; + /** - * General storage object of the adressbook + * Contacts storage object * - * The contact storage has 3 operation modi (contact_repository): - * - sql: contacts are stored in the SQL table egw_addressbook & egw_addressbook_extra (custom fields) - * - ldap: contacts are stored in LDAP (accounts have to be stored in LDAP too!!!). - * Custom fields are not availible in that case! - * - sql-ldap: contacts are read and searched in SQL, but saved to both SQL and LDAP. - * Other clients (Thunderbird, ...) can use LDAP readonly. The get maintained via eGroupWare only. - * - * The accounts can be stored in SQL or LDAP too (account_repository): - * If the account-repository is different from the contacts-repository, the filter all (no owner set) - * will only search the contacts and NOT the accounts! Only the filter accounts (owner=0) shows accounts. - * - * If sql-ldap is used as contact-storage (LDAP is managed from eGroupWare) the filter all, searches - * the accounts in the SQL contacts-table too. Change in made in LDAP, are not detected in that case! + * @depreacated use Api\Contacts */ -class addressbook_so -{ - /** - * name of customefields table - * - * @var string - */ - var $extra_table = 'egw_addressbook_extra'; - - /** - * @var string - */ - var $extra_id = 'contact_id'; - - /** - * @var string - */ - var $extra_owner = 'contact_owner'; - - /** - * @var string - */ - var $extra_key = 'contact_name'; - - /** - * @var string - */ - var $extra_value = 'contact_value'; - - /** - * view for distributionlistsmembership - * - * @var string - */ - var $distributionlist_view ='(SELECT contact_id, egw_addressbook_lists.list_id as list_id, egw_addressbook_lists.list_name as list_name, egw_addressbook_lists.list_owner as list_owner FROM egw_addressbook_lists, egw_addressbook2list where egw_addressbook_lists.list_id=egw_addressbook2list.list_id) d_view '; - var $distributionlist_tabledef = array(); - /** - * @var string - */ - var $distri_id = 'contact_id'; - - /** - * @var string - */ - var $distri_owner = 'list_owner'; - - /** - * @var string - */ - var $distri_key = 'list_id'; - - /** - * @var string - */ - var $distri_value = 'list_name'; - - /** - * Contact repository in 'sql' or 'ldap' - * - * @var string - */ - var $contact_repository = 'sql'; - - /** - * Grants as account_id => rights pairs - * - * @var array - */ - var $grants; - - /** - * userid of current user - * - * @var int - */ - var $user; - - /** - * memberships of the current user - * - * @var array - */ - var $memberships; - - /** - * In SQL we can search all columns, though a view make on real sense - */ - var $sql_cols_not_to_search = array( - 'jpegphoto','owner','tid','private','cat_id','etag', - 'modified','modifier','creator','created','tz','account_id', - 'uid','carddav_name','freebusy_uri','calendar_uri', - 'geo','pubkey', - ); - /** - * columns to search, if we search for a single pattern - * - * @var array - */ - var $columns_to_search = array(); - /** - * extra columns to search if accounts are included, eg. account_lid - * - * @var array - */ - var $account_extra_search = array(); - /** - * columns to search for accounts, if stored in different repository - * - * @var array - */ - var $account_cols_to_search = array(); - - /** - * customfields name => array(...) pairs - * - * @var array - */ - var $customfields = array(); - /** - * content-types as name => array(...) pairs - * - * @var array - */ - var $content_types = array(); - - /** - * Special content type to indicate a deleted addressbook - * - * @var String; - */ - const DELETED_TYPE = 'D'; - - /** - * total number of matches of last search - * - * @var int - */ - var $total; - - /** - * storage object: sql (addressbook_sql) or ldap (addressbook_ldap) backend class - * - * @var addressbook_sql - */ - var $somain; - /** - * storage object for accounts, if not identical to somain (eg. accounts in ldap, contacts in sql) - * - * @var so_ldap - */ - var $so_accounts; - /** - * account repository sql or ldap - * - * @var string - */ - var $account_repository = 'sql'; - /** - * custom fields backend - * - * @var addressbook_sql - */ - var $soextra; - var $sodistrib_list; - - /** - * Constructor - * - * @param string $contact_app='addressbook' used for acl->get_grants() - * @param egw_db $db=null - */ - function __construct($contact_app='addressbook',egw_db $db=null) - { - $this->db = is_null($db) ? $GLOBALS['egw']->db : $db; - - $this->user = $GLOBALS['egw_info']['user']['account_id']; - $this->memberships = $GLOBALS['egw']->accounts->memberships($this->user,true); - - // account backend used - if ($GLOBALS['egw_info']['server']['account_repository']) - { - $this->account_repository = $GLOBALS['egw_info']['server']['account_repository']; - } - elseif ($GLOBALS['egw_info']['server']['auth_type']) - { - $this->account_repository = $GLOBALS['egw_info']['server']['auth_type']; - } - $this->customfields = config::get_customfields('addressbook'); - // contacts backend (contacts in LDAP require accounts in LDAP!) - if($GLOBALS['egw_info']['server']['contact_repository'] == 'ldap' && $this->account_repository == 'ldap') - { - $this->contact_repository = 'ldap'; - $this->somain = new addressbook_ldap(); - $this->columns_to_search = $this->somain->search_attributes; - } - else // sql or sql->ldap - { - if ($GLOBALS['egw_info']['server']['contact_repository'] == 'sql-ldap') - { - $this->contact_repository = 'sql-ldap'; - } - $this->somain = new addressbook_sql($db); - - // remove some columns, absolutly not necessary to search in sql - $this->columns_to_search = array_diff(array_values($this->somain->db_cols),$this->sql_cols_not_to_search); - if ($this->customfields) // add custom fields, if configured - { - $this->columns_to_search[] = addressbook_sql::EXTRA_TABLE.'.'.addressbook_sql::EXTRA_VALUE; - } - } - if ($this->user) - { - $this->grants = $this->get_grants($this->user,$contact_app); - } - if ($this->account_repository != 'sql' && $this->contact_repository == 'sql') - { - if ($this->account_repository != $this->contact_repository) - { - $class = 'addressbook_'.$this->account_repository; - $this->so_accounts = new $class(); - $this->account_cols_to_search = $this->so_accounts->search_attributes; - } - else - { - $this->account_extra_search = array('uid'); - } - } - if ($this->contact_repository == 'sql' || $this->contact_repository == 'sql-ldap') - { - $tda2list = $this->db->get_table_definitions('phpgwapi','egw_addressbook2list'); - $tdlists = $this->db->get_table_definitions('phpgwapi','egw_addressbook_lists'); - $this->distributionlist_tabledef = array('fd' => array( - $this->distri_id => $tda2list['fd'][$this->distri_id], - $this->distri_owner => $tdlists['fd'][$this->distri_owner], - $this->distri_key => $tdlists['fd'][$this->distri_key], - $this->distri_value => $tdlists['fd'][$this->distri_value], - ), 'pk' => array(), 'fk' => array(), 'ix' => array(), 'uc' => array(), - ); - } - // ToDo: it should be the other way arround, the backend should set the grants it uses - $this->somain->grants =& $this->grants; - - if($this->somain instanceof addressbook_sql) - { - $this->soextra =& $this->somain; - } - else - { - $this->soextra = new addressbook_sql($db); - } - - $this->content_types = config::get_content_types('addressbook'); - if (!$this->content_types) - { - $this->content_types = array('n' => array( - 'name' => 'contact', - 'options' => array( - 'template' => 'addressbook.edit', - 'icon' => 'navbar.png' - ))); - } - - // Add in deleted type, if holding deleted contacts - $config = config::read('phpgwapi'); - if($config['history']) - { - $this->content_types[self::DELETED_TYPE] = array( - 'name' => lang('Deleted'), - 'options' => array( - 'template' => 'addressbook.edit', - 'icon' => 'deleted.png' - ) - ); - } - } - - /** - * Get grants for a given user, taking into account static LDAP ACL - * - * @param int $user - * @param string $contact_app='addressbook' - * @return array - */ - function get_grants($user, $contact_app='addressbook', $preferences=null) - { - if (!isset($preferences)) $preferences = $GLOBALS['egw_info']['user']['preferences']; - - if ($user) - { - // contacts backend (contacts in LDAP require accounts in LDAP!) - if($GLOBALS['egw_info']['server']['contact_repository'] == 'ldap' && $this->account_repository == 'ldap') - { - // static grants from ldap: all rights for the own personal addressbook and the group ones of the meberships - $grants = array($user => ~0); - foreach($GLOBALS['egw']->accounts->memberships($user,true) as $gid) - { - $grants[$gid] = ~0; - } - } - else // sql or sql->ldap - { - // group grants are now grants for the group addressbook and NOT grants for all its members, - // therefor the param false! - $grants = $GLOBALS['egw']->acl->get_grants($contact_app,false,$user); - } - // add grants for accounts: if account_selection not in ('none','groupmembers'): everyone has read access, - // if he has not set the hide_accounts preference - // ToDo: be more specific for 'groupmembers', they should be able to see the groupmembers - if (!in_array($preferences['common']['account_selection'], array('none','groupmembers'))) - { - $grants[0] = EGW_ACL_READ; - } - // add account grants for admins (only for current user!) - if ($user == $this->user && $this->is_admin()) // admin rights can be limited by ACL! - { - $grants[0] = EGW_ACL_READ; // admins always have read-access - if (!$GLOBALS['egw']->acl->check('account_access',16,'admin')) $grants[0] |= EGW_ACL_EDIT; - if (!$GLOBALS['egw']->acl->check('account_access',4,'admin')) $grants[0] |= EGW_ACL_ADD; - if (!$GLOBALS['egw']->acl->check('account_access',32,'admin')) $grants[0] |= EGW_ACL_DELETE; - } - // allow certain groups to edit contact-data of accounts - if (self::allow_account_edit($user)) - { - $grants[0] |= EGW_ACL_READ|EGW_ACL_EDIT; - } - } - else - { - $grants = array(); - } - //error_log(__METHOD__."($user, '$contact_app') returning ".array2string($grants)); - return $grants; - } - - /** - * Check if the user is an admin (can unconditionally edit accounts) - * - * We check now the admin ACL for edit users, as the admin app does it for editing accounts. - * - * @param array $contact=null for future use, where admins might not be admins for all accounts - * @return boolean - */ - function is_admin($contact=null) - { - return isset($GLOBALS['egw_info']['user']['apps']['admin']) && !$GLOBALS['egw']->acl->check('account_access',16,'admin'); - } - - /** - * Check if current user is in a group, which is allowed to edit accounts - * - * @param int $user =null default $this->user - * @return boolean - */ - function allow_account_edit($user=null) - { - return $GLOBALS['egw_info']['server']['allow_account_edit'] && - array_intersect($GLOBALS['egw_info']['server']['allow_account_edit'], - $GLOBALS['egw']->accounts->memberships($user ? $user : $this->user, true)); - } - - /** - * Read all customfields of the given id's - * - * @param int|array $ids - * @param array $field_names=null custom fields to read, default all - * @return array id => name => value - */ - function read_customfields($ids,$field_names=null) - { - return $this->soextra->read_customfields($ids,$field_names); - } - - /** - * Read all distributionlists of the given id's - * - * @param int|array $ids - * @return array id => name => value - */ - function read_distributionlist($ids, $dl_allowed=array()) - { - if ($this->contact_repository == 'ldap') - { - return array(); // ldap does not support distributionlists - } - foreach($ids as $key => $id) - { - if (!is_numeric($id)) unset($ids[$key]); - } - if (!$ids) return array(); // nothing to do, eg. all these contacts are in ldap - $fields = array(); - $filter[$this->distri_id]=$ids; - if (count($dl_allowed)) $filter[$this->distri_key]=$dl_allowed; - $distri_view = str_replace(') d_view',' and '.$this->distri_id.' in ('.implode(',',$ids).')) d_view',$this->distributionlist_view); - #_debug_array($this->distributionlist_tabledef); - foreach($this->db->select($distri_view,'*',$filter,__LINE__,__FILE__, - false,'ORDER BY '.$this->distri_id,false,$num_rows=0,$join='',$this->distributionlist_tabledef) as $row) - { - if ((isset($row[$this->distri_id])&&strlen($row[$this->distri_value])>0)) - { - $fields[$row[$this->distri_id]][$row[$this->distri_key]] = $row[$this->distri_value].' ('.$GLOBALS['egw']->common->grab_owner_name($row[$this->distri_owner]).')'; - } - } - return $fields; - } - - /** - * changes the data from the db-format to your work-format - * - * it gets called everytime when data is read from the db - * This function needs to be reimplemented in the derived class - * - * @param array $data - */ - function db2data($data) - { - return $data; - } - - /** - * changes the data from your work-format to the db-format - * - * It gets called everytime when data gets writen into db or on keys for db-searches - * this needs to be reimplemented in the derived class - * - * @param array $data - */ - function data2db($data) - { - return $data; - } - - /** - * deletes contact entry including custom fields - * - * @param mixed $contact array with id or just the id - * @param int $check_etag=null - * @return boolean|int true on success or false on failiure, 0 if etag does not match - */ - function delete($contact,$check_etag=null) - { - if (is_array($contact)) $contact = $contact['id']; - - $where = array('id' => $contact); - if ($check_etag) $where['etag'] = $check_etag; - - // delete mainfields - if ($this->somain->delete($where)) - { - // delete customfields, can return 0 if there are no customfields - if(!($this->somain instanceof addressbook_sql)) - { - $this->soextra->delete_customfields(array($this->extra_id => $contact)); - } - - // delete from distribution list(s) - $this->remove_from_list($contact); - - if ($this->contact_repository == 'sql-ldap') - { - if ($contact['account_id']) - { - // LDAP uses the uid attributes for the contact-id (dn), - // which need to be the account_lid for accounts! - $contact['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']); - } - ExecMethod('addressbook.addressbook_ldap.delete',$contact); - } - return true; - } - return $check_etag ? 0 : false; // if etag given, we return 0 on failure, thought it could also mean the whole contact does not exist - } - - /** - * saves contact data including custom fields - * - * @param array &$contact contact data from etemplate::exec - * @return bool false on success, errornumber on failure - */ - function save(&$contact) - { - // save mainfields - if ($contact['id'] && $this->contact_repository != $this->account_repository && is_object($this->so_accounts) && - ($this->contact_repository == 'sql' && !is_numeric($contact['id']) || - $this->contact_repository == 'ldap' && is_numeric($contact['id']))) - { - $this->so_accounts->data = $this->data2db($contact); - $error_nr = $this->so_accounts->save(); - $contact['id'] = $this->so_accounts->data['id']; - } - else - { - // contact_repository sql-ldap (accounts in ldap) the person_id is the uid (account_lid) - // for the sql write here we need to find out the existing contact_id - if ($this->contact_repository == 'sql-ldap' && $contact['id'] && !is_numeric($contact['id']) && - $contact['account_id'] && ($old = $this->somain->read(array('account_id' => $contact['account_id'])))) - { - $contact['id'] = $old['id']; - } - $this->somain->data = $this->data2db($contact); - - if (!($error_nr = $this->somain->save())) - { - $contact['id'] = $this->somain->data['id']; - $contact['uid'] = $this->somain->data['uid']; - $contact['etag'] = $this->somain->data['etag']; - - if ($this->contact_repository == 'sql-ldap') - { - $data = $this->somain->data; - if ($contact['account_id']) - { - // LDAP uses the uid attributes for the contact-id (dn), - // which need to be the account_lid for accounts! - $data['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']); - } - $error_nr = ExecMethod('addressbook.addressbook_ldap.save',$data); - } - } - } - if($error_nr) return $error_nr; - - return false; // no error - } - - /** - * reads contact data including custom fields - * - * @param int|string $contact_id contact_id or 'a'.account_id - * @return array|boolean data if row could be retrived else False - */ - function read($contact_id) - { - if (!is_array($contact_id) && substr($contact_id,0,8) == 'account:') - { - $contact_id = array('account_id' => (int) substr($contact_id,8)); - } - // read main data - $backend =& $this->get_backend($contact_id); - if (!($contact = $backend->read($contact_id))) - { - return $contact; - } - $dl_list=$this->read_distributionlist(array($contact['id'])); - if (count($dl_list)) $contact['distrib_lists']=implode("\n",$dl_list[$contact['id']]); - return $this->db2data($contact); - } - - /** - * searches db for rows matching searchcriteria - * - * '*' and '?' are replaced with sql-wildcards '%' and '_' - * - * @param array|string $criteria array of key and data cols, OR string to search over all standard search fields - * @param boolean|string $only_keys=true True returns only keys, False returns all cols. comma seperated list of keys to return - * @param string $order_by='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY) - * @param string|array $extra_cols='' string or array of strings to be added to the SELECT, eg. "count(*) as num" - * @param string $wildcard='' appended befor and after each criteria - * @param boolean $empty=false False=empty criteria are ignored in query, True=empty have to be empty in row - * @param string $op='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together - * @param mixed $start=false if != false, return only maxmatch rows begining with start, or array($start,$num) - * @param array $filter=null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards - * $filter['cols_to_search'] limit search columns to given columns, otherwise $this->columns_to_search is used - * @param string $join='' sql to do a join (only used by sql backend!), eg. " RIGHT JOIN egw_accounts USING(account_id)" - * @return array of matching rows (the row is an array of the cols) or False - */ - function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='') - { - //echo '

'.__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',$start,".array2string($filter,true).",'$join')

\n"; - //error_log(__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',".array2string($start).','.array2string($filter,true).",'$join')"); - - // Handle 'None' country option - if(is_array($filter) && $filter['adr_one_countrycode'] == '-custom-') - { - $filter[] = 'adr_one_countrycode IS NULL'; - unset($filter['adr_one_countrycode']); - } - // Hide deleted items unless type is specifically deleted - if(!is_array($filter)) $filter = $filter ? (array) $filter : array(); - - if (isset($filter['cols_to_search'])) - { - $cols_to_search = $filter['cols_to_search']; - unset($filter['cols_to_search']); - } - - // if no tid set or tid==='' do NOT return deleted entries ($tid === null returns all entries incl. deleted) - if(!array_key_exists('tid', $filter) || $filter['tid'] === '') - { - if ($join && strpos($join,'RIGHT JOIN') !== false) // used eg. to search for groups - { - $filter[] = '(contact_tid != \'' . self::DELETED_TYPE . '\' OR contact_tid IS NULL)'; - } - else - { - $filter[] = 'contact_tid != \'' . self::DELETED_TYPE . '\''; - } - } - elseif(is_null($filter['tid'])) - { - unset($filter['tid']); // return all entries incl. deleted - } - $backend = $this->get_backend(null,$filter['owner']); - // single string to search for --> create so_sql conformant search criterial for the standard search columns - if ($criteria && !is_array($criteria)) - { - $op = 'OR'; - $wildcard = '%'; - $search = $criteria; - $criteria = array(); - - if (isset($cols_to_search)) - { - $cols = $cols_to_search; - } - elseif ($backend === $this->somain) - { - $cols = $this->columns_to_search; - } - else - { - $cols = $this->account_cols_to_search; - } - if($backend instanceof addressbook_sql) - { - // Keep a string, let the parent handle it - $criteria = $search; - - foreach($cols as $key => &$col) - { - if($col != addressbook_sql::EXTRA_VALUE && - $col != addressbook_sql::EXTRA_TABLE.'.'.addressbook_sql::EXTRA_VALUE && - !array_key_exists($col, $backend->db_cols)) - { - if(!($col = array_search($col, $backend->db_cols))) - { - // Can't search this column, it will error if we try - unset($cols[$key]); - } - } - if ($col=='contact_id') $col='egw_addressbook.contact_id'; - } - - $backend->columns_to_search = $cols; - } - else - { - foreach($cols as $col) - { - // remove from LDAP backend not understood use-AND-syntax - $criteria[$col] = str_replace(' +',' ',$search); - } - } - } - if (is_array($criteria) && count($criteria)) - { - $criteria = $this->data2db($criteria); - } - if (is_array($filter) && count($filter)) - { - $filter = $this->data2db($filter); - } - else - { - $filter = $filter ? array($filter) : array(); - } - // get the used backend for the search and call it's search method - $rows = $backend->search($criteria,$only_keys,$order_by,$extra_cols,$wildcard,$empty,$op,$start,$filter,$join,$need_full_no_count); - $this->total = $backend->total; - - if ($rows) - { - foreach($rows as $n => $row) - { - $rows[$n] = $this->db2data($row); - } - } - return $rows; - } - - /** - * Query organisations by given parameters - * - * @var array $param - * @var string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group - * @var int $param[owner] addressbook to search - * @var string $param[search] search pattern for org_name - * @var string $param[searchletter] letter the org_name need to start with - * @var int $param[start] - * @var int $param[num_rows] - * @var string $param[sort] ASC or DESC - * @return array or arrays with keys org_name,count and evtl. adr_one_location or org_unit - */ - function organisations($param) - { - if (!method_exists($this->somain,'organisations')) - { - $this->total = 0; - return false; - } - if ($param['search'] && !is_array($param['search'])) - { - $search = $param['search']; - $param['search'] = array(); - if($this->somain instanceof addressbook_sql) - { - // Keep the string, let the parent deal with it - $param['search'] = $search; - } - else - { - foreach($this->columns_to_search as $col) - { - if ($col != 'contact_value') $param['search'][$col] = $search; // we dont search the customfields - } - } - } - if (is_array($param['search']) && count($param['search'])) - { - $param['search'] = $this->data2db($param['search']); - } - if(!array_key_exists('tid', $param['col_filter']) || $param['col_filter']['tid'] === '') - { - $param['col_filter'][] = 'contact_tid != \'' . self::DELETED_TYPE . '\''; - } - elseif(is_null($param['col_filter']['tid'])) - { - unset($param['col_filter']['tid']); // return all entries incl. deleted - } - - $rows = $this->somain->organisations($param); - $this->total = $this->somain->total; - //echo "

socontacts::organisations(".print_r($param,true).")
".$this->somain->db->Query_ID->sql."

\n"; - - if (!$rows) return array(); - - foreach($rows as $n => $row) - { - if (strpos($row['org_name'],'&')!==false) $row['org_name'] = str_replace('&','*AND*',$row['org_name']); //echo "Ampersand found
"; - $rows[$n]['id'] = 'org_name:'.$row['org_name']; - foreach(array( - 'org_unit' => lang('departments'), - 'adr_one_locality' => lang('locations'), - ) as $by => $by_label) - { - if ($row[$by.'_count'] > 1) - { - $rows[$n][$by] = $row[$by.'_count'].' '.$by_label; - } - else - { - if (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]); //echo "Ampersand found
"; - $rows[$n]['id'] .= '|||'.$by.':'.$row[$by]; - } - } - } - return $rows; - } - - /** - * gets all contact fields from database - * - * @return array of (internal) field-names - */ - function get_contact_columns() - { - $fields = $this->get_fields('all'); - foreach ((array)$this->customfields as $cfield => $coptions) - { - $fields[] = '#'.$cfield; - } - return $fields; - } - - /** - * delete / move all contacts of an addressbook - * - * @param array $data - * @param int $data['account_id'] owner to change - * @param int $data['new_owner'] new owner or 0 for delete - */ - function deleteaccount($data) - { - $account_id = $data['account_id']; - $new_owner = $data['new_owner']; - - if (!$new_owner) - { - $this->somain->delete(array('owner' => $account_id)); // so_sql_cf::delete() takes care of cfs too - - if (method_exists($this->somain, 'get_lists') && - ($lists = $this->somain->get_lists($account_id))) - { - $this->somain->delete_list(array_keys($lists)); - } - } - else - { - $this->somain->change_owner($account_id,$new_owner); - } - } - - /** - * return the backend, to be used for the given $contact_id - * - * @param array|string|int $keys=null - * @param int $owner=null account_id of owner or 0 for accounts - * @return addressbook_sql - */ - function get_backend($keys=null,$owner=null) - { - if ($owner === '') $owner = null; - - $contact_id = !is_array($keys) ? $keys : - (isset($keys['id']) ? $keys['id'] : $keys['contact_id']); - - if ($this->contact_repository != $this->account_repository && is_object($this->so_accounts) && - (!is_null($owner) && !$owner || is_array($keys) && $keys['account_id'] || !is_null($contact_id) && - ($this->contact_repository == 'sql' && (!is_numeric($contact_id) && !is_array($contact_id) )|| - $this->contact_repository == 'ldap' && is_numeric($contact_id)))) - { - return $this->so_accounts; - } - return $this->somain; - } - - /** - * Returns the supported, all or unsupported fields of the backend (depends on owner or contact_id) - * - * @param sting $type='all' 'supported', 'unsupported' or 'all' - * @param mixed $contact_id=null - * @param int $owner=null account_id of owner or 0 for accounts - * @return array with eGW contact field names - */ - function get_fields($type='all',$contact_id=null,$owner=null) - { - $def = $this->db->get_table_definitions('phpgwapi','egw_addressbook'); - - $all_fields = array(); - foreach($def['fd'] as $field => $data) - { - $all_fields[] = substr($field,0,8) == 'contact_' ? substr($field,8) : $field; - } - if ($type == 'all') - { - return $all_fields; - } - $backend =& $this->get_backend($contact_id,$owner); - - $supported_fields = method_exists($backend,supported_fields) ? $backend->supported_fields() : $all_fields; - //echo "supported fields=";_debug_array($supported_fields); - - if ($type == 'supported') - { - return $supported_fields; - } - //echo "unsupported fields=";_debug_array(array_diff($all_fields,$supported_fields)); - return array_diff($all_fields,$supported_fields); - } - - /** - * Migrates an SQL contact storage to LDAP, SQL-LDAP or back to SQL - * - * @param string|array $type comma-separated list or array of: - * - "contacts" contacts to ldap - * - "accounts" accounts to ldap - * - "accounts-back" accounts back to sql (for sql-ldap!) - * - "sql" contacts and accounts to sql - */ - function migrate2ldap($type) - { - //error_log(__METHOD__."(".array2string($type).")"); - $sql_contacts = new addressbook_sql(); - // we need an admin connection - $ds = $GLOBALS['egw']->ldap->ldapConnect(); - $ldap_contacts = new addressbook_ldap(null, $ds); - - if (!is_array($type)) $type = explode(',', $type); - - $start = $n = 0; - $num = 100; - - // direction SQL --> LDAP, either only accounts, or only contacts or both - if (($do = array_intersect($type, array('contacts', 'accounts')))) - { - $filter = count($do) == 2 ? null : - array($do[0] == 'contacts' ? 'contact_owner != 0' : 'contact_owner = 0'); - - while (($contacts = $sql_contacts->search(false,false,'n_family,n_given','','',false,'AND', - array($start,$num),$filter))) - { - foreach($contacts as $contact) - { - if ($contact['account_id']) $contact['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']); - - $ldap_contacts->data = $contact; - $n++; - if (!($err = $ldap_contacts->save())) - { - echo '

'.$n.': '.$contact['n_fn']. - ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '')." --> LDAP

\n"; - } - else - { - echo '

'.$n.': '.$contact['n_fn']. - ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '').': '.$err."

\n"; - } - } - $start += $num; - } - } - // direction LDAP --> SQL: either "sql" (contacts and accounts) or "accounts-back" (only accounts) - if (($do = array_intersect(array('accounts-back','sql'), $type))) - { - //error_log(__METHOD__."(".array2string($type).") do=".array2string($type)); - $filter = in_array('sql', $do) ? null : array('owner' => 0); - - foreach($ldap_contacts->search(false,false,'n_family,n_given','','',false,'AND', - false, $filter) as $contact) - { - //error_log(__METHOD__."(".array2string($type).") do=".array2string($type)." migrating ".array2string($contact)); - if ($contact['jpegphoto']) // photo is NOT read by LDAP backend on search, need to do an extra read - { - $contact = $ldap_contacts->read($contact['id']); - } - unset($contact['id']); // ldap uid/account_lid - if ($contact['account_id'] && ($old = $sql_contacts->read(array('account_id' => $contact['account_id'])))) - { - $contact['id'] = $old['id']; - } - $sql_contacts->data = $contact; - - $n++; - if (!($err = $sql_contacts->save())) - { - echo '

'.$n.': '.$contact['n_fn']. - ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '')." --> SQL (". - ($contact['owner']?lang('User'):lang('Contact')).")

\n"; - } - else - { - echo '

'.$n.': '.$contact['n_fn']. - ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '').': '.$err."

\n"; - } - } - } - } - - /** - * Get the availible distribution lists for a user - * - * @param int $required=EGW_ACL_READ required rights on the list or multiple rights or'ed together, - * to return only lists fullfilling all the given rights - * @param string $extra_labels=null first labels if given (already translated) - * @return array with id => label pairs or false if backend does not support lists - */ - function get_lists($required=EGW_ACL_READ,$extra_labels=null) - { - if (!method_exists($this->somain,'get_lists')) return false; - - $uids = array(); - foreach($this->grants as $uid => $rights) - { - if (($rights & $required) == $required) - { - $uids[] = $uid; - } - } - $lists = is_array($extra_labels) ? $extra_labels : array(); - - foreach($this->somain->get_lists($uids) as $list_id => $data) - { - $lists[$list_id] = $data['list_name']; - if ($data['list_owner'] != $this->user) - { - $lists[$list_id] .= ' ('.$GLOBALS['egw']->common->grab_owner_name($data['list_owner']).')'; - } - } - //echo "

socontacts_sql::get_lists($required,'$extra_label')

\n"; _debug_array($lists); - return $lists; - } - - /** - * Get the availible distribution lists for givens users and groups - * - * @param array $keys column-name => value(s) pairs, eg. array('list_uid'=>$uid) - * @param string $member_attr='contact_uid' null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute - * @param boolean $limit_in_ab=false if true only return members from the same owners addressbook - * @return array with list_id => array(list_id,list_name,list_owner,...) pairs - */ - function read_lists($keys,$member_attr=null,$limit_in_ab=false) - { - $backend = (string)$limit_in_ab === '0' && $this->so_accounts ? $this->so_accounts : $this->somain; - if (!method_exists($backend, 'get_lists')) return false; - - return $backend->get_lists($keys,null,$member_attr,$limit_in_ab); - } - - /** - * Adds / updates a distribution list - * - * @param string|array $keys list-name or array with column-name => value pairs to specify the list - * @param int $owner user- or group-id - * @param array $contacts=array() contacts to add (only for not yet existing lists!) - * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name' - * @return int|boolean integer list_id or false on error - */ - function add_list($keys,$owner,$contacts=array(),array &$data=array()) - { - $backend = (string)$owner === '0' && $this->so_accounts ? $this->so_accounts : $this->somain; - if (!method_exists($backend, 'add_list')) return false; - - return $backend->add_list($keys,$owner,$contacts,$data); - } - - /** - * Adds contact(s) to a distribution list - * - * @param int|array $contact contact_id(s) - * @param int $list list-id - * @param array $existing=null array of existing contact-id(s) of list, to not reread it, eg. array() - * @return false on error - */ - function add2list($contact,$list,array $existing=null) - { - if (!method_exists($this->somain,'add2list')) return false; - - return $this->somain->add2list($contact,$list,$existing); - } - - /** - * Removes one contact from distribution list(s) - * - * @param int|array $contact contact_id(s) - * @param int $list=null list-id or null to remove from all lists - * @return false on error - */ - function remove_from_list($contact,$list=null) - { - if (!method_exists($this->somain,'remove_from_list')) return false; - - return $this->somain->remove_from_list($contact,$list); - } - - /** - * Deletes a distribution list (incl. it's members) - * - * @param int|array $list list_id(s) - * @return number of members deleted or false if list does not exist - */ - function delete_list($list) - { - if (!method_exists($this->somain,'delete_list')) return false; - - return $this->somain->delete_list($list); - } - - /** - * Read data of a distribution list - * - * @param int $list list_id - * @return array of data or false if list does not exist - */ - function read_list($list) - { - if (!method_exists($this->somain,'read_list')) return false; - - return $this->somain->read_list($list); - } - - /** - * Check if distribution lists are availible for a given addressbook - * - * @param int|string $owner='' addressbook (eg. 0 = accounts), default '' = "all" addressbook (uses the main backend) - * @return boolean - */ - function lists_available($owner='') - { - $backend =& $this->get_backend(null,$owner); - - return method_exists($backend,'read_list'); - } - - /** - * Get ctag (max list_modified as timestamp) for lists - * - * @param int|array $owner=null null for all lists user has access too - * @return int - */ - function lists_ctag($owner=null) - { - if (!method_exists($this->somain,'lists_ctag')) return 0; - - return $this->somain->lists_ctag($owner); - } -} +class addressbook_so extends Api\Contacts\Storage {} diff --git a/addressbook/inc/class.addressbook_ui.inc.php b/addressbook/inc/class.addressbook_ui.inc.php index 12fed7a4ce..55e4b388b9 100644 --- a/addressbook/inc/class.addressbook_ui.inc.php +++ b/addressbook/inc/class.addressbook_ui.inc.php @@ -1138,9 +1138,9 @@ window.egw_LAB.wait(function() { if ($contact['owner'] || // regular contact or empty($contact['account_id']) || // accounts without account_id // already deleted account (should no longer happen, but needed to allow for cleanup) - $contact['tid'] == addressbook_so::DELETED_TYPE) + $contact['tid'] == self::DELETED_TYPE) { - $Ok = $this->delete($id, $contact['tid'] != addressbook_so::DELETED_TYPE && $contact['account_id']); + $Ok = $this->delete($id, $contact['tid'] != self::DELETED_TYPE && $contact['account_id']); } // delete single account --> redirect to admin elseif (count($checked) == 1 && $contact['account_id']) @@ -1684,7 +1684,7 @@ window.egw_LAB.wait(function() { { $row['class'] .= 'rowAccount rowNoDelete '; } - elseif (!$this->check_perms(EGW_ACL_DELETE,$row) || (!$GLOBALS['egw_info']['user']['apps']['admin'] && $this->config['history'] != 'userpurge' && $query['col_filter']['tid'] == addressbook_so::DELETED_TYPE)) + elseif (!$this->check_perms(EGW_ACL_DELETE,$row) || (!$GLOBALS['egw_info']['user']['apps']['admin'] && $this->config['history'] != 'userpurge' && $query['col_filter']['tid'] == self::DELETED_TYPE)) { $row['class'] .= 'rowNoDelete '; } @@ -2274,7 +2274,7 @@ window.egw_LAB.wait(function() { ); // Links for deleted entries - if($content['tid'] == addressbook_so::DELETED_TYPE) + if($content['tid'] == self::DELETED_TYPE) { $content['link_to']['show_deleted'] = true; if(!$GLOBALS['egw_info']['user']['apps']['admin'] && $this->config['history'] != 'userpurge') @@ -2604,7 +2604,7 @@ window.egw_LAB.wait(function() { 'to_id' => $content['id'], ); // Links for deleted entries - if($content['tid'] == addressbook_so::DELETED_TYPE) + if($content['tid'] == self::DELETED_TYPE) { $content['link_to']['show_deleted'] = true; } diff --git a/admin/inc/class.customfields.inc.php b/admin/inc/class.customfields.inc.php index da6397b39b..e7a336d365 100644 --- a/admin/inc/class.customfields.inc.php +++ b/admin/inc/class.customfields.inc.php @@ -1,6 +1,6 @@ @@ -10,6 +10,8 @@ * @version $Id$ */ +use EGroupware\Api; + /** * Customfields class - manages customfield definitions in egw_config table * @@ -103,10 +105,10 @@ class customfields { if (($this->appname = $appname)) { - $this->fields = egw_customfields::get($this->appname,true); - $this->content_types = config::get_content_types($this->appname); + $this->fields = Api\Storage\Customfields::get($this->appname,true); + $this->content_types = Api\Config::get_content_types($this->appname); } - $this->so = new so_sql('phpgwapi','egw_customfields',null,'',true); + $this->so = new Api\Storage\Base('phpgwapi','egw_customfields',null,'',true); } /** @@ -121,7 +123,7 @@ class customfields $this->use_private = !isset($_GET['use_private']) || (boolean)$_GET['use_private'] || $content['use_private']; // Read fields, constructor doesn't always know appname - $this->fields = egw_customfields::get($this->appname,true); + $this->fields = Api\Storage\Customfields::get($this->appname,true); $this->tmpl = new etemplate_new(); $this->tmpl->read('admin.customfields'); @@ -135,7 +137,7 @@ class customfields { if(count($this->content_types) == 0) { - $this->content_types = config::get_content_types($this->appname); + $this->content_types = Api\Config::get_content_types($this->appname); } if (count($this->content_types)==0) { @@ -152,7 +154,7 @@ class customfields } elseif($content['content_types']['create']) { - if($new_type = $this->create_content_type($content)) + if(($new_type = $this->create_content_type($content))) { $content['content_types']['types'] = $this->content_type = $new_type; } @@ -214,7 +216,6 @@ class customfields $content['type_template'] = $this->appname . '.admin.types'; $content['content_types']['appname'] = $this->appname; - $content_types = array_keys($this->content_types); $content['content_type_options'] = $this->content_types[$this->content_type]['options']; $content['content_type_options']['type'] = $this->types2[$this->content_type]; @@ -251,6 +252,7 @@ class customfields ); // Allow extending app a change to change content before display + $readonlys = null; static::app_index($content, $sel_options, $readonlys, $preserve); // Make sure app css gets loaded, extending app might cause et2 to miss it @@ -292,7 +294,7 @@ class customfields $this->use_private = !isset($_GET['use_private']) || (boolean)$_GET['use_private'] || $content['use_private']; // Read fields, constructor doesn't always know appname - $this->fields = egw_customfields::get($this->appname,true); + $this->fields = Api\Storage\Customfields::get($this->appname,true); // Update based on info returned from template if (is_array($content)) @@ -328,8 +330,8 @@ class customfields { foreach(explode("\n",trim($content['cf_values'])) as $line) { - list($var,$value) = explode('=',trim($line),2); - $var = trim($var); + list($var_raw,$value) = explode('=',trim($line),2); + $var = trim($var_raw); $values[$var] = trim($value)==='' ? $var : $value; } } @@ -343,10 +345,10 @@ class customfields $update_content[substr($key,3)] = $value; } } - egw_customfields::update($update_content); + Api\Storage\Customfields::update($update_content); if(!$cf_id) { - $this->fields = egw_customfields::get($this->appname,true); + $this->fields = Api\Storage\Customfields::get($this->appname,true); $cf_id = (int)$this->fields[$content['cf_name']]['id']; } egw_framework::refresh_opener('Saved', 'admin', $cf_id, 'edit'); @@ -415,7 +417,7 @@ class customfields { if(count($this->content_types) == 0) { - $this->content_types = config::get_content_types($this->appname); + $this->content_types = Api\Config::get_content_types($this->appname); } if (count($this->content_types)==0) { @@ -455,6 +457,7 @@ class customfields */ protected function app_index(&$content, &$sel_options, &$readonlys) { + unset($content, $sel_options, $readonlys); // not used, as this is a stub // This is just a stub. } @@ -494,78 +497,6 @@ class customfields return $actions; } - function update_fields(&$content) - { - foreach($content['fields'] as $field) - { - $name = trim($field['name']); - $old_name = $field['old_name']; - - if (!empty($delete) && $delete == $old_name) - { - unset($this->fields[$old_name]); - continue; - } - if (isset($field['old_name'])) - { - if (empty($name)) // empty name not allowed - { - $content['error_msg'] = lang('Name must not be empty !!!'); - $name = $old_name; - } - if (!empty($name) && $old_name != $name) // renamed - { - unset($this->fields[$old_name]); - } - } - elseif (empty($name)) // new item and empty ==> ignore it - { - continue; - } - $values = array(); - if (!empty($field['values'])) - { - foreach(explode("\n",$field['values']) as $line) - { - list($var,$value) = explode('=',trim($line),2); - $var = trim($var); - $values[$var] = empty($value) ? $var : $value; - } - } - $this->fields[$name] = array( - 'type' => $field['type'], - 'type2' => $field['type2'], - 'label' => empty($field['label']) ? $name : $field['label'], - 'help' => $field['help'], - 'values'=> $values, - 'len' => $field['len'], - 'rows' => (int)$field['rows'], - 'order' => (int)$field['order'], - 'private' => $field['private'], - 'needed' => $field['needed'], - ); - if(!$this->fields[$name]['type2'] && $this->manage_content_types) - { - $this->fields[$name]['type2'] = (string)0; - } - } - if (!function_exists('sort_by_order')) - { - function sort_by_order($arr1,$arr2) - { - return $arr1['order'] - $arr2['order']; - } - } - uasort($this->fields,sort_by_order); - - $n = 0; - foreach($this->fields as $name => $data) - { - $this->fields[$name]['order'] = ($n += 10); - } - } - - function update(&$content) { $this->content_types[$this->content_type]['options'] = $content['content_type_options']; @@ -627,7 +558,7 @@ class customfields } else { - foreach($this->content_types as $letter => $type) + foreach($this->content_types as $type) { if($type['name'] == $new_name) { @@ -640,8 +571,8 @@ class customfields { if (!$this->content_types[chr($i)] && // skip letter of deleted type for addressbook content-types, as it gives SQL error - // content-type are lowercase, addressbook_so::DELETED_TYPE === 'D', but DB is case-insensitive - ($this->appname !== 'addressbook' || chr($i) !== strtolower(addressbook_so::DELETED_TYPE))) + // content-type are lowercase, Api\Contacts::DELETED_TYPE === 'D', but DB is case-insensitive + ($this->appname !== 'addressbook' || chr($i) !== strtolower(Api\Contacts::DELETED_TYPE))) { $new_type = chr($i); break; @@ -664,32 +595,32 @@ class customfields $config->value('types',$this->content_types); $config->save_repository(); - egw_customfields::save($this->appname, $this->fields); + Api\Storage\Customfields::save($this->appname, $this->fields); } /** * get customfields of using application * - * @deprecated use egw_customfields::get() direct, no need to instanciate this UI class + * @deprecated use Api\Storage\Customfields::get() direct, no need to instanciate this UI class * @author Cornelius Weiss - * @param boolean $all_private_too=false should all the private fields be returned too + * @param boolean $all_private_too =false should all the private fields be returned too * @return array with customfields */ function get_customfields($all_private_too=false) { - return egw_customfields::get($this->appname,$all_private_too); + return Api\Storage\Customfields::get($this->appname,$all_private_too); } /** * get_content_types of using application * - * @deprecated use config::get_content_types() direct, no need to instanciate this UI class + * @deprecated use Api\Config::get_content_types() direct, no need to instanciate this UI class * @author Cornelius Weiss * @return array with content-types */ function get_content_types() { - return config::get_content_types($this->appname); + return Api\Config::get_content_types($this->appname); } /** diff --git a/api/src/Config.php b/api/src/Config.php index 233044796d..cdc1d71c43 100755 --- a/api/src/Config.php +++ b/api/src/Config.php @@ -228,13 +228,13 @@ class Config * @param string $app * @param boolean $all_private_too =false should all the private fields be returned too, default no * @param string $only_type2 =null if given only return fields of type2 == $only_type2 - * @deprecated use Api\Customfields::get() + * @deprecated use Api\Storage\Customfields::get() * @return array with customfields */ static function get_customfields($app, $all_private_too=false, $only_type2=null) { - //error_log(__METHOD__."('$app', $all_private_too, $only_type2) deprecated, use Customfields::get() in ". function_backtrace()); - return Customfields::get($app, $all_private_too, $only_type2); + //error_log(__METHOD__."('$app', $all_private_too, $only_type2) deprecated, use Storage\Customfields::get() in ". function_backtrace()); + return Storage\Customfields::get($app, $all_private_too, $only_type2); } /** diff --git a/api/src/Contacts.php b/api/src/Contacts.php new file mode 100755 index 0000000000..87bda53c27 --- /dev/null +++ b/api/src/Contacts.php @@ -0,0 +1,2402 @@ + + * @author Ralf Becker + * @author Joerg Lehrke + * @package api + * @subpackage contacts + * @copyright (c) 2005-16 by Ralf Becker + * @copyright (c) 2005/6 by Cornelius Weiss + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +namespace EGroupware\Api; + +// explicitly reference classes still in phpgwapi +use categories; +use common; // grab_owner_name +use egw_link; + +use calendar_bo; // to_do: do NOT require it, just use if there + +/** + * Business object for contacts + */ +class Contacts extends Contacts\Storage +{ + /** + * @var int $now_su actual user (!) time + */ + var $now_su; + + /** + * @var array $timestamps timestamps + */ + var $timestamps = array('modified','created'); + + /** + * @var array $fileas_types + */ + var $fileas_types = array( + 'org_name: n_family, n_given', + 'org_name: n_family, n_prefix', + 'org_name: n_given n_family', + 'org_name: n_fn', + 'org_name, org_unit: n_family, n_given', + 'org_name, adr_one_locality: n_family, n_given', + 'org_name, org_unit, adr_one_locality: n_family, n_given', + 'n_family, n_given: org_name', + 'n_family, n_given (org_name)', + 'n_family, n_prefix: org_name', + 'n_given n_family: org_name', + 'n_prefix n_family: org_name', + 'n_fn: org_name', + 'org_name', + 'org_name - org_unit', + 'n_given n_family', + 'n_prefix n_family', + 'n_family, n_given', + 'n_family, n_prefix', + 'n_fn', + ); + + /** + * @var array $org_fields fields belonging to the (virtual) organisation entry + */ + var $org_fields = array( + 'org_name', + 'org_unit', + 'adr_one_street', + 'adr_one_street2', + 'adr_one_locality', + 'adr_one_region', + 'adr_one_postalcode', + 'adr_one_countryname', + 'adr_one_countrycode', + 'label', + 'tel_work', + 'tel_fax', + 'tel_assistent', + 'assistent', + 'email', + 'url', + 'tz', + ); + + /** + * Which fields is a (non-admin) user allowed to edit in his own account + * + * @var array + */ + var $own_account_acl; + + /** + * @var double $org_common_factor minimum percentage of the contacts with identical values to construct the "common" (virtual) org-entry + */ + var $org_common_factor = 0.6; + + var $contact_fields = array(); + var $business_contact_fields = array(); + var $home_contact_fields = array(); + + /** + * Set Logging + * + * @var boolean + */ + var $log = false; + var $logfile = '/tmp/log-addressbook_bo'; + + /** + * Number and message of last error or false if no error, atm. only used for saving + * + * @var string/boolean + */ + var $error; + /** + * Addressbook preferences of the user + * + * @var array + */ + var $prefs; + /** + * Default addressbook for new contacts, if no addressbook is specified (user preference) + * + * @var int + */ + var $default_addressbook; + /** + * Default addressbook is the private one + * + * @var boolean + */ + var $default_private; + /** + * Use a separate private addressbook (former private flag), for contacts not shareable via regular read acl + * + * @var boolean + */ + var $private_addressbook = false; + /** + * Categories object + * + * @var categories + */ + var $categories; + + /** + * Tracking changes + * + * @var Contacts\Tracking + */ + protected $tracking; + + /** + * Keep deleted addresses, or really delete them + * Set in Admin -> Addressbook -> Site Configuration + * ''=really delete, 'history'=keep, only admins delete, 'userpurge'=keep, users delete + * + * @var string + */ + protected $delete_history = ''; + + /** + * Constructor + * + * @param string $contact_app ='addressbook' used for acl->get_grants() + * @param egw_db $db =null + */ + function __construct($contact_app='addressbook',egw_db $db=null) + { + parent::__construct($contact_app,$db); + if ($this->log) + { + $this->logfile = $GLOBALS['egw_info']['server']['temp_dir'].'/log-addressbook_bo'; + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__."($contact_app)\n", 3 ,$this->logfile); + } + + $this->now_su = DateTime::to('now','ts'); + + $this->prefs =& $GLOBALS['egw_info']['user']['preferences']['addressbook']; + // get the default addressbook from the users prefs + $this->default_addressbook = $GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'] ? + (int)$GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'] : $this->user; + $this->default_private = substr($GLOBALS['egw_info']['user']['preferences']['addressbook']['add_default'],-1) == 'p'; + if ($this->default_addressbook > 0 && $this->default_addressbook != $this->user && + ($this->default_private || + $this->default_addressbook == (int)$GLOBALS['egw']->preferences->forced['addressbook']['add_default'] || + $this->default_addressbook == (int)$GLOBALS['egw']->preferences->default['addressbook']['add_default'])) + { + $this->default_addressbook = $this->user; // admin set a default or forced pref for personal addressbook + } + $this->private_addressbook = self::private_addressbook($this->contact_repository == 'sql', $this->prefs); + + $this->contact_fields = array( + 'id' => lang('Contact ID'), + 'tid' => lang('Type'), + 'owner' => lang('Addressbook'), + 'private' => lang('private'), + 'cat_id' => lang('Category'), + 'n_prefix' => lang('prefix'), + 'n_given' => lang('first name'), + 'n_middle' => lang('middle name'), + 'n_family' => lang('last name'), + 'n_suffix' => lang('suffix'), + 'n_fn' => lang('full name'), + 'n_fileas' => lang('own sorting'), + 'bday' => lang('birthday'), + 'org_name' => lang('Organisation'), + 'org_unit' => lang('Department'), + 'title' => lang('title'), + 'role' => lang('role'), + 'assistent' => lang('Assistent'), + 'room' => lang('Room'), + 'adr_one_street' => lang('business street'), + 'adr_one_street2' => lang('business address line 2'), + 'adr_one_locality' => lang('business city'), + 'adr_one_region' => lang('business state'), + 'adr_one_postalcode' => lang('business zip code'), + 'adr_one_countryname' => lang('business country'), + 'adr_one_countrycode' => lang('business country code'), + 'label' => lang('label'), + 'adr_two_street' => lang('street (private)'), + 'adr_two_street2' => lang('address line 2 (private)'), + 'adr_two_locality' => lang('city (private)'), + 'adr_two_region' => lang('state (private)'), + 'adr_two_postalcode' => lang('zip code (private)'), + 'adr_two_countryname' => lang('country (private)'), + 'adr_two_countrycode' => lang('country code (private)'), + 'tel_work' => lang('work phone'), + 'tel_cell' => lang('mobile phone'), + 'tel_fax' => lang('business fax'), + 'tel_assistent' => lang('assistent phone'), + 'tel_car' => lang('car phone'), + 'tel_pager' => lang('pager'), + 'tel_home' => lang('home phone'), + 'tel_fax_home' => lang('fax (private)'), + 'tel_cell_private' => lang('mobile phone (private)'), + 'tel_other' => lang('other phone'), + 'tel_prefer' => lang('preferred phone'), + 'email' => lang('business email'), + 'email_home' => lang('email (private)'), + 'url' => lang('url (business)'), + 'url_home' => lang('url (private)'), + 'freebusy_uri' => lang('Freebusy URI'), + 'calendar_uri' => lang('Calendar URI'), + 'note' => lang('note'), + 'tz' => lang('time zone'), + 'geo' => lang('geo'), + 'pubkey' => lang('public key'), + 'created' => lang('created'), + 'creator' => lang('created by'), + 'modified' => lang('last modified'), + 'modifier' => lang('last modified by'), + 'jpegphoto' => lang('photo'), + 'account_id' => lang('Account ID'), + ); + $this->business_contact_fields = array( + 'org_name' => lang('Company'), + 'org_unit' => lang('Department'), + 'title' => lang('Title'), + 'role' => lang('Role'), + 'n_prefix' => lang('prefix'), + 'n_given' => lang('first name'), + 'n_middle' => lang('middle name'), + 'n_family' => lang('last name'), + 'n_suffix' => lang('suffix'), + 'adr_one_street' => lang('street').' ('.lang('business').')', + 'adr_one_street2' => lang('address line 2').' ('.lang('business').')', + 'adr_one_locality' => lang('city').' ('.lang('business').')', + 'adr_one_region' => lang('state').' ('.lang('business').')', + 'adr_one_postalcode' => lang('zip code').' ('.lang('business').')', + 'adr_one_countryname' => lang('country').' ('.lang('business').')', + ); + $this->home_contact_fields = array( + 'org_name' => lang('Company'), + 'org_unit' => lang('Department'), + 'title' => lang('Title'), + 'role' => lang('Role'), + 'n_prefix' => lang('prefix'), + 'n_given' => lang('first name'), + 'n_middle' => lang('middle name'), + 'n_family' => lang('last name'), + 'n_suffix' => lang('suffix'), + 'adr_two_street' => lang('street').' ('.lang('business').')', + 'adr_two_street2' => lang('address line 2').' ('.lang('business').')', + 'adr_two_locality' => lang('city').' ('.lang('business').')', + 'adr_two_region' => lang('state').' ('.lang('business').')', + 'adr_two_postalcode' => lang('zip code').' ('.lang('business').')', + 'adr_two_countryname' => lang('country').' ('.lang('business').')', + ); + //_debug_array($this->contact_fields); + $this->own_account_acl = $GLOBALS['egw_info']['server']['own_account_acl']; + if (!is_array($this->own_account_acl)) $this->own_account_acl = json_php_unserialize($this->own_account_acl, true); + // we have only one acl (n_fn) for the whole name, as not all backends store every part in an own field + if ($this->own_account_acl && in_array('n_fn',$this->own_account_acl)) + { + $this->own_account_acl = array_merge($this->own_account_acl,array('n_prefix','n_given','n_middle','n_family','n_suffix')); + } + if ($GLOBALS['egw_info']['server']['org_fileds_to_update']) + { + $this->org_fields = $GLOBALS['egw_info']['server']['org_fileds_to_update']; + if (!is_array($this->org_fields)) $this->org_fields = unserialize($this->org_fields); + + // Set country code if country name is selected + $supported_fields = $this->get_fields('supported',null,0); + if(in_array('adr_one_countrycode', $supported_fields) && in_array('adr_one_countryname',$this->org_fields)) + { + $this->org_fields[] = 'adr_one_countrycode'; + } + if(in_array('adr_two_countrycode', $supported_fields) && in_array('adr_two_countryname',$this->org_fields)) + { + $this->org_fields[] = 'adr_two_countrycode'; + } + } + $this->categories = new categories($this->user,'addressbook'); + + $this->delete_history = $GLOBALS['egw_info']['server']['history']; + } + + /** + * Do we use a private addressbook (in comparison to a personal one) + * + * Used to set $this->private_addressbook for current user. + * + * @param string $contact_repository + * @param array $prefs addressbook preferences + * @return boolean + */ + public static function private_addressbook($contact_repository, array $prefs=null) + { + return $contact_repository == 'sql' && $prefs['private_addressbook']; + } + + /** + * Get the availible addressbooks of the user + * + * @param int $required =EGW_ACL_READ required rights on the addressbook or multiple rights or'ed together, + * to return only addressbooks fullfilling all the given rights + * @param string $extra_label first label if given (already translated) + * @param int $user =null account_id or null for current user + * @return array with owner => label pairs + */ + function get_addressbooks($required=EGW_ACL_READ,$extra_label=null,$user=null) + { + //echo "uicontacts::get_addressbooks($required,$include_all) grants="; _debug_array($this->grants); + + if (is_null($user)) + { + $user = $this->user; + $preferences = $GLOBALS['egw_info']['user']['preferences']; + $grants = $this->grants; + } + else + { + $prefs_obj = new preferences($user); + $preferences = $prefs_obj->read_repository(); + $grants = $this->get_grants($user, 'addressbook', $preferences); + } + + $addressbooks = $to_sort = array(); + if ($extra_label) $addressbooks[''] = $extra_label; + $addressbooks[$user] = lang('Personal'); + // add all group addressbooks the user has the necessary rights too + foreach($grants as $uid => $rights) + { + if (($rights & $required) == $required && $GLOBALS['egw']->accounts->get_type($uid) == 'g') + { + $to_sort[$uid] = lang('Group %1',$GLOBALS['egw']->accounts->id2name($uid)); + } + } + if ($to_sort) + { + asort($to_sort); + $addressbooks += $to_sort; + } + if ($required != EGW_ACL_ADD && // do NOT allow to set accounts as default addressbook (AB can add accounts) + !$preferences['addressbook']['hide_accounts'] && ( + ($grants[0] & $required) == $required || + $preferences['common']['account_selection'] == 'groupmembers' && + $this->account_repository != 'ldap' && ($required & EGW_ACL_READ))) + { + $addressbooks[0] = lang('Accounts'); + } + // add all other user addressbooks the user has the necessary rights too + $to_sort = array(); + foreach($grants as $uid => $rights) + { + if ($uid != $user && ($rights & $required) == $required && $GLOBALS['egw']->accounts->get_type($uid) == 'u') + { + $to_sort[$uid] = common::grab_owner_name($uid); + } + } + if ($to_sort) + { + asort($to_sort); + $addressbooks += $to_sort; + } + if ($user > 0 && self::private_addressbook($this->contact_repository, $preferences['addressbook'])) + { + $addressbooks[$user.'p'] = lang('Private'); + } + //echo "

".__METHOD__."($required,'$extra_label')"; _debug_array($addressbooks); + return $addressbooks; + } + + /** + * calculate the file_as string from the contact and the file_as type + * + * @param array $contact + * @param string $type =null file_as type, default null to read it from the contact, unknown/not set type default to the first one + * @param boolean $isUpdate =false If true, reads the old record for any not set fields + * @return string + */ + function fileas($contact,$type=null, $isUpdate=false) + { + if (is_null($type)) $type = $contact['fileas_type']; + if (!$type) $type = $this->prefs['fileas_default'] ? $this->prefs['fileas_default'] : $this->fileas_types[0]; + + if (strpos($type,'n_fn') !== false) $contact['n_fn'] = $this->fullname($contact); + + if($isUpdate) + { + $fileas_fields = array('n_prefix','n_given','n_middle','n_family','n_suffix','n_fn','org_name','org_unit','adr_one_locality'); + $old = null; + foreach($fileas_fields as $field) + { + if(!isset($contact[$field])) + { + if(is_null($old)) $old = $this->read($contact['id']); + $contact[$field] = $old[$field]; + } + } + unset($old); + } + + // removing empty delimiters, caused by empty contact fields + $fileas = str_replace(array(', , : ',', : ',': , ',', , ',': : ',' ()'), + array(': ',': ',': ',', ',': ',''), + str_replace(array('n_prefix','n_given','n_middle','n_family','n_suffix','n_fn','org_name','org_unit','adr_one_locality'), + array($contact['n_prefix'],$contact['n_given'],$contact['n_middle'],$contact['n_family'],$contact['n_suffix'], + $contact['n_fn'], $contact['org_name'], $contact['org_unit'], $contact['adr_one_locality']), $type)); + + while ($fileas[0] == ':' || $fileas[0] == ',') + { + $fileas = substr($fileas,2); + } + while (substr($fileas,-2) == ': ' || substr($fileas,-2) == ', ') + { + $fileas = substr($fileas,0,-2); + } + //echo "

bocontacts::fileas(,$type)='$fileas'

\n"; + return $fileas; + } + + /** + * determine the file_as type from the file_as string and the contact + * + * @param array $contact + * @param string $file_as =null file_as type, default null to read it from the contact, unknown/not set type default to the first one + * @return string + */ + function fileas_type($contact,$file_as=null) + { + if (is_null($file_as)) $file_as = $contact['n_fileas']; + + if ($file_as) + { + foreach($this->fileas_types as $type) + { + if ($this->fileas($contact,$type) == $file_as) + { + return $type; + } + } + } + return $this->prefs['fileas_default'] ? $this->prefs['fileas_default'] : $this->fileas_types[0]; + } + + /** + * get selectbox options for the customfields + * + * @param array $field =null + * @return array with options: + */ + public static function cf_options() + { + $cf_fields = Storage\Customfields::get('addressbook',TRUE); + foreach ($cf_fields as $key => $value ) + { + $options[$key]= $value['label']; + } + return $options; + } + + /** + * get selectbox options for the fileas types with translated labels, or real content + * + * @param array $contact =null real content to use, default none + * @return array with options: fileas type => label pairs + */ + function fileas_options($contact=null) + { + $labels = array( + 'n_prefix' => lang('prefix'), + 'n_given' => lang('first name'), + 'n_middle' => lang('middle name'), + 'n_family' => lang('last name'), + 'n_suffix' => lang('suffix'), + 'n_fn' => lang('full name'), + 'org_name' => lang('company'), + 'org_unit' => lang('department'), + 'adr_one_locality' => lang('city'), + ); + foreach(array_keys($labels) as $name) + { + if ($contact[$name]) $labels[$name] = $contact[$name]; + } + foreach($this->fileas_types as $fileas_type) + { + $options[$fileas_type] = $this->fileas($labels,$fileas_type); + } + return $options; + } + + /** + * Set n_fileas (and n_fn) in contacts of all users (called by Admin >> Addressbook >> Site configuration (Admin only) + * + * If $all all fileas fields will be set, if !$all only empty ones + * + * @param string $fileas_type '' or type of $this->fileas_types + * @param int $all =false update all contacts or only ones with empty values + * @param int &$errors=null on return number of errors + * @return int|boolean number of contacts updated, false for wrong fileas type + */ + function set_all_fileas($fileas_type,$all=false,&$errors=null,$ignore_acl=false) + { + if ($fileas_type != '' && !in_array($fileas_type, $this->fileas_types)) + { + return false; + } + if ($ignore_acl) + { + unset($this->somain->grants); // to NOT limit search to contacts readable by current user + } + // to be able to work on huge contact repositories we read the contacts in chunks of 100 + for($n = $updated = $errors = 0; ($contacts = parent::search($all ? array() : array( + 'n_fileas IS NULL', + "n_fileas=''", + 'n_fn IS NULL', + "n_fn=''", + ),false,'','','',false,'OR',array($n*100,100))); ++$n) + { + foreach($contacts as $contact) + { + $old_fn = $contact['n_fn']; + $old_fileas = $contact['n_fileas']; + $contact['n_fn'] = $this->fullname($contact); + // only update fileas if type is given AND (all should be updated or n_fileas is empty) + if ($fileas_type && ($all || empty($contact['n_fileas']))) + { + $contact['n_fileas'] = $this->fileas($contact,$fileas_type); + } + if ($old_fileas != $contact['n_fileas'] || $old_fn != $contact['n_fn']) + { + //echo "

('$old_fileas' != '{$contact['n_fileas']}' || '$old_fn' != '{$contact['n_fn']}')=".array2string($old_fileas != $contact['n_fileas'] || $old_fn != $contact['n_fn'])."

\n"; + // only specify/write updated fields plus "keys" + $contact = array_intersect_key($contact,array( + 'id' => true, + 'owner' => true, + 'private' => true, + 'account_id' => true, + 'uid' => true, + )+($old_fileas != $contact['n_fileas'] ? array('n_fileas' => true) : array())+($old_fn != $contact['n_fn'] ? array('n_fn' => true) : array())); + if ($this->save($contact,$ignore_acl)) + { + $updated++; + } + else + { + $errors++; + } + } + } + } + return $updated; + } + + /** + * Cleanup all contacts db fields of all users (called by Admin >> Addressbook >> Site configuration (Admin only) + * + * Cleanup means to truncate all unnecessary chars like whitespaces or tabs, + * remove unneeded carriage returns or set empty fields to NULL + * + * @param int &$errors=null on return number of errors + * @return int|boolean number of contacts updated + */ + function set_all_cleanup(&$errors=null,$ignore_acl=false) + { + if ($ignore_acl) + { + unset($this->somain->grants); // to NOT limit search to contacts readable by current user + } + + // fields that must not be touched + $fields_exclude = array( + 'id' => true, + 'tid' => true, + 'owner' => true, + 'private' => true, + 'created' => true, + 'creator' => true, + 'modified' => true, + 'modifier' => true, + 'account_id' => true, + 'etag' => true, + 'uid' => true, + 'freebusy_uri' => true, + 'calendar_uri' => true, + 'photo' => true, + ); + + // to be able to work on huge contact repositories we read the contacts in chunks of 100 + for($n = $updated = $errors = 0; ($contacts = parent::search(array(),false,'','','',false,'OR',array($n*100,100))); ++$n) + { + foreach($contacts as $contact) + { + $fields_to_update = array(); + foreach($contact as $field_name => $field_value) + { + if($fields_exclude[$field_name] === true) continue; // dont touch specified field + + if (is_string($field_value) && $field_name != 'pubkey' && $field_name != 'jpegphoto') + { + // check if field has to be trimmed + if (strlen($field_value) != strlen(trim($field_value))) + { + $fields_to_update[$field_name] = $field_value = trim($field_value); + } + // check if field contains a carriage return - exclude notes + if ($field_name != 'note' && strpos($field_value,"\x0D\x0A") !== false) + { + $fields_to_update[$field_name] = $field_value = str_replace("\x0D\x0A"," ",$field_value); + } + } + // check if a field contains an empty string + if (is_string($field_value) && strlen($field_value) == 0) + { + $fields_to_update[$field_name] = $field_value = null; + } + // check for valid birthday date + if ($field_name == 'bday' && $field_value != null && + !preg_match('/^(18|19|20|21|22)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/',$field_value)) + { + $fields_to_update[$field_name] = $field_value = null; + } + } + + if(count($fields_to_update) > 0) + { + $contact_to_save = array( + 'id' => $contact['id'], + 'owner' => $contact['owner'], + 'private' => $contact['private'], + 'account_id' => $contact['account_id'], + 'uid' => $contact['uid']) + $fields_to_update; + + if ($this->save($contact_to_save,$ignore_acl)) + { + $updated++; + } + else + { + $errors++; + } + } + } + } + return $updated; + } + + /** + * get full name from the name-parts + * + * @param array $contact + * @return string full name + */ + function fullname($contact) + { + if (empty($contact['n_family']) && empty($contact['n_given'])) { + $cpart = array('org_name'); + } else { + $cpart = array('n_prefix','n_given','n_middle','n_family','n_suffix'); + } + $parts = array(); + foreach($cpart as $n) + { + if ($contact[$n]) $parts[] = $contact[$n]; + } + return implode(' ',$parts); + } + + /** + * changes the data from the db-format to your work-format + * + * it gets called everytime when data is read from the db + * This function needs to be reimplemented in the derived class + * + * @param array $data + * @param $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format + * + * @return array updated data + */ + function db2data($data, $date_format='ts') + { + static $fb_url = false; + + // convert timestamps from server-time in the db to user-time + foreach ($this->timestamps as $name) + { + if (isset($data[$name])) + { + $data[$name] = DateTime::server2user($data[$name], $date_format); + } + } + $data['photo'] = $this->photo_src($data['id'],$data['jpegphoto'],'',$data['etag']); + + // set freebusy_uri for accounts + if (!$data['freebusy_uri'] && !$data['owner'] && $data['account_id'] && !is_object($GLOBALS['egw_setup'])) + { + if ($fb_url || @is_dir(EGW_SERVER_ROOT.'/calendar/inc')) + { + $fb_url = true; + $user = isset($data['account_lid']) ? $data['account_lid'] : $GLOBALS['egw']->accounts->id2name($data['account_id']); + $data['freebusy_uri'] = calendar_bo::freebusy_url($user); + } + } + return $data; + } + + /** + * src for photo: returns array with linkparams if jpeg exists or the $default image-name if not + * @param int $id contact_id + * @param boolean $jpeg =false jpeg exists or not + * @param string $default ='' image-name to use if !$jpeg, eg. 'template' + * @param string $etag =null etag to set in url to allow caching with Expires header + * @return string/array + */ + function photo_src($id,$jpeg,$default='',$etag=null) + { + //error_log(__METHOD__."($id, ..., etag=$etag) ". function_backtrace()); + return $jpeg ? array( + 'menuaction' => 'addressbook.addressbook_ui.photo', + 'contact_id' => $id, + )+(isset($etag) ? array( + 'etag' => $etag, + ) : array()) : $default; + } + + /** + * changes the data from your work-format to the db-format + * + * It gets called everytime when data gets writen into db or on keys for db-searches + * this needs to be reimplemented in the derived class + * + * @param array $data + * @param $date_format ='ts' date-formats: 'ts'=timestamp, 'server'=timestamp in server-time, 'array'=array or string with date-format + * + * @return array upated data + */ + function data2db($data, $date_format='ts') + { + // convert timestamps from user-time to server-time in the db + foreach ($this->timestamps as $name) + { + if (isset($data[$name])) + { + $data[$name] = DateTime::user2server($data[$name], $date_format); + } + } + return $data; + } + + /** + * deletes contact in db + * + * @param mixed &$contact contact array with key id or (array of) id(s) + * @param boolean $deny_account_delete =true if true never allow to delete accounts + * @param int $check_etag =null + * @return boolean|int true on success or false on failiure, 0 if etag does not match + */ + function delete($contact,$deny_account_delete=true,$check_etag=null) + { + if (is_array($contact) && isset($contact['id'])) + { + $contact = array($contact); + } + elseif (!is_array($contact)) + { + $contact = array($contact); + } + foreach($contact as $c) + { + $id = is_array($c) ? $c['id'] : $c; + + $ok = false; + if ($this->check_perms(EGW_ACL_DELETE,$c,$deny_account_delete)) + { + if (!($old = $this->read($id))) return false; + // check if we only mark contacts as deleted, or really delete them + // already marked as deleted item and accounts are always really deleted + // we cant mark accounts as deleted, as no such thing exists for accounts! + if ($old['owner'] && $this->delete_history != '' && $old['tid'] != self::DELETED_TYPE) + { + $delete = $old; + $delete['tid'] = self::DELETED_TYPE; + if ($check_etag) $delete['etag'] = $check_etag; + if (($ok = $this->save($delete))) $ok = true; // we have to return true or false + egw_link::unlink(0,'addressbook',$id,'','','',true); + } + elseif (($ok = parent::delete($id,$check_etag))) + { + egw_link::unlink(0,'addressbook',$id); + } + + // Don't notify of final purge + if ($ok && $old['tid'] != self::DELETED_TYPE) + { + if (!isset($this->tracking)) $this->tracking = new Contacts\Tracking($this); + $this->tracking->track(array('id' => $id), array('id' => $id), null, true); + } + } + else + { + break; + } + } + //error_log(__METHOD__.'('.array2string($contact).', deny_account_delete='.array2string($deny_account_delete).', check_etag='.array2string($check_etag).' returning '.array2string($ok)); + return $ok; + } + + /** + * saves contact to db + * + * @param array &$contact contact array from etemplate::exec + * @param boolean $ignore_acl =false should the acl be checked or not + * @return int/string/boolean id on success, false on failure, the error-message is in $this->error + */ + function save(&$contact,$ignore_acl=false) + { + // remember if we add or update a entry + if (($isUpdate = $contact['id'])) + { + if (!isset($contact['owner']) || !isset($contact['private'])) // owner/private not set on update, eg. SyncML + { + if (($old = $this->read($contact['id']))) // --> try reading the old entry and set it from there + { + if(!isset($contact['owner'])) + { + $contact['owner'] = $old['owner']; + } + if(!isset($contact['private'])) + { + $contact['private'] = $old['private']; + } + } + else // entry not found --> create a new one + { + $isUpdate = $contact['id'] = null; + } + } + } + else + { + // if no owner/addressbook set use the setting of the add_default prefs (if set, otherwise the users personal addressbook) + if (!isset($contact['owner'])) $contact['owner'] = $this->default_addressbook; + if (!isset($contact['private'])) $contact['private'] = (int)$this->default_private; + // do NOT allow to create new accounts via addressbook, they are broken without an account_id + if (!$contact['owner'] && empty($contact['account_id'])) + { + $contact['owner'] = $this->default_addressbook ? $this->default_addressbook : $this->user; + } + // allow admins to import contacts with creator / created date set + if (!$contact['creator'] || !$this->is_admin($contact)) $contact['creator'] = $this->user; + if (!$contact['created'] || !$this->is_admin($contact)) $contact['created'] = $this->now_su; + + if (!$contact['tid']) $contact['tid'] = 'n'; + } + // ensure accounts and group addressbooks are never private! + if ($contact['owner'] <= 0) + { + $contact['private'] = 0; + } + if(!$ignore_acl && !$this->check_perms($isUpdate ? EGW_ACL_EDIT : EGW_ACL_ADD,$contact)) + { + $this->error = 'access denied'; + return false; + } + // resize image to 60px width + if (!empty($contact['jpegphoto'])) + { + $contact['jpegphoto'] = $this->resize_photo($contact['jpegphoto']); + } + // convert categories + if (is_array($contact['cat_id'])) + { + $contact['cat_id'] = implode(',',$contact['cat_id']); + } + + // Update country codes + foreach(array('adr_one_', 'adr_two_') as $c_prefix) { + if($contact[$c_prefix.'countryname'] && !$contact[$c_prefix.'countrycode'] && + $code = $GLOBALS['egw']->country->country_code($contact[$c_prefix.'countryname'])) + { + if(strlen($code) == 2) + { + $contact[$c_prefix.'countrycode'] = $code; + } + else + { + $contact[$c_prefix.'countrycode'] = null; + } + } + if($contact[$c_prefix.'countrycode'] != null) + { + $contact[$c_prefix.'countryname'] = null; + } + } + + // last modified + $contact['modifier'] = $this->user; + $contact['modified'] = $this->now_su; + // set full name and fileas from the content + if (!isset($contact['n_fn'])) + { + $contact['n_fn'] = $this->fullname($contact); + } + if (isset($contact['org_name'])) $contact['n_fileas'] = $this->fileas($contact, null, false); + + // Get old record for tracking changes + if (!isset($old) && $isUpdate) + { + $old = $this->read($contact['id']); + } + $to_write = $contact; + // (non-admin) user editing his own account, make sure he does not change fields he is not allowed to (eg. via SyncML or xmlrpc) + if (!$ignore_acl && !$contact['owner'] && !($this->is_admin($contact) || $this->allow_account_edit())) + { + foreach(array_keys($contact) as $field) + { + if (!in_array($field,$this->own_account_acl) && !in_array($field,array('id','owner','account_id','modified','modifier'))) + { + // user is not allowed to change that + if ($old) + { + $to_write[$field] = $contact[$field] = $old[$field]; + } + else + { + unset($to_write[$field]); + } + } + } + } + + // IF THE OLD ENTRY IS A ACCOUNT, dont allow to change the owner/location + // maybe we need that for id and account_id as well. + if (is_array($old) && (!isset($old['owner']) || empty($old['owner']))) + { + if (isset($to_write['owner']) && !empty($to_write['owner'])) + { + error_log(__METHOD__.__LINE__." Trying to change account to owner:". $to_write['owner'].' Account affected:'.array2string($old).' Data send:'.array2string($to_write)); + unset($to_write['owner']); + } + } + + if(!($this->error = parent::save($to_write))) + { + $contact['id'] = $to_write['id']; + $contact['uid'] = $to_write['uid']; + $contact['etag'] = $to_write['etag']; + + // if contact is an account and account-relevant data got updated, handle it like account got updated + if ($contact['account_id'] && $isUpdate && + ($old['email'] != $contact['email'] || $old['n_family'] != $contact['n_family'] || $old['n_given'] != $contact['n_given'])) + { + // invalidate the cache of the accounts class + $GLOBALS['egw']->accounts->cache_invalidate($contact['account_id']); + // call edit-accout hook, to let other apps know about changed account (names or email) + $GLOBALS['hook_values'] = $GLOBALS['egw']->accounts->read($contact['account_id']); + $GLOBALS['egw']->hooks->process($GLOBALS['hook_values']+array( + 'location' => 'editaccount', + ),False,True); // called for every app now, not only enabled ones) + } + // notify interested apps about changes in the account-contact data + if (!$to_write['owner'] && $to_write['account_id'] && $isUpdate) + { + $to_write['location'] = 'editaccountcontact'; + $GLOBALS['egw']->hooks->process($to_write,False,True); // called for every app now, not only enabled ones)); + } + // Notify linked apps about changes in the contact data + egw_link::notify_update('addressbook', $contact['id'], $contact); + + // Check for restore of deleted contact, restore held links + if($old && $old['tid'] == self::DELETED_TYPE && $contact['tid'] != self::DELETED_TYPE) + { + egw_link::restore('addressbook', $contact['id']); + } + + // Record change history for sql - doesn't work for LDAP accounts + if(!$contact['account_id'] || $contact['account_id'] && $this->account_repository == 'sql') + { + $deleted = ($old['tid'] == self::DELETED_TYPE || $contact['tid'] == self::DELETED_TYPE); + if (!isset($this->tracking)) $this->tracking = new Contacts\Tracking($this); + $this->tracking->track($to_write, $old ? $old : null, null, $deleted); + } + } + + return $this->error ? false : $contact['id']; + } + + /** + * Resizes photo to 60*80 pixel and returns it + * + * @param string|FILE $photo string with image or open filedescribtor + * @param int $dst_w =240 max width to resize to + * @return string with resized jpeg photo, null on error + */ + public static function resize_photo($photo,$dst_w=240) + { + if (is_resource($photo)) + { + $photo = stream_get_contents($photo); + } + if (empty($photo) || !($image = imagecreatefromstring($photo))) + { + error_log(__METHOD__."() invalid image!"); + return null; + } + $src_w = imagesx($image); + $src_h = imagesy($image); + //error_log(__METHOD__."() got image $src_w * $src_h, is_jpeg=".array2string(substr($photo,0,2) === "\377\330")); + + // if $photo is to width or not a jpeg image --> resize it + if ($src_w > $dst_w || cut_bytes($photo,0,2) !== "\377\330") + { + //error_log(__METHOD__."(,dst_w=$dst_w) src_w=$src_w, cut_bytes(photo,0,2)=".array2string(cut_bytes($photo,0,2)).' --> resizing'); + // scale the image to a width of 60 and a height according to the proportion of the source image + $resized = imagecreatetruecolor($dst_w,$dst_h = round($src_h * $dst_w / $src_w)); + imagecopyresized($resized,$image,0,0,0,0,$dst_w,$dst_h,$src_w,$src_h); + + ob_start(); + imagejpeg($resized,null,90); + $photo = ob_get_contents(); + ob_end_clean(); + + imagedestroy($resized); + //error_log(__METHOD__."() resized image $src_w*$src_h to $dst_w*$dst_h"); + } + //else error_log(__METHOD__."(,dst_w=$dst_w) src_w=$src_w, cut_bytes(photo,0,2)=".array2string(cut_bytes($photo,0,2)).' --> NOT resizing'); + + imagedestroy($image); + + return $photo; + } + + /** + * reads contacts matched by key and puts all cols in the data array + * + * @param int|string $contact_id + * @param boolean $ignore_acl =false true: no acl check + * @return array|boolean array with contact data, null if not found or false on no view perms + */ + function read($contact_id, $ignore_acl=false) + { + // get so_sql_cf to read private customfields too, if we ignore acl + if ($ignore_acl && is_a($this->somain, 'addressbook_sql')) + { + $cf_backup = (array)$this->somain->customfields; + $this->somain->customfields = Storage\Customfields::get('addressbook', true); + } + if (!($data = parent::read($contact_id))) + { + $data = null; // not found + } + elseif (!$ignore_acl && !$this->check_perms(EGW_ACL_READ,$data)) + { + $data = false; // no view perms + } + else + { + // determine the file-as type + $data['fileas_type'] = $this->fileas_type($data); + + // Update country name from code + if($data['adr_one_countrycode'] != null) { + $data['adr_one_countryname'] = $GLOBALS['egw']->country->get_full_name($data['adr_one_countrycode'], true); + } + if($data['adr_two_countrycode'] != null) { + $data['adr_two_countryname'] = $GLOBALS['egw']->country->get_full_name($data['adr_two_countrycode'], true); + } + } + if (isset($cf_backup)) + { + $this->somain->customfields = $cf_backup; + } + //error_log(__METHOD__.'('.array2string($contact_id).') returning '.array2string($data)); + return $data; + } + + /** + * Checks if the current user has the necessary ACL rights + * + * If the access of a contact is set to private, one need a private grant for a personal addressbook + * or the group membership for a group-addressbook + * + * @param int $needed necessary ACL right: EGW_ACL_{READ|EDIT|DELETE} + * @param mixed $contact contact as array or the contact-id + * @param boolean $deny_account_delete =false if true never allow to delete accounts + * @param int $user =null for which user to check, default current user + * @return boolean true permission granted, false for permission denied, null for contact does not exist + */ + function check_perms($needed,$contact,$deny_account_delete=false,$user=null) + { + if (!$user) $user = $this->user; + if ($user == $this->user) + { + $grants = $this->grants; + $memberships = $this->memberships; + } + else + { + $grants = $this->get_grants($user); + $memberships = $GLOBALS['egw']->accounts->memberships($user,true); + } + + if ((!is_array($contact) || !isset($contact['owner'])) && + !($contact = parent::read(is_array($contact) ? $contact['id'] : $contact))) + { + return null; + } + $owner = $contact['owner']; + + // allow the user to edit his own account + if (!$owner && $needed == EGW_ACL_EDIT && $contact['account_id'] == $user && $this->own_account_acl) + { + $access = true; + } + // dont allow to delete own account (as admin handels it too) + elseif (!$owner && $needed == EGW_ACL_DELETE && ($deny_account_delete || $contact['account_id'] == $user)) + { + $access = false; + } + // for reading accounts (owner == 0) and account_selection == groupmembers, check if current user and contact are groupmembers + elseif ($owner == 0 && $needed == EGW_ACL_READ && + $GLOBALS['egw_info']['user']['preferences']['common']['account_selection'] == 'groupmembers' && + !isset($GLOBALS['egw_info']['user']['apps']['admin'])) + { + $access = !!array_intersect($memberships,$GLOBALS['egw']->accounts->memberships($contact['account_id'],true)); + } + else + { + $access = ($grants[$owner] & $needed) && + (!$contact['private'] || ($grants[$owner] & EGW_ACL_PRIVATE) || in_array($owner,$memberships)); + } + //error_log(__METHOD__."($needed,$contact[id],$deny_account_delete,$user) returning ".array2string($access)); + return $access; + } + + /** + * Check access to the file store + * + * @param int|array $id id of entry or entry array + * @param int $check EGW_ACL_READ for read and EGW_ACL_EDIT for write or delete access + * @param string $rel_path =null currently not used in InfoLog + * @param int $user =null for which user to check, default current user + * @return boolean true if access is granted or false otherwise + */ + function file_access($id,$check,$rel_path=null,$user=null) + { + unset($rel_path); // not used, but required by function signature + + return $this->check_perms($check,$id,false,$user); + } + + /** + * Read (virtual) org-entry (values "common" for most contacts in the given org) + * + * @param string $org_id org_name:oooooo|||org_unit:uuuuuuuuu|||adr_one_locality:lllllll (org_unit and adr_one_locality are optional) + * @return array/boolean array with common org fields or false if org not found + */ + function read_org($org_id) + { + if (!$org_id) return false; + if (strpos($org_id,'*AND*')!== false) $org_id = str_replace('*AND*','&',$org_id); + $org = array(); + foreach(explode('|||',$org_id) as $part) + { + list($name,$value) = explode(':',$part,2); + $org[$name] = $value; + } + $csvs = array('cat_id'); // fields with comma-separated-values + + // split regular fields and custom fields + $custom_fields = $regular_fields = array(); + foreach($this->org_fields as $name) + { + if ($name[0] != '#') + { + $regular_fields[] = $name; + } + else + { + $custom_fields[] = $name = substr($name,1); + $regular_fields['id'] = 'id'; + if (substr($this->customfields[$name]['type'],0,6)=='select' && $this->customfields[$name]['rows'] || // multiselection + $this->customfields[$name]['type'] == 'radio') + { + $csvs[] = '#'.$name; + } + } + } + // read the regular fields + $contacts = parent::search('',$regular_fields,'','','',false,'AND',false,$org); + if (!$contacts) return false; + + // if we have custom fields, read and merge them in + if ($custom_fields) + { + foreach($contacts as $contact) + { + $ids[] = $contact['id']; + } + if (($cfs = $this->read_customfields($ids,$custom_fields))) + { + foreach ($contacts as &$contact) + { + $id = $contact['id']; + if (isset($cfs[$id])) + { + foreach($cfs[$id] as $name => $value) + { + $contact['#'.$name] = $value; + } + } + } + unset($contact); + } + } + + // create a statistic about the commonness of each fields values + $fields = array(); + foreach($contacts as $contact) + { + foreach($contact as $name => $value) + { + if (!in_array($name,$csvs)) + { + $fields[$name][$value]++; + } + else + { + // for comma separated fields, we have to use each single value + foreach(explode(',',$value) as $val) + { + $fields[$name][$val]++; + } + } + } + } + foreach($fields as $name => $values) + { + if (!in_array($name,$this->org_fields)) continue; + + arsort($values,SORT_NUMERIC); + list($value,$num) = each($values); + //echo "

$name: '$value' $num/".count($contacts)."=".($num / (double) count($contacts))." >= $this->org_common_factor = ".($num / (double) count($contacts) >= $this->org_common_factor ? 'true' : 'false')."

\n"; + if ($value && $num / (double) count($contacts) >= $this->org_common_factor) + { + if (!in_array($name,$csvs)) + { + $org[$name] = $value; + } + else + { + $org[$name] = array(); + foreach ($values as $value => $num) + { + if ($value && $num / (double) count($contacts) >= $this->org_common_factor) + { + $org[$name][] = $value; + } + } + $org[$name] = implode(',',$org[$name]); + } + } + } + return $org; + } + + /** + * Return all org-members with same content in one or more of the given fields (only org_fields are counting) + * + * @param string $org_name + * @param array $fields field-name => value pairs + * @return array with contacts + */ + function org_similar($org_name,$fields) + { + $criteria = array(); + foreach($this->org_fields as $name) + { + if (isset($fields[$name])) + { + if (empty($fields[$name])) + { + $criteria[] = "($name IS NULL OR $name='')"; + } + else + { + $criteria[$name] = $fields[$name]; + } + } + } + return parent::search($criteria,false,'n_family,n_given','','',false,'OR',false,array('org_name'=>$org_name)); + } + + /** + * Return the changed fields from two versions of a contact (not modified or modifier) + * + * @param array $from original/old version of the contact + * @param array $to changed/new version of the contact + * @param boolean $only_org_fields =true check and return only org_fields, default true + * @return array with field-name => value from $from + */ + function changed_fields($from,$to,$only_org_fields=true) + { + // we only care about countryname, if contrycode is empty + foreach(array( + 'adr_one_countryname' => 'adr_one_countrycode', + 'adr_two_countryname' => 'adr_one_countrycode', + ) as $name => $code) + { + if (!empty($from[$code])) $from[$name] = ''; + if (!empty($to[$code])) $to[$name] = ''; + } + $changed = array(); + foreach($only_org_fields ? $this->org_fields : array_keys($this->contact_fields) as $name) + { + if (in_array($name,array('modified','modifier'))) // never count these + { + continue; + } + if ((string) $from[$name] != (string) $to[$name]) + { + $changed[$name] = $from[$name]; + } + } + return $changed; + } + + /** + * Change given fields in all members of the org with identical content in the field + * + * @param string $org_name + * @param array $from original/old version of the contact + * @param array $to changed/new version of the contact + * @param array $members =null org-members to change, default null --> function queries them itself + * @return array/boolean (changed-members,changed-fields,failed-members) or false if no org_fields changed or no (other) members matching that fields + */ + function change_org($org_name,$from,$to,$members=null) + { + if (!($changed = $this->changed_fields($from,$to,true))) return false; + + if (is_null($members) || !is_array($members)) + { + $members = $this->org_similar($org_name,$changed); + } + if (!$members) return false; + + $ids = array(); + foreach($members as $member) + { + $ids[] = $member['id']; + } + $customfields = $this->read_customfields($ids); + + $changed_members = $changed_fields = $failed_members = 0; + foreach($members as $member) + { + if (isset($customfields[$member['id']])) + { + foreach(array_keys($this->customfields) as $name) + { + $member['#'.$name] = $customfields[$member['id']][$name]; + } + } + $fields = 0; + foreach($changed as $name => $value) + { + if ((string)$value == (string)$member[$name]) + { + $member[$name] = $to[$name]; + //echo "

$member[n_family], $member[n_given]: $name='{$to[$name]}'

\n"; + ++$fields; + } + } + if ($fields) + { + if (!$this->check_perms(EGW_ACL_EDIT,$member) || !$this->save($member)) + { + ++$failed_members; + } + else + { + ++$changed_members; + $changed_fields += $fields; + } + } + } + return array($changed_members,$changed_fields,$failed_members); + } + + /** + * get title for a contact identified by $contact + * + * Is called as hook to participate in the linking. The format is determined by the link_title preference. + * + * @param int|string|array $contact int/string id or array with contact + * @return string/boolean string with the title, null if contact does not exitst, false if no perms to view it + */ + function link_title($contact) + { + if (!is_array($contact) && $contact) + { + $contact = $this->read($contact); + } + if (!is_array($contact)) + { + return $contact; + } + $type = $this->prefs['link_title']; + if (!$type || $type === 'n_fileas') + { + if ($contact['n_fileas']) return $contact['n_fileas']; + $type = null; + } + $title = $this->fileas($contact,$type); + if ($this->prefs['link_title_cf'] && $contact['#'.$this->prefs['link_title_cf']]) + { + $title .= ' ' . $contact['#'.$this->prefs['link_title_cf']]; + } + return $title ; + } + + /** + * get title for multiple contacts identified by $ids + * + * Is called as hook to participate in the linking. The format is determined by the link_title preference. + * + * @param array $ids array with contact-id's + * @return array with titles, see link_title + */ + function link_titles(array $ids) + { + $titles = array(); + if (($contacts =& $this->search(array('contact_id' => $ids),false))) + { + $ids = array(); + foreach($contacts as $contact) + { + $ids[] = $contact['id']; + } + $cfs = $this->read_customfields($ids); + foreach($contacts as $contact) + { + $titles[$contact['id']] = $this->link_title($contact+(array)$cfs[$contact['id']]); + } + } + // we assume all not returned contacts are not readable for the user (as we report all deleted contacts to egw_link) + foreach($ids as $id) + { + if (!isset($titles[$id])) + { + $titles[$id] = false; + } + } + return $titles; + } + + /** + * query addressbook for contacts matching $pattern + * + * Is called as hook to participate in the linking + * + * @param string|array $pattern pattern to search, or an array with a 'search' key + * @param array $options Array of options for the search + * @return array with id - title pairs of the matching entries + */ + function link_query($pattern, Array &$options = array()) + { + $result = $criteria = array(); + $limit = false; + if ($pattern) + { + $criteria = is_array($pattern) ? $pattern['search'] : $pattern; + } + if($options['start'] || $options['num_rows']) + { + $limit = array($options['start'], $options['num_rows']); + } + $filter = (array)$options['filter']; + if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts']) $filter['account_id'] = null; + if (($contacts =& parent::search($criteria,false,'org_name,n_family,n_given,cat_id,contact_email','','%',false,'OR', $limit, $filter))) + { + $ids = array(); + foreach($contacts as $contact) + { + $ids[] = $contact['id']; + } + $cfs = $this->read_customfields($ids); + foreach($contacts as $contact) + { + $result[$contact['id']] = $this->link_title($contact+(array)$cfs[$contact['id']]); + // make sure to return a correctly quoted rfc822 address, if requested + if ($options['type'] === 'email') + { + $args = explode('@', $contact['email']); + $args[] = $result[$contact['id']]; + $result[$contact['id']] = call_user_func_array('imap_rfc822_write_address', $args); + } + // show category color + if ($contact['cat_id'] && ($color = etemplate::cats2color($contact['cat_id']))) + { + $result[$contact['id']] = array( + 'label' => $result[$contact['id']], + 'style.backgroundColor' => $color, + ); + } + } + } + $options['total'] = $this->total; + return $result; + } + + /** + * Query for subtype email (returns only contacts with email address set) + * + * @param string|array $pattern + * @param array $options + * @return Ambigous string > + */ + function link_query_email($pattern, Array &$options = array()) + { + if (isset($options['filter']) && !is_array($options['filter'])) + { + $options['filter'] = (array)$options['filter']; + } + // return only contacts with email set + $options['filter'][] = "contact_email ".$this->db->capabilities[egw_db::CAPABILITY_CASE_INSENSITIV_LIKE]." '%@%'"; + + // let link query know, to append email to list + $options['type'] = 'email'; + + return $this->link_query($pattern,$options); + } + + /** + * returns info about contacts for calender + * + * @param int|array $ids single contact-id or array of id's + * @return array + */ + function calendar_info($ids) + { + if (!$ids) return null; + + $data = array(); + foreach(!is_array($ids) ? array($ids) : $ids as $id) + { + if (!($contact = $this->read($id))) continue; + + $data[] = array( + 'res_id' => $id, + 'email' => $contact['email'] ? $contact['email'] : $contact['email_home'], + 'rights' => EGW_ACL_READ_FOR_PARTICIPANTS, + 'name' => $this->link_title($contact), + 'cn' => trim($contact['n_given'].' '.$contact['n_family']), + ); + } + //echo "

calendar_info(".print_r($ids,true).")="; _debug_array($data); + return $data; + } + + /** + * Read the next and last event of given contacts + * + * @param array $ids contact_id's + * @param boolean $extra_title =true if true, use a short date only title and put the full title as extra_title (tooltip) + * @return array + */ + function read_calendar($ids,$extra_title=true) + { + if (!$GLOBALS['egw_info']['user']['apps']['calendar']) return array(); + + $uids = array(); + foreach($ids as $id) + { + if (is_numeric($id)) $uids[] = 'c'.$id; + } + if (!$uids) return array(); + + $bocal = new calendar_bo(); + $events = $bocal->search(array( + 'users' => $uids, + 'enum_recuring' => true, + )); + if (!$events) return array(); + + //_debug_array($events); + $calendars = array(); + foreach($events as $event) + { + foreach($event['participants'] as $uid => $status) + { + if ($uid[0] != 'c' || ($status == 'R' && !$GLOBALS['egw_info']['user']['preferences']['calendar']['show_rejected'])) + { + continue; + } + $id = (int)substr($uid,1); + + if ($event['start'] < $this->now_su) // past event --> check for last event + { + if (!isset($calendars[$id]['last_event']) || $event['start'] > $calendars[$id]['last_event']) + { + $calendars[$id]['last_event'] = $event['start']; + $link = array( + 'id' => $event['id'], + 'app' => 'calendar', + 'title' => $bocal->link_title($event), + 'extra_args' => array( + 'date' => date('Ymd',$event['start']), + ), + ); + if ($extra_title) + { + $link['extra_title'] = $link['title']; + $link['title'] = date($GLOBALS['egw_info']['user']['preferences']['common']['dateformat'],$event['start']); + } + $calendars[$id]['last_link'] = $link; + } + } + else // future event --> check for next event + { + if (!isset($calendars[$id]['next_event']) || $event['start'] < $calendars[$id]['next_event']) + { + $calendars[$id]['next_event'] = $event['start']; + $link = array( + 'id' => $event['id'], + 'app' => 'calendar', + 'title' => $bocal->link_title($event), + 'extra_args' => array( + 'date' => date('Ymd',$event['start']), + ), + ); + if ($extra_title) + { + $link['extra_title'] = $link['title']; + $link['title'] = date($GLOBALS['egw_info']['user']['preferences']['common']['dateformat'],$event['start']); + } + $calendars[$id]['next_link'] = $link; + } + } + } + } + return $calendars; + } + + /** + * Called by delete-account hook, when an account get deleted --> deletes/moves the personal addressbook + * + * @param array $data + */ + function deleteaccount($data) + { + // delete/move personal addressbook + parent::deleteaccount($data); + } + + /** + * Called by delete_category hook, when a category gets deleted. + * Removes the category from addresses + */ + function delete_category($data) + { + // get all cats if you want to drop sub cats + $drop_subs = ($data['drop_subs'] && !$data['modify_subs']); + if($drop_subs) + { + $cats = new categories('', 'addressbook'); + $cat_ids = $cats->return_all_children($data['cat_id']); + } + else + { + $cat_ids = array($data['cat_id']); + } + + // Get addresses that use the category + @set_time_limit( 0 ); + foreach($cat_ids as $cat_id) + { + if (($ids = $this->search(array('cat_id' => $cat_id), false))) + { + foreach($ids as &$info) + { + $info['cat_id'] = implode(',',array_diff(explode(',',$info['cat_id']), $cat_ids)); + $this->save($info); + } + } + } + } + + /** + * Merges some given addresses into the first one and delete the others + * + * If one of the other addresses is an account, everything is merged into the account. + * If two accounts are in $ids, the function fails (returns false). + * + * @param array $ids contact-id's to merge + * @return int number of successful merged contacts, false on a fatal error (eg. cant merge two accounts) + */ + function merge($ids) + { + $this->error = false; + $account = null; + $custom_fields = Storage\Customfields::get('addressbook', true); + $custom_field_list = $this->read_customfields($ids); + foreach(parent::search(array('id'=>$ids),false) as $contact) // $this->search calls the extended search from ui! + { + if ($contact['account_id']) + { + if (!is_null($account)) + { + echo $this->error = 'Can not merge more then one account!'; + return false; // we dont deal with two accounts! + } + $account = $contact; + continue; + } + // Add in custom fields + if (is_array($custom_field_list[$contact['id']])) $contact = array_merge($contact, $custom_field_list[$contact['id']]); + + $pos = array_search($contact['id'],$ids); + $contacts[$pos] = $contact; + } + if (!is_null($account)) // we found an account, so we merge the contacts into it + { + $target = $account; + unset($account); + } + else // we found no account, so we merge all but the first into the first + { + $target = $contacts[0]; + unset($contacts[0]); + } + if (!$this->check_perms(EGW_ACL_EDIT,$target)) + { + echo $this->error = 'No edit permission for the target contact!'; + return 0; + } + foreach($contacts as $contact) + { + foreach($contact as $name => $value) + { + if (!$value) continue; + + switch($name) + { + case 'id': + case 'tid': + case 'owner': + case 'private': + case 'etag'; + break; // ignored + + case 'cat_id': // cats are all merged together + if (!is_array($target['cat_id'])) $target['cat_id'] = $target['cat_id'] ? explode(',',$target['cat_id']) : array(); + $target['cat_id'] = array_unique(array_merge($target['cat_id'],is_array($value)?$value:explode(',',$value))); + break; + + default: + // Multi-select custom fields can also be merged + if($name[0] == '#') { + $c_name = substr($name, 1); + if($custom_fields[$c_name]['type'] == 'select' && $custom_fields[$c_name]['rows'] > 1) { + if (!is_array($target[$name])) $target[$name] = $target[$name] ? explode(',',$target[$name]) : array(); + $target[$name] = implode(',',array_unique(array_merge($target[$name],is_array($value)?$value:explode(',',$value)))); + } + } + if (!$target[$name]) $target[$name] = $value; + break; + } + } + } + if (!$this->save($target)) return 0; + + $success = 1; + foreach($contacts as $contact) + { + if (!$this->check_perms(EGW_ACL_DELETE,$contact)) + { + continue; + } + foreach(egw_link::get_links('addressbook',$contact['id']) as $data) + { + //_debug_array(array('function'=>__METHOD__,'line'=>__LINE__,'app'=>'addressbook','id'=>$contact['id'],'data:'=>$data,'target'=>$target['id'])); + // info_from and info_link_id (main link) + $newlinkID = egw_link::link('addressbook',$target['id'],$data['app'],$data['id'],$data['remark'],$target['owner']); + //_debug_array(array('newLinkID'=>$newlinkID)); + if ($newlinkID) + { + // update egw_infolog set info_link_id=$newlinkID where info_id=$data['id'] and info_link_id=$data['link_id'] + if ($data['app']=='infolog') + { + $this->db->update('egw_infolog',array( + 'info_link_id' => $newlinkID + ),array( + 'info_id' => $data['id'], + 'info_link_id' => $data['link_id'] + ),__LINE__,__FILE__,'infolog'); + } + unset($newlinkID); + } + } + if ($this->delete($contact['id'])) $success++; + } + return $success; + } + + /** + * Some caching for lists within request + * + * @var array + */ + private static $list_cache = array(); + + /** + * Check if user has required rights for a list or list-owner + * + * @param int $list + * @param int $required + * @param int $owner =null + * @return boolean + */ + function check_list($list,$required,$owner=null) + { + if ($list && ($list_data = $this->read_list($list))) + { + $owner = $list_data['list_owner']; + } + //error_log(__METHOD__."($list, $required, $owner) grants[$owner]=".$this->grants[$owner]." returning ".array2string(!!($this->grants[$owner] & $required))); + return !!($this->grants[$owner] & $required); + } + + /** + * Adds / updates a distribution list + * + * @param string|array $keys list-name or array with column-name => value pairs to specify the list + * @param int $owner user- or group-id + * @param array $contacts =array() contacts to add (only for not yet existing lists!) + * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name' + * @return int|boolean integer list_id or false on error + */ + function add_list($keys,$owner,$contacts=array(),array &$data=array()) + { + if (!$this->check_list(null,EGW_ACL_ADD|EGW_ACL_EDIT,$owner)) return false; + + try { + $ret = parent::add_list($keys,$owner,$contacts,$data); + if ($ret) unset(self::$list_cache[$ret]); + } + // catch sql error, as creating same name&owner list gives a sql error doublicate key + catch(Api\Db\Exception\InvalidSql $e) { + unset($e); // not used + return false; + } + return $ret; + } + + /** + * Adds contacts to a distribution list + * + * @param int|array $contact contact_id(s) + * @param int $list list-id + * @param array $existing =null array of existing contact-id(s) of list, to not reread it, eg. array() + * @return false on error + */ + function add2list($contact,$list,array $existing=null) + { + if (!$this->check_list($list,EGW_ACL_EDIT)) return false; + + unset(self::$list_cache[$list]); + + return parent::add2list($contact,$list,$existing); + } + + /** + * Removes one contact from distribution list(s) + * + * @param int|array $contact contact_id(s) + * @param int $list list-id + * @return false on error + */ + function remove_from_list($contact,$list=null) + { + if ($list && !$this->check_list($list,EGW_ACL_EDIT)) return false; + + if ($list) + { + unset(self::$list_cache[$list]); + } + else + { + self::$list_cache = array(); + } + + return parent::remove_from_list($contact,$list); + } + + /** + * Deletes a distribution list (incl. it's members) + * + * @param int|array $list list_id(s) + * @return number of members deleted or false if list does not exist + */ + function delete_list($list) + { + if (!$this->check_list($list,EGW_ACL_DELETE)) return false; + + foreach((array)$list as $l) + { + unset(self::$list_cache[$l]); + } + + return parent::delete_list($list); + } + + /** + * Read data of a distribution list + * + * @param int $list list_id + * @return array of data or false if list does not exist + */ + function read_list($list) + { + if (isset(self::$list_cache[$list])) return self::$list_cache[$list]; + + return self::$list_cache[$list] = parent::read_list($list); + } + + /** + * Get the address-format of a country + * + * This is a good reference where I got nearly all information, thanks to mikaelarhelger-AT-gmail.com + * http://www.bitboost.com/ref/international-address-formats.html + * + * Mail me (RalfBecker-AT-outdoor-training.de) if you want your nation added or fixed. + * + * @param string $country + * @return string 'city_state_postcode' (eg. US) or 'postcode_city' (eg. DE) + */ + function addr_format_by_country($country) + { + $code = $GLOBALS['egw']->country->country_code($country); + + switch($code) + { + case 'AU': + case 'CA': + case 'GB': // not exactly right, postcode is in separate line + case 'HK': // not exactly right, they have no postcode + case 'IN': + case 'ID': + case 'IE': // not exactly right, they have no postcode + case 'JP': // not exactly right + case 'KR': + case 'LV': + case 'NZ': + case 'TW': + case 'SA': // not exactly right, postcode is in separate line + case 'SG': + case 'US': + $adr_format = 'city_state_postcode'; + break; + + case 'AR': + case 'AT': + case 'BE': + case 'CH': + case 'CZ': + case 'DK': + case 'EE': + case 'ES': + case 'FI': + case 'FR': + case 'DE': + case 'GL': + case 'IS': + case 'IL': + case 'IT': + case 'LT': + case 'LU': + case 'MY': + case 'MX': + case 'NL': + case 'NO': + case 'PL': + case 'PT': + case 'RO': + case 'RU': + case 'SE': + $adr_format = 'postcode_city'; + break; + + default: + $adr_format = $this->prefs['addr_format'] ? $this->prefs['addr_format'] : 'postcode_city'; + } + //echo "

bocontacts::addr_format_by_country('$country'='$code') = '$adr_format'

\n"; + return $adr_format; + } + + /** + * Find existing categories in database by name or add categories that do not exist yet + * currently used for vcard import + * + * @param array $catname_list names of the categories which should be found or added + * @param int $contact_id =null match against existing contact and expand the returned category ids + * by the ones the user normally does not see due to category permissions - used to preserve categories + * @return array category ids (found, added and preserved categories) + */ + function find_or_add_categories($catname_list, $contact_id=null) + { + if ($contact_id && $contact_id > 0 && ($old_contact = $this->read($contact_id))) + { + // preserve categories without users read access + $old_categories = explode(',',$old_contact['cat_id']); + $old_cats_preserve = array(); + if (is_array($old_categories) && count($old_categories) > 0) + { + foreach ($old_categories as $cat_id) + { + if (!$this->categories->check_perms(EGW_ACL_READ, $cat_id)) + { + $old_cats_preserve[] = $cat_id; + } + } + } + } + + $cat_id_list = array(); + foreach ((array)$catname_list as $cat_name) + { + $cat_name = trim($cat_name); + $cat_id = $this->categories->name2id($cat_name, 'X-'); + if (!$cat_id) + { + // some SyncML clients (mostly phones) add an X- to the category names + if (strncmp($cat_name, 'X-', 2) == 0) + { + $cat_name = substr($cat_name, 2); + } + $cat_id = $this->categories->add(array('name' => $cat_name, 'descr' => $cat_name, 'access' => 'private')); + } + + if ($cat_id) + { + $cat_id_list[] = $cat_id; + } + } + + if (is_array($old_cats_preserve) && count($old_cats_preserve) > 0) + { + $cat_id_list = array_merge($cat_id_list, $old_cats_preserve); + } + + if (count($cat_id_list) > 1) + { + $cat_id_list = array_unique($cat_id_list); + sort($cat_id_list, SORT_NUMERIC); + } + + //error_log(__METHOD__."(".array2string($catname_list).", $contact_id) returning ".array2string($cat_id_list)); + return $cat_id_list; + } + + function get_categories($cat_id_list) + { + if (!is_object($this->categories)) + { + $this->categories = new categories($this->user,'addressbook'); + } + + if (!is_array($cat_id_list)) + { + $cat_id_list = explode(',',$cat_id_list); + } + $cat_list = array(); + foreach($cat_id_list as $cat_id) + { + if ($cat_id && $this->categories->check_perms(EGW_ACL_READ, $cat_id) && + ($cat_name = $this->categories->id2name($cat_id)) && $cat_name != '--') + { + $cat_list[] = $cat_name; + } + } + + return $cat_list; + } + + function fixup_contact(&$contact) + { + if (empty($contact['n_fn'])) + { + $contact['n_fn'] = $this->fullname($contact); + } + + if (empty($contact['n_fileas'])) + { + $contact['n_fileas'] = $this->fileas($contact); + } + } + + /** + * Try to find a matching db entry + * + * @param array $contact the contact data we try to find + * @param boolean $relax =false if asked to relax, we only match against some key fields + * @return array od matching contact_ids + */ + function find_contact($contact, $relax=false) + { + $empty_addr_one = $empty_addr_two = true; + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '('. ($relax ? 'RELAX': 'EXACT') . ')[ContactData]:' + . array2string($contact) + . "\n", 3, $this->logfile); + } + + $matchingContacts = array(); + if ($contact['id'] && ($found = $this->read($contact['id']))) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[ContactID]: ' . $contact['id'] + . "\n", 3, $this->logfile); + } + // We only do a simple consistency check + if (!$relax || ((empty($found['n_family']) || $found['n_family'] == $contact['n_family']) + && (empty($found['n_given']) || $found['n_given'] == $contact['n_given']) + && (empty($found['org_name']) || $found['org_name'] == $contact['org_name']))) + { + return array($found['id']); + } + } + unset($contact['id']); + + if (!$relax && !empty($contact['uid'])) + { + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[ContactUID]: ' . $contact['uid'] + . "\n", 3, $this->logfile); + } + // Try the given UID first + $criteria = array ('contact_uid' => $contact['uid']); + if (($foundContacts = parent::search($criteria))) + { + foreach ($foundContacts as $egwContact) + { + $matchingContacts[] = $egwContact['id']; + } + } + return $matchingContacts; + } + unset($contact['uid']); + + $columns_to_search = array('n_family', 'n_given', 'n_middle', 'n_prefix', 'n_suffix', + 'bday', 'org_name', 'org_unit', 'title', 'role', + 'email', 'email_home'); + $tolerance_fields = array('n_middle', 'n_prefix', 'n_suffix', + 'bday', 'org_unit', 'title', 'role', + 'email', 'email_home'); + $addr_one_fields = array('adr_one_street', 'adr_one_locality', + 'adr_one_region', 'adr_one_postalcode'); + $addr_two_fields = array('adr_two_street', 'adr_two_locality', + 'adr_two_region', 'adr_two_postalcode'); + + if (!empty($contact['owner'])) + { + $columns_to_search += array('owner'); + } + + $criteria = array(); + + foreach ($columns_to_search as $field) + { + if ($relax && in_array($field, $tolerance_fields)) continue; + + if (empty($contact[$field])) + { + // Not every device supports all fields + if (!in_array($field, $tolerance_fields)) + { + $criteria[$field] = ''; + } + } + else + { + $criteria[$field] = $contact[$field]; + } + } + + if (!$relax) + { + // We use addresses only for strong matching + + foreach ($addr_one_fields as $field) + { + if (empty($contact[$field])) + { + $criteria[$field] = ''; + } + else + { + $empty_addr_one = false; + $criteria[$field] = $contact[$field]; + } + } + + foreach ($addr_two_fields as $field) + { + if (empty($contact[$field])) + { + $criteria[$field] = ''; + } + else + { + $empty_addr_two = false; + $criteria[$field] = $contact[$field]; + } + } + } + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[Addressbook FIND Step 1]: ' + . 'CRITERIA = ' . array2string($criteria) + . "\n", 3, $this->logfile); + } + + // first try full match + if (($foundContacts = parent::search($criteria, true, '', '', '', true))) + { + foreach ($foundContacts as $egwContact) + { + $matchingContacts[] = $egwContact['id']; + } + } + + // No need for more searches for relaxed matching + if ($relax || count($matchingContacts)) return $matchingContacts; + + + if (!$empty_addr_one && $empty_addr_two) + { + // try given address and ignore the second one in EGW + foreach ($addr_two_fields as $field) + { + unset($criteria[$field]); + } + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[Addressbook FIND Step 2]: ' + . 'CRITERIA = ' . array2string($criteria) + . "\n", 3, $this->logfile); + } + + if (($foundContacts = parent::search($criteria, true, '', '', '', true))) + { + foreach ($foundContacts as $egwContact) + { + $matchingContacts[] = $egwContact['id']; + } + } + else + { + // try address as home address -- some devices don't qualify addresses + foreach ($addr_two_fields as $key => $field) + { + $criteria[$field] = $criteria[$addr_one_fields[$key]]; + unset($criteria[$addr_one_fields[$key]]); + } + + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[Addressbook FIND Step 3]: ' + . 'CRITERIA = ' . array2string($criteria) + . "\n", 3, $this->logfile); + } + + if (($foundContacts = parent::search($criteria, true, '', '', '', true))) + { + foreach ($foundContacts as $egwContact) + { + $matchingContacts[] = $egwContact['id']; + } + } + } + } + elseif (!$empty_addr_one && !$empty_addr_two) + { // try again after address swap + + foreach ($addr_one_fields as $key => $field) + { + $_temp = $criteria[$field]; + $criteria[$field] = $criteria[$addr_two_fields[$key]]; + $criteria[$addr_two_fields[$key]] = $_temp; + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[Addressbook FIND Step 4]: ' + . 'CRITERIA = ' . array2string($criteria) + . "\n", 3, $this->logfile); + } + if (($foundContacts = parent::search($criteria, true, '', '', '', true))) + { + foreach ($foundContacts as $egwContact) + { + $matchingContacts[] = $egwContact['id']; + } + } + } + if ($this->log) + { + error_log(__FILE__.'['.__LINE__.'] '.__METHOD__ + . '()[FOUND]: ' . array2string($matchingContacts) + . "\n", 3, $this->logfile); + } + return $matchingContacts; + } + + /** + * Get a ctag (collection tag) for one addressbook or all addressbooks readable by a user + * + * Currently implemented as maximum modification date (1 seconde granularity!) + * + * We have to include deleted entries, as otherwise the ctag will not change if an entry gets deleted! + * (Only works if tracking of deleted entries / history is switched on!) + * + * @param int|array $owner =null 0=accounts, null=all addressbooks or integer account_id of user or group + * @return string + */ + public function get_ctag($owner=null) + { + $filter = array('tid' => null); // tid=null --> use all entries incl. deleted (tid='D') + // show addressbook of a single user? + if (!is_null($owner)) $filter['owner'] = $owner; + + // should we hide the accounts addressbook + if (!$owner && $GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts']) + { + $filter['account_id'] = null; + } + $result = $this->search(array(),'contact_modified','contact_modified DESC','','',false,'AND',array(0,1),$filter); + + if (!$result || !isset($result[0]['modified'])) + { + $ctag = 'empty'; // ctag for empty addressbook + } + else + { + // need to convert modified time back to server-time (was converted to user-time by search) + // as we use it direct in server-queries eg. CardDAV sync-report and to be consistent with CalDAV + $ctag = DateTime::user2server($result[0]['modified']); + } + //error_log(__METHOD__.'('.array2string($owner).') returning '.array2string($ctag)); + return $ctag; + } +} diff --git a/addressbook/inc/class.addressbook_ads.inc.php b/api/src/Contacts/Ads.php similarity index 78% rename from addressbook/inc/class.addressbook_ads.inc.php rename to api/src/Contacts/Ads.php index 9713a6d66f..f97b7de9f2 100644 --- a/addressbook/inc/class.addressbook_ads.inc.php +++ b/api/src/Contacts/Ads.php @@ -1,14 +1,22 @@ - * @package addressbook + * @package api + * @subpackage contacts * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ +namespace EGroupware\Api\Contacts; + +use EGroupware\Api; + +// explicitly reference classes still in phpgwapi +use accounts_ads; + /** * Active directory backend for accounts (not yet AD contacts) * @@ -22,7 +30,7 @@ * All values used to construct filters need to run through ldap::quote(), * to be save against LDAP query injection!!! */ -class addressbook_ads extends addressbook_ldap +class Ads extends Ldap { /** * LDAP searches only a limited set of attributes for performance reasons, @@ -74,11 +82,13 @@ class addressbook_ads extends addressbook_ldap /** * constructor of the class * - * @param array $ldap_config=null default use from $GLOBALS['egw_info']['server'] - * @param resource $ds=null ldap connection to use + * @param array $ldap_config =null default use from $GLOBALS['egw_info']['server'] + * @param resource $ds =null ldap connection to use */ function __construct(array $ldap_config=null, $ds=null) { + if (false) parent::__construct (); // quiten IDE warning, we are explicitly NOT calling parrent constructor! + $this->accountName = $GLOBALS['egw_info']['user']['account_lid']; if ($ldap_config) @@ -103,8 +113,8 @@ class addressbook_ads extends addressbook_ldap { $this->connect(); } - $this->ldapServerInfo = ldapserverinfo::get($this->ds, $this->ldap_config['ads_host']); - $this->is_samba4 = $this->ldapServerInfo->serverType == SAMBA4_LDAPSERVER; + $this->ldapServerInfo = Api\Ldap\ServerInfo::get($this->ds, $this->ldap_config['ads_host']); + $this->is_samba4 = $this->ldapServerInfo->serverType == Api\Ldap\ServerInfo::SAMBA4; // AD seems to use user, instead of inetOrgPerson unset($this->schema2egw['posixaccount']); @@ -118,22 +128,24 @@ class addressbook_ads extends addressbook_ldap unset($this->schema2egw['user']['n_fileas']); unset($this->schema2egw['inetorgperson']); - foreach($this->schema2egw as $schema => $attributes) + foreach($this->schema2egw as $attributes) { $this->all_attributes = array_merge($this->all_attributes,array_values($attributes)); } $this->all_attributes = array_values(array_unique($this->all_attributes)); - $this->charset = translation::charset(); + $this->charset = Api\Translation::charset(); } /** * connect to LDAP server * - * @param boolean $admin=false true (re-)connect with admin not user credentials, eg. to modify accounts + * @param boolean $admin =false true (re-)connect with admin not user credentials, eg. to modify accounts */ function connect($admin=false) { + unset($admin); // not used, but required by function signature + $this->ds = $this->accounts_ads->ldap_connection(); } @@ -159,21 +171,21 @@ class addressbook_ads extends addressbook_ldap /** * reads contact data * - * @param string/array $contact_id contact_id or array with values for id or account_id + * @param string/array $_contact_id contact_id or array with values for id or account_id * @return array/boolean data if row could be retrived else False */ - function read($contact_id) + function read($_contact_id) { - if (is_array($contact_id) && isset($contact_id['account_id']) || - !is_array($contact_id) && substr($contact_id,0,8) == 'account:') + if (is_array($_contact_id) && isset($_contact_id['account_id']) || + !is_array($_contact_id) && substr($_contact_id,0,8) == 'account:') { - $account_id = (int)(is_array($contact_id) ? $contact_id['account_id'] : substr($contact_id,8)); - $contact_id = $GLOBALS['egw']->accounts->id2name($account_id, 'person_id'); + $account_id = (int)(is_array($_contact_id) ? $_contact_id['account_id'] : substr($_contact_id,8)); + $_contact_id = $GLOBALS['egw']->accounts->id2name($account_id, 'person_id'); } - $contact_id = !is_array($contact_id) ? $contact_id : - (isset ($contact_id['id']) ? $contact_id['id'] : $contact_id['uid']); + $contact_id = !is_array($_contact_id) ? $_contact_id : + (isset ($_contact_id['id']) ? $_contact_id['id'] : $_contact_id['uid']); - $rows = $this->_searchLDAP($this->allContactsDN, $filter=$this->id_filter($contact_id), $this->all_attributes, ADDRESSBOOK_ALL); + $rows = $this->_searchLDAP($this->allContactsDN, $filter=$this->id_filter($contact_id), $this->all_attributes, Ldap::ALL); //error_log(__METHOD__."('$contact_id') _searchLDAP($this->allContactsDN, '$filter',...)=".array2string($rows)); return $rows ? $rows[0] : false; } @@ -214,5 +226,4 @@ class addressbook_ads extends addressbook_ldap parent::sanitize_update($ldapContact); } - } diff --git a/addressbook/inc/class.addressbook_ldap.inc.php b/api/src/Contacts/Ldap.php similarity index 95% rename from addressbook/inc/class.addressbook_ldap.inc.php rename to api/src/Contacts/Ldap.php index acfcc9bc04..3c126e2a1d 100644 --- a/addressbook/inc/class.addressbook_ldap.inc.php +++ b/api/src/Contacts/Ldap.php @@ -1,20 +1,23 @@ * @author Lars Kneschke * @author Ralf Becker - * @package addressbook + * @package api + * @subpackage contacts * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ -define('ADDRESSBOOK_ALL',0); -define('ADDRESSBOOK_ACCOUNTS',1); -define('ADDRESSBOOK_PERSONAL',2); -define('ADDRESSBOOK_GROUP',3); +namespace EGroupware\Api\Contacts; + +use EGroupware\Api; + +// explicitly reference classes still in phpgwapi +use common; // randomstring /** * LDAP Backend for contacts, compatible with vars and parameters of eTemplate's so_sql. @@ -23,8 +26,14 @@ define('ADDRESSBOOK_GROUP',3); * All values used to construct filters need to run through ldap::quote(), * to be save against LDAP query injection!!! */ -class addressbook_ldap +class Ldap { + + const ALL = 0; + const ACCOUNTS = 1; + const PERSONAL = 2; + const GROUP = 3; + var $data; /** @@ -298,7 +307,7 @@ class addressbook_ldap } $this->all_attributes = array_values(array_unique($this->all_attributes)); - $this->charset = translation::charset(); + $this->charset = Api\Translation::charset(); } /** @@ -414,7 +423,7 @@ class addressbook_ldap $filter = $this->id_filter($contact_id); } $rows = $this->_searchLDAP($this->allContactsDN, - $filter, $this->all_attributes, ADDRESSBOOK_ALL, array('_posixaccount2egw')); + $filter, $this->all_attributes, self::ALL, array('_posixaccount2egw')); return $rows ? $rows[0] : false; } @@ -508,7 +517,7 @@ class addressbook_ldap if(empty($contactUID)) { - $ldapContact[$this->dn_attribute] = $this->data[$this->contacts_id] = $contactUID = md5($GLOBALS['egw']->common->randomstring(15)); + $ldapContact[$this->dn_attribute] = $this->data[$this->contacts_id] = $contactUID = md5(common::randomstring(15)); } //error_log(__METHOD__."() contactUID='$contactUID', isUpdate=".array2string($isUpdate).", oldContactInfo=".array2string($oldContactInfo)); // add for all supported objectclasses the objectclass and it's attributes @@ -537,7 +546,7 @@ class addressbook_ldap { // dont convert the (binary) jpegPhoto! $ldapContact[$ldapFieldName] = $ldapFieldName == 'jpegphoto' ? $data[$egwFieldName] : - translation::convert(trim($data[$egwFieldName]),$this->charset,'utf-8'); + Api\Translation::convert(trim($data[$egwFieldName]),$this->charset,'utf-8'); } elseif($isUpdate && isset($data[$egwFieldName])) { @@ -593,7 +602,7 @@ class addressbook_ldap { $result = ldap_read($this->ds, $dn, 'objectclass=*'); $entries = ldap_get_entries($this->ds, $result); - $oldContact = ldap::result2array($entries[0]); + $oldContact = Api\Ldap::result2array($entries[0]); unset($oldContact['dn']); $newContact = $oldContact; @@ -681,7 +690,7 @@ class addressbook_ldap foreach($keys as $entry) { - $entry = ldap::quote(is_array($entry) ? $entry['id'] : $entry); + $entry = Api\Ldap::quote(is_array($entry) ? $entry['id'] : $entry); if(($result = ldap_search($this->ds, $this->allContactsDN, "(|(entryUUID=$entry)(uid=$entry))", $attributes))) { @@ -759,33 +768,33 @@ class addressbook_ldap { if (!($accountName = $GLOBALS['egw']->accounts->id2name($filter['owner']))) return false; - $searchDN = 'cn='. ldap::quote(strtolower($accountName)) .','; + $searchDN = 'cn='. Api\Ldap::quote(strtolower($accountName)) .','; if ($filter['owner'] < 0) { $searchDN .= $this->sharedContactsDN; - $addressbookType = ADDRESSBOOK_GROUP; + $addressbookType = self::GROUP; } else { $searchDN .= $this->personalContactsDN; - $addressbookType = ADDRESSBOOK_PERSONAL; + $addressbookType = self::PERSONAL; } } elseif (!isset($filter['owner'])) { $searchDN = $this->allContactsDN; - $addressbookType = ADDRESSBOOK_ALL; + $addressbookType = self::ALL; } else { $searchDN = $this->accountContactsDN; - $addressbookType = ADDRESSBOOK_ACCOUNTS; + $addressbookType = self::ACCOUNTS; } // create the search filter switch($addressbookType) { - case ADDRESSBOOK_ACCOUNTS: + case self::ACCOUNTS: $objectFilter = $this->accountsFilter; break; default: @@ -813,8 +822,8 @@ class addressbook_ldap { if(($ldapSearchKey = $mapping[$egwSearchKey])) { - $searchString = translation::convert($searchValue,$this->charset,'utf-8'); - $searchFilter .= '('.$ldapSearchKey.'='.$wildcard.ldap::quote($searchString).$wildcard.')'; + $searchString = Api\Translation::convert($searchValue,$this->charset,'utf-8'); + $searchFilter .= '('.$ldapSearchKey.'='.$wildcard.Api\Ldap::quote($searchString).$wildcard.')'; break; } } @@ -915,7 +924,7 @@ class addressbook_ldap } elseif ($value) { - $filters .= '(uidNumber='.ldap::quote($value).')'; + $filters .= '(uidNumber='.Api\Ldap::quote($value).')'; } break; @@ -935,9 +944,9 @@ class addressbook_ldap if (count($cats) > 1) $filters .= '(|'; foreach($cats as $cat) { - $catName = translation::convert( + $catName = Api\Translation::convert( $GLOBALS['egw']->categories->id2name($cat),$this->charset,'utf-8'); - $filters .= '(category='.ldap::quote($catName).')'; + $filters .= '(category='.Api\Ldap::quote($catName).')'; } if (count($cats) > 1) $filters .= ')'; } @@ -965,13 +974,13 @@ class addressbook_ldap { // todo: value = "!''" $filters .= '('.$mapping[$key].'='.($value === "!''" ? '*' : - ldap::quote(translation::convert($value,$this->charset,'utf-8'))).')'; + Api\Ldap::quote(Api\Translation::convert($value,$this->charset,'utf-8'))).')'; break; } } } // filter for letter-search - elseif (preg_match("/^([^ ]+) ".preg_quote($GLOBALS['egw']->db->capabilities[egw_db::CAPABILITY_CASE_INSENSITIV_LIKE])." '(.*)%'$/",$value,$matches)) + elseif (preg_match("/^([^ ]+) ".preg_quote($GLOBALS['egw']->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE])." '(.*)%'$/",$value,$matches)) { list(,$name,$value) = $matches; if (strpos($name,'.') !== false) list(,$name) = explode('.',$name); @@ -979,8 +988,8 @@ class addressbook_ldap { if (isset($mapping[$name])) { - $filters .= '('.$mapping[$name].'='.ldap::quote( - translation::convert($value,$this->charset,'utf-8')).'*)'; + $filters .= '('.$mapping[$name].'='.Api\Ldap::quote( + Api\Translation::convert($value,$this->charset,'utf-8')).'*)'; break; } } @@ -1017,7 +1026,7 @@ class addressbook_ldap //error_log(__METHOD__."('$_ldapContext', '$_filter', ".array2string($_attributes).", $_addressbooktype)"); - if($_addressbooktype == ADDRESSBOOK_ALL || $_ldapContext == $this->allContactsDN) + if($_addressbooktype == self::ALL || $_ldapContext == $this->allContactsDN) { $result = ldap_search($this->ds, $_ldapContext, $_filter, $_attributes, 0, $this->ldapLimit); } @@ -1048,7 +1057,7 @@ class addressbook_ldap { if(!empty($entry[$ldapFieldName][0]) && !is_int($egwFieldName) && !isset($contact[$egwFieldName])) { - $contact[$egwFieldName] = translation::convert($entry[$ldapFieldName][0],'utf-8'); + $contact[$egwFieldName] = Api\Translation::convert($entry[$ldapFieldName][0],'utf-8'); } } $objectclass2egw = '_'.$objectclass.'2egw'; @@ -1206,7 +1215,7 @@ class addressbook_ldap $ldapContact['category'] = array(); foreach(is_array($data['cat_id']) ? $data['cat_id'] : explode(',',$data['cat_id']) as $cat) { - $ldapContact['category'][] = translation::convert( + $ldapContact['category'][] = Api\Translation::convert( ExecMethod('phpgwapi.categories.id2name',$cat),$this->charset,'utf-8'); } } @@ -1217,7 +1226,7 @@ class addressbook_ldap { if($value != '$, $$$') { - $ldapContact[$attr] = translation::convert($value,$this->charset,'utf-8'); + $ldapContact[$attr] = Api\Translation::convert($value,$this->charset,'utf-8'); } elseif($isUpdate) { @@ -1421,6 +1430,6 @@ class addressbook_ldap */ function change_owner($account_id,$new_owner) { - error_log("so_ldap::change_owner($account_id,$new_owner) not yet implemented"); + error_log(__METHOD__."($account_id,$new_owner) not yet implemented"); } } diff --git a/addressbook/inc/class.addressbook_sql.inc.php b/api/src/Contacts/Sql.php similarity index 88% rename from addressbook/inc/class.addressbook_sql.inc.php rename to api/src/Contacts/Sql.php index 48d871cfaa..bb785b80da 100644 --- a/addressbook/inc/class.addressbook_sql.inc.php +++ b/api/src/Contacts/Sql.php @@ -1,19 +1,27 @@ - * @package addressbook - * @copyright (c) 2006-13 by Ralf Becker + * @package api + * @subpackage contacts + * @copyright (c) 2006-16 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ +namespace EGroupware\Api\Contacts; + +use EGroupware\Api; + +// explicitly reference classes still in phpgwapi +use common; // common::generate_uid + /** - * SQL storage object of the adressbook + * Contacts - SQL storage */ -class addressbook_sql extends so_sql_cf +class Sql extends Api\Storage { /** * name of custom fields table @@ -60,15 +68,15 @@ class addressbook_sql extends so_sql_cf /** * Constructor * - * @param egw_db $db=null + * @param Api\Db $db =null */ - function __construct(egw_db $db=null) + function __construct(Api\Db $db=null) { - parent::__construct('phpgwapi', 'egw_addressbook', self::EXTRA_TABLE, 'contact_', - $extra_key='_name',$extra_value='_value',$extra_id='_id',$db); + parent::__construct('phpgwapi', 'egw_addressbook', self::EXTRA_TABLE, + 'contact_', '_name', '_value', '_id', $db); // Get custom fields from addressbook instead of phpgwapi - $this->customfields = config::get_customfields('addressbook'); + $this->customfields = Api\Storage\Customfields::get('addressbook'); if ($GLOBALS['egw_info']['server']['account_repository']) { @@ -146,7 +154,7 @@ class addressbook_sql extends so_sql_cf } if ($param['searchletter']) { - $filter[] = 'org_name '.$this->db->capabilities[egw_db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($param['searchletter'].'%'); + $filter[] = 'org_name '.$this->db->capabilities[Api\Db::CAPABILITY_CASE_INSENSITIV_LIKE].' '.$this->db->quote($param['searchletter'].'%'); } else { @@ -169,8 +177,8 @@ class addressbook_sql extends so_sql_cf // org total for more then one $by $by_expr = $by == 'org_unit_count' ? "COUNT(DISTINCT CASE WHEN org_unit IS NULL THEN '' ELSE org_unit END)" : "COUNT(DISTINCT CASE WHEN adr_one_locality IS NULL THEN '' ELSE adr_one_locality END)"; - $append = "GROUP BY org_name HAVING $by_expr > 1 ORDER BY org_name $sort"; - parent::search($param['search'],array('org_name'),$append,array( + parent::search($param['search'],array('org_name'), + "GROUP BY org_name HAVING $by_expr > 1 ORDER BY org_name $sort", array( "NULL AS $by", '1 AS is_main', 'COUNT(DISTINCT egw_addressbook.contact_id) AS org_count', @@ -178,8 +186,8 @@ class addressbook_sql extends so_sql_cf "COUNT(DISTINCT CASE WHEN adr_one_locality IS NULL THEN '' ELSE adr_one_locality END) AS adr_one_locality_count", ),$wildcard,false,$op/*'OR'*/,'UNION',$filter); // org by location - $append = "GROUP BY org_name,$by ORDER BY org_name $sort,$by $sort"; - parent::search($param['search'],array('org_name'),$append,array( + parent::search($param['search'],array('org_name'), + "GROUP BY org_name,$by ORDER BY org_name $sort,$by $sort", array( "CASE WHEN $by IS NULL THEN '' ELSE $by END AS $by", '0 AS is_main', 'COUNT(DISTINCT egw_addressbook.contact_id) AS org_count', @@ -195,7 +203,7 @@ class addressbook_sql extends so_sql_cf // query the values for *_count == 1, to display them instead $filter['org_name'] = $orgs = array(); - foreach($rows as $n => $row) + foreach($rows as $row) { if ($row['org_unit_count'] == 1 || $row['adr_one_locality_count'] == 1) { @@ -208,7 +216,8 @@ class addressbook_sql extends so_sql_cf if (count($filter['org_name'])) { - foreach((array) parent::search($criteria,array('org_name','org_unit','adr_one_locality'),'GROUP BY org_name,org_unit,adr_one_locality', + foreach((array) parent::search(null, array('org_name','org_unit','adr_one_locality'), + 'GROUP BY org_name,org_unit,adr_one_locality', '',$wildcard,false,$op/*'AND'*/,false,$filter) as $row) { $org_key = $row['org_name'].($by ? '|||'.$row[$by] : ''); @@ -243,19 +252,19 @@ class addressbook_sql extends so_sql_cf * * For a union-query you call search for each query with $start=='UNION' and one more with only $order_by and $start set to run the union-query. * - * @param array/string $criteria array of key and data cols, OR a SQL query (content for WHERE), fully quoted (!) - * @param boolean/string/array $only_keys=true True returns only keys, False returns all cols. or + * @param array|string $criteria array of key and data cols, OR a SQL query (content for WHERE), fully quoted (!) + * @param boolean|string|array $only_keys =true True returns only keys, False returns all cols. or * comma seperated list or array of columns to return - * @param string $order_by='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY) - * @param string/array $extra_cols='' string or array of strings to be added to the SELECT, eg. "count(*) as num" - * @param string $wildcard='' appended befor and after each criteria - * @param boolean $empty=false False=empty criteria are ignored in query, True=empty have to be empty in row - * @param string $op='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together - * @param mixed $start=false if != false, return only maxmatch rows begining with start, or array($start,$num), or 'UNION' for a part of a union query - * @param array $filter=null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards - * @param string $join='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or + * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY) + * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num" + * @param string $wildcard ='' appended befor and after each criteria + * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row + * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together + * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num), or 'UNION' for a part of a union query + * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards + * @param string $join ='' sql to do a join, added as is after the table-name, eg. ", table2 WHERE x=y" or * "LEFT JOIN table2 ON (x=y)", Note: there's no quoting done on $join! - * @param boolean $need_full_no_count=false If true an unlimited query is run to determine the total number of rows, default false + * @param boolean $need_full_no_count =false If true an unlimited query is run to determine the total number of rows, default false * @return boolean/array of matching rows (the row is an array of the cols) or False */ function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='',$need_full_no_count=false) @@ -360,7 +369,9 @@ class addressbook_sql extends so_sql_cf // fall through } // postgres requires that expressions in order by appear in the columns of a distinct select - if ($this->db->Type != 'mysql' && preg_match_all("/(#?[a-zA-Z_.]+) *(<> *''|IS NULL|IS NOT NULL)? *(ASC|DESC)?(,|$)/ui",$order_by,$all_matches,PREG_SET_ORDER)) + $all_matches = null; + if ($this->db->Type != 'mysql' && preg_match_all("/(#?[a-zA-Z_.]+) *(<> *''|IS NULL|IS NOT NULL)? *(ASC|DESC)?(,|$)/ui", + $order_by, $all_matches, PREG_SET_ORDER)) { if (!is_array($extra_cols)) $extra_cols = $extra_cols ? explode(',',$extra_cols) : array(); foreach($all_matches as $matches) @@ -394,10 +405,12 @@ class addressbook_sql extends so_sql_cf // Understand search by date with wildcard (????.10.??) according to user date preference if(is_string($criteria) && strpos($criteria, '?') !== false) { - $date_format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat']; // First, check for a 'date', with wildcards, in the user's format - $date_regex = str_replace(array('Y','m','d','.','-'), array('(?P(?:\?|\Q){4})','(?P(?:\?|\Q){2})','(?P(?:\?|\Q){2})','\.','\-'),$date_format); - $date_regex = str_replace('Q','d',$date_regex); + $date_regex = str_replace('Q','d', + str_replace(array('Y','m','d','.','-'), + array('(?P(?:\?|\Q){4})','(?P(?:\?|\Q){2})','(?P(?:\?|\Q){2})','\.','\-'), + $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'])); + if(preg_match_all('$'.$date_regex.'$', $criteria, $matches)) { foreach($matches[0] as $m_id => $match) @@ -467,7 +480,7 @@ class addressbook_sql extends so_sql_cf { if (!$new_owner) // otherwise we would create an account (contact_owner==0) { - throw egw_exception_wrong_parameter(__METHOD__."($account_id, $new_owner) new owner must not be 0!"); + throw Api\Exception\WrongParameter(__METHOD__."($account_id, $new_owner) new owner must not be 0!"); } // contacts $this->db->update($this->table_name,array( @@ -496,9 +509,9 @@ class addressbook_sql extends so_sql_cf * * @param array $uids array of user or group id's for $uid_column='list_owners', or values for $uid_column, * or whole where array: column-name => value(s) pairs - * @param string $uid_column='list_owner' column-name or null to use $uids as where array - * @param string $member_attr=null null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute - * @param boolean|int|array $limit_in_ab=false if true only return members from the same owners addressbook, + * @param string $uid_column ='list_owner' column-name or null to use $uids as where array + * @param string $member_attr =null null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute + * @param boolean|int|array $limit_in_ab =false if true only return members from the same owners addressbook, * if int|array only return members from the given owners addressbook(s) * @return array with list_id => array(list_id,list_name,list_owner,...) pairs */ @@ -547,7 +560,7 @@ class addressbook_sql extends so_sql_cf * * @param string|array $keys list-name or array with column-name => value pairs to specify the list * @param int $owner user- or group-id - * @param array $contacts=array() contacts to add (only for not yet existing lists!) + * @param array $contacts =array() contacts to add (only for not yet existing lists!) * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name' * @return int|boolean integer list_id or false on error */ @@ -606,7 +619,7 @@ class addressbook_sql extends so_sql_cf * * @param int|array $contact contact_id(s) * @param int $list list-id - * @param array $existing=null array of existing contact-id(s) of list, to not reread it, eg. array() + * @param array $existing =null array of existing contact-id(s) of list, to not reread it, eg. array() * @return false on error */ function add2list($contact,$list,array $existing=null) @@ -648,7 +661,7 @@ class addressbook_sql extends so_sql_cf * Removes one contact from distribution list(s) * * @param int|array $contact contact_id(s) - * @param int $list=null list-id or null to remove from all lists + * @param int $list =null list-id or null to remove from all lists * @return false on error */ function remove_from_list($contact,$list=null) @@ -690,7 +703,7 @@ class addressbook_sql extends so_sql_cf /** * Deletes a distribution list (incl. it's members) * - * @param int/array $list list_id(s) + * @param int|array $list list_id(s) * @return number of members deleted or false if list does not exist */ function delete_list($list) @@ -705,7 +718,7 @@ class addressbook_sql extends so_sql_cf /** * Get ctag (max list_modified as timestamp) for lists * - * @param int|array $owner=null null for all lists user has access too + * @param int|array $owner =null null for all lists user has access too * @return int */ function lists_ctag($owner=null) @@ -745,7 +758,7 @@ class addressbook_sql extends so_sql_cf } // catch Illegal mix of collations (ascii_general_ci,IMPLICIT) and (utf8_general_ci,COERCIBLE) for operation '=' (1267) // caused by non-ascii chars compared with ascii field uid - catch(egw_exception_db $e) { + catch(Api\Db\Exception $e) { _egw_log_exception($e); return false; } @@ -762,11 +775,13 @@ class addressbook_sql extends so_sql_cf * Saves a contact, reimplemented to check a given etag and set a uid * * @param array $keys if given $keys are copied to data before saveing => allows a save as - * @param string|array $extra_where=null extra where clause, eg. to check the etag, returns 'nothing_affected' if not affected rows + * @param string|array $extra_where =null extra where clause, eg. to check the etag, returns 'nothing_affected' if not affected rows * @return int 0 on success and errno != 0 else */ function save($keys = NULL, $extra_where = NULL) { + unset($extra_where); // not used, but required by function signature + if (isset($GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length'])) { $minimum_uid_length = $GLOBALS['egw_info']['user']['preferences']['syncml']['minimum_uid_length']; } else { diff --git a/api/src/Contacts/Storage.php b/api/src/Contacts/Storage.php new file mode 100755 index 0000000000..8672a79902 --- /dev/null +++ b/api/src/Contacts/Storage.php @@ -0,0 +1,1138 @@ + + * @author Ralf Becker + * @package addressbook + * @copyright (c) 2005-16 by Ralf Becker + * @copyright (c) 2005/6 by Cornelius Weiss + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +namespace EGroupware\Api\Contacts; + +use EGroupware\Api; + +// explicitly reference classes still in phpgwapi + + +/** + * Contacts storage object + * + * The contact storage has 3 operation modi (contact_repository): + * - sql: contacts are stored in the SQL table egw_addressbook & egw_addressbook_extra (custom fields) + * - ldap: contacts are stored in LDAP (accounts have to be stored in LDAP too!!!). + * Custom fields are not availible in that case! + * - sql-ldap: contacts are read and searched in SQL, but saved to both SQL and LDAP. + * Other clients (Thunderbird, ...) can use LDAP readonly. The get maintained via eGroupWare only. + * + * The accounts can be stored in SQL or LDAP too (account_repository): + * If the account-repository is different from the contacts-repository, the filter all (no owner set) + * will only search the contacts and NOT the accounts! Only the filter accounts (owner=0) shows accounts. + * + * If sql-ldap is used as contact-storage (LDAP is managed from eGroupWare) the filter all, searches + * the accounts in the SQL contacts-table too. Change in made in LDAP, are not detected in that case! + */ + +class Storage +{ + /** + * name of customefields table + * + * @var string + */ + var $extra_table = 'egw_addressbook_extra'; + + /** + * @var string + */ + var $extra_id = 'contact_id'; + + /** + * @var string + */ + var $extra_owner = 'contact_owner'; + + /** + * @var string + */ + var $extra_key = 'contact_name'; + + /** + * @var string + */ + var $extra_value = 'contact_value'; + + /** + * view for distributionlistsmembership + * + * @var string + */ + var $distributionlist_view ='(SELECT contact_id, egw_addressbook_lists.list_id as list_id, egw_addressbook_lists.list_name as list_name, egw_addressbook_lists.list_owner as list_owner FROM egw_addressbook_lists, egw_addressbook2list where egw_addressbook_lists.list_id=egw_addressbook2list.list_id) d_view '; + var $distributionlist_tabledef = array(); + /** + * @var string + */ + var $distri_id = 'contact_id'; + + /** + * @var string + */ + var $distri_owner = 'list_owner'; + + /** + * @var string + */ + var $distri_key = 'list_id'; + + /** + * @var string + */ + var $distri_value = 'list_name'; + + /** + * Contact repository in 'sql' or 'ldap' + * + * @var string + */ + var $contact_repository = 'sql'; + + /** + * Grants as account_id => rights pairs + * + * @var array + */ + var $grants; + + /** + * userid of current user + * + * @var int + */ + var $user; + + /** + * memberships of the current user + * + * @var array + */ + var $memberships; + + /** + * In SQL we can search all columns, though a view make on real sense + */ + var $sql_cols_not_to_search = array( + 'jpegphoto','owner','tid','private','cat_id','etag', + 'modified','modifier','creator','created','tz','account_id', + 'uid','carddav_name','freebusy_uri','calendar_uri', + 'geo','pubkey', + ); + /** + * columns to search, if we search for a single pattern + * + * @var array + */ + var $columns_to_search = array(); + /** + * extra columns to search if accounts are included, eg. account_lid + * + * @var array + */ + var $account_extra_search = array(); + /** + * columns to search for accounts, if stored in different repository + * + * @var array + */ + var $account_cols_to_search = array(); + + /** + * customfields name => array(...) pairs + * + * @var array + */ + var $customfields = array(); + /** + * content-types as name => array(...) pairs + * + * @var array + */ + var $content_types = array(); + + /** + * Special content type to indicate a deleted addressbook + * + * @var String; + */ + const DELETED_TYPE = 'D'; + + /** + * total number of matches of last search + * + * @var int + */ + var $total; + + /** + * storage object: sql (Sql) or ldap (addressbook_ldap) backend class + * + * @var Sql + */ + var $somain; + /** + * storage object for accounts, if not identical to somain (eg. accounts in ldap, contacts in sql) + * + * @var Ldap + */ + var $so_accounts; + /** + * account repository sql or ldap + * + * @var string + */ + var $account_repository = 'sql'; + /** + * custom fields backend + * + * @var Sql + */ + var $soextra; + var $sodistrib_list; + + /** + * Constructor + * + * @param string $contact_app ='addressbook' used for acl->get_grants() + * @param Api\Db $db =null + */ + function __construct($contact_app='addressbook',Api\Db $db=null) + { + $this->db = is_null($db) ? $GLOBALS['egw']->db : $db; + + $this->user = $GLOBALS['egw_info']['user']['account_id']; + $this->memberships = $GLOBALS['egw']->accounts->memberships($this->user,true); + + // account backend used + if ($GLOBALS['egw_info']['server']['account_repository']) + { + $this->account_repository = $GLOBALS['egw_info']['server']['account_repository']; + } + elseif ($GLOBALS['egw_info']['server']['auth_type']) + { + $this->account_repository = $GLOBALS['egw_info']['server']['auth_type']; + } + $this->customfields = Api\Storage\Customfields::get('addressbook'); + // contacts backend (contacts in LDAP require accounts in LDAP!) + if($GLOBALS['egw_info']['server']['contact_repository'] == 'ldap' && $this->account_repository == 'ldap') + { + $this->contact_repository = 'ldap'; + $this->somain = new Ldap(); + $this->columns_to_search = $this->somain->search_attributes; + } + else // sql or sql->ldap + { + if ($GLOBALS['egw_info']['server']['contact_repository'] == 'sql-ldap') + { + $this->contact_repository = 'sql-ldap'; + } + $this->somain = new Sql($db); + + // remove some columns, absolutly not necessary to search in sql + $this->columns_to_search = array_diff(array_values($this->somain->db_cols),$this->sql_cols_not_to_search); + if ($this->customfields) // add custom fields, if configured + { + $this->columns_to_search[] = Sql::EXTRA_TABLE.'.'.Sql::EXTRA_VALUE; + } + } + if ($this->user) + { + $this->grants = $this->get_grants($this->user,$contact_app); + } + if ($this->account_repository != 'sql' && $this->contact_repository == 'sql') + { + if ($this->account_repository != $this->contact_repository) + { + $class = 'EGroupware\\Contacts\\'.ucfirst($this->account_repository); + $this->so_accounts = new $class(); + $this->account_cols_to_search = $this->so_accounts->search_attributes; + } + else + { + $this->account_extra_search = array('uid'); + } + } + if ($this->contact_repository == 'sql' || $this->contact_repository == 'sql-ldap') + { + $tda2list = $this->db->get_table_definitions('phpgwapi','egw_addressbook2list'); + $tdlists = $this->db->get_table_definitions('phpgwapi','egw_addressbook_lists'); + $this->distributionlist_tabledef = array('fd' => array( + $this->distri_id => $tda2list['fd'][$this->distri_id], + $this->distri_owner => $tdlists['fd'][$this->distri_owner], + $this->distri_key => $tdlists['fd'][$this->distri_key], + $this->distri_value => $tdlists['fd'][$this->distri_value], + ), 'pk' => array(), 'fk' => array(), 'ix' => array(), 'uc' => array(), + ); + } + // ToDo: it should be the other way arround, the backend should set the grants it uses + $this->somain->grants =& $this->grants; + + if($this->somain instanceof Sql) + { + $this->soextra =& $this->somain; + } + else + { + $this->soextra = new Sql($db); + } + + $this->content_types = Api\Config::get_content_types('addressbook'); + if (!$this->content_types) + { + $this->content_types = array('n' => array( + 'name' => 'contact', + 'options' => array( + 'template' => 'addressbook.edit', + 'icon' => 'navbar.png' + ))); + } + + // Add in deleted type, if holding deleted contacts + $config = Api\Config::read('phpgwapi'); + if($config['history']) + { + $this->content_types[self::DELETED_TYPE] = array( + 'name' => lang('Deleted'), + 'options' => array( + 'template' => 'addressbook.edit', + 'icon' => 'deleted.png' + ) + ); + } + } + + /** + * Get grants for a given user, taking into account static LDAP ACL + * + * @param int $user + * @param string $contact_app ='addressbook' + * @return array + */ + function get_grants($user, $contact_app='addressbook', $preferences=null) + { + if (!isset($preferences)) $preferences = $GLOBALS['egw_info']['user']['preferences']; + + if ($user) + { + // contacts backend (contacts in LDAP require accounts in LDAP!) + if($GLOBALS['egw_info']['server']['contact_repository'] == 'ldap' && $this->account_repository == 'ldap') + { + // static grants from ldap: all rights for the own personal addressbook and the group ones of the meberships + $grants = array($user => ~0); + foreach($GLOBALS['egw']->accounts->memberships($user,true) as $gid) + { + $grants[$gid] = ~0; + } + } + else // sql or sql->ldap + { + // group grants are now grants for the group addressbook and NOT grants for all its members, + // therefor the param false! + $grants = $GLOBALS['egw']->acl->get_grants($contact_app,false,$user); + } + // add grants for accounts: if account_selection not in ('none','groupmembers'): everyone has read access, + // if he has not set the hide_accounts preference + // ToDo: be more specific for 'groupmembers', they should be able to see the groupmembers + if (!in_array($preferences['common']['account_selection'], array('none','groupmembers'))) + { + $grants[0] = EGW_ACL_READ; + } + // add account grants for admins (only for current user!) + if ($user == $this->user && $this->is_admin()) // admin rights can be limited by ACL! + { + $grants[0] = EGW_ACL_READ; // admins always have read-access + if (!$GLOBALS['egw']->acl->check('account_access',16,'admin')) $grants[0] |= EGW_ACL_EDIT; + if (!$GLOBALS['egw']->acl->check('account_access',4,'admin')) $grants[0] |= EGW_ACL_ADD; + if (!$GLOBALS['egw']->acl->check('account_access',32,'admin')) $grants[0] |= EGW_ACL_DELETE; + } + // allow certain groups to edit contact-data of accounts + if (self::allow_account_edit($user)) + { + $grants[0] |= EGW_ACL_READ|EGW_ACL_EDIT; + } + } + else + { + $grants = array(); + } + //error_log(__METHOD__."($user, '$contact_app') returning ".array2string($grants)); + return $grants; + } + + /** + * Check if the user is an admin (can unconditionally edit accounts) + * + * We check now the admin ACL for edit users, as the admin app does it for editing accounts. + * + * @param array $contact =null for future use, where admins might not be admins for all accounts + * @return boolean + */ + function is_admin($contact=null) + { + unset($contact); // not (yet) used + + return isset($GLOBALS['egw_info']['user']['apps']['admin']) && !$GLOBALS['egw']->acl->check('account_access',16,'admin'); + } + + /** + * Check if current user is in a group, which is allowed to edit accounts + * + * @param int $user =null default $this->user + * @return boolean + */ + function allow_account_edit($user=null) + { + return $GLOBALS['egw_info']['server']['allow_account_edit'] && + array_intersect($GLOBALS['egw_info']['server']['allow_account_edit'], + $GLOBALS['egw']->accounts->memberships($user ? $user : $this->user, true)); + } + + /** + * Read all customfields of the given id's + * + * @param int|array $ids + * @param array $field_names =null custom fields to read, default all + * @return array id => name => value + */ + function read_customfields($ids,$field_names=null) + { + return $this->soextra->read_customfields($ids,$field_names); + } + + /** + * Read all distributionlists of the given id's + * + * @param int|array $ids + * @return array id => name => value + */ + function read_distributionlist($ids, $dl_allowed=array()) + { + if ($this->contact_repository == 'ldap') + { + return array(); // ldap does not support distributionlists + } + foreach($ids as $key => $id) + { + if (!is_numeric($id)) unset($ids[$key]); + } + if (!$ids) return array(); // nothing to do, eg. all these contacts are in ldap + $fields = array(); + $filter[$this->distri_id]=$ids; + if (count($dl_allowed)) $filter[$this->distri_key]=$dl_allowed; + $distri_view = str_replace(') d_view',' and '.$this->distri_id.' in ('.implode(',',$ids).')) d_view',$this->distributionlist_view); + #_debug_array($this->distributionlist_tabledef); + foreach($this->db->select($distri_view, '*', $filter, __LINE__, __FILE__, + false, 'ORDER BY '.$this->distri_id, false, 0, '', $this->distributionlist_tabledef) as $row) + { + if ((isset($row[$this->distri_id])&&strlen($row[$this->distri_value])>0)) + { + $fields[$row[$this->distri_id]][$row[$this->distri_key]] = $row[$this->distri_value].' ('.$GLOBALS['egw']->common->grab_owner_name($row[$this->distri_owner]).')'; + } + } + return $fields; + } + + /** + * changes the data from the db-format to your work-format + * + * it gets called everytime when data is read from the db + * This function needs to be reimplemented in the derived class + * + * @param array $data + */ + function db2data($data) + { + return $data; + } + + /** + * changes the data from your work-format to the db-format + * + * It gets called everytime when data gets writen into db or on keys for db-searches + * this needs to be reimplemented in the derived class + * + * @param array $data + */ + function data2db($data) + { + return $data; + } + + /** + * deletes contact entry including custom fields + * + * @param mixed $contact array with id or just the id + * @param int $check_etag =null + * @return boolean|int true on success or false on failiure, 0 if etag does not match + */ + function delete($contact,$check_etag=null) + { + if (is_array($contact)) $contact = $contact['id']; + + $where = array('id' => $contact); + if ($check_etag) $where['etag'] = $check_etag; + + // delete mainfields + if ($this->somain->delete($where)) + { + // delete customfields, can return 0 if there are no customfields + if(!($this->somain instanceof Sql)) + { + $this->soextra->delete_customfields(array($this->extra_id => $contact)); + } + + // delete from distribution list(s) + $this->remove_from_list($contact); + + if ($this->contact_repository == 'sql-ldap') + { + if ($contact['account_id']) + { + // LDAP uses the uid attributes for the contact-id (dn), + // which need to be the account_lid for accounts! + $contact['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']); + } + (new Ldap())->delete($contact); + } + return true; + } + return $check_etag ? 0 : false; // if etag given, we return 0 on failure, thought it could also mean the whole contact does not exist + } + + /** + * saves contact data including custom fields + * + * @param array &$contact contact data from etemplate::exec + * @return bool false on success, errornumber on failure + */ + function save(&$contact) + { + // save mainfields + if ($contact['id'] && $this->contact_repository != $this->account_repository && is_object($this->so_accounts) && + ($this->contact_repository == 'sql' && !is_numeric($contact['id']) || + $this->contact_repository == 'ldap' && is_numeric($contact['id']))) + { + $this->so_accounts->data = $this->data2db($contact); + $error_nr = $this->so_accounts->save(); + $contact['id'] = $this->so_accounts->data['id']; + } + else + { + // contact_repository sql-ldap (accounts in ldap) the person_id is the uid (account_lid) + // for the sql write here we need to find out the existing contact_id + if ($this->contact_repository == 'sql-ldap' && $contact['id'] && !is_numeric($contact['id']) && + $contact['account_id'] && ($old = $this->somain->read(array('account_id' => $contact['account_id'])))) + { + $contact['id'] = $old['id']; + } + $this->somain->data = $this->data2db($contact); + + if (!($error_nr = $this->somain->save())) + { + $contact['id'] = $this->somain->data['id']; + $contact['uid'] = $this->somain->data['uid']; + $contact['etag'] = $this->somain->data['etag']; + + if ($this->contact_repository == 'sql-ldap') + { + $data = $this->somain->data; + if ($contact['account_id']) + { + // LDAP uses the uid attributes for the contact-id (dn), + // which need to be the account_lid for accounts! + $data['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']); + } + $error_nr = (new Ldap())->save($data); + } + } + } + if($error_nr) return $error_nr; + + return false; // no error + } + + /** + * reads contact data including custom fields + * + * @param int|string $contact_id contact_id or 'a'.account_id + * @return array|boolean data if row could be retrived else False + */ + function read($contact_id) + { + if (!is_array($contact_id) && substr($contact_id,0,8) == 'account:') + { + $contact_id = array('account_id' => (int) substr($contact_id,8)); + } + // read main data + $backend =& $this->get_backend($contact_id); + if (!($contact = $backend->read($contact_id))) + { + return $contact; + } + $dl_list=$this->read_distributionlist(array($contact['id'])); + if (count($dl_list)) $contact['distrib_lists']=implode("\n",$dl_list[$contact['id']]); + return $this->db2data($contact); + } + + /** + * searches db for rows matching searchcriteria + * + * '*' and '?' are replaced with sql-wildcards '%' and '_' + * + * @param array|string $criteria array of key and data cols, OR string to search over all standard search fields + * @param boolean|string $only_keys =true True returns only keys, False returns all cols. comma seperated list of keys to return + * @param string $order_by ='' fieldnames + {ASC|DESC} separated by colons ',', can also contain a GROUP BY (if it contains ORDER BY) + * @param string|array $extra_cols ='' string or array of strings to be added to the SELECT, eg. "count(*) as num" + * @param string $wildcard ='' appended befor and after each criteria + * @param boolean $empty =false False=empty criteria are ignored in query, True=empty have to be empty in row + * @param string $op ='AND' defaults to 'AND', can be set to 'OR' too, then criteria's are OR'ed together + * @param mixed $start =false if != false, return only maxmatch rows begining with start, or array($start,$num) + * @param array $filter =null if set (!=null) col-data pairs, to be and-ed (!) into the query without wildcards + * $filter['cols_to_search'] limit search columns to given columns, otherwise $this->columns_to_search is used + * @param string $join ='' sql to do a join (only used by sql backend!), eg. " RIGHT JOIN egw_accounts USING(account_id)" + * @return array of matching rows (the row is an array of the cols) or False + */ + function &search($criteria,$only_keys=True,$order_by='',$extra_cols='',$wildcard='',$empty=False,$op='AND',$start=false,$filter=null,$join='') + { + //echo '

'.__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',$start,".array2string($filter,true).",'$join')

\n"; + //error_log(__METHOD__.'('.array2string($criteria,true).','.array2string($only_keys).",'$order_by','$extra_cols','$wildcard','$empty','$op',".array2string($start).','.array2string($filter,true).",'$join')"); + + // Handle 'None' country option + if(is_array($filter) && $filter['adr_one_countrycode'] == '-custom-') + { + $filter[] = 'adr_one_countrycode IS NULL'; + unset($filter['adr_one_countrycode']); + } + // Hide deleted items unless type is specifically deleted + if(!is_array($filter)) $filter = $filter ? (array) $filter : array(); + + if (isset($filter['cols_to_search'])) + { + $cols_to_search = $filter['cols_to_search']; + unset($filter['cols_to_search']); + } + + // if no tid set or tid==='' do NOT return deleted entries ($tid === null returns all entries incl. deleted) + if(!array_key_exists('tid', $filter) || $filter['tid'] === '') + { + if ($join && strpos($join,'RIGHT JOIN') !== false) // used eg. to search for groups + { + $filter[] = '(contact_tid != \'' . self::DELETED_TYPE . '\' OR contact_tid IS NULL)'; + } + else + { + $filter[] = 'contact_tid != \'' . self::DELETED_TYPE . '\''; + } + } + elseif(is_null($filter['tid'])) + { + unset($filter['tid']); // return all entries incl. deleted + } + $backend = $this->get_backend(null,$filter['owner']); + // single string to search for --> create so_sql conformant search criterial for the standard search columns + if ($criteria && !is_array($criteria)) + { + $op = 'OR'; + $wildcard = '%'; + $search = $criteria; + $criteria = array(); + + if (isset($cols_to_search)) + { + $cols = $cols_to_search; + } + elseif ($backend === $this->somain) + { + $cols = $this->columns_to_search; + } + else + { + $cols = $this->account_cols_to_search; + } + if($backend instanceof Sql) + { + // Keep a string, let the parent handle it + $criteria = $search; + + foreach($cols as $key => &$col) + { + if($col != Sql::EXTRA_VALUE && + $col != Sql::EXTRA_TABLE.'.'.Sql::EXTRA_VALUE && + !array_key_exists($col, $backend->db_cols)) + { + if(!($col = array_search($col, $backend->db_cols))) + { + // Can't search this column, it will error if we try + unset($cols[$key]); + } + } + if ($col=='contact_id') $col='egw_addressbook.contact_id'; + } + + $backend->columns_to_search = $cols; + } + else + { + foreach($cols as $col) + { + // remove from LDAP backend not understood use-AND-syntax + $criteria[$col] = str_replace(' +',' ',$search); + } + } + } + if (is_array($criteria) && count($criteria)) + { + $criteria = $this->data2db($criteria); + } + if (is_array($filter) && count($filter)) + { + $filter = $this->data2db($filter); + } + else + { + $filter = $filter ? array($filter) : array(); + } + // get the used backend for the search and call it's search method + $rows = $backend->search($criteria, $only_keys, $order_by, $extra_cols, + $wildcard, $empty, $op, $start, $filter, $join); + + $this->total = $backend->total; + + if ($rows) + { + foreach($rows as $n => $row) + { + $rows[$n] = $this->db2data($row); + } + } + return $rows; + } + + /** + * Query organisations by given parameters + * + * @var array $param + * @var string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group + * @var int $param[owner] addressbook to search + * @var string $param[search] search pattern for org_name + * @var string $param[searchletter] letter the org_name need to start with + * @var int $param[start] + * @var int $param[num_rows] + * @var string $param[sort] ASC or DESC + * @return array or arrays with keys org_name,count and evtl. adr_one_location or org_unit + */ + function organisations($param) + { + if (!method_exists($this->somain,'organisations')) + { + $this->total = 0; + return false; + } + if ($param['search'] && !is_array($param['search'])) + { + $search = $param['search']; + $param['search'] = array(); + if($this->somain instanceof Sql) + { + // Keep the string, let the parent deal with it + $param['search'] = $search; + } + else + { + foreach($this->columns_to_search as $col) + { + if ($col != 'contact_value') $param['search'][$col] = $search; // we dont search the customfields + } + } + } + if (is_array($param['search']) && count($param['search'])) + { + $param['search'] = $this->data2db($param['search']); + } + if(!array_key_exists('tid', $param['col_filter']) || $param['col_filter']['tid'] === '') + { + $param['col_filter'][] = 'contact_tid != \'' . self::DELETED_TYPE . '\''; + } + elseif(is_null($param['col_filter']['tid'])) + { + unset($param['col_filter']['tid']); // return all entries incl. deleted + } + + $rows = $this->somain->organisations($param); + $this->total = $this->somain->total; + //echo "

socontacts::organisations(".print_r($param,true).")
".$this->somain->db->Query_ID->sql."

\n"; + + if (!$rows) return array(); + + foreach($rows as $n => $row) + { + if (strpos($row['org_name'],'&')!==false) $row['org_name'] = str_replace('&','*AND*',$row['org_name']); //echo "Ampersand found
"; + $rows[$n]['id'] = 'org_name:'.$row['org_name']; + foreach(array( + 'org_unit' => lang('departments'), + 'adr_one_locality' => lang('locations'), + ) as $by => $by_label) + { + if ($row[$by.'_count'] > 1) + { + $rows[$n][$by] = $row[$by.'_count'].' '.$by_label; + } + else + { + if (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]); //echo "Ampersand found
"; + $rows[$n]['id'] .= '|||'.$by.':'.$row[$by]; + } + } + } + return $rows; + } + + /** + * gets all contact fields from database + * + * @return array of (internal) field-names + */ + function get_contact_columns() + { + $fields = $this->get_fields('all'); + foreach (array_keys((array)$this->customfields) as $cfield) + { + $fields[] = '#'.$cfield; + } + return $fields; + } + + /** + * delete / move all contacts of an addressbook + * + * @param array $data + * @param int $data['account_id'] owner to change + * @param int $data['new_owner'] new owner or 0 for delete + */ + function deleteaccount($data) + { + $account_id = $data['account_id']; + $new_owner = $data['new_owner']; + + if (!$new_owner) + { + $this->somain->delete(array('owner' => $account_id)); // so_sql_cf::delete() takes care of cfs too + + if (method_exists($this->somain, 'get_lists') && + ($lists = $this->somain->get_lists($account_id))) + { + $this->somain->delete_list(array_keys($lists)); + } + } + else + { + $this->somain->change_owner($account_id,$new_owner); + } + } + + /** + * return the backend, to be used for the given $contact_id + * + * @param array|string|int $keys =null + * @param int $owner =null account_id of owner or 0 for accounts + * @return Sql + */ + function get_backend($keys=null,$owner=null) + { + if ($owner === '') $owner = null; + + $contact_id = !is_array($keys) ? $keys : + (isset($keys['id']) ? $keys['id'] : $keys['contact_id']); + + if ($this->contact_repository != $this->account_repository && is_object($this->so_accounts) && + (!is_null($owner) && !$owner || is_array($keys) && $keys['account_id'] || !is_null($contact_id) && + ($this->contact_repository == 'sql' && (!is_numeric($contact_id) && !is_array($contact_id) )|| + $this->contact_repository == 'ldap' && is_numeric($contact_id)))) + { + return $this->so_accounts; + } + return $this->somain; + } + + /** + * Returns the supported, all or unsupported fields of the backend (depends on owner or contact_id) + * + * @param sting $type ='all' 'supported', 'unsupported' or 'all' + * @param mixed $contact_id =null + * @param int $owner =null account_id of owner or 0 for accounts + * @return array with eGW contact field names + */ + function get_fields($type='all',$contact_id=null,$owner=null) + { + $def = $this->db->get_table_definitions('phpgwapi','egw_addressbook'); + + $all_fields = array(); + foreach(array_keys($def['fd']) as $field) + { + $all_fields[] = substr($field,0,8) == 'contact_' ? substr($field,8) : $field; + } + if ($type == 'all') + { + return $all_fields; + } + $backend =& $this->get_backend($contact_id,$owner); + + $supported_fields = method_exists($backend,supported_fields) ? $backend->supported_fields() : $all_fields; + //echo "supported fields=";_debug_array($supported_fields); + + if ($type == 'supported') + { + return $supported_fields; + } + //echo "unsupported fields=";_debug_array(array_diff($all_fields,$supported_fields)); + return array_diff($all_fields,$supported_fields); + } + + /** + * Migrates an SQL contact storage to LDAP, SQL-LDAP or back to SQL + * + * @param string|array $type comma-separated list or array of: + * - "contacts" contacts to ldap + * - "accounts" accounts to ldap + * - "accounts-back" accounts back to sql (for sql-ldap!) + * - "sql" contacts and accounts to sql + */ + function migrate2ldap($type) + { + //error_log(__METHOD__."(".array2string($type).")"); + $sql_contacts = new Sql(); + // we need an admin connection + $ds = $GLOBALS['egw']->ldap->ldapConnect(); + $ldap_contacts = new addressbook_ldap(null, $ds); + + if (!is_array($type)) $type = explode(',', $type); + + $start = $n = 0; + $num = 100; + + // direction SQL --> LDAP, either only accounts, or only contacts or both + if (($do = array_intersect($type, array('contacts', 'accounts')))) + { + $filter = count($do) == 2 ? null : + array($do[0] == 'contacts' ? 'contact_owner != 0' : 'contact_owner = 0'); + + while (($contacts = $sql_contacts->search(false,false,'n_family,n_given','','',false,'AND', + array($start,$num),$filter))) + { + foreach($contacts as $contact) + { + if ($contact['account_id']) $contact['id'] = $GLOBALS['egw']->accounts->id2name($contact['account_id']); + + $ldap_contacts->data = $contact; + $n++; + if (!($err = $ldap_contacts->save())) + { + echo '

'.$n.': '.$contact['n_fn']. + ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '')." --> LDAP

\n"; + } + else + { + echo '

'.$n.': '.$contact['n_fn']. + ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '').': '.$err."

\n"; + } + } + $start += $num; + } + } + // direction LDAP --> SQL: either "sql" (contacts and accounts) or "accounts-back" (only accounts) + if (($do = array_intersect(array('accounts-back','sql'), $type))) + { + //error_log(__METHOD__."(".array2string($type).") do=".array2string($type)); + $filter = in_array('sql', $do) ? null : array('owner' => 0); + + foreach($ldap_contacts->search(false,false,'n_family,n_given','','',false,'AND', + false, $filter) as $contact) + { + //error_log(__METHOD__."(".array2string($type).") do=".array2string($type)." migrating ".array2string($contact)); + if ($contact['jpegphoto']) // photo is NOT read by LDAP backend on search, need to do an extra read + { + $contact = $ldap_contacts->read($contact['id']); + } + unset($contact['id']); // ldap uid/account_lid + if ($contact['account_id'] && ($old = $sql_contacts->read(array('account_id' => $contact['account_id'])))) + { + $contact['id'] = $old['id']; + } + $sql_contacts->data = $contact; + + $n++; + if (!($err = $sql_contacts->save())) + { + echo '

'.$n.': '.$contact['n_fn']. + ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '')." --> SQL (". + ($contact['owner']?lang('User'):lang('Contact')).")

\n"; + } + else + { + echo '

'.$n.': '.$contact['n_fn']. + ($contact['org_name'] ? ' ('.$contact['org_name'].')' : '').': '.$err."

\n"; + } + } + } + } + + /** + * Get the availible distribution lists for a user + * + * @param int $required =EGW_ACL_READ required rights on the list or multiple rights or'ed together, + * to return only lists fullfilling all the given rights + * @param string $extra_labels =null first labels if given (already translated) + * @return array with id => label pairs or false if backend does not support lists + */ + function get_lists($required=EGW_ACL_READ,$extra_labels=null) + { + if (!method_exists($this->somain,'get_lists')) return false; + + $uids = array(); + foreach($this->grants as $uid => $rights) + { + if (($rights & $required) == $required) + { + $uids[] = $uid; + } + } + $lists = is_array($extra_labels) ? $extra_labels : array(); + + foreach($this->somain->get_lists($uids) as $list_id => $data) + { + $lists[$list_id] = $data['list_name']; + if ($data['list_owner'] != $this->user) + { + $lists[$list_id] .= ' ('.$GLOBALS['egw']->common->grab_owner_name($data['list_owner']).')'; + } + } + //echo "

socontacts_sql::get_lists($required,'$extra_label')

\n"; _debug_array($lists); + return $lists; + } + + /** + * Get the availible distribution lists for givens users and groups + * + * @param array $keys column-name => value(s) pairs, eg. array('list_uid'=>$uid) + * @param string $member_attr ='contact_uid' null: no members, 'contact_uid', 'contact_id', 'caldav_name' return members as that attribute + * @param boolean $limit_in_ab =false if true only return members from the same owners addressbook + * @return array with list_id => array(list_id,list_name,list_owner,...) pairs + */ + function read_lists($keys,$member_attr=null,$limit_in_ab=false) + { + $backend = (string)$limit_in_ab === '0' && $this->so_accounts ? $this->so_accounts : $this->somain; + if (!method_exists($backend, 'get_lists')) return false; + + return $backend->get_lists($keys,null,$member_attr,$limit_in_ab); + } + + /** + * Adds / updates a distribution list + * + * @param string|array $keys list-name or array with column-name => value pairs to specify the list + * @param int $owner user- or group-id + * @param array $contacts =array() contacts to add (only for not yet existing lists!) + * @param array &$data=array() values for keys 'list_uid', 'list_carddav_name', 'list_name' + * @return int|boolean integer list_id or false on error + */ + function add_list($keys,$owner,$contacts=array(),array &$data=array()) + { + $backend = (string)$owner === '0' && $this->so_accounts ? $this->so_accounts : $this->somain; + if (!method_exists($backend, 'add_list')) return false; + + return $backend->add_list($keys,$owner,$contacts,$data); + } + + /** + * Adds contact(s) to a distribution list + * + * @param int|array $contact contact_id(s) + * @param int $list list-id + * @param array $existing =null array of existing contact-id(s) of list, to not reread it, eg. array() + * @return false on error + */ + function add2list($contact,$list,array $existing=null) + { + if (!method_exists($this->somain,'add2list')) return false; + + return $this->somain->add2list($contact,$list,$existing); + } + + /** + * Removes one contact from distribution list(s) + * + * @param int|array $contact contact_id(s) + * @param int $list =null list-id or null to remove from all lists + * @return false on error + */ + function remove_from_list($contact,$list=null) + { + if (!method_exists($this->somain,'remove_from_list')) return false; + + return $this->somain->remove_from_list($contact,$list); + } + + /** + * Deletes a distribution list (incl. it's members) + * + * @param int|array $list list_id(s) + * @return number of members deleted or false if list does not exist + */ + function delete_list($list) + { + if (!method_exists($this->somain,'delete_list')) return false; + + return $this->somain->delete_list($list); + } + + /** + * Read data of a distribution list + * + * @param int $list list_id + * @return array of data or false if list does not exist + */ + function read_list($list) + { + if (!method_exists($this->somain,'read_list')) return false; + + return $this->somain->read_list($list); + } + + /** + * Check if distribution lists are availible for a given addressbook + * + * @param int|string $owner ='' addressbook (eg. 0 = accounts), default '' = "all" addressbook (uses the main backend) + * @return boolean + */ + function lists_available($owner='') + { + $backend =& $this->get_backend(null,$owner); + + return method_exists($backend,'read_list'); + } + + /** + * Get ctag (max list_modified as timestamp) for lists + * + * @param int|array $owner =null null for all lists user has access too + * @return int + */ + function lists_ctag($owner=null) + { + if (!method_exists($this->somain,'lists_ctag')) return 0; + + return $this->somain->lists_ctag($owner); + } +} diff --git a/addressbook/inc/class.addressbook_tracking.inc.php b/api/src/Contacts/Tracking.php similarity index 84% rename from addressbook/inc/class.addressbook_tracking.inc.php rename to api/src/Contacts/Tracking.php index 62812f97fe..a8643e9acb 100644 --- a/addressbook/inc/class.addressbook_tracking.inc.php +++ b/api/src/Contacts/Tracking.php @@ -1,19 +1,27 @@ - * @package addressbook - * @copyright (c) 2007 by Ralf Becker + * @package api + * @subpackage contacts + * @copyright (c) 2007-16 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ +namespace EGroupware\Api\Contacts; + +use EGroupware\Api; + +// explicitly reference classes still in phpgwapi +use common; + /** - * Addressbook - tracking object + * Contacts history and notifications */ -class addressbook_tracking extends bo_tracking +class Tracking extends Api\Storage\Tracking { /** * Application we are tracking (required!) @@ -64,10 +72,10 @@ class addressbook_tracking extends bo_tracking /** * Constructor * - * @param addressbook_bo $bocontacts + * @param Api\Contacts $bocontacts * @return tracker_tracking */ - function __construct(addressbook_bo $bocontacts) + function __construct(Api\Contacts $bocontacts) { $this->contacts = $bocontacts; @@ -93,17 +101,19 @@ class addressbook_tracking extends bo_tracking /** * Get a notification-config value * - * @param string $what + * @param string $name * - 'copy' array of email addresses notifications should be copied too, can depend on $data * - 'lang' string lang code for copy mail * - 'sender' string send email address * @param array $data current entry - * @param array $old=null old/last state of the entry or null for a new entry + * @param array $old =null old/last state of the entry or null for a new entry * @return mixed */ function get_config($name,$data,$old=null) { - //echo "

addressbook_tracking::get_config($name,".print_r($data,true).",...)

\n"; + unset($old); // not used, but required by function signature + + //echo "

".__METHOD__."($name,".print_r($data,true).",...)

\n"; switch($name) { case 'copy': @@ -134,9 +144,9 @@ class addressbook_tracking extends bo_tracking * * @internal use only track($data,$old) * @param array $data current entry - * @param array $old=null old/last state of the entry or null for a new entry - * @param boolean $deleted=null can be set to true to let the tracking know the item got deleted or undelted - * @param array $changed_fields=null changed fields from ealier call to $this->changed_fields($data,$old), to not compute it again + * @param array $old =null old/last state of the entry or null for a new entry + * @param boolean $deleted =null can be set to true to let the tracking know the item got deleted or undelted + * @param array $changed_fields =null changed fields from ealier call to $this->changed_fields($data,$old), to not compute it again * @return int number of log-entries made */ protected function save_history(array $data,array $old=null,$deleted=null,array $changed_fields=null) @@ -180,6 +190,8 @@ class addressbook_tracking extends bo_tracking */ protected function get_message($data,$old,$receiver=null) { + unset($receiver); // not used, but required by function signature + if (!$data['modified'] || !$old) { return lang('New contact submitted by %1 at %2', @@ -196,12 +208,14 @@ class addressbook_tracking extends bo_tracking * * @param array $data * @param array $old - * @param boolean $deleted=null can be set to true to let the tracking know the item got deleted or undelted + * @param boolean $deleted =null can be set to true to let the tracking know the item got deleted or undelted * @param int|string $receiver nummeric account_id or email address * @return string */ protected function get_subject($data,$old,$deleted=null,$receiver=null) { + unset($old, $deleted, $receiver); // not used, but required by function signature + if ($data['is_contactform']) { $prefix = ($data['subject_contactform'] ? $data['subject_contactform'] : lang('Contactform')).': '; @@ -218,6 +232,8 @@ class addressbook_tracking extends bo_tracking */ function get_details($data,$receiver=null) { + unset($receiver); // not used, but required by function signature + foreach($this->contacts->contact_fields as $name => $label) { if (!$data[$name] && $name != 'owner') continue; diff --git a/addressbook/inc/class.addressbook_univention.inc.php b/api/src/Contacts/Univention.php similarity index 85% rename from addressbook/inc/class.addressbook_univention.inc.php rename to api/src/Contacts/Univention.php index 347300d54b..e05ae9084e 100644 --- a/addressbook/inc/class.addressbook_univention.inc.php +++ b/api/src/Contacts/Univention.php @@ -1,20 +1,23 @@ - * @package addressbook + * @package api + * @subpackage contacts * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ +namespace EGroupware\Api\Contacts; + /** * Univention backend for addressbook * * Different mail attribute is only difference to LDAP backend */ -class addressbook_univention extends addressbook_ldap +class Univention extends Ldap { function __construct($ldap_config = null, $ds = null) { diff --git a/api/src/Ldap.php b/api/src/Ldap.php new file mode 100644 index 0000000000..a9c91cafcb --- /dev/null +++ b/api/src/Ldap.php @@ -0,0 +1,263 @@ + + * @author Ralf Becker + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package api + * @subpackage ldap + * @version $Id$ + */ + +namespace EGroupware\Api; + +/** + * LDAP connection handling + * + * Please note for SSL or TLS connections hostname has to be: + * - SSL: "ldaps://host[:port]/" + * - TLS: "tls://host[:port]/" + * Both require certificats installed on the webserver, otherwise the connection will fail! + * + * If multiple (space-separated) ldap hosts or urls are given, try them in order and + * move first successful one to first place in session, to try not working ones + * only once per session. + */ +class Ldap +{ + /** + * Holds the LDAP link identifier + * + * @var resource $ds + */ + var $ds; + + /** + * Holds the detected information about the connected ldap server + * + * @var Ldap\ServerInfo $ldapserverinfo + */ + var $ldapserverinfo; + + /** + * Throw Exceptions in ldapConnect instead of echoing error and returning false + * + * @var boolean $exception_on_error + */ + var $exception_on_error=false; + + /** + * Constructor + * + * @param boolean $exception_on_error =false true: throw Exceptions in ldapConnect instead of echoing error and returning false + */ + function __construct($exception_on_error=false) + { + $this->exception_on_error = $exception_on_error; + $this->restoreSessionData(); + } + + /** + * Returns information about connected ldap server + * + * @return Ldap\ServerInfo|null + */ + function getLDAPServerInfo() + { + return $this->ldapserverinfo; + } + + /** + * escapes a string for use in searchfilters meant for ldap_search. + * + * Escaped Characters are: '*', '(', ')', ' ', '\', NUL + * It's actually a PHP-Bug, that we have to escape space. + * For all other Characters, refer to RFC2254. + * + * @param string|array $string either a string to be escaped, or an array of values to be escaped + * @return string + */ + static function quote($string) + { + return str_replace(array('\\','*','(',')','\0',' '),array('\\\\','\*','\(','\)','\\0','\20'),$string); + } + + /** + * Convert a single ldap result into a associative array + * + * @param array $ldap array with numerical and associative indexes and count's + * @return boolean|array with only associative index and no count's or false on error (parm is no array) + */ + static function result2array($ldap) + { + if (!is_array($ldap)) return false; + + $arr = array(); + foreach($ldap as $var => $val) + { + if (is_int($var) || $var == 'count') continue; + + if (is_array($val) && $val['count'] == 1) + { + $arr[$var] = $val[0]; + } + else + { + if (is_array($val)) unset($val['count']); + + $arr[$var] = $val; + } + } + return $arr; + } + + /** + * Connect to ldap server and return a handle + * + * If multiple (space-separated) ldap hosts or urls are given, try them in order and + * move first successful one to first place in session, to try not working ones + * only once per session. + * + * @param $host ='' ldap host, default $GLOBALS['egw_info']['server']['ldap_host'] + * @param $dn ='' ldap dn, default $GLOBALS['egw_info']['server']['ldap_root_dn'] + * @param $passwd ='' ldap pw, default $GLOBALS['egw_info']['server']['ldap_root_pw'] + * @return resource|boolean resource from ldap_connect() or false on error + * @throws Exception\AssertingFailed 'LDAP support unavailable!' (no ldap extension) + */ + function ldapConnect($host='', $dn='', $passwd='') + { + if(!function_exists('ldap_connect')) + { + if ($this->exception_on_error) throw new Exception\AssertionFailed('LDAP support unavailable!'); + + printf('Error: LDAP support unavailable
',$host); + return False; + } + if (empty($host)) + { + $host = $GLOBALS['egw_info']['server']['ldap_host']; + } + if (empty($dn)) + { + $dn = $GLOBALS['egw_info']['server']['ldap_root_dn']; + $passwd = $GLOBALS['egw_info']['server']['ldap_root_pw']; + } + + // if multiple hosts given, try them all, but only once per session! + if (isset($_SESSION) && isset($_SESSION['ldapConnect']) && isset($_SESSION['ldapConnect'][$host])) + { + $host = $_SESSION['ldapConnect'][$host]; + } + foreach($hosts=preg_split('/[ ,;]+/', $host) as $h) + { + if ($this->_connect($h, $dn, $passwd)) + { + if ($h !== $host) + { + if (isset($_SESSION)) // store working host as first choice in session + { + $_SESSION['ldapConnect'][$host] = implode(' ',array_unique(array_merge(array($h),$hosts))); + } + } + return $this->ds; + } + error_log(__METHOD__."('$h', '$dn', \$passwd) Can't connect/bind to ldap server!". + ($this->ds ? ' '.ldap_error($this->ds).' ('.ldap_errno($this->ds).')' : ''). + ' '.function_backtrace()); + } + // give visible error, only if we cant connect to any ldap server + if ($this->exception_on_error) throw new Exception\NoPermission("Can't connect/bind to LDAP server '$host' and dn='$dn'!"); + + return false; + } + + /** + * connect to the ldap server and return a handle + * + * @param string $host ldap host + * @param string $dn ldap dn + * @param string $passwd ldap pw + * @return resource|boolean resource from ldap_connect() or false on error + */ + private function _connect($host, $dn, $passwd) + { + if (($use_tls = substr($host,0,6) == 'tls://')) + { + $port = parse_url($host,PHP_URL_PORT); + $host = parse_url($host,PHP_URL_HOST); + } + // connect to ldap server (never fails, as connection happens in bind!) + if(!($this->ds = !empty($port) ? ldap_connect($host, $port) : ldap_connect($host))) + { + return False; + } + // set network timeout to not block for minutes + ldap_set_option($this->ds, LDAP_OPT_NETWORK_TIMEOUT, 5); + + if(ldap_set_option($this->ds, LDAP_OPT_PROTOCOL_VERSION, 3)) + { + $supportedLDAPVersion = 3; + } + else + { + $supportedLDAPVersion = 2; + } + if ($use_tls) ldap_start_tls($this->ds); + + if (!isset($this->ldapserverinfo) || + !is_a($this->ldapserverinfo,'EGroupware\Ldap\ServerInfo') || + $this->ldapserverinfo->host != $host) + { + //error_log("no ldap server info found"); + @ldap_bind($this->ds, $GLOBALS['egw_info']['server']['ldap_root_dn'], $GLOBALS['egw_info']['server']['ldap_root_pw']); + + $this->ldapserverinfo = Ldap\ServerInfo::get($this->ds, $host, $supportedLDAPVersion); + $this->saveSessionData(); + } + + if(!@ldap_bind($this->ds, $dn, $passwd)) + { + return False; + } + + return $this->ds; + } + + /** + * disconnect from the ldap server + */ + function ldapDisconnect() + { + if(is_resource($this->ds)) + { + ldap_unbind($this->ds); + unset($this->ds); + unset($this->ldapserverinfo); + } + } + + /** + * restore the session data + */ + function restoreSessionData() + { + if (isset($GLOBALS['egw']->session)) // no availible in setup + { + $this->ldapserverinfo = Cache::getSession(__CLASS__, 'ldapServerInfo'); + } + } + + /** + * save the session data + */ + function saveSessionData() + { + if (isset($GLOBALS['egw']->session)) // no availible in setup + { + Cache::getSession(__CLASS__, 'ldapServerInfo', $this->ldapserverinfo); + } + } +} diff --git a/api/src/Ldap/ServerInfo.php b/api/src/Ldap/ServerInfo.php new file mode 100644 index 0000000000..ab7838c67b --- /dev/null +++ b/api/src/Ldap/ServerInfo.php @@ -0,0 +1,243 @@ + + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package api + * @subpackage ldap + * @version $Id$ + */ + +namespace EGroupware\Api\Ldap; + +/** + * Class to store and retrieve information (eg. supported object classes) of a connected ldap server + */ +class ServerInfo +{ + /** + * Unknown LDAP server + */ + const UNKNOWN = 0; + /** + * OpenLDAP server + */ + const OPENLDAP = 1; + /** + * Samba4 LDAP server + */ + const SAMBA4 = 2; + + /** + * @var array $namingContext holds the supported namingcontexts + */ + var $namingContext = array(); + + /** + * @var string $version holds the LDAP server version + */ + var $version = 2; + + /** + * @var integer $serverType holds the type of LDAP server(OpenLDAP, ADS, NDS, ...) + */ + var $serverType = 0; + + /** + * @var string $_subSchemaEntry the subschema entry DN + */ + var $subSchemaEntry = ''; + + /** + * @var array $supportedObjectClasses the supported objectclasses + */ + var $supportedObjectClasses = array(); + + /** + * @var array $supportedOIDs the supported OIDs + */ + var $supportedOIDs = array(); + + /** + * Name of host + * + * @var string + */ + var $host; + + /** + * Constructor + * + * @param string $host + */ + function __construct($host) + { + $this->host = $host; + } + + /** + * gets the version + * + * @return integer the supported ldap version + */ + function getVersion() + { + return $this->version; + } + + /** + * sets the namingcontexts + * + * @param array $_namingContext the supported namingcontexts + */ + function setNamingContexts($_namingContext) + { + $this->namingContext = $_namingContext; + } + + /** + * sets the type of the ldap server(OpenLDAP, ADS, NDS, ...) + * + * @param integer $_serverType the type of ldap server + */ + function setServerType($_serverType) + { + $this->serverType = $_serverType; + } + + /** + * sets the DN for the subschema entry + * + * @param string $_subSchemaEntry the subschema entry DN + */ + function setSubSchemaEntry($_subSchemaEntry) + { + $this->subSchemaEntry = $_subSchemaEntry; + } + + /** + * sets the supported objectclasses + * + * @param array $_supportedObjectClasses the supported objectclasses + */ + function setSupportedObjectClasses($_supportedObjectClasses) + { + $this->supportedOIDs = $_supportedObjectClasses; + $this->supportedObjectClasses = array_flip($_supportedObjectClasses); + } + + /** + * sets the version + * + * @param integer $_version the supported ldap version + */ + function setVersion($_version) + { + $this->version = $_version; + } + + /** + * checks for supported objectclasses + * + * @return bool returns true if the ldap server supports this objectclass + */ + function supportsObjectClass($_objectClass) + { + if($this->supportedObjectClasses[strtolower($_objectClass)]) + { + return true; + } + return false; + } + + /** + * Query given ldap connection for available information + * + * @param resource $ds + * @param string $host + * @param int $version 2 or 3 + * @return ldapserverinfo + */ + public static function get($ds, $host, $version=3) + { + $filter='(objectclass=*)'; + $justthese = array('structuralObjectClass','namingContexts','supportedLDAPVersion','subschemaSubentry','vendorname'); + if(($sr = @ldap_read($ds, '', $filter, $justthese))) + { + if(($info = ldap_get_entries($ds, $sr))) + { + $ldapServerInfo = new ServerInfo($host); + $ldapServerInfo->setVersion($version); + + // check for naming contexts + if($info[0]['namingcontexts']) + { + for($i=0; $i<$info[0]['namingcontexts']['count']; $i++) + { + $namingcontexts[] = $info[0]['namingcontexts'][$i]; + } + $ldapServerInfo->setNamingContexts($namingcontexts); + } + + // check for ldap server type + if($info[0]['structuralobjectclass']) + { + switch($info[0]['structuralobjectclass'][0]) + { + case 'OpenLDAProotDSE': + $ldapServerType = OPENLDAP_LDAPSERVER; + break; + default: + $ldapServerType = UNKNOWN_LDAPSERVER; + break; + } + $ldapServerInfo->setServerType($ldapServerType); + } + if ($info[0]['vendorname'] && stripos($info[0]['vendorname'][0], 'samba') !== false) + { + $ldapServerInfo->setServerType(SAMBA4_LDAPSERVER); + } + + // check for subschema entry dn + if($info[0]['subschemasubentry']) + { + $subschemasubentry = $info[0]['subschemasubentry'][0]; + $ldapServerInfo->setSubSchemaEntry($subschemasubentry); + } + + // create list of supported objetclasses + if(!empty($subschemasubentry)) + { + $filter='(objectclass=*)'; + $justthese = array('objectClasses'); + + if(($sr = ldap_read($ds, $subschemasubentry, $filter, $justthese))) + { + if(($info = ldap_get_entries($ds, $sr))) + { + if($info[0]['objectclasses']) { + for($i=0; $i<$info[0]['objectclasses']['count']; $i++) + { + $matches = null; + if(preg_match('/^\( (.*) NAME \'(\w*)\' /', $info[0]['objectclasses'][$i], $matches)) + { + #_debug_array($matches); + if(count($matches) == 3) + { + $supportedObjectClasses[$matches[1]] = strtolower($matches[2]); + } + } + } + $ldapServerInfo->setSupportedObjectClasses($supportedObjectClasses); + } + } + } + } + } + } + return $ldapServerInfo; + } +} diff --git a/api/src/Storage.php b/api/src/Storage.php index 342c188f9c..b1df9cfdd2 100644 --- a/api/src/Storage.php +++ b/api/src/Storage.php @@ -167,7 +167,7 @@ class Storage extends Storage\Base $this->extra_join_order = " LEFT JOIN $extra_table extra_order ON $table.$this->autoinc_id=extra_order.$this->extra_id"; $this->extra_join_filter = " JOIN $extra_table extra_filter ON $table.$this->autoinc_id=extra_filter.$this->extra_id"; - $this->customfields = Customfields::get($app, false, null, $db); + $this->customfields = Storage\Customfields::get($app, false, null, $db); } /** diff --git a/api/src/Customfields.php b/api/src/Storage/Customfields.php similarity index 94% rename from api/src/Customfields.php rename to api/src/Storage/Customfields.php index 59b075b98e..c91847588f 100755 --- a/api/src/Customfields.php +++ b/api/src/Storage/Customfields.php @@ -10,7 +10,9 @@ * @version $Id$ */ -namespace EGroupware\Api; +namespace EGroupware\Api\Storage; + +use EGroupware\Api; // explicitly reference classes still in phpgwapi use common; @@ -28,7 +30,7 @@ class Customfields implements \IteratorAggregate /** * Reference to the global db class * - * @var Db + * @var Api\Db */ static protected $db; @@ -94,13 +96,13 @@ class Customfields implements \IteratorAggregate */ function getIterator() { - return new Db\CallbackIterator($this->iterator, function($_row) + return new Api\Db\CallbackIterator($this->iterator, function($_row) { - $row = Db::strip_array_keys($_row, 'cf_'); + $row = Api\Db::strip_array_keys($_row, 'cf_'); $row['private'] = $row['private'] ? explode(',', $row['private']) : array(); $row['type2'] = $row['type2'] ? explode(',', $row['type2']) : array(); $row['values'] = json_decode($row['values'], true); - $row['needed'] = Db::from_bool($row['needed']); + $row['needed'] = Api\Db::from_bool($row['needed']); return $row; }, array(), function($row) @@ -137,18 +139,18 @@ class Customfields implements \IteratorAggregate public static function get($app, $all_private_too=false, $only_type2=null, egw_db $db=null) { $cache_key = $app.':'.($all_private_too?'all':$GLOBALS['egw_info']['user']['account_id']).':'.$only_type2; - $cfs = Cache::getInstance(__CLASS__, $cache_key); + $cfs = Api\Cache::getInstance(__CLASS__, $cache_key); if (!isset($cfs)) { $cfs = iterator_to_array(new Customfields($app, $all_private_too, $only_type2, 0, null, $db)); - Cache::setInstance(__CLASS__, $cache_key, $cfs); - $cached = Cache::getInstance(__CLASS__, $app); + Api\Cache::setInstance(__CLASS__, $cache_key, $cfs); + $cached = Api\Cache::getInstance(__CLASS__, $app); if (!in_array($cache_key, (array)$cached)) { $cached[] = $cache_key; - Cache::setInstance(__CLASS__, $app, $cached); + Api\Cache::setInstance(__CLASS__, $app, $cached); } } //error_log(__METHOD__."('$app', $all_private_too, '$only_type2') returning fields: ".implode(', ', array_keys($cfs))); @@ -224,7 +226,7 @@ class Customfields implements \IteratorAggregate case 'date-time': if ($value) { - $value = DateTime::to($value, $field['type'] == 'date' ? true : ''); + $value = Api\DateTime::to($value, $field['type'] == 'date' ? true : ''); } break; @@ -461,13 +463,13 @@ class Customfields implements \IteratorAggregate */ protected static function invalidate_cache($app) { - if (($cached = Cache::getInstance(__CLASS__, $app))) + if (($cached = Api\Cache::getInstance(__CLASS__, $app))) { foreach($cached as $key) { - Cache::unsetInstance(__CLASS__, $key); + Api\Cache::unsetInstance(__CLASS__, $key); } - Cache::unsetInstance(__CLASS__, $app); + Api\Cache::unsetInstance(__CLASS__, $app); } } @@ -532,7 +534,7 @@ class Customfields implements \IteratorAggregate /** * Initialise our db * - * We use a reference here (no clone), as we no longer use Db::row() or Db::next_record()! + * We use a reference here (no clone), as we no longer use Api\Db::row() or Api\Db::next_record()! * */ public static function init_static() diff --git a/api/src/Storage/History.php b/api/src/Storage/History.php new file mode 100644 index 0000000000..38426e7b97 --- /dev/null +++ b/api/src/Storage/History.php @@ -0,0 +1,288 @@ + + * @copyright 2001 by Joseph Engo + * @author Ralf Becker new DB-methods and search + * + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @package api + * @subpackage storage + * @access public + * @version $Id$ + */ + +namespace EGroupware\Api\Storage; + +use EGroupware\Api; + +/** + * Record history logging service + * + * This class need to be instanciated for EACH app, which wishes to use it! + */ +class History +{ + /** + * Reference to the global db object + * + * @var Api\Db + */ + var $db; + const TABLE = 'egw_history_log'; + /** + * App.name this class is instanciated for / working on + * + * @var string + */ + var $appname; + var $user; + var $types = array( + 'C' => 'Created', + 'D' => 'Deleted', + 'E' => 'Edited' + ); + + /** + * Constructor + * + * @param string $appname app name this instance operates on + * @return historylog + */ + function __construct($appname='',$user=null) + { + $this->appname = $appname ? $appname : $GLOBALS['egw_info']['flags']['currentapp']; + $this->user = !is_null($user) ? $user : $GLOBALS['egw_info']['user']['account_id']; + + if (is_object($GLOBALS['egw_setup']->db)) + { + $this->db = $GLOBALS['egw_setup']->db; + } + else + { + $this->db = $GLOBALS['egw']->db; + } + } + + /** + * Delete the history-log of one or multiple records of $this->appname + * + * @param int|array $record_id one or more id's of $this->appname, or null to delete ALL records of $this->appname + * @return int number of deleted records/rows (0 is not necessaryly an error, it can just mean there's no record!) + */ + function delete($record_id) + { + $where = array('history_appname' => $this->appname); + + if (is_array($record_id) || is_numeric($record_id)) + { + $where['history_record_id'] = $record_id; + } + $this->db->delete(self::TABLE,$where,__LINE__,__FILE__); + + return $this->db->affected_rows(); + } + + /** + * Add a history record, if $new_value != $old_value + * + * @param string $status 2 letter code: eg. $this->types: C=Created, D=Deleted, E=Edited + * @param int $record_id it of the record in $this->appname (set by the constructor) + * @param string $new_value new value + * @param string $old_value old value + */ + function add($status,$record_id,$new_value,$old_value) + { + if ($new_value != $old_value) + { + $this->db->insert(self::TABLE,array( + 'history_record_id' => $record_id, + 'history_appname' => $this->appname, + 'history_owner' => $this->user, + 'history_status' => $status, + 'history_new_value' => $new_value, + 'history_old_value' => $old_value, + 'history_timestamp' => time(), + 'sessionid' => $GLOBALS['egw']->session->sessionid_access_log, + ),false,__LINE__,__FILE__); + } + } + + /** + * Static function to add a history record + */ + public static function static_add($appname, $id, $user, $field_code, $new_value, $old_value = '') + { + if ($new_value != $old_value) + { + $GLOBALS['egw']->db->insert(self::TABLE,array( + 'history_record_id' => $id, + 'history_appname' => $appname, + 'history_owner' => (int)$user, + 'history_status' => $field_code, + 'history_new_value' => $new_value, + 'history_old_value' => $old_value, + 'history_timestamp' => time(), + 'sessionid' => $GLOBALS['egw']->session->sessionid_access_log, + ),false,__LINE__,__FILE__); + } + } + + /** + * Search history-log + * + * @param array|int $filter array with filters, or int record_id + * @param string $order ='history_id' sorting after history_id is identical to history_timestamp + * @param string $sort ='DESC' + * @param int $limit =null only return this many entries + * @return array of arrays with keys id, record_id, appname, owner (account_id), status, new_value, old_value, + * timestamp (Y-m-d H:i:s in servertime), user_ts (timestamp in user-time) + */ + function search($filter,$order='history_id',$sort='DESC',$limit=null) + { + if (!is_array($filter)) $filter = is_numeric($filter) ? array('history_record_id' => $filter) : array(); + + if (!$order || !preg_match('/^[a-z0-9_]+$/i',$order) || !preg_match('/^(asc|desc)?$/i',$sort)) + { + $orderby = 'ORDER BY history_id DESC'; + } + else + { + $orderby = "ORDER BY $order $sort"; + } + foreach($filter as $col => $value) + { + if (!is_numeric($col) && substr($col,0,8) != 'history_') + { + $filter['history_'.$col] = $value; + unset($filter[$col]); + } + } + if (!isset($filter['history_appname'])) $filter['history_appname'] = $this->appname; + + // do not try to read all history entries of an app + if (!$filter['history_record_id']) return array(); + + $rows = array(); + foreach($this->db->select(self::TABLE, '*', $filter, __LINE__, __FILE__, + isset($limit) ? 0 : false, $orderby, 'phpgwapi', $limit) as $row) + { + $row['user_ts'] = $this->db->from_timestamp($row['history_timestamp']) + 3600 * $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset']; + $rows[] = Api\Db::strip_array_keys($row,'history_'); + } + return $rows; + } + + /** + * Get a slice of history records + * + * Similar to search(), except this one can take a start and a number of records + * + * @see Base::get_rows() + */ + public static function get_rows(&$query, &$rows) + { + $filter = array(); + $rows = array(); + $filter['history_appname'] = $query['appname']; + $filter['history_record_id'] = $query['record_id']; + if(is_array($query['colfilter'])) { + foreach($query['colfilter'] as $column => $value) { + $filter[$column] = $value; + } + } + if ($GLOBALS['egw']->db->Type == 'mysql' && $GLOBALS['egw']->db->ServerInfo['version'] >= 4.0) + { + $mysql_calc_rows = 'SQL_CALC_FOUND_ROWS '; + } + else + { + $total = $GLOBALS['egw']->db->select(self::TABLE,'COUNT(*)',$filter,__LINE__,__FILE__,false,'','phpgwapi',0)->fetchColumn(); + } + // filter out private (or no longer defined) custom fields + if ($filter['history_appname']) + { + $to_or[] = "history_status NOT LIKE '#%'"; + // explicitly allow "##" used to store iCal/vCard X-attributes + if (in_array($filter['history_appname'], array('calendar','infolog','addressbook'))) + { + $to_or[] = "history_status LIKE '##%'"; + } + if (($cfs = Customfields::get($filter['history_appname']))) + { + $to_or[] = 'history_status IN ('.implode(',', array_map(function($str) + { + return $GLOBALS['egw']->db->quote('#'.$str); + }, array_keys($cfs))).')'; + } + $filter[] = '('.implode(' OR ', $to_or).')'; + } + $_query = array(array( + 'table' => self::TABLE, + 'cols' => array('history_id', 'history_record_id','history_appname','history_owner','history_status','history_new_value', 'history_timestamp','history_old_value'), + 'where' => $filter, + )); + + // Add in files, if possible + if($GLOBALS['egw_info']['user']['apps']['filemanager'] && + $file = Api\Vfs\Sqlfs\StreamWrapper::url_stat("/apps/{$query['appname']}/{$query['record_id']}",STREAM_URL_STAT_LINK)) + { + $_query[] = array( + 'table' => Api\Vfs\Sqlfs\StreamWrapper::TABLE, + 'cols' =>array('fs_id', 'fs_dir', "'filemanager'",'COALESCE(fs_modifier,fs_creator)',"'~file~'",'fs_name','fs_modified', 'fs_mime'), + 'where' => array('fs_dir' => $file['ino']) + ); + } + $new_file_id = array(); + foreach($GLOBALS['egw']->db->union( + $_query, + __LINE__, __FILE__, + ' ORDER BY ' . ($query['order'] ? $query['order'] : 'history_timestamp') . ' ' . ($query['sort'] ? $query['sort'] : 'DESC'), + $query['start'], + $query['num_rows'] + ) as $row) + { + $row['user_ts'] = $GLOBALS['egw']->db->from_timestamp($row['history_timestamp']) + 3600 * $GLOBALS['egw_info']['user']['preferences']['common']['tz_offset']; + + // Explode multi-part values + foreach(array('history_new_value','history_old_value') as $field) + { + if(strpos($row[$field],Tracking::ONE2N_SEPERATOR) !== false) + { + $row[$field] = explode(Tracking::ONE2N_SEPERATOR,$row[$field]); + } + } + // Get information needed for proper display + if($row['history_appname'] == 'filemanager') + { + $new_version = $new_file_id[$row['history_new_value']]; + $new_file_id[$row['history_new_value']] = count($rows); + $path = Api\Vfs\Sqlfs\StreamWrapper::id2path($row['history_id']); + + // Apparently we don't have to do anything with it, just ask... + // without this, previous versions are not handled properly + Api\Vfs::getExtraInfo($path); + + $row['history_new_value'] = array( + 'path' => $path, + 'name' => Api\Vfs::basename($path), + 'mime' => $row['history_old_value'] + ); + $row['history_old_value'] = ''; + if($new_version !== null) + { + $rows[$new_version]['old_value'] = $row['history_new_value']; + } + } + $rows[] = Api\Db::strip_array_keys($row,'history_'); + } + if ($mysql_calc_rows) + { + $total = $GLOBALS['egw']->db->query('SELECT FOUND_ROWS()')->fetchColumn(); + } + + return $total; + } +} diff --git a/api/src/Storage/Tracking.php b/api/src/Storage/Tracking.php new file mode 100644 index 0000000000..7a747fe33a --- /dev/null +++ b/api/src/Storage/Tracking.php @@ -0,0 +1,1220 @@ + + * @package api + * @subpackage storage + * @copyright (c) 2007-16 by Ralf Becker + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +namespace EGroupware\Api\Storage; + +use EGroupware\Api; + +// explicitly reference classes still in phpgwapi +use egw_link; +use historylog; +use html; + +/** + * Abstract base class for trackering: + * - logging all modifications of an entry + * - notifying users about changes in an entry + * + * You need to extend these class in your application: + * 1. set the required class-vars: app, id_field + * 2. optional set class-vars: creator_field, assigned_field, check2prefs + * 3. implement the required methods: get_config, get_details + * 4. optionally re-implement: get_title, get_subject, get_body, get_attachments, get_link, get_notification_link, get_message + * They are all documented in this file via phpDocumentor comments. + * + * Translate field-name to history status field: + * As history status was only char(2) prior to EGroupware 1.6, a mapping was necessary. + * Now it's varchar(64) and a mapping makes no sense for new applications, just list + * all fields to log as key AND value! + * + * History login supports now 1:N relations on a base record. To use that you need: + * - to have the 1:N relation as array of arrays with the values of that releation, eg: + * $data = array( + * 'id' => 123, + * 'title' => 'Something', + * 'date' => '2009-08-21 14:42:00', + * 'participants' => array( + * array('account_id' => 15, 'name' => 'User Hugo', 'status' => 'A', 'quantity' => 1), + * array('account_id' => 17, 'name' => 'User Bert', 'status' => 'U', 'quantity' => 3), + * ), + * ); + * - set field2history as follows + * $field2history = array( + * 'id' => 'id', + * 'title' => 'title', + * 'participants' => array('uid','status','quantity'), + * ); + * - set content for history log widget: + * $content['history'] = array( + * 'id' => 123, + * 'app' => 'calendar', + * 'status-widgets' => array( + * 'title' => 'label', // no need to set, as default is label + * 'date' => 'datetime', + * 'participants' = array( + * 'select-account', + * array('U' => 'Unknown', 'A' => 'Accepted', 'R' => 'Rejected'), + * 'integer', + * ), + * ), + * ); + * - set lables for history: + * $sel_options['status'] = array( + * 'title' => 'Title', + * 'date' => 'Starttime', + * 'participants' => 'Participants: User, Status, Quantity', // a single label! + * ); + * + * The above is also an example for using regular history login in EGroupware (by skipping the 'participants' key). + */ +abstract class Tracking +{ + /** + * Application we are tracking + * + * @var string + */ + var $app; + /** + * Name of the id-field, used as id in the history log (required!) + * + * @var string + */ + var $id_field; + /** + * Name of the field with the creator id, if the creator of an entry should be notified + * + * @var string + */ + var $creator_field; + /** + * Name of the field with the id(s) of assinged users, if they should be notified + * + * @var string + */ + var $assigned_field; + /** + * Can be used to map the following prefs to different names: + * - notify_creator - user wants to be notified for items he created + * - notify_assigned - user wants to be notified for items assigned to him + * @var array + */ + var $check2pref; + /** + * Translate field-name to history status field (see comment in class header) + * + * @var array + */ + var $field2history = array(); + /** + * Should the user (passed to the track method or current user if not passed) be used as sender or get_config('sender') + * + * @var boolean + */ + var $prefer_user_as_sender = true; + /** + * Should the current user be email-notified (about change he made himself) + * + * Popup notifications are never send to the current user! + * + * @var boolean + */ + var $notify_current_user = false; + + /** + * Array with error-messages if track($data,$old) returns false + * + * @var array + */ + var $errors = array(); + + /** + * instance of the historylog object for the app we are tracking + * + * @access private + * @var historylog + */ + var $historylog; + + /** + * Current user, can be set via bo_tracking::track(,,$user) + * + * @access private + * @var int; + */ + var $user; + + /** + * Datetime format of the currently notified user (send_notificaton) + * + * @var string + */ + var $datetime_format; + /** + * Should the class allow html content (for notifications) + * + * @var boolean + */ + var $html_content_allow = false; + + /** + * Custom fields of type link entry or application + * + * Used to automatic create or update a link + * + * @var array field => application name pairs (or empty for link entry) + */ + var $cf_link_fields = array(); + + /** + * Separator for 1:N relations + * + */ + const ONE2N_SEPERATOR = '~|~'; + + /** + * Config name for custom notification message + */ + const CUSTOM_NOTIFICATION = 'custom_notification'; + + /** + * Constructor + * + * @param string $cf_app = null if set, custom field names get added to $field2history + * @return bo_tracking + */ + function __construct($cf_app = null) + { + if ($cf_app) + { + $linkable_cf_types = array('link-entry')+array_keys(egw_link::app_list()); + foreach(Customfields::get($cf_app, true) as $cf_name => $cf_data) + { + $this->field2history['#'.$cf_name] = '#'.$cf_name; + + if (in_array($cf_data['type'],$linkable_cf_types)) + { + $this->cf_link_fields['#'.$cf_name] = $cf_data['type'] == 'link-entry' ? '' : $cf_data['type']; + } + } + } + } + + /** + * Get the details of an entry + * + * You can/should call $this->get_customfields() to add custom fields. + * + * @param array|object $data + * @param int|string $receiver nummeric account_id or email address + * @return array of details as array with values for keys 'label','value','type' + */ + function get_details($data,$receiver=null) + { + unset($data, $receiver); // not uses as just a stub + + return array(); + } + + /** + * Get custom fields of an entry of an entry + * + * @param array|object $data + * @param string $only_type2 = null if given only return fields of type2 == $only_type2 + * @return array of details as array with values for keys 'label','value','type' + */ + function get_customfields($data, $only_type2=null) + { + $details = array(); + + if (($cfs = Customfields::get($this->app, $all_private_too=false, $only_type2))) + { + $header_done = false; + foreach($cfs as $name => $field) + { + if (in_array($field['type'], Customfields::$non_printable_fields)) continue; + + if (!$header_done) + { + $details['custom'] = array( + 'value' => lang('Custom fields').':', + 'type' => 'reply', + ); + $header_done = true; + } + //error_log(__METHOD__."() $name: data['#$name']=".array2string($data['#'.$name]).", field[values]=".array2string($field['values'])); + $details['#'.$name] = array( + 'label' => $field['label'], + 'value' => Customfields::format($field, $data['#'.$name]), + ); + //error_log("--> details['#$name']=".array2string($details['#'.$name])); + } + } + return $details; + } + + /** + * Get a config value, which can depend on $data and $old + * + * Need to be implemented in your extended tracking class! + * + * @param string $name possible values are: + * - 'assigned' array of users to use instead of a field in the data + * - 'copy' array of email addresses notifications should be copied too, can depend on $data + * - 'lang' string lang code for copy mail + * - 'subject' string subject line for the notification of $data,$old, defaults to link-title + * - 'link' string of link to view $data + * - 'sender' sender of email + * - 'skip_notify' array of email addresses that should _not_ be notified + * - CUSTOM_NOTIFICATION string notification body message. Merge print placeholders are allowed. + * @param array $data current entry + * @param array $old = null old/last state of the entry or null for a new entry + * @return mixed + */ + protected function get_config($name,$data,$old=null) + { + unset($name, $data, $old); // not used as just a stub + + return null; + } + + /** + * Tracks the changes in one entry $data, by comparing it with the last version in $old + * + * @param array $data current entry + * @param array $old = null old/last state of the entry or null for a new entry + * @param int $user = null user who made the changes, default to current user + * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undeleted + * @param array $changed_fields = null changed fields from ealier call to $this->changed_fields($data,$old), to not compute it again + * @param boolean $skip_notification = false do NOT send any notification + * @return int|boolean false on error, integer number of changes logged or true for new entries ($old == null) + */ + public function track(array $data,array $old=null,$user=null,$deleted=null,array $changed_fields=null,$skip_notification=false) + { + $this->user = !is_null($user) ? $user : $GLOBALS['egw_info']['user']['account_id']; + + $changes = true; + //error_log(__METHOD__.__LINE__); + if ($old && $this->field2history) + { + //error_log(__METHOD__.__LINE__.' Changedfields:'.print_r($changed_fields,true)); + $changes = $this->save_history($data,$old,$deleted,$changed_fields); + //error_log(__METHOD__.__LINE__.' Changedfields:'.print_r($changed_fields,true)); + //error_log(__METHOD__.__LINE__.' Changes:'.print_r($changes,true)); + } + + //error_log(__METHOD__.__LINE__.' LinkFields:'.array2string($this->cf_link_fields)); + if ($changes && $this->cf_link_fields) + { + $this->update_links($data,(array)$old); + } + // do not run do_notifications if we have no changes + if ($changes && !$skip_notification && !$this->do_notifications($data,$old,$deleted)) + { + $changes = false; + } + return $changes; + } + + /** + * Store a link for each custom field linking to an other application and update them + * + * @param array $data + * @param array $old + */ + protected function update_links(array $data, array $old) + { + //error_log(__METHOD__.__LINE__.array2string($data).function_backtrace()); + //error_log(__METHOD__.__LINE__.array2string($this->cf_link_fields)); + foreach(array_keys((array)$this->cf_link_fields) as $name) + { + //error_log(__METHOD__.__LINE__.' Field:'.$name. ' Value (new):'.array2string($data[$name])); + //error_log(__METHOD__.__LINE__.' Field:'.$name. ' Value (old):'.array2string($old[$name])); + if (is_array($data[$name]) && array_key_exists('id',$data[$name])) $data[$name] = $data[$name]['id']; + if (is_array($old[$name]) && array_key_exists('id',$old[$name])) $old[$name] = $old[$name]['id']; + //error_log(__METHOD__.__LINE__.'(After processing) Field:'.$name. ' Value (new):'.array2string($data[$name])); + //error_log(__METHOD__.__LINE__.'(After processing) Field:'.$name. ' Value (old):'.array2string($old[$name])); + } + $current_ids = array_unique(array_diff(array_intersect_key($data,$this->cf_link_fields),array('',0,NULL))); + $old_ids = $old ? array_unique(array_diff(array_intersect_key($old,$this->cf_link_fields),array('',0,NULL))) : array(); + //error_log(__METHOD__.__LINE__.array2string($current_ids)); + //error_log(__METHOD__.__LINE__.array2string($old_ids)); + // create links for added application entry + foreach(array_diff($current_ids,$old_ids) as $name => $id) + { + if (!($app = $this->cf_link_fields[$name])) + { + list($app,$id) = explode(':',$id); + if (!$id) continue; // can be eg. 'addressbook:', if no contact selected + } + $source_id = $data[$this->id_field]; + //error_log(__METHOD__.__LINE__.array2string($source_id)); + if ($source_id) egw_link::link($this->app,$source_id,$app,$id); + //error_log(__METHOD__.__LINE__."egw_link::link('$this->app',".array2string($source_id).",'$app',$id);"); + //echo "

egw_link::link('$this->app',{$data[$this->id_field]},'$app',$id);

\n"; + } + + // unlink removed application entries + foreach(array_diff($old_ids,$current_ids) as $name => $id) + { + if (!isset($data[$name])) continue; // ignore not set link cf's, eg. from sync clients + if (!($app = $this->cf_link_fields[$name])) + { + list($app,$id) = explode(':',$id); + if (!$id) continue; + } + $source_id = $data[$this->id_field]; + if ($source_id) egw_link::unlink(null,$this->app,$source_id,0,$app,$id); + //echo "

egw_link::unlink(NULL,'$this->app',{$data[$this->id_field]},0,'$app',$id);

\n"; + } + } + + /** + * Save changes to the history log + * + * @internal use only track($data,$old) + * @param array $data current entry + * @param array $old = null old/last state of the entry or null for a new entry + * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undelted + * @param array $changed_fields = null changed fields from ealier call to $this->changed_fields($data,$old), to not compute it again + * @return int number of log-entries made + */ + protected function save_history(array $data,array $old=null,$deleted=null,array $changed_fields=null) + { + unset($deleted); // not used, but required by function signature + + //error_log(__METHOD__.__LINE__.' Changedfields:'.array2string($changed_fields)); + if (is_null($changed_fields)) + { + $changed_fields = self::changed_fields($data,$old); + //error_log(__METHOD__.__LINE__.' Changedfields:'.array2string($changed_fields)); + } + if (!$changed_fields && ($old || !$GLOBALS['egw_info']['server']['log_user_agent_action'])) return 0; + + if (!is_object($this->historylog) || $this->historylog->user != $this->user) + { + $this->historylog = new historylog($this->app,$this->user); + } + // log user-agent and session-action + if ($GLOBALS['egw_info']['server']['log_user_agent_action'] && ($changed_fields || !$old)) + { + $this->historylog->add('user_agent_action', $data[$this->id_field], + $_SERVER['HTTP_USER_AGENT'], $_SESSION[Api\SessionEGW_SESSION_VAR]['session_action']); + } + foreach($changed_fields as $name) + { + $status = isset($this->field2history[$name]) ? $this->field2history[$name] : $name; + //error_log(__METHOD__.__LINE__." Name $name,".' Status:'.array2string($status)); + if (is_array($status)) // 1:N relation --> remove common rows + { + //error_log(__METHOD__.__LINE__.' is Array'); + self::compact_1_N_relation($data[$name],$status); + self::compact_1_N_relation($old[$name],$status); + $added = array_values(array_diff($data[$name],$old[$name])); + $removed = array_values(array_diff($old[$name],$data[$name])); + $n = max(array(count($added),count($removed))); + for($i = 0; $i < $n; ++$i) + { + //error_log(__METHOD__."() $i: historylog->add('$name',data['$this->id_field']={$data[$this->id_field]},".array2string($added[$i]).','.array2string($removed[$i])); + $this->historylog->add($name,$data[$this->id_field],$added[$i],$removed[$i]); + } + } + else + { + //error_log(__METHOD__.__LINE__.' IDField:'.array2string($this->id_field).' ->'.$data[$this->id_field].' New:'.$data[$name].' Old:'.$old[$name]); + $this->historylog->add($status,$data[$this->id_field], + is_array($data[$name]) ? implode(',',$data[$name]) : $data[$name], + is_array($old[$name]) ? implode(',',$old[$name]) : $old[$name]); + } + } + //error_log(__METHOD__.__LINE__.' return:'.count($changed_fields)); + return count($changed_fields); + } + + /** + * Compute changes between new and old data + * + * Can be used to check if saving the data is really necessary or user just pressed save + * + * @param array $data + * @param array $old = null + * @return array of keys with different values in $data and $old + */ + public function changed_fields(array $data,array $old=null) + { + if (is_null($old)) return array_keys($data); + $changed_fields = array(); + foreach($this->field2history as $name => $status) + { + if (!$old[$name] && !$data[$name]) continue; // treat all sorts of empty equally + + if ($name[0] == '#' && !isset($data[$name])) continue; // no set customfields are not stored, therefore not changed + + if (is_array($status)) // 1:N relation + { + self::compact_1_N_relation($data[$name],$status); + self::compact_1_N_relation($old[$name],$status); + } + if ($old[$name] != $data[$name]) + { + // normalize arrays, we do NOT care for the order of multiselections + if (is_array($data[$name]) || is_array($old[$name])) + { + if (!is_array($data[$name])) $data[$name] = explode(',',$data[$name]); + if (!is_array($old[$name])) $old[$name] = explode(',',$old[$name]); + if (count($data[$name]) == count($old[$name])) + { + sort($data[$name]); + sort($old[$name]); + if ($data[$name] == $old[$name]) continue; + } + } + elseif (str_replace("\r", '', $old[$name]) == str_replace("\r", '', $data[$name])) + { + continue; // change only in CR (eg. different OS) --> ignore + } + $changed_fields[] = $name; + //echo "

$name: ".array2string($data[$name]).' != '.array2string($old[$name])."

\n"; + } + } + foreach($data as $name => $value) + { + if ($name[0] == '#' && $name[1] == '#' && $value !== $old[$name]) + { + $changed_fields[] = $name; + } + } + //error_log(__METHOD__."() changed_fields=".array2string($changed_fields)); + return $changed_fields; + } + + /** + * Compact (spezified) fields of a 1:N relation into an array of strings + * + * @param array &$rows rows of the 1:N relation + * @param array $cols field names as values + */ + private static function compact_1_N_relation(&$rows,array $cols) + { + if (is_array($rows)) + { + foreach($rows as &$row) + { + $values = array(); + foreach($cols as $col) + { + $values[] = $row[$col]; + } + $row = implode(self::ONE2N_SEPERATOR,$values); + } + } + else + { + $rows = array(); + } + } + + /** + * sending all notifications for the changed entry + * + * @internal use only track($data,$old,$user) + * @param array $data current entry + * @param array $old = null old/last state of the entry or null for a new entry + * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undelted + * @param array $email_notified=null if present will return the emails notified, if given emails in that list will not be notified + * @return boolean true on success, false on error (error messages are in $this->errors) + */ + public function do_notifications($data,$old,$deleted=null,&$email_notified=null) + { + $this->errors = $email_sent = array(); + if (!empty($email_notified) && is_array($email_notified)) $email_sent = $email_notified; + + if (!$this->notify_current_user && $this->user) // do we have a current user and should we notify the current user about his own changes + { + //error_log("do_notificaton() adding user=$this->user to email_sent, to not notify him"); + $email_sent[] = $GLOBALS['egw']->accounts->id2name($this->user,'account_email'); + } + $skip_notify = $this->get_config('skip_notify',$data,$old); + if($skip_notify && is_array($skip_notify)) + { + $email_sent = array_merge($email_sent, $skip_notify); + } + + // entry creator + if ($this->creator_field && ($email = $GLOBALS['egw']->accounts->id2name($data[$this->creator_field],'account_email')) && + !in_array($email, $email_sent)) + { + if ($this->send_notification($data,$old,$email,$data[$this->creator_field],'notify_creator')) + { + $email_sent[] = $email; + } + } + + // members of group when entry owned by group + if ($this->creator_field && $GLOBALS['egw']->accounts->get_type($data[$this->creator_field]) == 'g') + { + foreach($GLOBALS['egw']->accounts->members($data[$this->creator_field],true) as $u) + { + if (($email = $GLOBALS['egw']->accounts->id2name($u,'account_email')) && + !in_array($email, $email_sent)) + { + if ($this->send_notification($data,$old,$email,$u,'notify_owner_group_member')) + { + $email_sent[] = $email; + } + } + } + } + + // assigned / responsible users + if ($this->assigned_field || $assigned = $this->get_config('assigned', $data)) + { + //error_log(__METHOD__."() data[$this->assigned_field]=".print_r($data[$this->assigned_field],true).", old[$this->assigned_field]=".print_r($old[$this->assigned_field],true)); + $old_assignees = array(); + $assignees = $assigned ? $assigned : array(); + if ($data[$this->assigned_field]) // current assignments + { + $assignees = is_array($data[$this->assigned_field]) ? + $data[$this->assigned_field] : explode(',',$data[$this->assigned_field]); + } + if ($old && $old[$this->assigned_field]) + { + $old_assignees = is_array($old[$this->assigned_field]) ? + $old[$this->assigned_field] : explode(',',$old[$this->assigned_field]); + } + foreach(array_unique(array_merge($assignees,$old_assignees)) as $assignee) + { + //error_log(__METHOD__."() assignee=$assignee, type=".$GLOBALS['egw']->accounts->get_type($assignee).", email=".$GLOBALS['egw']->accounts->id2name($assignee,'account_email')); + if (!$assignee) continue; + + // item assignee is a user + if ($GLOBALS['egw']->accounts->get_type($assignee) == 'u') + { + if (($email = $GLOBALS['egw']->accounts->id2name($assignee,'account_email')) && !in_array($email, $email_sent)) + { + if ($this->send_notification($data,$old,$email,$assignee,'notify_assigned', + in_array($assignee,$assignees) !== in_array($assignee,$old_assignees) || $deleted)) // assignment changed + { + $email_sent[] = $email; + } + } + } + else // item assignee is a group + { + foreach($GLOBALS['egw']->accounts->members($assignee,true) as $u) + { + if (($email = $GLOBALS['egw']->accounts->id2name($u,'account_email')) && !in_array($email, $email_sent)) + { + if ($this->send_notification($data,$old,$email,$u,'notify_assigned', + in_array($u,$assignees) !== in_array($u,$old_assignees) || $deleted)) // assignment changed + { + $email_sent[] = $email; + } + } + } + } + } + } + + // notification copies + if (($copies = $this->get_config('copy',$data,$old))) + { + $lang = $this->get_config('lang',$data,$old); + foreach($copies as $email) + { + if (strchr($email,'@') !== false && !in_array($email, $email_sent)) + { + if ($this->send_notification($data,$old,$email,$lang,'notify_copy')) + { + $email_sent[] = $email; + } + } + } + } + $email_notified = $email_sent; + return !count($this->errors); + } + + /** + * Cache for notificaton body + * + * Cache is by id, language, date-format and type text/html + */ + protected $body_cache = array(); + + /** + * method to clear the Cache for notificaton body + * + * Cache is by id, language, date-format and type text/html + */ + public function ClearBodyCache() + { + $this->body_cache = array(); + } + + /** + * Sending a notification to the given email-address + * + * Called by track() or externally for sending async notifications + * + * Method changes $GLOBALS['egw_info']['user'], so everything called by it, eg. get_(subject|body|links|attachements), + * must NOT store something from user enviroment! By the end of the method, everything get changed back. + * + * @param array $data current entry + * @param array $old = null old/last state of the entry or null for a new entry + * @param string $email address to send the notification to + * @param string $user_or_lang = 'en' user-id or 2 char lang-code for a non-system user + * @param string $check = null pref. to check if a notification is wanted + * @param boolean $assignment_changed = true the assignment of the user $user_or_lang changed + * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undelted + * @return boolean true on success or false if notification not requested or error (error-message is in $this->errors) + */ + public function send_notification($data,$old,$email,$user_or_lang,$check=null,$assignment_changed=true,$deleted=null) + { + //error_log(__METHOD__."(,,'$email',$user_or_lang,$check,$assignment_changed,$deleted)"); + if (!$email) return false; + + $save_user = $GLOBALS['egw_info']['user']; + $do_notify = true; + + if (is_numeric($user_or_lang)) // user --> read everything from his prefs + { + $GLOBALS['egw_info']['user']['account_id'] = $user_or_lang; + $GLOBALS['egw']->preferences->__construct($user_or_lang); + $GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository(false); // no session prefs! + + if ($check && $this->check2pref) $check = $this->check2pref[$check]; + + if ($check && !$GLOBALS['egw_info']['user']['preferences'][$this->app][$check] || // no notification requested + // only notification about changed assignment requested + $check && $GLOBALS['egw_info']['user']['preferences'][$this->app][$check] === 'assignment' && !$assignment_changed || + $this->user == $user_or_lang && !$this->notify_current_user) // no popup for own actions + { + $do_notify = false; // no notification requested / necessary + } + } + else + { + // for the notification copy, we use default (and forced) prefs plus the language from the the tracker config + $GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->default_prefs(); + $GLOBALS['egw_info']['user']['preferences']['common']['lang'] = $user_or_lang; + } + if ($GLOBALS['egw_info']['user']['preferences']['common']['lang'] != Api\Translation::$userlang) // load the right language if needed + { + Api\Translation::init(); + } + + $receiver = is_numeric($user_or_lang) ? $user_or_lang : $email; + + if ($do_notify) + { + // Load date/time preferences into egw_time + Api\DateTime::init(); + + // Cache message body to not have to re-generate it every time + $lang = Api\Translation::$userlang; + $date_format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'] . + $GLOBALS['egw_info']['user']['preferences']['common']['timeformat']; + + // Cache text body + $body_cache =& $this->body_cache[$data[$this->id_field]][$lang][$date_format]; + if(empty($data[$this->id_field]) || !isset($body_cache['text'])) + { + $body_cache['text'] = $this->get_body(false,$data,$old,false,$receiver); + } + // Cache HTML body + if(empty($data[$this->id_field]) || !isset($body_cache['html'])) + { + $body_cache['html'] = $this->get_body(true,$data,$old,false,$receiver); + } + + // get rest of notification message + $sender = $this->get_sender($data,$old,true,$receiver); + $subject = $this->get_subject($data,$old,$deleted,$receiver); + $link = $this->get_notification_link($data,$old,$receiver); + $attachments = $this->get_attachments($data,$old,$receiver); + } + + // restore user enviroment BEFORE calling notification class or returning + $GLOBALS['egw_info']['user'] = $save_user; + // need to call preferences constructor and read_repository, to set user timezone again + $GLOBALS['egw']->preferences->__construct($GLOBALS['egw_info']['user']['account_id']); + $GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository(false); // no session prefs! + + // Re-load date/time preferences + Api\DateTime::init(); + + if ($GLOBALS['egw_info']['user']['preferences']['common']['lang'] != Api\Translation::$userlang) + { + Api\Translation::init(); + } + + if (!$do_notify) + { + return false; + } + + // send over notification_app + if ($GLOBALS['egw_info']['apps']['notifications']['enabled']) + { + // send via notification_app + try { + $notification = new notifications(); + $notification->set_receivers(array($receiver)); + $notification->set_message($body_cache['text']); + $notification->set_message($body_cache['html']); + $notification->set_sender($sender); + $notification->set_subject($subject); + $notification->set_links(array($link)); + if ($attachments && is_array($attachments)) + { + $notification->set_attachments($attachments); + } + $notification->send(); + } + catch (Exception $exception) + { + $this->errors[] = $exception->getMessage(); + return false; + } + } + else + { + error_log('tracking: cannot send any notifications because notifications is not installed'); + } + + return true; + } + + /** + * Return date+time formatted for the currently notified user (prefs in $GLOBALS['egw_info']['user']['preferences']) + * + * @param int|string|DateTime $timestamp in server-time + * @param boolean $do_time =true true=allways (default), false=never print the time, null=print time if != 00:00 + * + * @return string + */ + public function datetime($timestamp,$do_time=true) + { + if (!is_a($timestamp,'DateTime')) + { + $timestamp = new Api\DateTime($timestamp,Api\DateTime::$server_timezone); + } + $timestamp->setTimezone(Api\DateTime::$user_timezone); + if (is_null($do_time)) + { + $do_time = ($timestamp->format('Hi') != '0000'); + } + $format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat']; + if ($do_time) $format .= ' '.($GLOBALS['egw_info']['user']['preferences']['common']['timeformat'] != 12 ? 'H:i' : 'h:i a'); + + return $timestamp->format($format); + } + + /** + * Get sender address + * + * The default implementation prefers depending on the prefer_user_as_sender class-var the user over + * what is returned by get_config('sender'). + * + * @param int $user account_lid of user + * @param array $data + * @param array $old + * @param bool $prefer_id returns the userid rather than email + * @param int|string $receiver nummeric account_id or email address + * @return string or userid + */ + protected function get_sender($data,$old,$prefer_id=false,$receiver=null) + { + unset($receiver); // not used, but required by function signature + + $sender = $this->get_config('sender',$data,$old); + //echo "

".__METHOD__."() get_config('sender',...)='".htmlspecialchars($sender)."'

\n"; + + if (($this->prefer_user_as_sender || !$sender) && $this->user && + ($email = $GLOBALS['egw']->accounts->id2name($this->user,'account_email'))) + { + $name = $GLOBALS['egw']->accounts->id2name($this->user,'account_fullname'); + + if($prefer_id) { + $sender = $this->user; + } else { + $sender = $name ? $name.' <'.$email.'>' : $email; + } + } + elseif(!$sender) + { + $sender = 'eGroupWare '.lang($this->app).' '; + } + //echo "

".__METHOD__."()='".htmlspecialchars($sender)."'

\n"; + return $sender; + } + + /** + * Get the title for a given entry, can be reimplemented + * + * @param array $data + * @param array $old + * @return string + */ + protected function get_title($data,$old) + { + unset($old); // not used, but required by function signature + + return egw_link::title($this->app,$data[$this->id_field]); + } + + /** + * Get the subject for a given entry, can be reimplemented + * + * Default implementation uses the link-title + * + * @param array $data + * @param array $old + * @param boolean $deleted =null can be set to true to let the tracking know the item got deleted or undelted + * @param int|string $receiver nummeric account_id or email address + * @return string + */ + protected function get_subject($data,$old,$deleted=null,$receiver=null) + { + unset($old, $deleted, $receiver); // not used, but required by function signature + + return egw_link::title($this->app,$data[$this->id_field]); + } + + /** + * Get the modified / new message (1. line of mail body) for a given entry, can be reimplemented + * + * Default implementation does nothing + * + * @param array $data + * @param array $old + * @param int|string $receiver nummeric account_id or email address + * @return string + */ + protected function get_message($data,$old,$receiver=null) + { + unset($data, $old, $receiver); // not used, but required by function signature + + return ''; + } + + /** + * Get a link to view the entry, can be reimplemented + * + * Default implementation checks get_config('link') (appending the id) or link::view($this->app,$id) + * + * @param array $data + * @param array $old + * @param string $allow_popup = false if true return array(link,popup-size) incl. session info an evtl. partial url (no host-part) + * @param int|string $receiver nummeric account_id or email address + * @return string|array string with link (!$allow_popup) or array(link,popup-size), popup size is something like '640x480' + */ + protected function get_link($data,$old,$allow_popup=false,$receiver=null) + { + unset($receiver); // not used, but required by function signature + + if (($link = $this->get_config('link',$data,$old))) + { + if (!$this->get_config('link_no_id', $data) && strpos($link,$this->id_field.'=') === false && isset($data[$this->id_field])) + { + $link .= strpos($link,'?') === false ? '?' : '&'; + $link .= $this->id_field.'='.$data[$this->id_field]; + } + } + else + { + if (($view = egw_link::view($this->app,$data[$this->id_field]))) + { + $link = $GLOBALS['egw']->link('/index.php',$view); + $popup = egw_link::is_popup($this->app,'view'); + } + } + if ($link[0] == '/') + { + $link = ($_SERVER['HTTPS'] || $GLOBALS['egw_info']['server']['enforce_ssl'] ? 'https://' : 'http://'). + ($GLOBALS['egw_info']['server']['hostname'] ? $GLOBALS['egw_info']['server']['hostname'] : $_SERVER['HTTP_HOST']).$link; + } + if (!$allow_popup) + { + // remove the session-id in the notification mail! + $link = preg_replace('/(sessionid|kp3|domain)=[^&]+&?/','',$link); + + if ($popup) $link .= '&nopopup=1'; + } + //error_log(__METHOD__."(..., $allow_popup, $receiver) returning ".array2string($allow_popup ? array($link,$popup) : $link)); + return $allow_popup ? array($link,$popup) : $link; + } + + /** + * Get a link for notifications to view the entry, can be reimplemented + * + * @param array $data + * @param array $old + * @param int|string $receiver nummeric account_id or email address + * @return array with link + */ + protected function get_notification_link($data,$old,$receiver=null) + { + unset($receiver); // not used, but required by function signature + + if (($view = egw_link::view($this->app,$data[$this->id_field]))) + { + return array( + 'text' => $this->get_title($data,$old), + 'app' => $this->app, + 'id' => $data[$this->id_field], + 'view' => $view, + 'popup' => egw_link::is_popup($this->app,'view'), + ); + } + return false; + } + + /** + * Get the body of the notification message, can be reimplemented + * + * @param boolean $html_email + * @param array $data + * @param array $old + * @param boolean $integrate_link to have links embedded inside the body + * @param int|string $receiver nummeric account_id or email address + * @return string + */ + public function get_body($html_email,$data,$old,$integrate_link = true,$receiver=null) + { + $body = ''; + if($this->get_config(self::CUSTOM_NOTIFICATION, $data, $old)) + { + $body = $this->get_custom_message($data,$old); + if(($sig = $this->get_signature($data,$old,$receiver))) + { + $body .= ($html_email ? '
':'') . "\n$sig"; + } + return $body; + } + if ($html_email) + { + $body = ''."\n"; + } + // new or modified message + if (($message = $this->get_message($data,$old,$receiver))) + { + foreach ((array)$message as $_message) + { + $body .= $this->format_line($html_email,'message',false,($_message=='---'?($html_email?'
':$_message):$_message)); + } + } + if ($integrate_link && ($link = $this->get_link($data,$old,false,$receiver))) + { + $body .= $this->format_line($html_email,'link',false,$integrate_link === true ? lang('You can respond by visiting:') : $integrate_link,$link); + } + foreach($this->get_details($data,$receiver) as $name => $detail) + { + // if there's no old entry, the entry is not modified by definition + // if both values are '', 0 or null, we count them as equal too + $modified = $old && $data[$name] != $old[$name] && !(!$data[$name] && !$old[$name]); + //if ($modified) error_log("data[$name]=".print_r($data[$name],true).", old[$name]=".print_r($old[$name],true)." --> modified=".(int)$modified); + if (empty($detail['value']) && !$modified) continue; // skip unchanged, empty values + + $body .= $this->format_line($html_email,$detail['type'],$modified, + $detail['label'] ? $detail['label'] : '', $detail['value']); + } + if ($html_email) + { + $body .= "
\n"; + } + if(($sig = $this->get_signature($data,$old,$receiver))) + { + $body .= ($html_email ? '
':'') . "\n$sig"; + } + return $body; + } + + /** + * Format one line to the mail body + * + * @internal + * @param boolean $html_mail + * @param string $type 'link', 'message', 'summary', 'multiline', 'reply' and ''=regular content + * @param boolean $modified mark field as modified + * @param string $line whole line or just label + * @param string $data = null data or null to display just $line over 2 columns + * @return string + */ + protected function format_line($html_mail,$type,$modified,$line,$data=null) + { + //error_log(__METHOD__.'('.array2string($html_mail).",'$type',".array2string($modified).",'$line',".array2string($data).')'); + $content = ''; + + if ($html_mail) + { + if (!$this->html_content_allow) $line = html::htmlspecialchars($line); // XSS + + $color = $modified ? 'red' : false; + $size = '110%'; + $bold = false; + $background = '#FFFFF1'; + switch($type) + { + case 'message': + $background = '#D3DCE3;'; + $bold = true; + break; + case 'link': + $background = '#F1F1F1'; + break; + case 'summary': + $background = '#F1F1F1'; + $bold = true; + break; + case 'multiline': + // Only Convert nl2br on non-html content + if (strpos($data, 'html_content_allow ? $data : html::htmlspecialchars($data)); + $this->html_content_allow = true; // to NOT do htmlspecialchars again + } + break; + case 'reply': + $background = '#F1F1F1'; + break; + default: + $size = false; + } + $style = ($bold ? 'font-weight:bold;' : '').($size ? 'font-size:'.$size.';' : '').($color?'color:'.$color:''); + + $content = ''; + } + else // text-mail + { + if ($type == 'reply') $content = str_repeat('-',64)."\n"; + + if ($modified) $content .= '> '; + } + $content .= $line; + + if ($html_mail) + { + if ($line && $data) $content .= ''; + if ($type == 'link') + { + // the link is often too long for html boxes chunk-split allows to break lines if needed + $content .= html::a_href(chunk_split(rawurldecode($data),40,'​'),$data,'','target="_blank"'); + } + elseif ($this->html_content_allow) + { + $content .= html::activate_links($data); + } + else + { + $content .= html::htmlspecialchars($data); + } + } + else + { + $content .= ($content&&$data?': ':'').$data; + } + if ($html_mail) $content .= ''; + + $content .= "\n"; + + return $content; + } + + /** + * Get the attachments for a notification + * + * @param array $data + * @param array $old + * @param int|string $receiver nummeric account_id or email address + * @return array or array with values for either 'string' or 'path' and optionally (mime-)'type', 'filename' and 'encoding' + */ + protected function get_attachments($data,$old,$receiver=null) + { + unset($data, $old, $receiver); // not used, but required by function signature + + return array(); + } + + /** + * Get a (global) signature to append to the change notificaiton + * @param array $data + * @param type $old + * @param type $receiver + */ + protected function get_signature($data, $old, $receiver) + { + unset($old, $receiver); // not used, but required by function signature + + $config = Api\Config::read('notifications'); + if(!isset($data[$this->id_field])) + { + error_log($this->app . ' did not properly implement bo_tracking->id_field. Merge skipped.'); + } + elseif(class_exists($this->app. '_merge')) + { + $merge_class = $this->app.'_merge'; + $merge = new $merge_class(); + $error = null; + $sig = $merge->merge_string($config['signature'], array($data[$this->id_field]), $error, 'text/html'); + if($error) + { + error_log($error); + return $config['signature']; + } + return $sig; + } + return $config['signature']; + } + + /** + * Get a custom notification message to be used instead of the standard one. + * It can use merge print placeholders to include data. + */ + protected function get_custom_message($data, $old, $merge_class = null) + { + $message = $this->get_config(self::CUSTOM_NOTIFICATION, $data, $old); + if(!$message) + { + return ''; + } + + // Automatically set merge class from naming conventions + if($merge_class == null) + { + $merge_class = $this->app.'_merge'; + } + if(!isset($data[$this->id_field])) + { + error_log($this->app . ' did not properly implement bo_tracking->id_field. Merge skipped.'); + return $message; + } + elseif(class_exists($merge_class)) + { + $merge = new $merge_class(); + $error = null; + $merged_message = $merge->merge_string($message, array($data[$this->id_field]), $error, 'text/html'); + if($error) + { + error_log($error); + return $message; + } + return $merged_message; + } + else + { + throw new Api\Exception\WrongParameter("Invalid merge class '$merge_class' for {$this->app} custom notification"); + } + } +} diff --git a/etemplate/inc/class.bo_tracking.inc.php b/etemplate/inc/class.bo_tracking.inc.php index 084a98f73b..9ca8755dff 100644 --- a/etemplate/inc/class.bo_tracking.inc.php +++ b/etemplate/inc/class.bo_tracking.inc.php @@ -6,1186 +6,18 @@ * @author Ralf Becker * @package etemplate * @subpackage api - * @copyright (c) 2007-13 by Ralf Becker + * @copyright (c) 2007-16 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ +use EGroupware\Api; + /** * Abstract base class for trackering: * - logging all modifications of an entry * - notifying users about changes in an entry * - * You need to extend these class in your application: - * 1. set the required class-vars: app, id_field - * 2. optional set class-vars: creator_field, assigned_field, check2prefs - * 3. implement the required methods: get_config, get_details - * 4. optionally re-implement: get_title, get_subject, get_body, get_attachments, get_link, get_notification_link, get_message - * They are all documented in this file via phpDocumentor comments. - * - * Translate field-name to history status field: - * As history status was only char(2) prior to EGroupware 1.6, a mapping was necessary. - * Now it's varchar(64) and a mapping makes no sense for new applications, just list - * all fields to log as key AND value! - * - * History login supports now 1:N relations on a base record. To use that you need: - * - to have the 1:N relation as array of arrays with the values of that releation, eg: - * $data = array( - * 'id' => 123, - * 'title' => 'Something', - * 'date' => '2009-08-21 14:42:00', - * 'participants' => array( - * array('account_id' => 15, 'name' => 'User Hugo', 'status' => 'A', 'quantity' => 1), - * array('account_id' => 17, 'name' => 'User Bert', 'status' => 'U', 'quantity' => 3), - * ), - * ); - * - set field2history as follows - * $field2history = array( - * 'id' => 'id', - * 'title' => 'title', - * 'participants' => array('uid','status','quantity'), - * ); - * - set content for history log widget: - * $content['history'] = array( - * 'id' => 123, - * 'app' => 'calendar', - * 'status-widgets' => array( - * 'title' => 'label', // no need to set, as default is label - * 'date' => 'datetime', - * 'participants' = array( - * 'select-account', - * array('U' => 'Unknown', 'A' => 'Accepted', 'R' => 'Rejected'), - * 'integer', - * ), - * ), - * ); - * - set lables for history: - * $sel_options['status'] = array( - * 'title' => 'Title', - * 'date' => 'Starttime', - * 'participants' => 'Participants: User, Status, Quantity', // a single label! - * ); - * - * The above is also an example for using regular history login in EGroupware (by skipping the 'participants' key). + * @deprecated use Api\Storage\Tracking */ -abstract class bo_tracking -{ - /** - * Application we are tracking - * - * @var string - */ - var $app; - /** - * Name of the id-field, used as id in the history log (required!) - * - * @var string - */ - var $id_field; - /** - * Name of the field with the creator id, if the creator of an entry should be notified - * - * @var string - */ - var $creator_field; - /** - * Name of the field with the id(s) of assinged users, if they should be notified - * - * @var string - */ - var $assigned_field; - /** - * Can be used to map the following prefs to different names: - * - notify_creator - user wants to be notified for items he created - * - notify_assigned - user wants to be notified for items assigned to him - * @var array - */ - var $check2pref; - /** - * Translate field-name to history status field (see comment in class header) - * - * @var array - */ - var $field2history = array(); - /** - * Should the user (passed to the track method or current user if not passed) be used as sender or get_config('sender') - * - * @var boolean - */ - var $prefer_user_as_sender = true; - /** - * Should the current user be email-notified (about change he made himself) - * - * Popup notifications are never send to the current user! - * - * @var boolean - */ - var $notify_current_user = false; - - /** - * Array with error-messages if track($data,$old) returns false - * - * @var array - */ - var $errors = array(); - - /** - * instance of the historylog object for the app we are tracking - * - * @access private - * @var historylog - */ - var $historylog; - - /** - * Current user, can be set via bo_tracking::track(,,$user) - * - * @access private - * @var int; - */ - var $user; - - /** - * Datetime format of the currently notified user (send_notificaton) - * - * @var string - */ - var $datetime_format; - /** - * Should the class allow html content (for notifications) - * - * @var boolean - */ - var $html_content_allow = false; - - /** - * Custom fields of type link entry or application - * - * Used to automatic create or update a link - * - * @var array field => application name pairs (or empty for link entry) - */ - var $cf_link_fields = array(); - - /** - * Separator for 1:N relations - * - */ - const ONE2N_SEPERATOR = '~|~'; - - /** - * Config name for custom notification message - */ - const CUSTOM_NOTIFICATION = 'custom_notification'; - - /** - * Constructor - * - * @param string $cf_app = null if set, custom field names get added to $field2history - * @return bo_tracking - */ - function __construct($cf_app = null) - { - if ($cf_app) - { - $linkable_cf_types = array('link-entry')+array_keys(egw_link::app_list()); - foreach(egw_customfields::get($cf_app, true) as $cf_name => $cf_data) - { - $this->field2history['#'.$cf_name] = '#'.$cf_name; - - if (in_array($cf_data['type'],$linkable_cf_types)) - { - $this->cf_link_fields['#'.$cf_name] = $cf_data['type'] == 'link-entry' ? '' : $cf_data['type']; - } - } - } - } - - function bo_tracking() - { - self::__construct(); - } - - /** - * Get the details of an entry - * - * You can/should call $this->get_customfields() to add custom fields. - * - * @param array|object $data - * @param int|string $receiver nummeric account_id or email address - * @return array of details as array with values for keys 'label','value','type' - */ - function get_details($data,$receiver=null) - { - return array(); - } - - /** - * Get custom fields of an entry of an entry - * - * @param array|object $data - * @param string $only_type2 = null if given only return fields of type2 == $only_type2 - * @return array of details as array with values for keys 'label','value','type' - */ - function get_customfields($data, $only_type2=null) - { - $details = array(); - - if (($cfs = egw_customfields::get($this->app, $all_private_too=false, $only_type2))) - { - $header_done = false; - foreach($cfs as $name => $field) - { - if (in_array($field['type'], egw_customfields::$non_printable_fields)) continue; - - if (!$header_done) - { - $details['custom'] = array( - 'value' => lang('Custom fields').':', - 'type' => 'reply', - ); - $header_done = true; - } - //error_log(__METHOD__."() $name: data['#$name']=".array2string($data['#'.$name]).", field[values]=".array2string($field['values'])); - $details['#'.$name] = array( - 'label' => $field['label'], - 'value' => egw_customfields::format($field, $data['#'.$name]), - ); - //error_log("--> details['#$name']=".array2string($details['#'.$name])); - } - } - return $details; - } - - /** - * Get a config value, which can depend on $data and $old - * - * Need to be implemented in your extended tracking class! - * - * @param string $name possible values are: - * - 'assigned' array of users to use instead of a field in the data - * - 'copy' array of email addresses notifications should be copied too, can depend on $data - * - 'lang' string lang code for copy mail - * - 'subject' string subject line for the notification of $data,$old, defaults to link-title - * - 'link' string of link to view $data - * - 'sender' sender of email - * - 'skip_notify' array of email addresses that should _not_ be notified - * - CUSTOM_NOTIFICATION string notification body message. Merge print placeholders are allowed. - * @param array $data current entry - * @param array $old = null old/last state of the entry or null for a new entry - * @return mixed - */ - protected function get_config($name,$data,$old=null) - { - return null; - } - - /** - * Tracks the changes in one entry $data, by comparing it with the last version in $old - * - * @param array $data current entry - * @param array $old = null old/last state of the entry or null for a new entry - * @param int $user = null user who made the changes, default to current user - * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undeleted - * @param array $changed_fields = null changed fields from ealier call to $this->changed_fields($data,$old), to not compute it again - * @param boolean $skip_notification = false do NOT send any notification - * @return int|boolean false on error, integer number of changes logged or true for new entries ($old == null) - */ - public function track(array $data,array $old=null,$user=null,$deleted=null,array $changed_fields=null,$skip_notification=false) - { - $this->user = !is_null($user) ? $user : $GLOBALS['egw_info']['user']['account_id']; - - $changes = true; - //error_log(__METHOD__.__LINE__); - if ($old && $this->field2history) - { - //error_log(__METHOD__.__LINE__.' Changedfields:'.print_r($changed_fields,true)); - $changes = $this->save_history($data,$old,$deleted,$changed_fields); - //error_log(__METHOD__.__LINE__.' Changedfields:'.print_r($changed_fields,true)); - //error_log(__METHOD__.__LINE__.' Changes:'.print_r($changes,true)); - } - - //error_log(__METHOD__.__LINE__.' LinkFields:'.array2string($this->cf_link_fields)); - if ($changes && $this->cf_link_fields) - { - $this->update_links($data,(array)$old); - } - // do not run do_notifications if we have no changes - if ($changes && !$skip_notification && !$this->do_notifications($data,$old,$deleted)) - { - $changes = false; - } - return $changes; - } - - /** - * Store a link for each custom field linking to an other application and update them - * - * @param array $data - * @param array $old - */ - protected function update_links(array $data, array $old) - { - //error_log(__METHOD__.__LINE__.array2string($data).function_backtrace()); - //error_log(__METHOD__.__LINE__.array2string($this->cf_link_fields)); - foreach((array)$this->cf_link_fields as $name => $val) - { - //error_log(__METHOD__.__LINE__.' Field:'.$name. ' Value (new):'.array2string($data[$name])); - //error_log(__METHOD__.__LINE__.' Field:'.$name. ' Value (old):'.array2string($old[$name])); - if (is_array($data[$name]) && array_key_exists('id',$data[$name])) $data[$name] = $data[$name]['id']; - if (is_array($old[$name]) && array_key_exists('id',$old[$name])) $old[$name] = $old[$name]['id']; - //error_log(__METHOD__.__LINE__.'(After processing) Field:'.$name. ' Value (new):'.array2string($data[$name])); - //error_log(__METHOD__.__LINE__.'(After processing) Field:'.$name. ' Value (old):'.array2string($old[$name])); - } - $current_ids = array_unique(array_diff(array_intersect_key($data,$this->cf_link_fields),array('',0,NULL))); - $old_ids = $old ? array_unique(array_diff(array_intersect_key($old,$this->cf_link_fields),array('',0,NULL))) : array(); - //error_log(__METHOD__.__LINE__.array2string($current_ids)); - //error_log(__METHOD__.__LINE__.array2string($old_ids)); - // create links for added application entry - foreach(array_diff($current_ids,$old_ids) as $name => $id) - { - if (!($app = $this->cf_link_fields[$name])) - { - list($app,$id) = explode(':',$id); - if (!$id) continue; // can be eg. 'addressbook:', if no contact selected - } - $source_id = $data[$this->id_field]; - //error_log(__METHOD__.__LINE__.array2string($source_id)); - if ($source_id) egw_link::link($this->app,$source_id,$app,$id); - //error_log(__METHOD__.__LINE__."egw_link::link('$this->app',".array2string($source_id).",'$app',$id);"); - //echo "

egw_link::link('$this->app',{$data[$this->id_field]},'$app',$id);

\n"; - } - - // unlink removed application entries - foreach(array_diff($old_ids,$current_ids) as $name => $id) - { - if (!isset($data[$name])) continue; // ignore not set link cf's, eg. from sync clients - if (!($app = $this->cf_link_fields[$name])) - { - list($app,$id) = explode(':',$id); - if (!$id) continue; - } - $source_id = $data[$this->id_field]; - if ($source_id) egw_link::unlink(null,$this->app,$source_id,0,$app,$id); - //echo "

egw_link::unlink(NULL,'$this->app',{$data[$this->id_field]},0,'$app',$id);

\n"; - } - } - - /** - * Save changes to the history log - * - * @internal use only track($data,$old) - * @param array $data current entry - * @param array $old = null old/last state of the entry or null for a new entry - * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undelted - * @param array $changed_fields = null changed fields from ealier call to $this->changed_fields($data,$old), to not compute it again - * @return int number of log-entries made - */ - protected function save_history(array $data,array $old=null,$deleted=null,array $changed_fields=null) - { - //error_log(__METHOD__.__LINE__.' Changedfields:'.array2string($changed_fields)); - if (is_null($changed_fields)) - { - $changed_fields = self::changed_fields($data,$old); - //error_log(__METHOD__.__LINE__.' Changedfields:'.array2string($changed_fields)); - } - if (!$changed_fields && ($old || !$GLOBALS['egw_info']['server']['log_user_agent_action'])) return 0; - - if (!is_object($this->historylog) || $this->historylog->user != $this->user) - { - $this->historylog = new historylog($this->app,$this->user); - } - // log user-agent and session-action - if ($GLOBALS['egw_info']['server']['log_user_agent_action'] && ($changed_fields || !$old)) - { - $this->historylog->add('user_agent_action', $data[$this->id_field], - $_SERVER['HTTP_USER_AGENT'], $_SESSION[egw_session::EGW_SESSION_VAR]['session_action']); - } - foreach($changed_fields as $name) - { - $status = isset($this->field2history[$name]) ? $this->field2history[$name] : $name; - //error_log(__METHOD__.__LINE__." Name $name,".' Status:'.array2string($status)); - if (is_array($status)) // 1:N relation --> remove common rows - { - //error_log(__METHOD__.__LINE__.' is Array'); - self::compact_1_N_relation($data[$name],$status); - self::compact_1_N_relation($old[$name],$status); - $added = array_values(array_diff($data[$name],$old[$name])); - $removed = array_values(array_diff($old[$name],$data[$name])); - $n = max(array(count($added),count($removed))); - for($i = 0; $i < $n; ++$i) - { - //error_log(__METHOD__."() $i: historylog->add('$name',data['$this->id_field']={$data[$this->id_field]},".array2string($added[$i]).','.array2string($removed[$i])); - $this->historylog->add($name,$data[$this->id_field],$added[$i],$removed[$i]); - } - } - else - { - //error_log(__METHOD__.__LINE__.' IDField:'.array2string($this->id_field).' ->'.$data[$this->id_field].' New:'.$data[$name].' Old:'.$old[$name]); - $this->historylog->add($status,$data[$this->id_field], - is_array($data[$name]) ? implode(',',$data[$name]) : $data[$name], - is_array($old[$name]) ? implode(',',$old[$name]) : $old[$name]); - } - } - //error_log(__METHOD__.__LINE__.' return:'.count($changed_fields)); - return count($changed_fields); - } - - /** - * Compute changes between new and old data - * - * Can be used to check if saving the data is really necessary or user just pressed save - * - * @param array $data - * @param array $old = null - * @return array of keys with different values in $data and $old - */ - public function changed_fields(array $data,array $old=null) - { - if (is_null($old)) return array_keys($data); - $changed_fields = array(); - foreach($this->field2history as $name => $status) - { - if (!$old[$name] && !$data[$name]) continue; // treat all sorts of empty equally - - if ($name[0] == '#' && !isset($data[$name])) continue; // no set customfields are not stored, therefore not changed - - if (is_array($status)) // 1:N relation - { - self::compact_1_N_relation($data[$name],$status); - self::compact_1_N_relation($old[$name],$status); - } - if ($old[$name] != $data[$name]) - { - // normalize arrays, we do NOT care for the order of multiselections - if (is_array($data[$name]) || is_array($old[$name])) - { - if (!is_array($data[$name])) $data[$name] = explode(',',$data[$name]); - if (!is_array($old[$name])) $old[$name] = explode(',',$old[$name]); - if (count($data[$name]) == count($old[$name])) - { - sort($data[$name]); - sort($old[$name]); - if ($data[$name] == $old[$name]) continue; - } - } - elseif (str_replace("\r", '', $old[$name]) == str_replace("\r", '', $data[$name])) - { - continue; // change only in CR (eg. different OS) --> ignore - } - $changed_fields[] = $name; - //echo "

$name: ".array2string($data[$name]).' != '.array2string($old[$name])."

\n"; - } - } - foreach($data as $name => $value) - { - if ($name[0] == '#' && $name[1] == '#' && $value !== $old[$name]) - { - $changed_fields[] = $name; - } - } - //error_log(__METHOD__."() changed_fields=".array2string($changed_fields)); - return $changed_fields; - } - - /** - * Compact (spezified) fields of a 1:N relation into an array of strings - * - * @param array &$rows rows of the 1:N relation - * @param array $cols field names as values - */ - private static function compact_1_N_relation(&$rows,array $cols) - { - if (is_array($rows)) - { - foreach($rows as $key => &$row) - { - $values = array(); - foreach($cols as $col) - { - $values[] = $row[$col]; - } - $row = implode(self::ONE2N_SEPERATOR,$values); - } - } - else - { - $rows = array(); - } - } - - /** - * sending all notifications for the changed entry - * - * @internal use only track($data,$old,$user) - * @param array $data current entry - * @param array $old = null old/last state of the entry or null for a new entry - * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undelted - * @param array $email_notified=null if present will return the emails notified, if given emails in that list will not be notified - * @return boolean true on success, false on error (error messages are in $this->errors) - */ - public function do_notifications($data,$old,$deleted=null,&$email_notified=null) - { - $this->errors = $email_sent = array(); - if (!empty($email_notified) && is_array($email_notified)) $email_sent = $email_notified; - - if (!$this->notify_current_user && $this->user) // do we have a current user and should we notify the current user about his own changes - { - //error_log("do_notificaton() adding user=$this->user to email_sent, to not notify him"); - $email_sent[] = $GLOBALS['egw']->accounts->id2name($this->user,'account_email'); - } - $skip_notify = $this->get_config('skip_notify',$data,$old); - if($skip_notify && is_array($skip_notify)) - { - $email_sent = array_merge($email_sent, $skip_notify); - } - - // entry creator - if ($this->creator_field && ($email = $GLOBALS['egw']->accounts->id2name($data[$this->creator_field],'account_email')) && - !in_array($email, $email_sent)) - { - if ($this->send_notification($data,$old,$email,$data[$this->creator_field],'notify_creator')) - { - $email_sent[] = $email; - } - } - - // members of group when entry owned by group - if ($this->creator_field && $GLOBALS['egw']->accounts->get_type($data[$this->creator_field]) == 'g') - { - foreach($GLOBALS['egw']->accounts->members($data[$this->creator_field],true) as $u) - { - if (($email = $GLOBALS['egw']->accounts->id2name($u,'account_email')) && - !in_array($email, $email_sent)) - { - if ($this->send_notification($data,$old,$email,$u,'notify_owner_group_member')) - { - $email_sent[] = $email; - } - } - } - } - - // assigned / responsible users - if ($this->assigned_field || $assigned = $this->get_config('assigned', $data)) - { - //error_log("bo_tracking::do_notifications() data[$this->assigned_field]=".print_r($data[$this->assigned_field],true).", old[$this->assigned_field]=".print_r($old[$this->assigned_field],true)); - $assignees = $old_assignees = array(); - $assignees = $assigned ? $assigned : $assignees; - if ($data[$this->assigned_field]) // current assignments - { - $assignees = is_array($data[$this->assigned_field]) ? - $data[$this->assigned_field] : explode(',',$data[$this->assigned_field]); - } - if ($old && $old[$this->assigned_field]) - { - $old_assignees = is_array($old[$this->assigned_field]) ? - $old[$this->assigned_field] : explode(',',$old[$this->assigned_field]); - } - foreach(array_unique(array_merge($assignees,$old_assignees)) as $assignee) - { - //error_log("bo_tracking::do_notifications() assignee=$assignee, type=".$GLOBALS['egw']->accounts->get_type($assignee).", email=".$GLOBALS['egw']->accounts->id2name($assignee,'account_email')); - if (!$assignee) continue; - - // item assignee is a user - if ($GLOBALS['egw']->accounts->get_type($assignee) == 'u') - { - if (($email = $GLOBALS['egw']->accounts->id2name($assignee,'account_email')) && !in_array($email, $email_sent)) - { - if ($this->send_notification($data,$old,$email,$assignee,'notify_assigned', - in_array($assignee,$assignees) !== in_array($assignee,$old_assignees) || $deleted)) // assignment changed - { - $email_sent[] = $email; - } - } - } - else // item assignee is a group - { - foreach($GLOBALS['egw']->accounts->members($assignee,true) as $u) - { - if (($email = $GLOBALS['egw']->accounts->id2name($u,'account_email')) && !in_array($email, $email_sent)) - { - if ($this->send_notification($data,$old,$email,$u,'notify_assigned', - in_array($u,$assignees) !== in_array($u,$old_assignees) || $deleted)) // assignment changed - { - $email_sent[] = $email; - } - } - } - } - } - } - - // notification copies - if (($copies = $this->get_config('copy',$data,$old))) - { - $lang = $this->get_config('lang',$data,$old); - foreach($copies as $email) - { - if (strchr($email,'@') !== false && !in_array($email, $email_sent)) - { - if ($this->send_notification($data,$old,$email,$lang,'notify_copy')) - { - $email_sent[] = $email; - } - } - } - } - $email_notified = $email_sent; - return !count($this->errors); - } - - /** - * Cache for notificaton body - * - * Cache is by id, language, date-format and type text/html - */ - protected $body_cache = array(); - - /** - * method to clear the Cache for notificaton body - * - * Cache is by id, language, date-format and type text/html - */ - public function ClearBodyCache() - { - $this->body_cache = array(); - } - - /** - * Sending a notification to the given email-address - * - * Called by track() or externally for sending async notifications - * - * Method changes $GLOBALS['egw_info']['user'], so everything called by it, eg. get_(subject|body|links|attachements), - * must NOT store something from user enviroment! By the end of the method, everything get changed back. - * - * @param array $data current entry - * @param array $old = null old/last state of the entry or null for a new entry - * @param string $email address to send the notification to - * @param string $user_or_lang = 'en' user-id or 2 char lang-code for a non-system user - * @param string $check = null pref. to check if a notification is wanted - * @param boolean $assignment_changed = true the assignment of the user $user_or_lang changed - * @param boolean $deleted = null can be set to true to let the tracking know the item got deleted or undelted - * @return boolean true on success or false if notification not requested or error (error-message is in $this->errors) - */ - public function send_notification($data,$old,$email,$user_or_lang,$check=null,$assignment_changed=true,$deleted=null) - { - //error_log(__METHOD__."(,,'$email',$user_or_lang,$check,$assignment_changed,$deleted)"); - if (!$email) return false; - - $save_user = $GLOBALS['egw_info']['user']; - $do_notify = true; - - if (is_numeric($user_or_lang)) // user --> read everything from his prefs - { - $GLOBALS['egw_info']['user']['account_id'] = $user_or_lang; - $GLOBALS['egw']->preferences->__construct($user_or_lang); - $GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository(false); // no session prefs! - - if ($check && $this->check2pref) $check = $this->check2pref[$check]; - - if ($check && !$GLOBALS['egw_info']['user']['preferences'][$this->app][$check] || // no notification requested - // only notification about changed assignment requested - $check && $GLOBALS['egw_info']['user']['preferences'][$this->app][$check] === 'assignment' && !$assignment_changed || - $this->user == $user_or_lang && !$this->notify_current_user) // no popup for own actions - { - $do_notify = false; // no notification requested / necessary - } - } - else - { - // for the notification copy, we use default (and forced) prefs plus the language from the the tracker config - $GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->default_prefs(); - $GLOBALS['egw_info']['user']['preferences']['common']['lang'] = $user_or_lang; - } - if ($GLOBALS['egw_info']['user']['preferences']['common']['lang'] != translation::$userlang) // load the right language if needed - { - translation::init(); - } - - if ($do_notify) - { - // Load date/time preferences into egw_time - egw_time::init(); - - // Cache message body to not have to re-generate it every time - $lang = translation::$userlang; - $date_format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat'] . - $GLOBALS['egw_info']['user']['preferences']['common']['timeformat']; - - // Cache text body - $body_cache =& $this->body_cache[$data[$this->id_field]][$lang][$date_format]; - if(empty($data[$this->id_field]) || !isset($body_cache['text'])) - { - $body_cache['text'] = $this->get_body(false,$data,$old,false,$receiver); - } - // Cache HTML body - if(empty($data[$this->id_field]) || !isset($body_cache['html'])) - { - $body_cache['html'] = $this->get_body(true,$data,$old,false,$receiver); - } - - // get rest of notification message - $sender = $this->get_sender($data,$old,true,$receiver); - $subject = $this->get_subject($data,$old,$deleted,$receiver); - $link = $this->get_notification_link($data,$old,$receiver); - $attachments = $this->get_attachments($data,$old,$receiver); - } - - // restore user enviroment BEFORE calling notification class or returning - $GLOBALS['egw_info']['user'] = $save_user; - // need to call preferences constructor and read_repository, to set user timezone again - $GLOBALS['egw']->preferences->__construct($GLOBALS['egw_info']['user']['account_id']); - $GLOBALS['egw_info']['user']['preferences'] = $GLOBALS['egw']->preferences->read_repository(false); // no session prefs! - - // Re-load date/time preferences - egw_time::init(); - - if ($GLOBALS['egw_info']['user']['preferences']['common']['lang'] != translation::$userlang) - { - translation::init(); - } - - if (!$do_notify) - { - return false; - } - - // send over notification_app - if ($GLOBALS['egw_info']['apps']['notifications']['enabled']) - { - // send via notification_app - $receiver = is_numeric($user_or_lang) ? $user_or_lang : $email; - try { - $notification = new notifications(); - $notification->set_receivers(array($receiver)); - $notification->set_message($body_cache['text']); - $notification->set_message($body_cache['html']); - $notification->set_sender($sender); - $notification->set_subject($subject); - $notification->set_links(array($link)); - if ($attachments && is_array($attachments)) - { - $notification->set_attachments($attachments); - } - $notification->send(); - } - catch (Exception $exception) - { - $this->errors[] = $exception->getMessage(); - return false; - } - } - else - { - error_log('tracking: cannot send any notifications because notifications is not installed'); - } - - return true; - } - - /** - * Return date+time formatted for the currently notified user (prefs in $GLOBALS['egw_info']['user']['preferences']) - * - * @param int|string|DateTime $timestamp in server-time - * @param boolean $do_time=true true=allways (default), false=never print the time, null=print time if != 00:00 - * - * @return string - */ - public function datetime($timestamp,$do_time=true) - { - if (!is_a($timestamp,'DateTime')) - { - $timestamp = new egw_time($timestamp,egw_time::$server_timezone); - } - $timestamp->setTimezone(egw_time::$user_timezone); - if (is_null($do_time)) - { - $do_time = ($timestamp->format('Hi') != '0000'); - } - $format = $GLOBALS['egw_info']['user']['preferences']['common']['dateformat']; - if ($do_time) $format .= ' '.($GLOBALS['egw_info']['user']['preferences']['common']['timeformat'] != 12 ? 'H:i' : 'h:i a'); - - return $timestamp->format($format); - } - - /** - * Get sender address - * - * The default implementation prefers depending on the prefer_user_as_sender class-var the user over - * what is returned by get_config('sender'). - * - * @param int $user account_lid of user - * @param array $data - * @param array $old - * @param bool $prefer_id returns the userid rather than email - * @param int|string $receiver nummeric account_id or email address - * @return string or userid - */ - protected function get_sender($data,$old,$prefer_id=false,$receiver=null) - { - $sender = $this->get_config('sender',$data,$old); - //echo "

bo_tracking::get_sender() get_config('sender',...)='".htmlspecialchars($sender)."'

\n"; - - if (($this->prefer_user_as_sender || !$sender) && $this->user && - ($email = $GLOBALS['egw']->accounts->id2name($this->user,'account_email'))) - { - $name = $GLOBALS['egw']->accounts->id2name($this->user,'account_fullname'); - - if($prefer_id) { - $sender = $this->user; - } else { - $sender = $name ? $name.' <'.$email.'>' : $email; - } - } - elseif(!$sender) - { - $sender = 'eGroupWare '.lang($this->app).' '; - } - //echo "

bo_tracking::get_sender()='".htmlspecialchars($sender)."'

\n"; - return $sender; - } - - /** - * Get the title for a given entry, can be reimplemented - * - * @param array $data - * @param array $old - * @return string - */ - protected function get_title($data,$old) - { - return egw_link::title($this->app,$data[$this->id_field]); - } - - /** - * Get the subject for a given entry, can be reimplemented - * - * Default implementation uses the link-title - * - * @param array $data - * @param array $old - * @param boolean $deleted=null can be set to true to let the tracking know the item got deleted or undelted - * @param int|string $receiver nummeric account_id or email address - * @return string - */ - protected function get_subject($data,$old,$deleted=null,$receiver=null) - { - return egw_link::title($this->app,$data[$this->id_field]); - } - - /** - * Get the modified / new message (1. line of mail body) for a given entry, can be reimplemented - * - * Default implementation does nothing - * - * @param array $data - * @param array $old - * @param int|string $receiver nummeric account_id or email address - * @return string - */ - protected function get_message($data,$old,$receiver=null) - { - return ''; - } - - /** - * Get a link to view the entry, can be reimplemented - * - * Default implementation checks get_config('link') (appending the id) or link::view($this->app,$id) - * - * @param array $data - * @param array $old - * @param string $allow_popup = false if true return array(link,popup-size) incl. session info an evtl. partial url (no host-part) - * @param int|string $receiver nummeric account_id or email address - * @return string|array string with link (!$allow_popup) or array(link,popup-size), popup size is something like '640x480' - */ - protected function get_link($data,$old,$allow_popup=false,$receiver=null) - { - if (($link = $this->get_config('link',$data,$old))) - { - if (!$this->get_config('link_no_id', $data) && strpos($link,$this->id_field.'=') === false && isset($data[$this->id_field])) - { - $link .= strpos($link,'?') === false ? '?' : '&'; - $link .= $this->id_field.'='.$data[$this->id_field]; - } - } - else - { - if (($view = egw_link::view($this->app,$data[$this->id_field]))) - { - $link = $GLOBALS['egw']->link('/index.php',$view); - $popup = egw_link::is_popup($this->app,'view'); - } - } - if ($link[0] == '/') - { - $link = ($_SERVER['HTTPS'] || $GLOBALS['egw_info']['server']['enforce_ssl'] ? 'https://' : 'http://'). - ($GLOBALS['egw_info']['server']['hostname'] ? $GLOBALS['egw_info']['server']['hostname'] : $_SERVER['HTTP_HOST']).$link; - } - if (!$allow_popup) - { - // remove the session-id in the notification mail! - $link = preg_replace('/(sessionid|kp3|domain)=[^&]+&?/','',$link); - - if ($popup) $link .= '&nopopup=1'; - } - //error_log(__METHOD__."(..., $allow_popup, $receiver) returning ".array2string($allow_popup ? array($link,$popup) : $link)); - return $allow_popup ? array($link,$popup) : $link; - } - - /** - * Get a link for notifications to view the entry, can be reimplemented - * - * @param array $data - * @param array $old - * @param int|string $receiver nummeric account_id or email address - * @return array with link - */ - protected function get_notification_link($data,$old,$receiver=null) - { - if (($view = egw_link::view($this->app,$data[$this->id_field]))) - { - return array( - 'text' => $this->get_title($data,$old), - 'app' => $this->app, - 'id' => $data[$this->id_field], - 'view' => $view, - 'popup' => egw_link::is_popup($this->app,'view'), - ); - } - return false; - } - - /** - * Get the body of the notification message, can be reimplemented - * - * @param boolean $html_email - * @param array $data - * @param array $old - * @param boolean $integrate_link to have links embedded inside the body - * @param int|string $receiver nummeric account_id or email address - * @return string - */ - public function get_body($html_email,$data,$old,$integrate_link = true,$receiver=null) - { - $body = ''; - if($this->get_config(self::CUSTOM_NOTIFICATION, $data, $old)) - { - $body = $this->get_custom_message($data,$old); - if(($sig = $this->get_signature($data,$old,$receiver))) - { - $body .= ($html_email ? '
':'') . "\n$sig"; - } - return $body; - } - if ($html_email) - { - $body = ''."\n"; - } - // new or modified message - if (($message = $this->get_message($data,$old,$receiver))) - { - foreach ((array)$message as $k => $_message) - { - $body .= $this->format_line($html_email,'message',false,($_message=='---'?($html_email?'
':$_message):$_message)); - } - } - if ($integrate_link && ($link = $this->get_link($data,$old,false,$receiver))) - { - $body .= $this->format_line($html_email,'link',false,$integrate_link === true ? lang('You can respond by visiting:') : $integrate_link,$link); - } - foreach($this->get_details($data,$receiver) as $name => $detail) - { - // if there's no old entry, the entry is not modified by definition - // if both values are '', 0 or null, we count them as equal too - $modified = $old && $data[$name] != $old[$name] && !(!$data[$name] && !$old[$name]); - //if ($modified) error_log("data[$name]=".print_r($data[$name],true).", old[$name]=".print_r($old[$name],true)." --> modified=".(int)$modified); - if (empty($detail['value']) && !$modified) continue; // skip unchanged, empty values - - $body .= $this->format_line($html_email,$detail['type'],$modified, - $detail['label'] ? $detail['label'] : '', $detail['value']); - } - if ($html_email) - { - $body .= "
\n"; - } - if(($sig = $this->get_signature($data,$old,$receiver))) - { - $body .= ($html_email ? '
':'') . "\n$sig"; - } - return $body; - } - - /** - * Format one line to the mail body - * - * @internal - * @param boolean $html_mail - * @param string $type 'link', 'message', 'summary', 'multiline', 'reply' and ''=regular content - * @param boolean $modified mark field as modified - * @param string $line whole line or just label - * @param string $data = null data or null to display just $line over 2 columns - * @return string - */ - protected function format_line($html_mail,$type,$modified,$line,$data=null) - { - //error_log(__METHOD__.'('.array2string($html_mail).",'$type',".array2string($modified).",'$line',".array2string($data).')'); - $content = ''; - - if ($html_mail) - { - if (!$this->html_content_allow) $line = html::htmlspecialchars($line); // XSS - - $color = $modified ? 'red' : false; - $size = '110%'; - $bold = false; - $background = '#FFFFF1'; - switch($type) - { - case 'message': - $background = '#D3DCE3;'; - $bold = true; - break; - case 'link': - $background = '#F1F1F1'; - break; - case 'summary': - $background = '#F1F1F1'; - $bold = true; - break; - case 'multiline': - // Only Convert nl2br on non-html content - if (strpos($data, 'html_content_allow ? $data : html::htmlspecialchars($data)); - $this->html_content_allow = true; // to NOT do htmlspecialchars again - } - break; - case 'reply': - $background = '#F1F1F1'; - break; - default: - $size = false; - } - $style = ($bold ? 'font-weight:bold;' : '').($size ? 'font-size:'.$size.';' : '').($color?'color:'.$color:''); - - $content = ''; - } - else // text-mail - { - if ($type == 'reply') $content = str_repeat('-',64)."\n"; - - if ($modified) $content .= '> '; - } - $content .= $line; - - if ($html_mail) - { - if ($line && $data) $content .= ''; - if ($type == 'link') - { - // the link is often too long for html boxes chunk-split allows to break lines if needed - $content .= html::a_href(chunk_split(rawurldecode($data),40,'​'),$data,'','target="_blank"'); - } - elseif ($this->html_content_allow) - { - $content .= html::activate_links($data); - } - else - { - $content .= html::htmlspecialchars($data); - } - } - else - { - $content .= ($content&&$data?': ':'').$data; - } - if ($html_mail) $content .= ''; - - $content .= "\n"; - - return $content; - } - - /** - * Get the attachments for a notification - * - * @param array $data - * @param array $old - * @param int|string $receiver nummeric account_id or email address - * @return array or array with values for either 'string' or 'path' and optionally (mime-)'type', 'filename' and 'encoding' - */ - protected function get_attachments($data,$old,$receiver=null) - { - return array(); - } - - /** - * Get a (global) signature to append to the change notificaiton - * @param array $data - * @param type $old - * @param type $receiver - */ - protected function get_signature($data, $old, $receiver) - { - $config = config::read('notifications'); - if(!isset($data[$this->id_field])) - { - error_log($this->app . ' did not properly implement bo_tracking->id_field. Merge skipped.'); - } - elseif(class_exists($this->app. '_merge')) - { - $merge_class = $this->app.'_merge'; - $merge = new $merge_class(); - $sig = $merge->merge_string($config['signature'], array($data[$this->id_field]), $error, 'text/html'); - if($error) - { - error_log($error); - return $config['signature']; - } - return $sig; - } - return $config['signature']; - } - - /** - * Get a custom notification message to be used instead of the standard one. - * It can use merge print placeholders to include data. - */ - protected function get_custom_message($data, $old, $merge_class = null) - { - $message = $this->get_config(self::CUSTOM_NOTIFICATION, $data, $old); - if(!$message) - { - return ''; - } - - // Automatically set merge class from naming conventions - if($merge_class == null) - { - $merge_class = $this->app.'_merge'; - } - if(!isset($data[$this->id_field])) - { - error_log($this->app . ' did not properly implement bo_tracking->id_field. Merge skipped.'); - return $message; - } - elseif(class_exists($merge_class)) - { - $merge = new $merge_class(); - $merged_message = $merge->merge_string($message, array($data[$this->id_field]), $error, 'text/html'); - if($error) - { - error_log($error); - return $message; - } - return $merged_message; - } - else - { - throw new egw_exception_wrong_parameter("Invalid merge class '$merge_class' for {$this->app} custom notification"); - } - } -} +abstract class bo_tracking extends Api\Storage\Tracking {} diff --git a/setup/inc/class.setup_cmd_ldap.inc.php b/setup/inc/class.setup_cmd_ldap.inc.php index 9f617b7f1c..28f0a71023 100644 --- a/setup/inc/class.setup_cmd_ldap.inc.php +++ b/setup/inc/class.setup_cmd_ldap.inc.php @@ -5,11 +5,13 @@ * @link http://www.egroupware.org * @author Ralf Becker * @package setup - * @copyright (c) 2007-13 by Ralf Becker + * @copyright (c) 2007-16 by Ralf Becker * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License * @version $Id$ */ +use EGroupware\Api; + /** * setup command: test or create the ldap connection and hierarchy * @@ -121,7 +123,7 @@ class setup_cmd_ldap extends setup_cmd { if (!empty($this->domain) && !preg_match('/^([a-z0-9_-]+\.)*[a-z0-9]+/i',$this->domain)) { - throw new egw_exception_wrong_userinput(lang("'%1' is no valid domain name!",$this->domain)); + throw new Api\Exception\WrongUserinput(lang("'%1' is no valid domain name!",$this->domain)); } if ($this->remote_id && $check_only && !in_array($this->sub_command, array('set_mailbox', 'sid2uidnumber', 'copy2ad'))) { @@ -186,7 +188,7 @@ class setup_cmd_ldap extends setup_cmd // check if base does exist if (!@ldap_read($this->test_ldap->ds,$this->ldap_base,'objectClass=*')) { - throw new egw_exception_wrong_userinput(lang('Base dn "%1" NOT found!',$this->ldap_base)); + throw new Api\Exception\WrongUserinput(lang('Base dn "%1" NOT found!',$this->ldap_base)); } if (!($sr = ldap_search($this->test_ldap->ds,$this->ldap_base, @@ -194,7 +196,7 @@ class setup_cmd_ldap extends setup_cmd array('uidNumber','gidNumber','uid','cn', 'objectClass',self::sambaSID))) || !($entries = ldap_get_entries($this->test_ldap->ds, $sr))) { - throw new egw_exception(lang('Error searching "dn=%1" for "%2"!',$this->ldap_base, $search)); + throw new Api\Exception(lang('Error searching "dn=%1" for "%2"!',$this->ldap_base, $search)); } $change = $accounts = array(); $cmd_change_account_id = 'admin/admin-cli.php --change-account-id @,'; @@ -203,7 +205,7 @@ class setup_cmd_ldap extends setup_cmd { if ($key === 'count') continue; - $entry = ldap::result2array($entry); + $entry = Api\Ldap::result2array($entry); $accounts[$entry['dn']] = $entry; //print_r($entry); @@ -247,7 +249,7 @@ class setup_cmd_ldap extends setup_cmd } if (!$check_only && $modify && !ldap_modify($this->test_ldap->ds, $dn, $modify)) { - throw new egw_exception("Failed to modify ldap: !ldap_modify({$this->test_ldap->ds}, '$dn', ".array2string($modify).") ".ldap_error($this->test_ldap->ds). + throw new Api\Exception("Failed to modify ldap: !ldap_modify({$this->test_ldap->ds}, '$dn', ".array2string($modify).") ".ldap_error($this->test_ldap->ds). "\n- ".implode("\n- ", $msg)); // EGroupware change already run successful } if ($modify) ++$changed; @@ -313,7 +315,7 @@ class setup_cmd_ldap extends setup_cmd // check if ads base does exist if (!@ldap_read($ads->ds, $this->ads_context, 'objectClass=*')) { - throw new egw_exception_wrong_userinput(lang('Ads dn "%1" NOT found!',$this->ads_context)); + throw new Api\Exception\WrongUserInput(lang('Ads dn "%1" NOT found!',$this->ads_context)); } // connect to source ldap @@ -322,7 +324,7 @@ class setup_cmd_ldap extends setup_cmd // check if ldap base does exist if (!@ldap_read($this->test_ldap->ds,$this->ldap_base,'objectClass=*')) { - throw new egw_exception_wrong_userinput(lang('Base dn "%1" NOT found!',$this->ldap_base)); + throw new Api\Exception\WrongUserInput(lang('Base dn "%1" NOT found!',$this->ldap_base)); } if (!($sr = ldap_search($this->test_ldap->ds,$this->ldap_base, @@ -330,7 +332,7 @@ class setup_cmd_ldap extends setup_cmd '(&(objectClass=posixAccount)('.self::sambaSID.'=*)(!(uid=*$)))', $attrs)) || !($entries = ldap_get_entries($this->test_ldap->ds, $sr))) { - throw new egw_exception(lang('Error searching "dn=%1" for "%2"!',$this->ldap_base, $search)); + throw new Api\Exception(lang('Error searching "dn=%1" for "%2"!',$this->ldap_base, $search)); } $changed = 0; $utc_diff = null; @@ -338,12 +340,12 @@ class setup_cmd_ldap extends setup_cmd { if ($key === 'count') continue; - $entry_arr = ldap::result2array($entry); + $entry_arr = Api\Ldap::result2array($entry); $uid = $entry_arr['uid']; $entry = array_diff_key($entry_arr, $ignore_attr); if (!($sr = ldap_search($ads->ds, $this->ads_context, - $search='(&(objectClass=user)(sAMAccountName='.ldap::quote($uid).'))', array('dn'))) || + $search='(&(objectClass=user)(sAMAccountName='.Api\Ldap::quote($uid).'))', array('dn'))) || !($dest = ldap_get_entries($ads->ds, $sr))) { $msg[] = lang('User "%1" not found!', $uid); @@ -591,7 +593,7 @@ class setup_cmd_ldap extends setup_cmd // should we run any or some addAccount hooks if ($this->add_account_hook) { - // setting up egw_info array with new ldap information, so hook can use ldap::ldapConnect() + // setting up egw_info array with new ldap information, so hook can use Api\Ldap::ldapConnect() if (!$egw_info_set++) { foreach(array('ldap_host','ldap_root_dn','ldap_root_pw','ldap_context','ldap_group_context','ldap_search_filter','ldap_encryptin_type','mail_suffix','mail_login_type') as $name) @@ -798,7 +800,7 @@ class setup_cmd_ldap extends setup_cmd * @param string $dn =null default $this->ldap_root_dn * @param string $pw =null default $this->ldap_root_pw * @param string $host =null default $this->ldap_host, hostname, ip or ldap-url - * @throws egw_exception_wrong_userinput Can not connect to ldap ... + * @throws Api\Exception\WrongUserInput Can not connect to ldap ... */ private function connect($dn=null,$pw=null,$host=null) { @@ -806,9 +808,9 @@ class setup_cmd_ldap extends setup_cmd if (is_null($pw)) $pw = $this->ldap_root_pw; if (is_null($host)) $host = $this->ldap_host; - if (!$pw) // ldap::ldapConnect use the current eGW's pw otherwise + if (!$pw) // Api\Ldap::ldapConnect use the current eGW's pw otherwise { - throw new egw_exception_wrong_userinput(lang('You need to specify a password!')); + throw new Api\Exception\WrongUserInput(lang('You need to specify a password!')); } $this->test_ldap = new ldap(); @@ -821,7 +823,7 @@ class setup_cmd_ldap extends setup_cmd if (!$ds) { - throw new egw_exception_wrong_userinput(lang('Can not connect to LDAP server on host %1 using DN %2!', + throw new Api\Exception\WrongUserInput(lang('Can not connect to LDAP server on host %1 using DN %2!', $host,$dn).($this->test_ldap->ds ? ' ('.ldap_error($this->test_ldap->ds).')' : '')); } return lang('Successful connected to LDAP server on %1 using DN %2.',$this->ldap_host,$dn); @@ -831,7 +833,7 @@ class setup_cmd_ldap extends setup_cmd * Count active (not expired) users * * @return int number of active users - * @throws egw_exception_wrong_userinput + * @throws Api\Exception\WrongUserInput */ private function users() { @@ -840,7 +842,7 @@ class setup_cmd_ldap extends setup_cmd $sr = ldap_list($this->test_ldap->ds,$this->ldap_context,'ObjectClass=posixAccount',array('dn','shadowExpire')); if (!($entries = ldap_get_entries($this->test_ldap->ds, $sr))) { - throw new egw_exception('Error listing "dn=%1"!',$this->ldap_context); + throw new Api\Exception('Error listing "dn=%1"!',$this->ldap_context); } $num = 0; foreach($entries as $n => $entry) @@ -856,7 +858,7 @@ class setup_cmd_ldap extends setup_cmd * Check and if does not yet exist create the new database and user * * @return string with success message - * @throws egw_exception_wrong_userinput + * @throws Api\Exception\WrongUserInput */ private function create() { @@ -883,7 +885,7 @@ class setup_cmd_ldap extends setup_cmd * Delete whole LDAP tree of an instance dn=$this->ldap_base using $this->ldap_admin/_pw * * @return string with success message - * @throws egw_exception if dn not found, not listable or delete fails + * @throws Api\Exception if dn not found, not listable or delete fails */ private function delete_base() { @@ -897,12 +899,12 @@ class setup_cmd_ldap extends setup_cmd // some precausion to not delete whole ldap tree! if (count(explode(',',$this->ldap_base)) < 2) { - throw new egw_exception_assertion_failed(lang('Refusing to delete dn "%1"!',$this->ldap_base)); + throw new Api\Exception\AssertionFailed(lang('Refusing to delete dn "%1"!',$this->ldap_base)); } // check if base does exist if (!@ldap_read($this->test_ldap->ds,$this->ldap_base,'objectClass=*')) { - throw new egw_exception_wrong_userinput(lang('Base dn "%1" NOT found!',$this->ldap_base)); + throw new Api\Exception\WrongUserInput(lang('Base dn "%1" NOT found!',$this->ldap_base)); } return lang('LDAP dn="%1" with %2 entries deleted.', $this->ldap_base,$this->rdelete($this->ldap_base)); @@ -913,14 +915,14 @@ class setup_cmd_ldap extends setup_cmd * * @param string $dn * @return int integer number of deleted entries - * @throws egw_exception if dn not listable or delete fails + * @throws Api\Exception if dn not listable or delete fails */ private function rdelete($dn) { if (!($sr = ldap_list($this->test_ldap->ds,$dn,'ObjectClass=*',array(''))) || !($entries = ldap_get_entries($this->test_ldap->ds, $sr))) { - throw new egw_exception(lang('Error listing "dn=%1"!',$dn)); + throw new Api\Exception(lang('Error listing "dn=%1"!',$dn)); } $deleted = 0; foreach($entries as $n => $entry) @@ -930,7 +932,7 @@ class setup_cmd_ldap extends setup_cmd } if (!ldap_delete($this->test_ldap->ds,$dn)) { - throw new egw_exception(lang('Error deleting "dn=%1"!',$dn)); + throw new Api\Exception(lang('Error deleting "dn=%1"!',$dn)); } return ++$deleted; } @@ -944,7 +946,7 @@ class setup_cmd_ldap extends setup_cmd * @param string $this->mbox_attr ='mailmessagestore' lowercase!!! * @param string $this->mail_login_type ='email' 'email', 'vmailmgr', 'standard' or 'uidNumber' * @return string with success message N entries modified - * @throws egw_exception if dn not found, not listable or delete fails + * @throws Api\Exception if dn not found, not listable or delete fails */ private function set_mailbox($check_only=false) { @@ -958,7 +960,7 @@ class setup_cmd_ldap extends setup_cmd // check if base does exist if (!@ldap_read($this->test_ldap->ds,$this->ldap_base,'objectClass=*')) { - throw new egw_exception_wrong_userinput(lang('Base dn "%1" NOT found!',$this->ldap_base)); + throw new Api\Exception\WrongUserInput(lang('Base dn "%1" NOT found!',$this->ldap_base)); } $object_class = $this->object_class ? $this->object_class : 'qmailUser'; $mbox_attr = $this->mbox_attr ? $this->mbox_attr : 'mailmessagestore'; @@ -968,7 +970,7 @@ class setup_cmd_ldap extends setup_cmd 'objectClass='.$object_class,array('mail','uidNumber','uid',$mbox_attr))) || !($entries = ldap_get_entries($this->test_ldap->ds, $sr))) { - throw new egw_exception(lang('Error listing "dn=%1"!',$this->ldap_base)); + throw new Api\Exception(lang('Error listing "dn=%1"!',$this->ldap_base)); } $modified = 0; foreach($entries as $n => $entry) @@ -987,7 +989,7 @@ class setup_cmd_ldap extends setup_cmd $mbox_attr => $mbox, ))) { - throw new egw_exception(lang("Error modifying dn=%1: %2='%3'!",$entry['dn'],$mbox_attr,$mbox)); + throw new Api\Exception(lang("Error modifying dn=%1: %2='%3'!",$entry['dn'],$mbox_attr,$mbox)); } ++$modified; if ($check_only) echo "$modified: $entry[dn]: $mbox_attr={$entry[$mbox_attr][0]} --> $mbox\n"; @@ -1015,7 +1017,7 @@ class setup_cmd_ldap extends setup_cmd * @param string $dn dn to create, eg. "cn=admin,dc=local" * @param array $extra =array() extra attributes to set * @return boolean true if the node was create, false if it was already there - * @throws egw_exception_wrong_userinput + * @throws Api\Exception\WrongUserinput */ private function _create_node($dn,$extra=array()) { @@ -1035,7 +1037,7 @@ class setup_cmd_ldap extends setup_cmd if (!isset(self::$requiredObjectclasses[$name])) { - throw new egw_exception_wrong_userinput(lang('Can not create DN %1!',$dn).' '. + throw new Api\Exception\WrongUserinput(lang('Can not create DN %1!',$dn).' '. lang('Supported node types:').implode(', ',array_keys(self::$requiredObjectclasses))); } if ($name == 'dc') $extra['o'] = $value; // required by organisation @@ -1046,7 +1048,7 @@ class setup_cmd_ldap extends setup_cmd 'objectClass' => self::$requiredObjectclasses[$name], )+$extra)) { - throw new egw_exception_wrong_userinput(lang('Can not create DN %1!',$dn). + throw new Api\Exception\WrongUserinput(lang('Can not create DN %1!',$dn). ' ('.ldap_error($this->test_ldap->ds).', attributes='.print_r($attr,true).')'); } return true;