Page MenuHomePhorge

D4668.1775482177.diff
No OneTemporary

Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None

D4668.1775482177.diff

diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -3572,7 +3572,7 @@
if (node && node.id && me.calendars[node.id]) {
me.select_calendar(node.id, true);
rcmail.enable_command('calendar-edit', 'calendar-showurl', 'calendar-showfburl', true);
- rcmail.enable_command('calendar-delete', me.calendars[node.id].editable);
+ rcmail.enable_command('calendar-delete', me.has_permission(me.calendars[node.id], 'xa'));
rcmail.enable_command('calendar-remove', me.calendars[node.id] && me.calendars[node.id].removable);
}
});
diff --git a/plugins/calendar/drivers/caldav/caldav_calendar.php b/plugins/calendar/drivers/caldav/caldav_calendar.php
--- a/plugins/calendar/drivers/caldav/caldav_calendar.php
+++ b/plugins/calendar/drivers/caldav/caldav_calendar.php
@@ -75,18 +75,12 @@
$this->alarms = !isset($this->attributes['alarms']) || $this->attributes['alarms'];
if ($this->storage->get_namespace() == 'personal') {
- $this->editable = true;
$this->rights = 'lrswikxteav';
} else {
- $rights = $this->storage->get_myrights();
- if ($rights) {
- $this->rights = $rights;
- if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) {
- $this->editable = strpos($rights, 'i');
- ;
- }
- }
+ $this->rights = $this->storage->get_myrights();
}
+
+ $this->editable = strpos($this->rights, 'i') !== false;
}
}
diff --git a/plugins/calendar/drivers/caldav/caldav_driver.php b/plugins/calendar/drivers/caldav/caldav_driver.php
--- a/plugins/calendar/drivers/caldav/caldav_driver.php
+++ b/plugins/calendar/drivers/caldav/caldav_driver.php
@@ -184,9 +184,9 @@
$calendars[$cal->id] = [
'id' => $cal->id,
'name' => $cal->get_name(),
- 'listname' => $cal->get_foldername(),
+ 'listname' => $cal->get_name(),
'editname' => $cal->get_foldername(),
- 'title' => '', // $cal->get_title(),
+ 'title' => null,
'color' => $cal->get_color(),
'editable' => $cal->editable,
'group' => $is_user ? 'other user' : $cal->get_namespace(), // @phpstan-ignore-line
diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -92,7 +92,7 @@
$arr['name'] = html::quote($arr['name']);
$arr['listname'] = html::quote($arr['name']);
$arr['rights'] = 'lrswikxteav';
- $arr['editable'] = true;
+ $arr['editable'] = true;
$this->calendars[$arr['calendar_id']] = $arr;
$calendar_ids[] = $db->quote($arr['calendar_id']);
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -358,7 +358,7 @@
$label_id = 'cl:' . $id;
$content = html::a(
['class' => 'calname', 'id' => $label_id, 'title' => $title, 'href' => '#'],
- rcube::Q(!empty($prop['editname']) ? $prop['editname'] : $prop['listname'])
+ rcube::Q(!empty($prop['listname']) ? $prop['listname'] : $prop['name'])
);
if (empty($prop['virtual'])) {
diff --git a/plugins/kolab_addressbook/drivers/carddav/carddav_contacts.php b/plugins/kolab_addressbook/drivers/carddav/carddav_contacts.php
--- a/plugins/kolab_addressbook/drivers/carddav/carddav_contacts.php
+++ b/plugins/kolab_addressbook/drivers/carddav/carddav_contacts.php
@@ -122,17 +122,12 @@
// Set readonly and rights flags according to folder permissions
if ($this->ready) {
- if ($this->storage->get_owner() == $_SESSION['username']) {
+ if ($this->storage->get_namespace() == 'personal') {
$this->readonly = false;
$this->rights = 'lrswikxtea';
} else {
- $rights = $this->storage->get_myrights();
- if ($rights && !PEAR::isError($rights)) {
- $this->rights = $rights;
- if (strpos($rights, 'i') !== false && strpos($rights, 't') !== false) {
- $this->readonly = false;
- }
- }
+ $this->rights = $this->storage->get_myrights();
+ $this->readonly = strpos($this->rights, 'i') === false;
}
}
}
diff --git a/plugins/kolab_addressbook/drivers/carddav/carddav_contacts_driver.php b/plugins/kolab_addressbook/drivers/carddav/carddav_contacts_driver.php
--- a/plugins/kolab_addressbook/drivers/carddav/carddav_contacts_driver.php
+++ b/plugins/kolab_addressbook/drivers/carddav/carddav_contacts_driver.php
@@ -118,7 +118,7 @@
$name = '';
if ($source && ($book = $this->get_address_book($source))) {
- $name = $book->get_name();
+ $name = $book->get_foldername();
$folder = $book->storage;
}
@@ -198,12 +198,11 @@
return [
'id' => $id,
'name' => $abook->get_name(),
- 'listname' => $abook->get_foldername(),
+ 'listname' => $abook->get_name(),
'readonly' => $abook->readonly,
'rights' => $abook->rights,
'groups' => $abook->groups,
'undelete' => $abook->undelete && $this->rc->config->get('undo_timeout'),
- 'realname' => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name
'group' => $abook->get_namespace(),
'subscribed' => $abook->is_subscribed(),
'carddavurl' => $abook->get_carddav_url(),
diff --git a/plugins/kolab_addressbook/kolab_addressbook.js b/plugins/kolab_addressbook/kolab_addressbook.js
--- a/plugins/kolab_addressbook/kolab_addressbook.js
+++ b/plugins/kolab_addressbook/kolab_addressbook.js
@@ -121,11 +121,12 @@
{
var source = !this.env.group ? this.env.source : null,
sources = this.env.address_sources || {},
- props = source && sources[source] && sources[source].kolab ? sources[source] : { removable: false, rights: '' };
+ props = source && sources[source] && sources[source].kolab ? sources[source] : { removable: false, rights: '' },
+ can_delete = props.rights.indexOf('x') >= 0 || props.rights.indexOf('a') >= 0;
this.enable_command('book-create', true);
- this.enable_command('book-edit', props.rights.indexOf('a') >= 0);
- this.enable_command('book-delete', props.rights.indexOf('x') >= 0 || props.rights.indexOf('a') >= 0);
+ this.enable_command('book-edit', can_delete);
+ this.enable_command('book-delete', can_delete);
this.enable_command('book-remove', props.removable);
this.enable_command('book-showurl', !!props.carddavurl || source == this.env.kolab_addressbook_carddav_ldap);
};
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -862,7 +862,6 @@
return $args;
}
-
/**
* Handler for plugin actions
*/
@@ -1014,7 +1013,6 @@
$this->rc->output->send();
}
-
/**
* Handler for address book delete action (AJAX)
*/
diff --git a/plugins/libkolab/config.inc.php.dist b/plugins/libkolab/config.inc.php.dist
--- a/plugins/libkolab/config.inc.php.dist
+++ b/plugins/libkolab/config.inc.php.dist
@@ -76,6 +76,12 @@
// possible units: s, m, h, d, w
$config['kolab_users_cache_ttl'] = '10d';
+// Enable sharing of DAV folders. Default: null (disabled)
+// Possible options:
+// 'acl': Use standard DAV ACL permissions system
+// 'sharing': Use draft-pot-webdav-resource-sharing standard (for Cyrus-DAV v3)
+$config['kolab_dav_sharing'] = null;
+
// JSON-RPC endpoint configuration of the Bonnie web service providing historic data for groupware objects
$config['kolab_bonnie_api'] = null;
/*
diff --git a/plugins/libkolab/lib/kolab_dav_acl.php b/plugins/libkolab/lib/kolab_dav_acl.php
--- a/plugins/libkolab/lib/kolab_dav_acl.php
+++ b/plugins/libkolab/lib/kolab_dav_acl.php
@@ -73,8 +73,7 @@
}
// Return if not folder admin
- $myrights = explode(',', $myrights);
- if (!in_array('admin', $myrights) && !in_array('write-acl', $myrights)) {
+ if (strpos($myrights, 'a') === false) {
return null;
}
diff --git a/plugins/libkolab/lib/kolab_dav_client.php b/plugins/libkolab/lib/kolab_dav_client.php
--- a/plugins/libkolab/lib/kolab_dav_client.php
+++ b/plugins/libkolab/lib/kolab_dav_client.php
@@ -28,6 +28,15 @@
public const ACL_PRINCIPAL_AUTH = 'authenticated';
public const ACL_PRINCIPAL_UNAUTH = 'unauthenticated';
+ public const INVITE_ACCEPTED = 'accepted';
+ public const INVITE_DECLINED = 'declined';
+
+ public const SHARING_READ = 'read';
+ public const SHARING_READ_WRITE = 'read-write';
+ public const SHARING_NO_ACCESS = 'no-access';
+ public const SHARING_OWNER = 'shared-owner';
+ public const SHARING_NOT_SHARED = 'not-shared';
+
public $url;
protected $user;
@@ -118,19 +127,17 @@
}
/**
- * Discover DAV home (root) collection of specified type.
- *
- * @param string $component Component to filter by (VEVENT, VTODO, VCARD)
+ * Discover (common) DAV home collections.
*
- * @return string|false Home collection location or False on error
+ * @return array|false Homes locations or False on error
*/
- public function discover($component = 'VEVENT')
+ public function discover()
{
if ($cache = $this->get_cache()) {
- $cache_key = "discover.{$component}." . md5($this->url);
+ $cache_key = "discover." . md5($this->url);
- if ($response = $cache->get($cache_key)) {
- return $response;
+ if ($homes = $cache->get($cache_key)) {
+ return $homes;
}
}
@@ -164,22 +171,12 @@
$principal_href = substr($principal_href, strlen($path));
}
- $homes = [
- 'VEVENT' => 'calendar-home-set',
- 'VTODO' => 'calendar-home-set',
- 'VCARD' => 'addressbook-home-set',
- ];
-
- $ns = [
- 'VEVENT' => 'caldav',
- 'VTODO' => 'caldav',
- 'VCARD' => 'carddav',
- ];
-
$body = '<?xml version="1.0" encoding="utf-8"?>'
- . '<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:' . $ns[$component] . '">'
+ . '<d:propfind xmlns:d="DAV:" xmlns:cal="urn:ietf:params:xml:ns:caldav" xmlns:card="urn:ietf:params:xml:ns:carddav">'
. '<d:prop>'
- . '<c:' . $homes[$component] . ' />'
+ . '<cal:calendar-home-set/>'
+ . '<card:addressbook-home-set/>'
+ . '<d:notification-URL/>'
. '</d:prop>'
. '</d:propfind>';
@@ -190,27 +187,54 @@
}
$elements = $response->getElementsByTagName('response');
+ $homes = [];
- foreach ($elements as $element) {
- foreach ($element->getElementsByTagName($homes[$component]) as $prop) {
- $root_href = $prop->nodeValue;
- break;
+ if ($element = $response->getElementsByTagName('response')->item(0)) {
+ if ($prop = $element->getElementsByTagName('prop')->item(0)) {
+ foreach ($prop->childNodes as $home) {
+ if ($home->firstChild && $home->firstChild->localName == 'href') {
+ $href = $home->firstChild->nodeValue;
+
+ if ($path && strpos($href, $path) === 0) {
+ $href = substr($href, strlen($path));
+ }
+
+ $homes[$home->localName] = $href;
+ }
+ }
}
}
- if (empty($root_href)) {
- return false;
+ if ($cache) {
+ $cache->set($cache_key, $homes);
}
- if ($path && strpos($root_href, $path) === 0) {
- $root_href = substr($root_href, strlen($path));
- }
+ return $homes;
+ }
- if ($cache) {
- $cache->set($cache_key, $root_href);
+ /**
+ * Get user home folder of specified type
+ *
+ * @param string $type Home type or component name
+ *
+ * @return string|null Folder location href
+ */
+ public function getHome($type)
+ {
+ $options = [
+ 'VEVENT' => 'calendar-home-set',
+ 'VTODO' => 'calendar-home-set',
+ 'VCARD' => 'addressbook-home-set',
+ 'NOTIFICATION' => 'notification-URL',
+ ];
+
+ $homes = $this->discover();
+
+ if (is_array($homes) && isset($options[$type])) {
+ return $homes[$options[$type]] ?? null;
}
- return $root_href;
+ return null;
}
/**
@@ -222,9 +246,9 @@
*/
public function listFolders($component = 'VEVENT')
{
- $root_href = $this->discover($component);
+ $root_href = $this->getHome($component);
- if ($root_href === false) {
+ if ($root_href === null) {
return false;
}
@@ -243,6 +267,8 @@
. '<d:prop>'
. '<d:resourcetype />'
. '<d:displayname />'
+ . '<d:share-access/>' // draft-pot-webdav-resource-sharing-04
+ . '<d:owner/>' // RFC 3744 (ACL)
. '<cs:getctag />'
. $props
. '</d:prop>'
@@ -319,7 +345,7 @@
*/
public function delete($location)
{
- $response = $this->request($location, 'DELETE', '', ['Depth' => 1, 'Prefer' => 'return-minimal']);
+ $response = $this->request($location, 'DELETE');
return $response !== false;
}
@@ -369,6 +395,9 @@
. '<d:current-user-privilege-set/>'
. '<d:resourcetype/>'
. '<d:displayname/>'
+ . '<d:share-access/>' // draft-pot-webdav-resource-sharing-04
+ . '<d:owner/>' // RFC 3744 (ACL)
+ . '<d:invite/>'
. '<k:alarms/>'
. '</d:prop>'
. '</d:propfind>';
@@ -511,6 +540,112 @@
return $response !== false;
}
+ /**
+ * Fetch DAV notifications
+ *
+ * @param ?array $types Notification types to return
+ *
+ * @return false|array Notification objects on success, False on error
+ */
+ public function listNotifications($types = [])
+ {
+ $root_href = $this->getHome('NOTIFICATION');
+
+ if ($root_href === null) {
+ return false;
+ }
+
+ $body = '<?xml version="1.0" encoding="utf-8"?>'
+ . ' <d:propfind xmlns:d="DAV:">'
+ . '<d:prop>'
+ . '<d:notificationtype/>'
+ . '</d:prop>'
+ . '</d:propfind>';
+
+ $response = $this->request($root_href, 'PROPFIND', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
+
+ if (empty($response)) {
+ return false;
+ }
+
+ $objects = [];
+
+ foreach ($response->getElementsByTagName('response') as $element) {
+ $type = $element->getElementsByTagName('notificationtype')->item(0);
+ if ($type && $type->firstChild) {
+ $type = $type->firstChild->localName;
+
+ if (empty($types) || in_array($type, $types)) {
+ $href = $element->getElementsByTagName('href')->item(0);
+ if ($notification = $this->getNotification($href->nodeValue)) {
+ $objects[] = $notification;
+ }
+ }
+ }
+ }
+
+ return $objects;
+ }
+
+ /**
+ * Get a single DAV notification
+ *
+ * @param string $location Notification href
+ *
+ * @return false|array Notification data on success, False on error
+ */
+ public function getNotification($location)
+ {
+ $response = $this->request($location, 'GET', '', ['Content-Type' => 'application/davnotification+xml']);
+
+ if (empty($response)) {
+ return false;
+ }
+
+ // Note: Cyrus implements draft-pot-webdav-resource-sharing v02, not v04, even v02 support
+ // is broken in some places
+
+ $result = [
+ 'href' => $location,
+ ];
+
+ if ($access = $response->getElementsByTagName('access')->item(0)) {
+ $access = $access->firstChild;
+ $result['access'] = $access->localName; // 'read' or 'read-write'
+ }
+
+ foreach (['invite-noresponse', 'invite-accepted', 'invite-declined', 'invite-invalid', 'invite-deleted'] as $name) {
+ if ($node = $response->getElementsByTagName($name)->item(0)) {
+ $result['status'] = str_replace('invite-', '', $node->localName);
+ }
+ }
+
+ if ($organizer = $response->getElementsByTagName('organizer')->item(0)) {
+ if ($href = $organizer->getElementsByTagName('href')->item(0)) {
+ $result['organizer'] = $href->nodeValue;
+ }
+ // There should be also 'displayname', but Cyrus uses 'common-name',
+ // we'll ignore it for now anyway.
+ } elseif ($organizer = $response->getElementsByTagName('principal')->item(0)) {
+ if ($href = $organizer->getElementsByTagName('href')->item(0)) {
+ $result['organizer'] = $href->nodeValue;
+ }
+ // There should be also 'displayname', but Cyrus uses 'common-name',
+ // we'll ignore it for now anyway.
+ }
+
+ // Cyrus uses 'summary', but it's 'comment' in more recent standard
+ foreach (['dtstamp', 'summary', 'comment'] as $name) {
+ if ($node = $response->getElementsByTagName($name)->item(0)) {
+ $result[$name] = $node->nodeValue;
+ }
+ }
+
+ // In more recent standard there might be also 'displayname' and 'resourcetype' props
+
+ return $result;
+ }
+
/**
* Fetch DAV objects metadata (ETag, href) a folder
*
@@ -625,6 +760,35 @@
return $objects;
}
+ /**
+ * Accept/Deny a share invitation (draft-pot-webdav-resource-sharing)
+ *
+ * @param string $location Notification location
+ * @param string $action Reply action ('accepted' or 'declined')
+ * @param array $props Additional reply properties (slug, comment)
+ *
+ * @return bool True on success, False on error
+ */
+ public function inviteReply($location, $action = self::INVITE_ACCEPTED, $props = [])
+ {
+ $reply = '<d:invite-' . $action . '/>';
+
+ // Note: <create-in> and <slug> are ignored by Cyrus
+
+ if (!empty($props['comment'])) {
+ $reply .= '<d:comment>' . htmlspecialchars($props['comment'], ENT_XML1, 'UTF-8') . '</d:comment>';
+ }
+
+ $headers = ['Content-Type' => 'application/davsharing+xml; charset=utf-8'];
+
+ $body = '<?xml version="1.0" encoding="utf-8"?>'
+ . '<d:invite-reply xmlns:d="DAV:">' . $reply . '</d:invite-reply>';
+
+ $response = $this->request($location, 'POST', $body, $headers);
+
+ return $response !== false;
+ }
+
/**
* Set ACL on a DAV folder
*
@@ -689,6 +853,47 @@
return $response !== false;
}
+ /**
+ * Share a reasource (draft-pot-webdav-resource-sharing)
+ *
+ * @param string $location Resource location
+ * @param array $sharees Sharees list
+ *
+ * @return bool True on success, False on error
+ */
+ public function shareResource($location, $sharees = [])
+ {
+ $props = '';
+
+ foreach ($sharees as $href => $sharee) {
+ $props .= '<d:sharee>'
+ . '<d:href>' . htmlspecialchars($href, ENT_XML1, 'UTF-8') . '</d:href>'
+ . '<d:share-access><d:' . ($sharee['access'] ?? self::SHARING_NO_ACCESS) . '/></d:share-access>'
+ . '<d:' . ($sharee['status'] ?? 'noresponse') . '/>';
+
+ if (isset($sharee['comment']) && strlen($sharee['comment'])) {
+ $props .= '<d:comment>' . htmlspecialchars($sharee['comment'], ENT_XML1, 'UTF-8') . '</d:comment>';
+ }
+
+ if (isset($sharee['displayname']) && strlen($sharee['displayname'])) {
+ $props .= '<d:prop><d:displayname>'
+ . htmlspecialchars($sharee['comment'], ENT_XML1, 'UTF-8')
+ . '</d:displayname></d:prop>';
+ }
+
+ $props .= '</d:sharee>';
+ }
+
+ $headers = ['Content-Type' => 'application/davsharing+xml; charset=utf-8'];
+
+ $body = '<?xml version="1.0" encoding="utf-8"?>'
+ . '<d:share-resource xmlns:d="DAV:">' . $props . '</d:share-resource>';
+
+ $response = $this->request($location, 'POST', $body, $headers);
+
+ return $response !== false;
+ }
+
/**
* Parse XML content
*/
@@ -774,8 +979,7 @@
$types = [];
if ($type_element = $element->getElementsByTagName('resourcetype')->item(0)) {
foreach ($type_element->childNodes as $node) {
- $_type = explode(':', $node->nodeName);
- $types[] = count($_type) > 1 ? $_type[1] : $_type[0];
+ $types[] = $node->localName;
}
}
@@ -852,6 +1056,57 @@
$result['myrights'] = $rights;
}
+ if ($owner = $element->getElementsByTagName('owner')->item(0)) {
+ if ($owner->firstChild) {
+ $result['owner'] = $owner->firstChild->nodeValue;
+ }
+ }
+
+ // 'share-access' from draft-pot-webdav-resource-sharing
+ if ($share = $element->getElementsByTagName('share-access')->item(0)) {
+ if ($share->firstChild) {
+ $result['share-access'] = $share->firstChild->localName;
+ }
+ }
+
+ // 'invite' from draft-pot-webdav-resource-sharing
+ if ($invite_element = $element->getElementsByTagName('invite')->item(0)) {
+ $invites = [];
+ foreach ($invite_element->childNodes as $sharee) {
+ $href = $sharee->getElementsByTagName('href')->item(0)->nodeValue;
+ $status = 'noresponse';
+
+ if ($comment = $sharee->getElementsByTagName('comment')->item(0)) {
+ $comment = $comment->nodeValue;
+ }
+
+ if ($displayname = $sharee->getElementsByTagName('displayname')->item(0)) {
+ $displayname = $displayname->nodeValue;
+ }
+
+ if ($access = $sharee->getElementsByTagName('share-access')->item(0)) {
+ $access = $access->firstChild->localName;
+ } else {
+ $access = self::SHARING_NOT_SHARED;
+ }
+
+ foreach (['invite-noresponse', 'invite-accepted', 'invite-declined', 'invite-invalid', 'invite-deleted'] as $name) {
+ if ($node = $sharee->getElementsByTagName($name)->item(0)) {
+ $status = str_replace('invite-', '', $node->localName);
+ }
+ }
+
+ $invites[$href] = [
+ 'access' => $access,
+ 'status' => $status,
+ 'comment' => $comment,
+ 'displayname' => $displayname,
+ ];
+ }
+
+ $result['invite'] = $invites;
+ }
+
foreach (['alarms'] as $tag) {
if ($el = $element->getElementsByTagName($tag)->item(0)) {
if (strlen($el->nodeValue) > 0) {
diff --git a/plugins/libkolab/lib/kolab_dav_acl.php b/plugins/libkolab/lib/kolab_dav_sharing.php
copy from plugins/libkolab/lib/kolab_dav_acl.php
copy to plugins/libkolab/lib/kolab_dav_sharing.php
--- a/plugins/libkolab/lib/kolab_dav_acl.php
+++ b/plugins/libkolab/lib/kolab_dav_sharing.php
@@ -1,7 +1,7 @@
<?php
/*
- * DAV Access Control Lists Management
+ * DAV sharing based on draft-pot-webdav-resource-sharing standard
*
* Copyright (C) Apheleia IT <contact@aphelaia-it.ch>
*
@@ -20,27 +20,22 @@
*/
/**
- * A class providing DAV ACL management functionality
+ * A class providing DAV sharing management functionality
*/
-class kolab_dav_acl
+class kolab_dav_sharing
{
- public const PRIVILEGE_ALL = 'all';
public const PRIVILEGE_READ = 'read';
- public const PRIVILEGE_FREE_BUSY = 'read-free-busy';
public const PRIVILEGE_WRITE = 'write';
/** @var ?kolab_storage_dav_folder $folder Current folder */
private static $folder;
/** @var array Special principals */
- private static $specials = [
- kolab_dav_client::ACL_PRINCIPAL_AUTH,
- kolab_dav_client::ACL_PRINCIPAL_ALL,
- ];
+ private static $specials = [];
/**
- * Handler for actions from the ACL dialog (AJAX)
+ * Handler for actions from the Sharing dialog (AJAX)
*/
public static function actions()
{
@@ -72,9 +67,8 @@
return null;
}
- // Return if not folder admin
- $myrights = explode(',', $myrights);
- if (!in_array('admin', $myrights) && !in_array('write-acl', $myrights)) {
+ // Return if not folder admin nor can share
+ if (strpos($myrights, 'a') === false && strpos($myrights, '1') === false) {
return null;
}
@@ -103,7 +97,7 @@
}
/**
- * Creates ACL rights table
+ * Creates sharees table
*
* @param array $attrib Template object attributes
*
@@ -124,7 +118,7 @@
}
/**
- * Creates ACL rights form (rights list part)
+ * Creates sharing form (rights list part)
*
* @param array $attrib Template object attributes
*
@@ -140,13 +134,8 @@
$rights = [
self::PRIVILEGE_READ,
self::PRIVILEGE_WRITE,
- self::PRIVILEGE_ALL,
];
- if (self::$folder->type == 'event' || self::$folder->type == 'task') {
- array_unshift($rights, self::PRIVILEGE_FREE_BUSY);
- }
-
foreach ($rights as $right) {
$id = "acl{$right}";
$label = $rcmail->gettext($rcmail->text_exists("libkolab.acllong{$right}") ? "libkolab.acllong{$right}" : "libkolab.acl{$right}");
@@ -160,7 +149,7 @@
}
/**
- * Creates ACL rights form (user part)
+ * Creates sharing form (user part)
*
* @param array $attrib Template object attributes
*
@@ -188,19 +177,24 @@
$fields[$type] = html::label(['for' => 'id' . $type], $rcmail->gettext("libkolab.{$type}"));
}
- // Create list with radio buttons
$ul = '';
- foreach ($fields as $key => $val) {
- $radio = new html_radiobutton(['name' => 'usertype']);
- $radio = $radio->show($key == 'user' ? 'user' : '', ['value' => $key, 'id' => 'id' . $key]);
- $ul .= html::tag('li', null, $radio . $val);
+
+ if (count($fields) == 1) {
+ $ul = html::tag('li', null, $fields['user']);
+ } else {
+ // Create list with radio buttons
+ foreach ($fields as $key => $val) {
+ $radio = new html_radiobutton(['name' => 'usertype']);
+ $radio = $radio->show($key == 'user' ? 'user' : '', ['value' => $key, 'id' => 'id' . $key]);
+ $ul .= html::tag('li', null, $radio . $val);
+ }
}
return html::tag('ul', ['id' => 'usertype', 'class' => $class], $ul, html::$common_attrib);
}
/**
- * Creates ACL rights table
+ * Creates sharees table
*
* @param array $attrib Template object attributes
*
@@ -211,11 +205,7 @@
$rcmail = rcmail::get_instance();
// Get ACL for the folder
- $acl = self::$folder->get_acl();
-
- // Remove 'self' entry
- // TODO: Do it only on folders user == owner
- unset($acl[kolab_dav_client::ACL_PRINCIPAL_SELF]);
+ $acl = self::$folder->get_sharees();
// Sort the list by username
uksort($acl, 'strnatcasecmp');
@@ -236,13 +226,8 @@
$cols = [
self::PRIVILEGE_READ,
self::PRIVILEGE_WRITE,
- self::PRIVILEGE_ALL,
];
- if (self::$folder->type == 'event' || self::$folder->type == 'task') {
- array_unshift($cols, self::PRIVILEGE_FREE_BUSY);
- }
-
// Create the table
$attrib['noheader'] = true;
$table = new html_table($attrib);
@@ -255,12 +240,12 @@
$table->add_header(['class' => "acl{$right}", 'title' => $label], $label);
}
- foreach ($acl as $user => $rights) {
- // We do not support 'deny' privileges
- if (!empty($rights['deny']) || empty($rights['grant'])) {
+ foreach ($acl as $user => $sharee) {
+ if (!in_array($sharee['access'], [kolab_dav_client::SHARING_READ, kolab_dav_client::SHARING_READ_WRITE])) {
continue;
}
+ $access = $sharee['access'];
$userid = rcube_utils::html_identifier($user);
$title = null;
@@ -275,14 +260,24 @@
html::a(['id' => 'rcmlinkrow' . $userid], rcube::Q($username))
);
- $rights = self::from_dav($rights['grant']);
-
+ $rights = [];
foreach ($cols as $right) {
- $class = in_array($right, $rights) ? 'enabled' : 'disabled';
+ $enabled = (
+ $right == self::PRIVILEGE_READ
+ && ($access == kolab_dav_client::SHARING_READ || $access == kolab_dav_client::SHARING_READ_WRITE)
+ ) || (
+ $right == self::PRIVILEGE_WRITE
+ && $access == kolab_dav_client::SHARING_READ_WRITE
+ );
+ $class = $enabled ? 'enabled' : 'disabled';
$table->add('acl' . $right . ' ' . $class, '<span></span>');
+
+ if ($enabled) {
+ $rights[] = $right;
+ }
}
- $js_table[$userid] = $rights;
+ $js_table[$userid] = $access;
}
$rcmail->output->set_env('acl', $js_table);
@@ -292,7 +287,7 @@
}
/**
- * Handler for ACL update/create action
+ * Handler for sharee update/create action
*/
private static function action_save()
{
@@ -313,7 +308,7 @@
return;
}
- $folder_acl = $folder->get_acl();
+ $sharees = $folder->get_sharees();
$acl = explode(',', $acl);
foreach ($users as $user) {
@@ -339,19 +334,34 @@
continue;
}
- if ($user != $self && $username != $self) {
- $folder_acl[$user] = ['grant' => self::to_dav($acl), 'deny' => []];
- $updates[] = [
- 'id' => rcube_utils::html_identifier($user),
- 'username' => $user,
- 'display' => $username,
- 'acl' => $acl,
- 'old' => $oldid,
- ];
+ if ($user == $self && $username == $self) {
+ continue;
}
+
+ if (in_array(self::PRIVILEGE_WRITE, $acl)) {
+ $access = kolab_dav_client::SHARING_READ_WRITE;
+ } elseif (in_array(self::PRIVILEGE_READ, $acl)) {
+ $access = kolab_dav_client::SHARING_READ;
+ } else {
+ $access = kolab_dav_client::SHARING_NO_ACCESS;
+ }
+
+ if (isset($sharees[$user])) {
+ $sharees[$user]['access'] = $access;
+ } else {
+ $sharees[$user] = ['access' => $access];
+ }
+
+ $updates[] = [
+ 'id' => rcube_utils::html_identifier($user),
+ 'username' => $user,
+ 'display' => $username,
+ 'acl' => $acl,
+ 'old' => $oldid,
+ ];
}
- if (count($updates) > 0 && $folder->set_acl($folder_acl)) {
+ if (count($updates) > 0 && $folder->set_sharees($sharees)) {
foreach ($updates as $command) {
$rcmail->output->command('acl_update', $command);
}
@@ -362,7 +372,7 @@
}
/**
- * Handler for ACL delete action
+ * Handler for sharee delete action
*/
private static function action_delete()
{
@@ -378,14 +388,14 @@
return;
}
- $folder_acl = $folder->get_acl();
+ $sharees = $folder->get_sharees();
foreach ($users as $user) {
$user = trim($user);
- unset($folder_acl[$user]);
+ $sharees[$user]['access'] = kolab_dav_client::SHARING_NO_ACCESS;
}
- if ($folder->set_acl($folder_acl)) {
+ if ($folder->set_sharees($sharees)) {
foreach ($users as $user) {
$rcmail->output->command('acl_remove_row', rcube_utils::html_identifier($user));
}
@@ -396,87 +406,6 @@
}
}
- /**
- * Convert DAV privileges into simplified "groups"
- *
- * @param array $list ACL privileges
- *
- * @return array
- */
- private static function from_dav($list)
- {
- /*
- DAV ACL is a complicated system, we don't really want to implement it in full,
- we rather keep it simple and similar to what we have for mail folders.
- Therefore we implement it like this:
-
- - free-busy:
- - CALDAV:read-free-busy (not for addressbooks)
- - read:
- - DAV:read
- - write:
- - DAV:write-content
- - CYRUS:remove-resource
- - all (all the above plus administration):
- - DAV:all
-
- Reference: https://datatracker.ietf.org/doc/html/rfc3744
- Reference: https://www.cyrusimap.org/imap/download/installation/http/caldav.html#calendar-acl
- */
-
- // TODO: Don't use CYRUS:remove-resource on non-Cyrus servers
-
- $result = [];
-
- if ($all = in_array('all', $list)) {
- $result[] = self::PRIVILEGE_ALL;
- }
-
- if ($all || in_array('read-free-busy', $list) || in_array('read', $list)) {
- $result[] = self::PRIVILEGE_FREE_BUSY;
- }
-
- if ($all || in_array('read', $list)) {
- $result[] = self::PRIVILEGE_READ;
- }
-
- if ($all || (in_array('write-content', $list) && in_array('remove-resource', $list)) || in_array('write', $list)) {
- $result[] = self::PRIVILEGE_WRITE;
- }
-
- return $result;
- }
-
- /**
- * Convert simplified privileges into ACL privileges
- *
- * @param array $list ACL privileges
- *
- * @return array
- * @see self::from_dav()
- */
- private static function to_dav($list)
- {
- $result = [];
-
- if (in_array(self::PRIVILEGE_ALL, $list)) {
- return ['all'];
- }
-
- if (in_array(self::PRIVILEGE_WRITE, $list)) {
- $result[] = 'read';
- $result[] = 'write-content';
- // TODO: Don't use CYRUS:remove-resource on non-Cyrus servers
- $result[] = 'remove-resource';
- } elseif (in_array(self::PRIVILEGE_READ, $list)) {
- $result[] = 'read';
- } elseif (in_array(self::PRIVILEGE_FREE_BUSY, $list)) {
- $result[] = 'read-free-busy';
- }
-
- return $result;
- }
-
/**
* Username realm detection.
*
diff --git a/plugins/libkolab/lib/kolab_storage_dav.php b/plugins/libkolab/lib/kolab_storage_dav.php
--- a/plugins/libkolab/lib/kolab_storage_dav.php
+++ b/plugins/libkolab/lib/kolab_storage_dav.php
@@ -247,8 +247,10 @@
// sanity checks
if (!isset($prop['name']) || !is_string($prop['name']) || !strlen($prop['name'])) {
- self::$last_error = 'cannotbeempty';
- return false;
+ if (empty($prop['id'])) {
+ self::$last_error = 'cannotbeempty';
+ return false;
+ }
} elseif (strlen($prop['name']) > 256) {
self::$last_error = 'nametoolong';
return false;
diff --git a/plugins/libkolab/lib/kolab_storage_dav_folder.php b/plugins/libkolab/lib/kolab_storage_dav_folder.php
--- a/plugins/libkolab/lib/kolab_storage_dav_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_dav_folder.php
@@ -101,16 +101,20 @@
*/
public function get_owner($fully_qualified = false)
{
- // return cached value
if (isset($this->owner)) {
return $this->owner;
}
- $rcube = rcube::get_instance();
- $this->owner = $rcube->get_user_name();
- $this->valid = true;
+ // TODO: Support global shared folders?
- // TODO: Support shared folders
+ if (isset($this->attributes['owner'])) {
+ // Assume username/email is the last part of a principal href
+ $path = explode('/', trim($this->attributes['owner'], '/'));
+ $this->owner = end($path);
+ } else {
+ $rcube = rcube::get_instance();
+ $this->owner = $rcube->get_user_name();
+ }
return $this->owner;
}
@@ -137,7 +141,15 @@
*/
public function get_namespace()
{
- // TODO: Support shared folders
+ // TODO: Support for global shared folders?
+
+ $owner = $this->get_owner();
+ $user = rcube::get_instance()->get_user_name();
+
+ if ($owner != $user) {
+ return 'other';
+ }
+
return 'personal';
}
@@ -148,7 +160,13 @@
*/
public function get_name()
{
- return kolab_storage_dav::object_name($this->attributes['name']);
+ $name = $this->attributes['name'];
+
+ if ($this->get_namespace() == 'other') {
+ $name = $this->get_owner() . ': ' . $name;
+ }
+
+ return $name;
}
/**
@@ -244,8 +262,85 @@
$this->get_folder_info();
}
+ $rights = $this->attributes['myrights'] ?? [];
+ $types = $this->attributes['resource_type'] ?? [];
+ $acl = 'lr';
+
+ // Convert DAV privileges to IMAP-like ACL format
+ foreach ($rights as $right) {
+ switch ($right) {
+ case 'all':
+ $acl .= 'lrswikxteav';
+ break;
+ case 'admin':
+ $acl .= 'a';
+ break;
+ case 'read':
+ case 'read-free-busy':
+ case 'read-current-user-privilege-set':
+ $acl .= 'lr';
+ break;
+ case 'write':
+ //case 'write-properties':
+ //case 'write-content':
+ $acl .= 'wni';
+ break;
+ case 'bind':
+ $acl .='pk';
+ break;
+ case 'unbind':
+ $acl .= 'xte';
+ break;
+ case 'share':
+ $acl .= '1';
+ break;
+ }
+ }
+
+ // Shared collection can be deleted even if myrights says otherwise
+ if (in_array('shared', $types)) {
+ $acl .= 'x';
+ }
+
+ if (!empty($this->attributes['share-access'])) {
+ if ($this->attributes['share-access'] === kolab_dav_client::SHARING_OWNER) {
+ $acl .= 'a';
+ } elseif ($this->attributes['share-access'] === kolab_dav_client::SHARING_READ
+ || $this->attributes['share-access'] === kolab_dav_client::SHARING_READ_WRITE
+ ) {
+ $acl .= 'x';
+ }
+ }
+
// Note: We return a string for compat. with the parent class
- return implode(',', $this->attributes['myrights'] ?? []);
+ return implode('', array_unique(str_split($acl)));
+ }
+
+ /**
+ * Get the invitations' sharees
+ *
+ * @return array Sharees list
+ */
+ public function get_sharees()
+ {
+ if (!isset($this->attributes['invite'])) {
+ $this->get_folder_info();
+ }
+
+ $sharees = [];
+ foreach ($this->attributes['invite'] ?? [] as $principal => $sharee) {
+ // Convert a principal href into a user identifier
+ if (stripos($principal, 'mailto:') === 0) {
+ $principal = substr($principal, 7);
+ } elseif (strpos($principal, '/') !== false) {
+ $tokens = explode('/', trim($principal, '/'));
+ $principal = end($tokens);
+ }
+
+ $sharees[$principal] = $sharee;
+ }
+
+ return $sharees;
}
/**
@@ -856,6 +951,42 @@
return $this->dav->setACL($this->href, $request);
}
+ /**
+ * Set folder sharing invites.
+ *
+ * @param array $sharees Sharees list
+ *
+ * @return bool True if successful, false on error
+ */
+ public function set_sharees($sharees)
+ {
+ if (!$this->valid) {
+ return false;
+ }
+
+ // In Kolab the users (principals) are under /principals/user/<user>
+ // TODO: This might need to be configurable or discovered somehow
+ $path = '/principals/user/';
+ if ($host_path = parse_url($this->dav->url, PHP_URL_PATH)) {
+ $path = '/' . trim($host_path, '/') . $path;
+ }
+
+ $request = [];
+
+ foreach ($sharees as $principal => $sharee) {
+ // Convert a user identifier into a principal href or mailto: href
+ if (strpos($principal, '@')) {
+ $principal = 'mailto:' . $principal;
+ } else {
+ $principal = $path . $principal;
+ }
+
+ $request[$principal] = $sharee;
+ }
+
+ return $this->dav->shareResource($this->href, $request);
+ }
+
/**
* Return folder name as string representation of this object
*
diff --git a/plugins/libkolab/lib/kolab_utils.php b/plugins/libkolab/lib/kolab_utils.php
--- a/plugins/libkolab/lib/kolab_utils.php
+++ b/plugins/libkolab/lib/kolab_utils.php
@@ -27,14 +27,15 @@
{
public static function folder_form($form, $folder, $domain, $hidden_fields = [], $no_acl = false)
{
- $rcmail = rcube::get_instance();
+ $rcmail = rcmail::get_instance();
// add folder ACL tab
if (!$no_acl && $folder) {
if (($folder instanceof kolab_storage_dav_folder) || (is_string($folder) && strlen($folder))) {
+ $sharing_content = self::folder_acl_form($folder);
$form['sharing'] = [
'name' => rcube::Q($rcmail->gettext('libkolab.tabsharing')),
- 'content' => self::folder_acl_form($folder),
+ 'content' => $sharing_content,
];
}
}
@@ -76,7 +77,7 @@
*
* @param string|kolab_storage_dav_folder $folder DAV folder object or IMAP folder name
*
- * @return string HTML content
+ * @return ?string HTML content
*/
private static function folder_acl_form($folder)
{
@@ -84,7 +85,7 @@
return self::folder_dav_acl_form($folder);
}
- $rcmail = rcube::get_instance();
+ $rcmail = rcmail::get_instance();
$storage = $rcmail->get_storage();
$options = $storage->folder_info($folder);
@@ -109,11 +110,20 @@
*
* @param kolab_storage_dav_folder $folder DAV folder object
*
- * @return string HTML content
+ * @return ?string HTML content
*/
private static function folder_dav_acl_form($folder)
{
- if ($form = kolab_dav_acl::form($folder)) {
+ $rcmail = rcmail::get_instance();
+ $sharing = $rcmail->config->get('kolab_dav_sharing');
+
+ if (!$sharing) {
+ return null;
+ }
+
+ $class = 'kolab_dav_' . $sharing;
+
+ if ($form = $class::form($folder)) {
return $form;
}
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
--- a/plugins/libkolab/libkolab.php
+++ b/plugins/libkolab/libkolab.php
@@ -35,6 +35,8 @@
*/
public function init()
{
+ $rcmail = rcube::get_instance();
+
// load local config
$this->load_config();
$this->require_plugin('libcalendaring');
@@ -51,10 +53,12 @@
$this->add_hook('folder_mod', ['kolab_storage', 'folder_mod']);
// For DAV ACL
- $this->register_action('plugin.davacl', 'kolab_dav_acl::actions');
- // $this->register_action('plugin.davacl-autocomplete', 'kolab_dav_acl::autocomplete');
+ if ($sharing = $rcmail->config->get('kolab_dav_sharing')) {
+ $class = 'kolab_dav_' . $sharing;
+ $this->register_action('plugin.davacl', "$class::actions");
+ $this->register_action('plugin.davacl-autocomplete', "$class::autocomplete");
+ }
- $rcmail = rcube::get_instance();
try {
kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
} catch (Exception $e) {
diff --git a/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php b/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php
--- a/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php
+++ b/plugins/tasklist/drivers/caldav/tasklist_caldav_driver.php
@@ -97,16 +97,10 @@
$alarms = true;
} else {
$alarms = false;
- $rights = 'lr';
$editable = false;
- if ($myrights = $folder->get_myrights()) {
- $rights = $myrights;
- if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) {
- $editable = strpos($rights, 'i') !== false;
- }
- }
- $info = $folder->get_folder_info();
- $norename = !$editable || $info['norename'] || $info['protected'];
+ $rights = $folder->get_myrights();
+ $editable = strpos($rights, 'i') !== false;
+ $norename = strpos($rights, 'x') === false;
}
$list_id = $folder->id;
@@ -114,7 +108,7 @@
return [
'id' => $list_id,
'name' => $folder->get_name(),
- 'listname' => $folder->get_foldername(),
+ 'listname' => $folder->get_name(),
'editname' => $folder->get_foldername(),
'color' => $folder->get_color('0000CC'),
'showalarms' => $prefs[$list_id]['showalarms'] ?? $alarms,
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js
--- a/plugins/tasklist/tasklist.js
+++ b/plugins/tasklist/tasklist.js
@@ -177,7 +177,7 @@
});
tasklists_widget.addEventListener('select', function(node) {
var id = $(this).data('id');
- rcmail.enable_command('list-edit', me.has_permission(me.tasklists[node.id], 'wa'));
+ rcmail.enable_command('list-edit', me.has_permission(me.tasklists[node.id], 'xwa'));
rcmail.enable_command('list-delete', me.has_permission(me.tasklists[node.id], 'xa'));
rcmail.enable_command('list-import', me.has_permission(me.tasklists[node.id], 'i'));
rcmail.enable_command('list-remove', me.tasklists[node.id] && me.tasklists[node.id].removable);
@@ -3179,6 +3179,7 @@
}
if (me.tasklists[id] && li) {
+ prop = $.extend({}, me.tasklists[id], prop);
delete me.tasklists[id];
me.tasklists[prop.id] = prop;
$(li).find('input').first().val(prop.id);

File Metadata

Mime Type
text/plain
Expires
Mon, Apr 6, 1:29 PM (1 d, 4 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18837653
Default Alt Text
D4668.1775482177.diff (48 KB)

Event Timeline