summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksander Machniak <alec@alec.pl>2013-05-24 12:01:21 (GMT)
committerAleksander Machniak <alec@alec.pl>2013-05-24 12:02:15 (GMT)
commit6219efe930d3da306222cff62a10e191625e9526 (patch)
treeb0f2415c62ebbef095eea5212157156457d8f56d
parent1eac719d26a9a3443b2c00abc146a869e0801fe1 (diff)
downloadiRony-6219efe930d3da306222cff62a10e191625e9526.tar.gz
Implemented webDAV access to Kolab files (Chwala)
-rw-r--r--README.md6
-rw-r--r--composer.json2
-rw-r--r--lib/Kolab/DAV/Backend.php97
-rw-r--r--lib/Kolab/DAV/Collection.php188
-rw-r--r--lib/Kolab/DAV/File.php158
-rw-r--r--lib/Kolab/DAV/Node.php182
-rw-r--r--public_html/index.php2
7 files changed, 632 insertions, 3 deletions
diff --git a/README.md b/README.md
index e115099..6655171 100644
--- a/README.md
+++ b/README.md
@@ -18,10 +18,11 @@ This will create a file named composer.phar in the project directory.
$ php composer.phar install
-3. Import the Roundcube framework and Kolab plugins
+3. Import the Roundcube Framework, Kolab plugins and Chwala
3.1. Either copy or symlink the Roundcube framework package into lib/Roundcube
3.2. Either copy or symlink the roundcubemail-plugins-kolab into lib/plugins
+3.3. Either copy or symlink the Chwala lib/ directory into lib/FileAPI
4. Create local config
@@ -37,9 +38,10 @@ Edit the local config/dav.inc.php file according to your setup and taste.
These settings override the default config options from the Roundcube
configuration.
-5. Give write access for the webserver user to the 'log' folder:
+5. Give write access for the webserver user to the 'logs' and 'temp' folders:
$ chown <www-user> logs
+$ chown <www-user> temp
6. Configure your webserver to point to the 'public_html' directory of this
package as document root.
diff --git a/composer.json b/composer.json
index 602aefb..de9ae7b 100644
--- a/composer.json
+++ b/composer.json
@@ -15,7 +15,7 @@
],
"autoload": {
"psr-0": { "": "lib/" },
- "classmap": [ "lib/Roundcube" ]
+ "classmap": [ "lib/Roundcube", "lib/FileAPI" ]
},
"require": {
"php": ">=5.3.3",
diff --git a/lib/Kolab/DAV/Backend.php b/lib/Kolab/DAV/Backend.php
new file mode 100644
index 0000000..9638060
--- /dev/null
+++ b/lib/Kolab/DAV/Backend.php
@@ -0,0 +1,97 @@
+<?php
+
+/**
+ * SabreDAV File Backend implementation for Kolab.
+ *
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Kolab\DAV;
+
+use \rcube;
+
+class Backend
+{
+ protected static $instance;
+ protected $api;
+ protected $conf;
+ protected $app_name = 'Kolab File API';
+ protected $config = array(
+ 'date_format' => 'Y-m-d H:i',
+ 'language' => 'en_US',
+ );
+
+
+ /**
+ * This implements the 'singleton' design pattern
+ *
+ * @return Backend The one and only instance
+ */
+ static function get_instance()
+ {
+ if (!self::$instance) {
+ self::$instance = new Backend();
+ self::$instance->init();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Private constructor
+ */
+ protected function __construct()
+ {
+ $rcube = rcube::get_instance();
+ $this->conf = $rcube->config;
+ }
+
+ /**
+ * Returns file API backend
+ */
+ public function get_backend()
+ {
+ return $this->api;
+ }
+
+ /**
+ * Initialise backend class
+ */
+ protected function init()
+ {
+ $driver = $this->conf->get('fileapi_backend', 'kolab');
+ $class = $driver . '_file_storage';
+
+ $this->api = new $class;
+ }
+
+ /*
+ * Returns API capabilities
+ */
+ protected function capabilities()
+ {
+ foreach ($this->api->capabilities() as $name => $value) {
+ // skip disabled capabilities
+ if ($value !== false) {
+ $caps[$name] = $value;
+ }
+ }
+
+ return $caps;
+ }
+}
diff --git a/lib/Kolab/DAV/Collection.php b/lib/Kolab/DAV/Collection.php
new file mode 100644
index 0000000..e391162
--- /dev/null
+++ b/lib/Kolab/DAV/Collection.php
@@ -0,0 +1,188 @@
+<?php
+
+/**
+ * SabreDAV File Backend implementation for Kolab.
+ *
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Kolab\DAV;
+
+use \Exception;
+
+/**
+ * Collection class
+ */
+class Collection extends \Kolab\DAV\Node implements \Sabre\DAV\ICollection
+{
+ const ROOT_DIRECTORY = 'files';
+
+ public $children;
+
+
+ function getChildren()
+ {
+ // @TODO: maybe children array is too big to keep it in memory?
+ if (is_array($this->children)) {
+ return $this->children;
+ }
+
+ $level = substr_count($this->path, '/');
+ $this->children = array();
+
+ try {
+ // @TODO: This should be cached too (out of this class)
+ $folders = $this->backend->folder_list();
+ }
+ catch (Exception $e) {
+ }
+
+ // get subfolders
+ foreach ($folders as $folder) {
+ $f_level = substr_count($folder, '/');
+
+ if (($this->path === '' && $f_level == 0)
+ || ($level == $f_level-1 && strpos($folder, $this->path . '/') === 0)
+ ) {
+ $this->children[] = new Collection(Collection::ROOT_DIRECTORY . '/' . $folder, $this);
+ }
+ }
+
+ // non-root folder, get files list
+ if ($this->path !== '') {
+ try {
+ $files = $this->backend->file_list($this->path);
+ }
+ catch (Exception $e) {
+ }
+
+ foreach ($files as $filename => $file) {
+ $this->children[] = new File(Collection::ROOT_DIRECTORY . '/' . $filename, $this, $file);
+ }
+ }
+
+ return $this->children;
+ }
+
+ /**
+ * Returns a child object, by its name.
+ *
+ * This method makes use of the getChildren method to grab all the child
+ * nodes, and compares the name.
+ * Generally its wise to override this, as this can usually be optimized
+ *
+ * This method must throw Sabre\DAV\Exception\NotFound if the node does not
+ * exist.
+ *
+ * @param string $name
+ * @throws Sabre\DAV\Exception\NotFound
+ * @return INode
+ */
+ public function getChild($name)
+ {
+ // @TODO: optimise this?
+ foreach ($this->getChildren() as $child) {
+ if ($child->getName() == $name) {
+ return $child;
+ }
+ }
+
+ throw new \Sabre\DAV\Exception\NotFound('File not found: ' . $name);
+ }
+
+ /**
+ * Checks if a child-node exists.
+ *
+ * It is generally a good idea to try and override this. Usually it can be optimized.
+ *
+ * @param string $name
+ * @return bool
+ */
+ public function childExists($name)
+ {
+ try {
+ $this->getChild($name);
+ return true;
+ }
+ catch (\Sabre\DAV\Exception\NotFound $e) {
+ return false;
+ }
+ }
+
+ /**
+ * Creates a new file in the directory
+ *
+ * Data will either be supplied as a stream resource, or in certain cases
+ * as a string. Keep in mind that you may have to support either.
+ *
+ * After succesful creation of the file, you may choose to return the ETag
+ * of the new file here.
+ *
+ * The returned ETag must be surrounded by double-quotes (The quotes should
+ * be part of the actual string).
+ *
+ * If you cannot accurately determine the ETag, you should not return it.
+ * If you don't store the file exactly as-is (you're transforming it
+ * somehow) you should also not return an ETag.
+ *
+ * This means that if a subsequent GET to this new file does not exactly
+ * return the same contents of what was submitted here, you are strongly
+ * recommended to omit the ETag.
+ *
+ * @param string $name Name of the file
+ * @param resource|string $data Initial payload
+ * @return null|string
+ */
+ public function createFile($name, $data = null)
+ {
+ $filename = $this->path . '/' . $name;
+ $filedata = $this->fileData($name, $data);
+
+ try {
+ $this->backend->file_create($filename, $filedata);
+ }
+ catch (Exception $e) {
+// throw new \Sabre\DAV\Exception\Forbidden($e->getMessage());
+ }
+
+ // reset cache
+ $this->children = null;
+ }
+
+ /**
+ * Creates a new subdirectory
+ *
+ * @param string $name
+ * @throws Exception\Forbidden
+ * @return void
+ */
+ public function createDirectory($name)
+ {
+ $folder = $this->path . '/' . $name;
+
+ try {
+ $this->backend->folder_create($folder);
+ }
+ catch (Exception $e) {
+ throw new \Sabre\DAV\Exception\Forbidden($e->getMessage());
+ }
+
+ // reset cache
+ $this->children = null;
+ }
+}
diff --git a/lib/Kolab/DAV/File.php b/lib/Kolab/DAV/File.php
new file mode 100644
index 0000000..ff6c698
--- /dev/null
+++ b/lib/Kolab/DAV/File.php
@@ -0,0 +1,158 @@
+<?php
+
+/**
+ * SabreDAV File Backend implementation for Kolab.
+ *
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Kolab\DAV;
+
+use \rcube;
+use \Exception;
+
+/**
+ * File class
+ */
+class File extends Node implements \Sabre\DAV\IFile
+{
+
+ /**
+ * Updates the data
+ *
+ * The data argument is a readable stream resource.
+ *
+ * After a succesful put operation, you may choose to return an ETag. The
+ * etag must always be surrounded by double-quotes. These quotes must
+ * appear in the actual string you're returning.
+ *
+ * Clients may use the ETag from a PUT request to later on make sure that
+ * when they update the file, the contents haven't changed in the mean
+ * time.
+ *
+ * If you don't plan to store the file byte-by-byte, and you return a
+ * different object on a subsequent GET you are strongly recommended to not
+ * return an ETag, and just return null.
+ *
+ * @param resource $data
+ * @return string|null
+ */
+ public function put($data)
+ {
+ $filedata = $this->fileData($this->path, $data);
+
+ try {
+ $this->backend->file_update($this->path, $filedata);
+ }
+ catch (Exception $e) {
+// throw new \Sabre\DAV\Exception\Forbidden($e->getMessage());
+ }
+
+ try {
+ $this->data = $this->backend->file_info($this->path);
+ }
+ catch (Exception $e) {
+ }
+
+ return $this->getETag();
+ }
+
+ /**
+ * Returns the data
+ *
+ * This method may either return a string or a readable stream resource
+ *
+ * @return mixed
+ */
+ public function get()
+ {
+ // @TODO: make file path to be based on user ID and file ETag
+ // and check if it exists before - use it for better performance
+ $rcube = rcube::get_instance();
+ $temp_dir = unslashify($rcube->config->get('temp_dir'));
+ $file_path = tempnam($temp_dir, 'davFile');
+
+ $fp = @fopen($file_path, 'bw+');
+
+ try {
+ $this->backend->file_get($this->path, array(), $fp);
+ }
+ catch (Exception $e) {
+// throw new \Sabre\DAV\Exception\Forbidden($e->getMessage());
+ }
+
+ return $fp;
+ }
+
+ /**
+ * Delete the current file
+ *
+ * @return void
+ */
+ public function delete()
+ {
+ try {
+ $this->backend->file_delete($this->path);
+ }
+ catch (Exception $e) {
+// throw new \Sabre\DAV\Exception\Forbidden($e->getMessage());
+ }
+
+ // reset cache
+ if ($this->parent) {
+ $this->parent->children = null;
+ }
+ }
+
+ /**
+ * Returns the size of the node, in bytes
+ *
+ * @return int
+ */
+ public function getSize()
+ {
+ return $this->data['size'];
+ }
+
+ /**
+ * Returns the ETag for a file
+ *
+ * An ETag is a unique identifier representing the current version of the file. If the file changes, the ETag MUST change.
+ * The ETag is an arbitrary string, but MUST be surrounded by double-quotes.
+ *
+ * Return null if the ETag can not effectively be determined
+ *
+ * @return mixed
+ */
+ public function getETag()
+ {
+ return sprintf('"%s-%d"', substr(md5($this->path . ':' . $this->data['size']), 0, 16), $this->data['modified']);
+ }
+
+ /**
+ * Returns the mime-type for a file
+ *
+ * If null is returned, we'll assume application/octet-stream
+ *
+ * @return mixed
+ */
+ public function getContentType()
+ {
+ return $this->data['type'];
+ }
+}
diff --git a/lib/Kolab/DAV/Node.php b/lib/Kolab/DAV/Node.php
new file mode 100644
index 0000000..e12b7b5
--- /dev/null
+++ b/lib/Kolab/DAV/Node.php
@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * SabreDAV File Backend implementation for Kolab.
+ *
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+namespace Kolab\DAV;
+
+use \rcube;
+use \rcube_mime;
+use \Exception;
+
+/**
+ * Node class
+ */
+class Node implements \Sabre\DAV\INode
+{
+ /**
+ * The path to the current node
+ *
+ * @var string
+ */
+ protected $path;
+
+ /**
+ * The file API backend class
+ *
+ * @var file_api_storage
+ */
+ protected $backend;
+
+ /**
+ * Internal node data (e.g. file parameters)
+ *
+ * @var array
+ */
+ protected $data;
+
+ /**
+ * Parent node
+ *
+ * @var Kolab\DAV\Node
+ */
+ protected $parent;
+
+
+ /**
+ * @brief Sets up the node, expects a full path name
+ * @param string $path Node name with path
+ * @param Kolab\DAV\Node $parent Parent node
+ * @param array $data Node data
+ *
+ * @return void
+ */
+ public function __construct($path, $parent = null, $data = array())
+ {
+ $this->data = $data;
+ $this->path = $path;
+ $this->parent = $parent;
+ $this->backend = Backend::get_instance()->get_backend();
+
+ if ($this->path == Collection::ROOT_DIRECTORY) {
+ $this->path = '';
+ }
+ else if (strpos($this->path, Collection::ROOT_DIRECTORY . '/') === 0) {
+ $this->path = substr($this->path, strlen(Collection::ROOT_DIRECTORY . '/'));
+ }
+ }
+
+ /**
+ * Returns the last modification time
+ *
+ * In this case, it will simply return the current time
+ *
+ * @return int
+ */
+ public function getLastModified()
+ {
+ return $this->data['modified'] ? $this->data['modified'] : null;
+ }
+
+ /**
+ * Deletes the current node (folder)
+ *
+ * @throws Sabre\DAV\Exception\Forbidden
+ * @return void
+ */
+ public function delete()
+ {
+ try {
+ $this->storage->folder_delete($this->path);
+ }
+ catch (Exception $e) {
+ throw new \Sabre\DAV\Exception\Forbidden($e->getMessage());
+ }
+
+ // reset cache
+ if ($this->parent) {
+ $this->parent->children = null;
+ }
+ }
+
+ /**
+ * Renames the node
+ *
+ * @throws Sabre\DAV\Exception\Forbidden
+ * @param string $name The new name
+ * @return void
+ */
+ public function setName($name)
+ {
+ $path = explode('/', $this->path);
+ array_pop($path);
+ $newname = implode('/', $path) . '/' . $name;
+
+ $method = (is_a($this, 'File') ? 'file' : 'folder') . '_move';
+
+ try {
+ $this->backend->$method($this->path, $newname);
+ }
+ catch (Exception $e) {
+ throw new \Sabre\DAV\Exception\Forbidden($e->getMessage());
+ }
+
+ // reset cache
+ if ($this->parent) {
+ $this->parent->children = null;
+ }
+ }
+
+ /**
+ * @brief Returns the name of the node
+ * @return string
+ */
+ public function getName()
+ {
+ if ($this->path === '') {
+ return Collection::ROOT_DIRECTORY;
+ }
+
+ return array_pop(explode('/', $this->path));
+ }
+
+ /**
+ * Build file data array to pass into backend
+ */
+ protected function fileData($name, $data = null)
+ {
+ $rcube = rcube::get_instance();
+ $temp_dir = unslashify($rcube->config->get('temp_dir'));
+ $file_path = tempnam($temp_dir, 'davFile');
+
+ // @TODO: support $data as a resource in the backend
+ // @TODO: support $data as string in the backend
+ file_put_contents($file_path, $data);
+
+ $filedata = array(
+ 'path' => $file_path,
+ 'size' => filesize($file_path),
+ 'type' => rcube_mime::file_content_type($file_path, $name),
+ );
+
+ return $filedata;
+ }
+}
diff --git a/public_html/index.php b/public_html/index.php
index 9f36579..7a9cf8c 100644
--- a/public_html/index.php
+++ b/public_html/index.php
@@ -101,6 +101,8 @@ $caldav_backend->setUserAgent($_SERVER['HTTP_USER_AGENT']);
// Build the directory tree
// This is an array which contains the 'top-level' directories in the WebDAV server.
$nodes = array(
+ // files
+ new \Kolab\DAV\Collection(\Kolab\DAV\Collection::ROOT_DIRECTORY),
// /principals
new \Sabre\CalDAV\Principal\Collection($principal_backend),
// /calendars