diff --git a/addressbook/inc/class.addressbook_ui.inc.php b/addressbook/inc/class.addressbook_ui.inc.php index 51e3a63789..71eea30414 100644 --- a/addressbook/inc/class.addressbook_ui.inc.php +++ b/addressbook/inc/class.addressbook_ui.inc.php @@ -2070,6 +2070,30 @@ class addressbook_ui extends addressbook_bo { if (is_array($content)) { + // sync $content['shared'] with $content['shared_values'] + foreach($content['shared'] as $key => $shared) + { + $shared_value = $shared['shared_id'].':'.$shared['shared_with'].':'.$shared['shared_by'].':'.$shared['shared_writable']; + if (($k = array_search($shared_value, $content['shared_values'])) === false) + { + unset($content['shared'][$key]); + } + else + { + unset($content['shared_values'][$k]); + } + } + foreach($content['shared_values'] as $account_id) + { + $content['shared'][] = [ + 'shared_with' => $account_id, + 'shared_by' => $this->user, + 'shared_at' => new Api\DateTime(), + 'shared_writable' => (int)(bool)$content['shared_writable'], + ]; + } + unset($content['shared_values']); + $button = @key($content['button']); unset($content['button']); $content['private'] = (int) ($content['owner'] && substr($content['owner'],-1) == 'p'); @@ -2403,6 +2427,20 @@ class addressbook_ui extends addressbook_bo } } } + // set $content[shared_options/_values] from $content[shared] + $content['shared_options'] = []; + foreach((array)$content['shared'] as $shared) + { + $content['shared_options'][$shared['shared_id'].':'.$shared['shared_with'].':'.$shared['shared_by'].':'.$shared['shared_writable']] = [ + 'label' => Accounts::username($shared['shared_with']), + 'title' => lang('%1 shared this contact on %2 with %3 %4', + Accounts::username($shared['shared_by']), Api\DateTime::to($shared['shared_at']), + Accounts::username($shared['shared_with']), $shared['shared_writable'] ? lang('writable') : lang('readonly')), + 'icon' => $shared['shared_writable'] ? 'edit' : 'view', + ]; + } + $content['shared_values'] = array_keys($content['shared_options']); + if ($content['id']) { // last and next calendar date diff --git a/addressbook/lang/egw_de.lang b/addressbook/lang/egw_de.lang index 5fe1a8723b..4af799a8b9 100644 --- a/addressbook/lang/egw_de.lang +++ b/addressbook/lang/egw_de.lang @@ -8,6 +8,7 @@ %1 public keys added. addressbook de %1 öffentliche Schlüssel gespeichert. %1 records imported addressbook de %1 Datensätze importiert %1 records read (not yet imported, you may go %2back%3 and uncheck test import) addressbook de %1 Datensätze gelesen (noch nicht importiert, sie können %2zurück%3 gehen und Test-Import ausschalten) +%1 shared this contact on %2 with %3 %4 addressbook de %1 teilte diesen Kontakt am %2 mit %3 %4 %1 starts with '%2' addressbook de %1 beginnt mit '%2' %s please calculate the result addressbook de %s Bitte berechnen Sie das Ergebnis (e.g. 1969) addressbook de (z.B. 1969) @@ -454,6 +455,7 @@ send succeeded to %1 common de erfolgreich versandt an seperator addressbook de Feldtrenner set full name and file as field in contacts of all users (either all or only empty values) admin de Setzt vollen Namen und eigene Sortierung in Kontakten aller Benutzer (entweder alle oder nur leere Werte) set only full name addressbook de Nur vollen Namen setzen +shared with addressbook de Geteilt mit should the columns photo and home address always be displayed, even if they are empty. addressbook de Sollen die Spalten Foto und Privatadresse immer angezeigt werden, auch wenn sie leer sind? show addressbook de Anzeigen show active accounts addressbook de Zeigt nur aktive Benutzer an diff --git a/addressbook/lang/egw_en.lang b/addressbook/lang/egw_en.lang index a85eaaf1e5..6fac5eea37 100644 --- a/addressbook/lang/egw_en.lang +++ b/addressbook/lang/egw_en.lang @@ -8,6 +8,7 @@ %1 public keys added. addressbook en %1 public keys added. %1 records imported addressbook en %1 records imported. %1 records read (not yet imported, you may go %2back%3 and uncheck test import) addressbook en %1 records read. Not yet imported, you may go %2back%3 and un-check Test import. +%1 shared this contact on %2 with %3 %4 addressbook en %1 shared this contact on %2 with %3 %4 %1 starts with '%2' addressbook en %1 starts with '%2' %s please calculate the result addressbook en %s please calculate the result (e.g. 1969) addressbook en (e.g. 1969) @@ -454,6 +455,7 @@ send succeeded to %1 common en Send succeeded to %1 seperator addressbook en Separator set full name and file as field in contacts of all users (either all or only empty values) admin en Set full name and 'fileas' field in contacts of all users. Either all or only empty values. set only full name addressbook en Set only full name +shared with addressbook en Shared with should the columns photo and home address always be displayed, even if they are empty. addressbook en Are photo and home address always displayed, even if columns are empty. show addressbook en Show show active accounts addressbook en Show active accounts diff --git a/addressbook/templates/default/edit.xet b/addressbook/templates/default/edit.xet index 8b3ab7ea81..d3325a8fb1 100644 --- a/addressbook/templates/default/edit.xet +++ b/addressbook/templates/default/edit.xet @@ -143,6 +143,11 @@ + + + + + diff --git a/api/src/Contacts.php b/api/src/Contacts.php index 6b47c60c62..fecc672380 100755 --- a/api/src/Contacts.php +++ b/api/src/Contacts.php @@ -1233,6 +1233,19 @@ class Contacts extends Contacts\Storage $access = ($grants[$owner] & $needed) && (!$contact['private'] || ($grants[$owner] & Acl::PRIVAT) || in_array($owner,$memberships)); } + // check if we might have access via sharing (not for delete) + if ($access === false && !empty($contact['shared']) && $needed != Acl::DELETE) + { + foreach($contact['shared'] as $shared) + { + if (isset($grants[$shared['shared_with']]) && ($shared['shared_writable'] || !($needed & Acl::EDIT))) + { + $access = true; + error_log(__METHOD__."($needed,$contact[id],$deny_account_delete,$user) shared=".json_encode($shared)." returning ".array2string($access)); + break; + } + } + } //error_log(__METHOD__."($needed,$contact[id],$deny_account_delete,$user) returning ".array2string($access)); return $access; } diff --git a/api/src/Contacts/Sql.php b/api/src/Contacts/Sql.php index 263dce9d51..2a354a939a 100644 --- a/api/src/Contacts/Sql.php +++ b/api/src/Contacts/Sql.php @@ -62,6 +62,8 @@ class Sql extends Api\Storage const EXTRA_TABLE = 'egw_addressbook_extra'; const EXTRA_VALUE = 'contact_value'; + const SHARED_TABLE = 'egw_addressbook_shared'; + /** * Constructor * @@ -497,6 +499,11 @@ class Sql extends Api\Storage unset($filter['cat_id']); } + // SQL to get all shared contacts to be OR-ed into ACL filter + // ToDo: do we need a sharing filter for $ignore_acl + $shared_sql = 'contact_id IN (SELECT contact_id FROM '.self::SHARED_TABLE.' WHERE '. + $this->db->expression(self::SHARED_TABLE, ['shared_with' => $filter['owner'] ?? array_keys($this->grants)]).')'; + // add filter for read ACL in sql, if user is NOT the owner of the addressbook if (isset($this->grants) && !$ignore_acl && !(isset($filter['owner']) && $filter['owner'] == $GLOBALS['egw_info']['user']['account_id'])) @@ -524,18 +531,21 @@ class Sql extends Api\Storage if (!array_intersect((array)$filter['owner'],array_keys($this->grants))) { if (!isset($groupmember_sql)) return false; - $filter[] = substr($groupmember_sql,4); + $filter[] = '('.substr($groupmember_sql,4)." OR $shared_sql)"; unset($filter['owner']); } // for an owner filter, which does NOT include current user, filter out private entries elseif (!in_array($GLOBALS['egw_info']['user']['account_id'], (array)$filter['owner'])) { - $filter['private'] = 0; + $filter[] = '('.$this->db->expression($this->table_name, $this->table_name.'.', ['contact_owner' => $filter['owner'], 'contact_private' => 0]). + " OR $shared_sql)"; + unset($filter['owner']); } // if multiple addressbooks (incl. current owner) are searched, we need full acl filter elseif(is_array($filter['owner']) && count($filter['owner']) > 1) { $filter[] = "($this->table_name.contact_owner=".(int)$GLOBALS['egw_info']['user']['account_id']. + " OR $shared_sql". " OR contact_private=0 AND $this->table_name.contact_owner IN (". implode(',',array_keys($this->grants)).") $groupmember_sql OR $this->table_name.contact_owner IS NULL)"; } @@ -547,6 +557,7 @@ class Sql extends Api\Storage $filter[] = $this->table_name.'.contact_owner != 0'; // in case there have been accounts in sql previously } $filter[] = "($this->table_name.contact_owner=".(int)$GLOBALS['egw_info']['user']['account_id']. + " OR $shared_sql". ($this->grants ? " OR contact_private=0 AND $this->table_name.contact_owner IN (". implode(',',array_keys($this->grants)).")" : ''). $groupmember_sql." OR $this->table_name.contact_owner IS NULL)"; @@ -1009,9 +1020,62 @@ class Sql extends Api\Storage || strlen($contact['uid']) < $minimum_uid_length)) { parent::update(array('uid' => Api\CalDAV::generate_uid('addressbook',$contact['id']))); } + if (is_array($contact)) + { + $contact['shared'] = $this->read_shared($contact['id']); + } return $contact; } + /** + * Read sharing information of a contact + * + * @param int $id contact_id to read + * @return array of array with values for keys "shared_(with|writable|by|at|id)" + */ + function read_shared($id) + { + $shared = []; + foreach($this->db->select(self::SHARED_TABLE, '*', ['contact_id' => $id], + __LINE__, __FILE__, false) as $row) + { + $row['shared_at'] = Api\DateTime::server2user($row['shared_at'], 'object'); + $shared[] = $row; + } + return $shared; + } + + /** + * @param int $id + * @param array $shared array of array with values for keys "shared_(with|writable|by|at|id)" + * @return array of array with values for keys "shared_(with|writable|by|at|id)" + */ + function save_shared($id, array $shared) + { + $ids = []; + foreach($shared as &$data) + { + if (empty($data['shared_id'])) + { + unset($data['shared_id']); + $data['contact_id'] = $id; + $data['shared_at'] = Api\DateTime::user2server($data['shared_at'] ?: 'now'); + $data['shared_by'] = $data['shared_by'] ?: $GLOBALS['egw_info']['user']['account_id']; + $this->db->insert(self::SHARED_TABLE, $data, false, __LINE__, __FILE__); + $data['shared_id'] = $this->db->get_last_insert_id(self::SHARED_TABLE, 'share_id'); + } + $ids[] = (int)$data['shared_id']; + } + $delete = ['contact_id' => $id]; + if ($ids) $delete[] = 'shared_id NOT IN ('.implode(',', $ids).')'; + $this->db->delete(self::SHARED_TABLE, $delete, __LINE__, __FILE__); + foreach($shared as &$data) + { + $data['shared_at'] = Api\DateTime::server2user($data['shared_at'], 'object'); + } + return $shared; + } + /** * Saves a contact, reimplemented to check a given etag and set a uid * @@ -1095,6 +1159,11 @@ class Sql extends Api\Storage { parent::update($update); } + // save sharing information + if (!$err) + { + $this->data['shared'] = $this->save_shared($this->data['id'], (array)$this->data['shared']); + } return $err; } diff --git a/api/src/Contacts/Storage.php b/api/src/Contacts/Storage.php index 13b3b1cf29..1b9a07a173 100755 --- a/api/src/Contacts/Storage.php +++ b/api/src/Contacts/Storage.php @@ -608,7 +608,7 @@ class Storage $contact_id = array('account_id' => (int) substr($contact_id,8)); } // read main data - $backend =& $this->get_backend($contact_id); + $backend = $this->get_backend($contact_id); if (!($contact = $backend->read($contact_id))) { return $contact;