diff --git a/src/app/Backends/DAV.php b/src/app/Backends/DAV.php
--- a/src/app/Backends/DAV.php
+++ b/src/app/Backends/DAV.php
@@ -249,6 +249,34 @@
return $response !== false;
}
+ /**
+ * Create a DAV folder (collection)
+ *
+ * @param DAV\Folder $folder Folder object
+ *
+ * @return bool True on success, False on error
+ */
+ public function folderCreate(DAV\Folder $folder)
+ {
+ $response = $this->request($folder->href, 'MKCOL', $folder->toXML('mkcol'));
+
+ return $response !== false;
+ }
+
+ /**
+ * Delete a DAV folder (collection)
+ *
+ * @param string $location Folder location
+ *
+ * @return bool True on success, False on error
+ */
+ public function folderDelete($location)
+ {
+ $response = $this->request($location, 'DELETE');
+
+ return $response !== false;
+ }
+
/**
* Get all properties of a folder.
*
@@ -258,15 +286,10 @@
*/
public function folderInfo(string $location)
{
- $body = ''
- . ''
- . ''
- . '';
+ $body = DAV\Folder::propfindXML();
// Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it)
- $headers = ['Depth' => 1, 'Prefer' => 'return-minimal'];
-
- $response = $this->request($location, 'PROPFIND', $body, $headers);
+ $response = $this->request($location, 'PROPFIND', $body, ['Depth' => 0, 'Prefer' => 'return-minimal']);
if (!empty($response) && ($element = $response->getElementsByTagName('response')->item(0))) {
return DAV\Folder::fromDomElement($element);
@@ -275,6 +298,61 @@
return false;
}
+ /**
+ * Update a DAV folder (collection)
+ *
+ * @param DAV\Folder $folder Folder object
+ *
+ * @return bool True on success, False on error
+ */
+ public function folderUpdate(DAV\Folder $folder)
+ {
+ // Note: Changing resourcetype property is forbidden (at least by Cyrus)
+
+ $response = $this->request($folder->href, 'PROPPATCH', $folder->toXML('propertyupdate'));
+
+ return $response !== false;
+ }
+
+ /**
+ * Initialize DAV folders (collections)
+ *
+ * @param \App\User $user User object
+ * @param array $folders Folders list (path, displayname, type, components)
+ *
+ * @return bool True on success, False on error
+ */
+ public static function initDefaultFolders(\App\User $user, array $folders)
+ {
+ // FIXME: It looks like we'll need a way to authenticate the user, or cyrus admin
+ $dav = new self($email, $password);
+
+ foreach ($folders as $props) {
+ $folder = new DAV\Folder();
+ $folder->href = "addressbooks/user/{$user->email}/{$props['path']}";
+ $folder->name = $props['displayname'] ?? '';
+ $folder->types = ['collection', $props['type']];
+ $folder->components = $props['components'] ?? [];
+
+ // folder already exists? check the properties and update if needed
+ if ($existing = $dav->folderInfo($folder->href)) {
+ if ($existing->name != $folder->name || $existing->components != $folder->components) {
+ if (!$dav->folderUpdate($folder)) {
+ \Log::error("Failed to update DAV folder {$folder->href}");
+ return false;
+ }
+ }
+ }
+
+ if (!$dav->folderCreate($folder)) {
+ \Log::error("Failed to create DAV folder {$folder->href}");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
/**
* Search DAV objects in a folder.
*
diff --git a/src/app/Backends/DAV/Folder.php b/src/app/Backends/DAV/Folder.php
--- a/src/app/Backends/DAV/Folder.php
+++ b/src/app/Backends/DAV/Folder.php
@@ -74,4 +74,82 @@
return $folder;
}
+
+ /**
+ * Parse folder properties input into XML string to use in a request
+ *
+ * @return string
+ */
+ public function toXML($tag)
+ {
+ $ns = 'xmlns:d="DAV:"';
+ $props = '';
+ $type = null;
+
+ if (in_array('addressbook', $this->types)) {
+ $ns .= ' xmlns:c="urn:ietf:params:xml:ns:carddav"';
+ $type = 'addressbook';
+ } elseif (in_array('calendar', $this->types)) {
+ $ns .= ' xmlns:c="urn:ietf:params:xml:ns:caldav"';
+ $type = 'calendar';
+ }
+
+ $props .= '' . ($type ? "" : '') . '';
+
+ if (!empty($this->components)) {
+ $props .= '';
+ foreach ($this->components as $component) {
+ $props .= '';
+ }
+ $props .= '';
+ }
+
+ if ($this->name !== null) {
+ $props .= '' . htmlspecialchars($this->name, ENT_XML1, 'UTF-8') . '';
+ }
+
+ if ($this->color !== null) {
+ $color = $this->color;
+ if (strlen($color) && $color[0] != '#') {
+ $color = '#' . $color;
+ }
+
+ $ns .= ' xmlns:a="http://apple.com/ns/ical/"';
+ $props .= '' . htmlspecialchars($color, ENT_XML1, 'UTF-8') . '';
+ }
+
+ return ''
+ . "{$props}";
+ }
+
+ /**
+ * Get XML string for PROPFIND query on a folder
+ *
+ * @return string
+ */
+ public static function propfindXML()
+ {
+ $ns = implode(' ', [
+ 'xmlns:d="DAV:"',
+ // 'xmlns:cs="http://calendarserver.org/ns/"',
+ 'xmlns:c="urn:ietf:params:xml:ns:caldav"',
+ // 'xmlns:a="http://apple.com/ns/ical/"',
+ // 'xmlns:k="Kolab:"'
+ ]);
+
+ // Note: does not include some of the properties we're interested in
+ return ''
+ . ''
+ . ''
+ // . ''
+ . ''
+ // . ''
+ // . ''
+ // . ''
+ . ''
+ . ''
+ // . ''
+ . ''
+ . '';
+ }
}
diff --git a/src/app/Jobs/User/CreateJob.php b/src/app/Jobs/User/CreateJob.php
--- a/src/app/Jobs/User/CreateJob.php
+++ b/src/app/Jobs/User/CreateJob.php
@@ -104,6 +104,13 @@
$user->status |= \App\User::STATUS_IMAP_READY;
}
+ $folders = \config('services.dav.default_folders');
+ if (count($folders)) {
+ if (!\App\Backends\DAV::initDefaultFolders($user, $folders)) {
+ throw new \Exception("Failed to initialize DAV folders for user {$this->userId}.");
+ }
+ }
+
// Make user active in non-mandate mode only
if (
!($wallet = $user->wallet())
diff --git a/src/config/services.php b/src/config/services.php
--- a/src/config/services.php
+++ b/src/config/services.php
@@ -1,5 +1,32 @@
'Default',
+ 'displayname' => 'Calendar',
+ 'components' => ['VEVENT'],
+ 'type' => 'calendar',
+ ],
+ [
+ 'path' => 'Tasks',
+ 'displayname' => 'Tasks',
+ 'components' => ['VTODO'],
+ 'type' => 'calendar',
+ ],
+ [
+ // FIXME: Same here, should we use 'Contacts'?
+ 'path' => 'Default',
+ 'displayname' => 'Contacts',
+ 'type' => 'addressbook',
+ ],
+ ];
+}
+
return [
/*
@@ -58,6 +85,7 @@
'dav' => [
'uri' => env('DAV_URI', 'https://proxy/'),
+ 'default_folders' => $dav_folders,
],
'activesync' => [