Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117944597
D4668.1775482177.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
48 KB
Referenced Files
None
Subscribers
None
D4668.1775482177.diff
View Options
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
Details
Attached
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)
Attached To
Mode
D4668: ACL management for DAV folders
Attached
Detach File
Event Timeline