* @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; /** * 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 = array(); /** * 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(); /** * Directory to store striped photo or public keys in VFS directory of entry */ const FILES_DIRECTORY = '.files'; const FILES_PHOTO = '.files/photo.jpeg'; const FILES_PGP_PUBKEY = '.files/pgp-pubkey.asc'; const FILES_SMIME_PUBKEY = '.files/smime-pubkey.crt'; /** * Constant for bit-field "contact_files" storing what files are available */ const FILES_BIT_PHOTO = 1; const FILES_BIT_PGP_PUBKEY = 2; const FILES_BIT_SMIME_PUBKEY = 4; /** * These fields are options for checking for duplicate contacts * * @var array */ public static $duplicate_fields = array( 'n_given' => 'first name', 'n_middle' => 'middle name', 'n_family' => 'last name', 'contact_bday' => 'birthday', 'org_name' => 'Organisation', 'org_unit' => 'Department', 'adr_one_locality' => 'Location', 'contact_title' => 'title', 'contact_email' => 'business email', 'contact_email_home'=> 'email (private)', ); /** * 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']??null) === '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']??null) === '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); } $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\\Api\\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('api','egw_addressbook2list'); $tdlists = $this->db->get_table_definitions('api','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' ))); } // Always add in deleted type, to show deleted contacts $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']??null) === 'ldap' && $this->account_repository === 'ldap') { // static grants from ldap: all rights for the own personal addressbook and the group ones of the memberships $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] = Api\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] = Api\Acl::READ; // admins always have read-access if (!$GLOBALS['egw']->acl->check('account_access',16,'admin')) $grants[0] |= Api\Acl::EDIT; if (!$GLOBALS['egw']->acl->check('account_access',4,'admin')) $grants[0] |= Api\Acl::ADD; if (!$GLOBALS['egw']->acl->check('account_access',32,'admin')) $grants[0] |= Api\Acl::DELETE; } // allow certain groups to edit contact-data of accounts if (self::allow_account_edit($user)) { $grants[0] |= Api\Acl::READ|Api\Acl::EDIT; } } // no user, eg. setup or not logged in, allow read access to accounts else { $grants = [0 => Api\Acl::READ]; } //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 !empty($GLOBALS['egw_info']['server']['allow_account_edit']) && array_intersect($GLOBALS['egw_info']['server']['allow_account_edit'], $GLOBALS['egw']->accounts->memberships($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].' ('. Api\Accounts::username($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']; $contact['files'] = $this->somain->data['files']; 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 (empty($contact_id)) { return false; // no need to pass to backend, will fail anyway } 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); } /** * @param array $ids * @param ?boolean $deleted false: no deleted, true: only deleted, null: both * @return array contact_id => array of array with sharing info */ function read_shared(array $ids, $deleted=false) { $contacts = []; if (method_exists($backend = $this->get_backend($ids[0]), 'read_shared')) { foreach($backend->read_shared($ids, $deleted) as $shared) { $contacts[$shared['contact_id']][] = $shared; } } return $contacts; } /** * 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)" * @param boolean $ignore_acl =false true: no acl check * @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='', $ignore_acl=false) { //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) && isset($filter['adr_one_countrycode']) && $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, isset($filter['list']) && $filter['list'] < 0 ? 0 : $filter['owner'] ?? null); // single string to search for --> create so_sql conformant search criteria 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, false, $ignore_acl); $this->total = $backend->total; if ($rows) { foreach($rows as $n => $row) { $rows[$n] = $this->db2data($row); } // allow other apps to hook into search Api\Hooks::process(array( 'hook_location' => 'contacts_search', 'criteria' => $criteria, 'filter' => $filter, 'ignore_acl' => $ignore_acl, 'obj' => $this, 'rows' => &$rows, 'total' => &$this->total, ), array(), true); // true = no permission check } 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; if (!$rows) return array(); foreach($rows as $n => $row) { if (strpos($row['org_name'],'&')!==false) $row['org_name'] = str_replace('&','*AND*',$row['org_name']); $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]); $rows[$n]['id'] .= '|||'.$by.':'.$row[$by]; } } } return $rows; } /** * Find contacts that appear to be duplicates * * @param Array $param * @param string $param[org_view] 'org_name', 'org_name,adr_one_location', 'org_name,org_unit' how to group * @param int $param[owner] addressbook to search * @param string $param[search] search pattern for org_name * @param string $param[searchletter] letter the org_name need to start with * @param int $param[start] * @param int $param[num_rows] * @param string $param[sort] ASC or DESC * * @return array of arrays */ public function duplicates($param) { if (!method_exists($this->somain,'duplicates')) { $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) { // we don't search the customfields if ($col != 'contact_value') $param['search'][$col] = $search; } } } 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'][] = $this->somain->table_name.'.contact_tid != \'' . self::DELETED_TYPE . '\''; } elseif(is_null($param['col_filter']['tid'])) { // return all entries including deleted unset($param['col_filter']['tid']); } if(array_key_exists('filter', $param) && $param['filter'] != '') { $param['owner'] = $param['filter']; unset($param['filter']); } if(array_key_exists('owner', $param['col_filter']) && $param['col_filter']['owner'] != '') { $param['owner'] = $param['col_filter']['owner']; unset($param['col_filter']['owner']); } $rows = $this->somain->duplicates($param); $this->total = $this->somain->total; if (!$rows) return array(); foreach($rows as $n => $row) { $rows[$n]['id'] = 'duplicate:'; foreach(array_keys(static::$duplicate_fields) as $by) { if (strpos($row[$by],'&')!==false) $row[$by] = str_replace('&','*AND*',$row[$by]); if($row[$by]) { $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|Ldap|Ads|Univention */ 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('api','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; if ($type == 'supported') { return $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 * - "accounts-back-ads" accounts back from ads to sql */ function migrate2ldap($type) { //error_log(__METHOD__."(".array2string($type).")"); $sql_contacts = new Sql(); if ($type == 'accounts-back-ads') { $ldap_contacts = new Ads(); } else { // we need an admin connection $ds = $GLOBALS['egw']->ldap->ldapConnect(); $ldap_contacts = new 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']); } $old_contact_id = $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"; $new_contact_id = $sql_contacts->data['id']; echo "    " . $old_contact_id . " --> " . $new_contact_id . " / "; $this->db->update('egw_links',array( 'link_id1' => $new_contact_id, ),array( 'link_app1' => 'addressbook', 'link_id1' => $old_contact_id ),__LINE__,__FILE__); $this->db->update('egw_links',array( 'link_id2' => $new_contact_id, ),array( 'link_app2' => 'addressbook', 'link_id2' => $old_contact_id ),__LINE__,__FILE__); echo "

\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 =Api\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=Api\Acl::READ,$extra_labels=null) { $lists = is_array($extra_labels) ? $extra_labels : array(); if (method_exists($this->somain,'get_lists')) { $uids = array(); foreach($this->grants as $uid => $rights) { // only requests groups / list in accounts addressbook for read if (!$uid && $required != Api\Acl::READ) continue; if (($rights & $required) == $required) { $uids[] = $uid; } } 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] .= ' ('.Api\Accounts::username($data['list_owner']).')'; } } } // add groups for all backends, if accounts addressbook is not hidden & // preference has not turned them off if ($GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_accounts'] !== '1' && $GLOBALS['egw_info']['user']['preferences']['addressbook']['hide_groups_as_lists'] == '0') { foreach($GLOBALS['egw']->accounts->search(array( 'type' => 'groups' )) as $account_id => $group) { $lists[(string)$account_id] = Api\Accounts::format_username($group['account_lid'], '', '', $account_id); } } 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|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 */ 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 available 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='') { return method_exists($this->get_backend(null, $owner),'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); } }