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;