Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/lib/api/common.php b/lib/api/common.php
new file mode 100644
index 0000000..bbde2e2
--- /dev/null
+++ b/lib/api/common.php
@@ -0,0 +1,121 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+class file_api_common
+{
+ protected $api;
+ protected $args = array();
+
+
+ public function __construct($api)
+ {
+ $this->rc = rcube::get_instance();
+ $this->api = $api;
+ }
+
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ // GET arguments
+ $this->args = &$_GET;
+
+ // POST arguments (JSON)
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ $post = file_get_contents('php://input');
+ $this->args += (array) json_decode($post, true);
+ unset($post);
+ }
+
+ // disable script execution time limit, so we can handle big files
+ @set_time_limit(0);
+ }
+
+ /**
+ * File uploads handler
+ */
+ protected function upload()
+ {
+ $files = array();
+
+ if (is_array($_FILES['file']['tmp_name'])) {
+ foreach ($_FILES['file']['tmp_name'] as $i => $filepath) {
+ if ($err = $_FILES['file']['error'][$i]) {
+ if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
+ $maxsize = ini_get('upload_max_filesize');
+ $maxsize = $this->show_bytes(parse_bytes($maxsize));
+
+ throw new Exception("Maximum file size ($maxsize) exceeded", file_api::ERROR_CODE);
+ }
+
+ throw new Exception("File upload failed", file_api::ERROR_CODE);
+ }
+
+ $files[] = array(
+ 'path' => $filepath,
+ 'name' => $_FILES['file']['name'][$i],
+ 'size' => filesize($filepath),
+ 'type' => rcube_mime::file_content_type($filepath, $_FILES['file']['name'][$i], $_FILES['file']['type']),
+ );
+ }
+ }
+ else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
+ // if filesize exceeds post_max_size then $_FILES array is empty,
+ if ($maxsize = ini_get('post_max_size')) {
+ $maxsize = $this->show_bytes(parse_bytes($maxsize));
+ throw new Exception("Maximum file size ($maxsize) exceeded", file_api::ERROR_CODE);
+ }
+
+ throw new Exception("File upload failed", file_api::ERROR_CODE);
+ }
+
+ return $files;
+ }
+
+ /**
+ * Return built-in viewer opbject for specified mimetype
+ *
+ * @return object Viewer object
+ */
+ protected function find_viewer($mimetype)
+ {
+ $dir = RCUBE_INSTALL_PATH . 'lib/viewers';
+
+ if ($handle = opendir($dir)) {
+ while (false !== ($file = readdir($handle))) {
+ if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) {
+ include_once $dir . '/' . $file;
+ $class = 'file_viewer_' . $matches[1];
+ $viewer = new $class($this->api);
+
+ if ($viewer->supports($mimetype)) {
+ return $viewer;
+ }
+ }
+ }
+ closedir($handle);
+ }
+ }
+}
diff --git a/lib/api/file_copy.php b/lib/api/file_copy.php
new file mode 100644
index 0000000..7509af1
--- /dev/null
+++ b/lib/api/file_copy.php
@@ -0,0 +1,29 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/file_move.php";
+
+class file_api_file_copy extends file_api_file_move
+{
+}
diff --git a/lib/api/file_create.php b/lib/api/file_create.php
new file mode 100644
index 0000000..210d7ec
--- /dev/null
+++ b/lib/api/file_create.php
@@ -0,0 +1,59 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_file_create extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ if (!isset($this->args['file']) || $this->args['file'] === '') {
+ throw new Exception("Missing file name", file_api::ERROR_CODE);
+ }
+
+ if (!isset($this->args['content'])) {
+ throw new Exception("Missing file content", file_api::ERROR_CODE);
+ }
+
+ $request = $this instanceof file_api_file_update ? 'file_update' : 'file_create';
+ $file = array(
+ 'content' => $this->args['content'],
+ 'type' => rcube_mime::file_content_type($this->args['content'],
+ $this->args['file'], $this->args['content-type'], true),
+ );
+
+ list($driver, $path) = $this->api->get_driver($this->args['file']);
+
+ $driver->$request($path, $file);
+
+ if (rcube_utils::get_boolean((string) $this->args['info'])) {
+ return $driver->file_info($path);
+ }
+ }
+}
diff --git a/lib/api/file_delete.php b/lib/api/file_delete.php
new file mode 100644
index 0000000..2458f42
--- /dev/null
+++ b/lib/api/file_delete.php
@@ -0,0 +1,45 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_file_delete extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ if (empty($this->args['file'])) {
+ throw new Exception("Missing file name", file_api::ERROR_CODE);
+ }
+
+ foreach ((array) $this->args['file'] as $file) {
+ list($driver, $file) = $this->api->get_driver($file);
+ $driver->file_delete($file);
+ }
+ }
+}
diff --git a/lib/api/file_get.php b/lib/api/file_get.php
new file mode 100644
index 0000000..def9751
--- /dev/null
+++ b/lib/api/file_get.php
@@ -0,0 +1,97 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_file_get extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ $this->api->output_type = file_api::OUTPUT_HTML;
+
+ if (!isset($this->args['file']) || $this->args['file'] === '') {
+ header("HTTP/1.0 ".file_api::ERROR_CODE." Missing file name");
+ }
+
+ $params = array(
+ 'force-download' => rcube_utils::get_boolean((string) $this->args['force-download']),
+ 'force-type' => $this->args['force-type'],
+ );
+
+ list($this->driver, $path) = $this->api->get_driver($this->args['file']);
+
+ if (!empty($this->args['viewer'])) {
+ $this->file_view($path, $this->args, $params);
+ }
+
+ try {
+ $this->driver->file_get($path, $params);
+ }
+ catch (Exception $e) {
+ header("HTTP/1.0 " . file_api::ERROR_CODE . " " . $e->getMessage());
+ }
+
+ exit;
+ }
+
+ /**
+ * File vieweing request handler
+ */
+ protected function file_view($file, $args, $params)
+ {
+ $viewer = $args['viewer'];
+ $path = RCUBE_INSTALL_PATH . "lib/viewers/$viewer.php";
+ $class = "file_viewer_$viewer";
+
+ if (!file_exists($path)) {
+ return;
+ }
+
+ // get file info
+ try {
+ $info = $this->driver->file_info($file);
+ }
+ catch (Exception $e) {
+ header("HTTP/1.0 " . file_api::ERROR_CODE . " " . $e->getMessage());
+ exit;
+ }
+
+ include_once $path;
+ $viewer = new $class($this->api);
+
+ // check if specified viewer supports file type
+ // otherwise return (fallback to file_get action)
+ if (!$viewer->supports($info['type'])) {
+ return;
+ }
+
+ $viewer->output($args['file'], $info['type']);
+ exit;
+ }
+}
diff --git a/lib/api/file_info.php b/lib/api/file_info.php
new file mode 100644
index 0000000..41d2639
--- /dev/null
+++ b/lib/api/file_info.php
@@ -0,0 +1,66 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_file_info extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ if (!isset($this->args['file']) || $this->args['file'] === '') {
+ throw new Exception("Missing file name", file_api::ERROR_CODE);
+ }
+
+ list($driver, $path) = $this->api->get_driver($this->args['file']);
+
+ $info = $driver->file_info($path);
+
+ if (rcube_utils::get_boolean((string) $this->args['viewer'])) {
+ $this->file_viewer_info($this->args['file'], $info);
+ }
+
+ return $info;
+ }
+
+ /**
+ * Merge file viewer data into file info
+ */
+ protected function file_viewer_info($file, &$info)
+ {
+ if ($viewer = $this->find_viewer($info['type'])) {
+ $info['viewer'] = array();
+ if ($frame = $viewer->frame($file, $info['type'])) {
+ $info['viewer']['frame'] = $frame;
+ }
+ else if ($href = $viewer->href($file, $info['type'])) {
+ $info['viewer']['href'] = $href;
+ }
+ }
+ }
+}
diff --git a/lib/api/file_list.php b/lib/api/file_list.php
new file mode 100644
index 0000000..994804e
--- /dev/null
+++ b/lib/api/file_list.php
@@ -0,0 +1,69 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_file_list extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ if (!isset($this->args['folder']) || $this->args['folder'] === '') {
+ throw new Exception("Missing folder name", file_api::ERROR_CODE);
+ }
+
+ $params = array(
+ 'reverse' => rcube_utils::get_boolean((string) $this->args['reverse']),
+ );
+
+ if (!empty($this->args['sort'])) {
+ $params['sort'] = strtolower($this->args['sort']);
+ }
+
+ if (!empty($this->args['search'])) {
+ $params['search'] = $this->args['search'];
+ if (!is_array($params['search'])) {
+ $params['search'] = array('name' => $params['search']);
+ }
+ }
+
+ list($driver, $path) = $this->api->get_driver($this->args['folder']);
+
+ // mount point contains only folders
+ if (!strlen($path)) {
+ return array();
+ }
+
+ // add mount point prefix to file paths
+ if ($path != $this->args['folder']) {
+ $params['prefix'] = substr($this->args['folder'], 0, -strlen($path));
+ }
+
+ return $driver->file_list($path, $params);
+ }
+}
diff --git a/lib/api/file_move.php b/lib/api/file_move.php
new file mode 100644
index 0000000..fea8b07
--- /dev/null
+++ b/lib/api/file_move.php
@@ -0,0 +1,160 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_file_move extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ if (!isset($this->args['file']) || $this->args['file'] === '') {
+ throw new Exception("Missing file name", file_api::ERROR_CODE);
+ }
+
+ if (is_array($this->args['file'])) {
+ if (empty($this->args['file'])) {
+ throw new Exception("Missing file name", file_api::ERROR_CODE);
+ }
+ }
+ else {
+ if (!isset($this->args['new']) || $this->args['new'] === '') {
+ throw new Exception("Missing new file name", file_api::ERROR_CODE);
+ }
+
+ $this->args['file'] = array($this->args['file'] => $this->args['new']);
+ }
+
+ $overwrite = rcube_utils::get_boolean((string) $this->args['overwrite']);
+ $request = $this instanceof file_api_file_copy ? 'file_copy' : 'file_move';
+ $errors = array();
+
+ foreach ((array) $this->args['file'] as $file => $new_file) {
+ if ($new_file === '') {
+ throw new Exception("Missing new file name", file_api::ERROR_CODE);
+ }
+
+ if ($new_file === $file) {
+ throw new Exception("Old and new file name is the same", file_api::ERROR_CODE);
+ }
+
+ list($driver, $path) = $this->api->get_driver($file);
+ list($new_driver, $new_path) = $this->api->get_driver($new_file);
+
+ try {
+ // source and destination on the same driver...
+ if ($driver == $new_driver) {
+ $driver->{$request}($path, $new_path);
+ }
+ // cross-driver move/copy...
+ else {
+ // first check if destination file exists
+ $info = null;
+ try {
+ $info = $new_driver->file_info($new_path);
+ }
+ catch (Exception $e) { }
+
+ if (!empty($info)) {
+ throw new Exception("File exists", file_storage::ERROR_FILE_EXISTS);
+ }
+
+ // copy/move between backends
+ $this->file_copy($driver, $new_driver, $path, $new_path, $request == 'file_move');
+ }
+ }
+ catch (Exception $e) {
+ if ($e->getCode() == file_storage::ERROR_FILE_EXISTS) {
+ // delete existing file and do copy/move again
+ if ($overwrite) {
+ $new_driver->file_delete($new_path);
+
+ if ($driver == $new_driver) {
+ $driver->{$request}($path, $new_path);
+ }
+ else {
+ $this->file_copy($driver, $new_driver, $path, $new_path, $request == 'file_move');
+ }
+ }
+ // collect file-exists errors, so the client can ask a user
+ // what to do and skip or replace file(s)
+ else {
+ $errors[] = array(
+ 'src' => $file,
+ 'dst' => $new_file,
+ );
+ }
+ }
+ else {
+ throw $e;
+ }
+ }
+ }
+
+ if (!empty($errors)) {
+ return array('already_exist' => $errors);
+ }
+ }
+
+ /**
+ * File copy/move between storage backends
+ */
+ protected function file_copy($driver, $new_driver, $path, $new_path, $move = false)
+ {
+ // unable to put file on mount point
+ if (strpos($new_path, file_storage::SEPARATOR) === false) {
+ throw new Exception("Unable to copy/move file into specified location", file_api::ERROR_CODE);
+ }
+
+ // get the file from source location
+ $fp = fopen('php://temp', 'w+');
+
+ if (!$fp) {
+ throw new Exception("Internal server error", file_api::ERROR_CODE);
+ }
+
+ $driver->file_get($path, null, $fp);
+
+ rewind($fp);
+
+ $chunk = stream_get_contents($fp, 102400);
+ $type = rcube_mime::file_content_type($chunk, $new_path, 'application/octet-stream', true);
+
+ rewind($fp);
+
+ // upload the file to new location
+ $new_driver->file_create($new_path, array('content' => $fp, 'type' => $type));
+
+ fclose($fp);
+
+ // now we can remove the original file if it was a move action
+ if ($move) {
+ $driver->file_delete($path);
+ }
+ }
+}
diff --git a/lib/api/file_update.php b/lib/api/file_update.php
new file mode 100644
index 0000000..6386064
--- /dev/null
+++ b/lib/api/file_update.php
@@ -0,0 +1,29 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/file_create.php";
+
+class file_api_file_update extends file_api_file_create
+{
+}
diff --git a/lib/api/file_upload.php b/lib/api/file_upload.php
new file mode 100644
index 0000000..42ff127
--- /dev/null
+++ b/lib/api/file_upload.php
@@ -0,0 +1,64 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_file_upload extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ // for Opera upload frame response cannot be application/json
+ $this->api->output_type = file_api::OUTPUT_HTML;
+
+ if (!isset($this->args['folder']) || $this->args['folder'] === '') {
+ throw new Exception("Missing folder name", file_api::ERROR_CODE);
+ }
+
+ $uploads = $this->upload();
+ $result = array();
+
+ list($driver, $path) = $this->api->get_driver($this->args['folder']);
+
+ if (strlen($path)) {
+ $path .= file_storage::SEPARATOR;
+ }
+
+ foreach ($uploads as $file) {
+ $driver->file_create($path . $file['name'], $file);
+
+ unset($file['path']);
+ $result[$file['name']] = array(
+ 'type' => $file['type'],
+ 'size' => $file['size'],
+ );
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/api/folder_create.php b/lib/api/folder_create.php
new file mode 100644
index 0000000..bf67b0e
--- /dev/null
+++ b/lib/api/folder_create.php
@@ -0,0 +1,44 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_folder_create extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ if (!isset($this->args['folder']) || $this->args['folder'] === '') {
+ throw new Exception("Missing folder name", file_api::ERROR_CODE);
+ }
+
+ list($driver, $path) = $this->api->get_driver($this->args['folder']);
+
+ return $driver->folder_create($path);
+ }
+}
diff --git a/lib/api/folder_delete.php b/lib/api/folder_delete.php
new file mode 100644
index 0000000..5fe4b13
--- /dev/null
+++ b/lib/api/folder_delete.php
@@ -0,0 +1,48 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_folder_delete extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ if (!isset($this->args['folder']) || $this->args['folder'] === '') {
+ throw new Exception("Missing folder name", file_api::ERROR_CODE);
+ }
+
+ list($driver, $path) = $this->api->get_driver($this->args['folder']);
+
+ if (!strlen($path)) {
+ // @TODO: delete mount point
+ }
+
+ return $driver->folder_delete($path);
+ }
+}
diff --git a/lib/api/folder_list.php b/lib/api/folder_list.php
new file mode 100644
index 0000000..c9f8e5d
--- /dev/null
+++ b/lib/api/folder_list.php
@@ -0,0 +1,100 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_folder_list extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ $backend = $this->api->get_backend();
+ $drivers = $this->api->get_drivers(true);
+
+ // get folders from main driver
+ $folders = $backend->folder_list();
+ $has_more = false;
+
+ // get folders from external sources
+ foreach ($drivers as $driver) {
+ $title = $driver->title();
+ $prefix = $title . file_storage::SEPARATOR;
+
+ // folder exists in main source, replace it with external one
+ if (($idx = array_search($title, $folders)) !== false) {
+ foreach ($folders as $idx => $folder) {
+ if ($folder == $title || strpos($folder, $prefix) === 0) {
+ unset($folders[$idx]);
+ }
+ }
+ }
+
+ $folders[] = $title;
+
+ if ($driver != $backend) {
+ try {
+ foreach ($driver->folder_list() as $folder) {
+ $folders[] = $prefix . $folder;
+ }
+ }
+ catch (Exception $e) {
+ if ($e->getCode() == file_storage::ERROR_NOAUTH) {
+ // inform UI about it to ask user for credentials
+ }
+ }
+ }
+ }
+
+ // re-sort the list
+ if ($has_more) {
+ usort($folders, array($this, 'sort_folder_comparator'));
+ }
+
+ return $folders;
+ }
+
+ /**
+ * Callback for uasort() that implements correct
+ * locale-aware case-sensitive sorting
+ */
+ protected function sort_folder_comparator($str1, $str2)
+ {
+ $path1 = explode(file_api::SEPARATOR, $str1);
+ $path2 = explode(file_api::SEPARATOR, $str2);
+
+ foreach ($path1 as $idx => $folder1) {
+ $folder2 = $path2[$idx];
+
+ if ($folder1 === $folder2) {
+ continue;
+ }
+
+ return strcoll($folder1, $folder2);
+ }
+ }
+}
diff --git a/lib/api/folder_move.php b/lib/api/folder_move.php
new file mode 100644
index 0000000..39f757e
--- /dev/null
+++ b/lib/api/folder_move.php
@@ -0,0 +1,58 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_folder_move extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ if (!isset($this->args['folder']) || $this->args['folder'] === '') {
+ throw new Exception("Missing folder name", file_api::ERROR_CODE);
+ }
+
+ if (!isset($this->args['new']) || $this->args['new'] === '') {
+ throw new Exception("Missing destination folder name", file_api::ERROR_CODE);
+ }
+
+ if ($this->args['new'] === $this->args['folder']) {
+ return;
+ }
+
+ list($driver, $path) = $this->api->get_driver($this->args['folder']);
+ list($new_driver, $new_path) = $this->api->get_driver($this->args['new']);
+
+ // @TODO: cross-driver move
+ if ($driver != $new_driver) {
+ throw new Exception("Unsupported operation", file_api::ERROR_CODE);
+ }
+
+ return $driver->folder_move($path, $new_path);
+ }
+}
diff --git a/lib/api/lock_create.php b/lib/api/lock_create.php
new file mode 100644
index 0000000..01c9f79
--- /dev/null
+++ b/lib/api/lock_create.php
@@ -0,0 +1,47 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_lock_create extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ // arguments: uri, owner, timeout, scope, depth, token
+ foreach (array('uri', 'token') as $arg) {
+ if (!isset($this->args[$arg]) || $this->args[$arg] === '') {
+ throw new Exception("Missing lock $arg", file_api::ERROR_CODE);
+ }
+ }
+
+ list($driver, $uri) = $this->api->get_driver($this->args['uri']);
+
+ $driver->lock($uri, $this->args);
+ }
+}
diff --git a/lib/api/lock_delete.php b/lib/api/lock_delete.php
new file mode 100644
index 0000000..ff90835
--- /dev/null
+++ b/lib/api/lock_delete.php
@@ -0,0 +1,47 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_lock_delete extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ // arguments: uri, owner, timeout, scope, depth, token
+ foreach (array('uri', 'token') as $arg) {
+ if (!isset($this->args[$arg]) || $this->args[$arg] === '') {
+ throw new Exception("Missing lock $arg", file_api::ERROR_CODE);
+ }
+ }
+
+ list($driver, $uri) = $this->api->get_driver($this->args['uri']);
+
+ $driver->unlock($uri, $this->args);
+ }
+}
diff --git a/lib/api/lock_list.php b/lib/api/lock_list.php
new file mode 100644
index 0000000..db0cfa5
--- /dev/null
+++ b/lib/api/lock_list.php
@@ -0,0 +1,42 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_lock_list extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ $child_locks = rcube_utils::get_boolean($this->args['child_locks']);
+
+ list($driver, $uri) = $this->api->get_driver($this->args['uri']);
+
+ return $driver->lock_list($uri, $child_locks);
+ }
+}
diff --git a/lib/api/quota.php b/lib/api/quota.php
new file mode 100644
index 0000000..2bda6f2
--- /dev/null
+++ b/lib/api/quota.php
@@ -0,0 +1,51 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2012-2014, Kolab Systems AG |
+ | |
+ | 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+require_once __DIR__ . "/common.php";
+
+class file_api_quota extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ list($driver, $path) = $this->api->get_driver($this->args['folder']);
+
+ $quota = $driver->quota($path);
+
+ if (!$quota['total']) {
+ $quota['percent'] = 0;
+ }
+ else if ($quota['total']) {
+ if (!isset($quota['percent'])) {
+ $quota['percent'] = min(100, round(($quota['used']/max(1, $quota['total']))*100));
+ }
+ }
+
+ return $quota;
+ }
+}
diff --git a/lib/drivers/kolab/kolab_file_storage.php b/lib/drivers/kolab/kolab_file_storage.php
index 6acfa3e..4a39607 100644
--- a/lib/drivers/kolab/kolab_file_storage.php
+++ b/lib/drivers/kolab/kolab_file_storage.php
@@ -1,1051 +1,1172 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| 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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class kolab_file_storage implements file_storage
{
/**
* @var rcube
*/
protected $rc;
/**
* @var array
*/
protected $folders;
/**
* @var array
*/
protected $config;
+ /**
+ * @var string
+ */
+ protected $title;
+
/**
* Class constructor
*/
public function __construct()
{
$this->rc = rcube::get_instance();
// Get list of plugins
// WARNING: We can use only plugins that are prepared for this
// e.g. are not using output or rcmail objects or
// doesn't throw errors when using them
$plugins = (array)$this->rc->config->get('fileapi_plugins', array('kolab_auth'));
$required = array('libkolab');
// Kolab WebDAV server supports plugins, no need to overwrite object
if (!is_a($this->rc->plugins, 'rcube_plugin_api')) {
// Initialize/load plugins
$this->rc->plugins = kolab_file_plugin_api::get_instance();
$this->rc->plugins->init($this, '');
}
$this->rc->plugins->load_plugins($plugins, $required);
$this->init();
}
/**
* Authenticates a user
*
* @param string $username User name
* @param string $password User password
*
* @param bool True on success, False on failure
*/
public function authenticate($username, $password)
{
$auth = $this->rc->plugins->exec_hook('authenticate', array(
'host' => $this->select_host($username),
'user' => $username,
'pass' => $password,
'valid' => true,
));
// Authenticate - get Roundcube user ID
if ($auth['valid'] && !$auth['abort']
&& ($this->login($auth['user'], $auth['pass'], $auth['host']))) {
return true;
}
$this->rc->plugins->exec_hook('login_failed', array(
'host' => $auth['host'],
'user' => $auth['user'],
));
}
/**
* Storage host selection
*/
private function select_host($username)
{
// Get IMAP host
$host = $this->rc->config->get('default_host');
if (is_array($host)) {
list($user, $domain) = explode('@', $username);
// try to select host by mail domain
if (!empty($domain)) {
foreach ($host as $storage_host => $mail_domains) {
if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) {
$host = $storage_host;
break;
}
else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) {
$host = is_numeric($storage_host) ? $mail_domains : $storage_host;
break;
}
}
}
// take the first entry if $host is not found
if (is_array($host)) {
list($key, $val) = each($default_host);
$host = is_numeric($key) ? $val : $key;
}
}
return rcube_utils::parse_host($host);
}
/**
* Authenticates a user in IMAP
*/
private function login($username, $password, $host)
{
if (empty($username)) {
return false;
}
$login_lc = $this->rc->config->get('login_lc');
$default_port = $this->rc->config->get('default_port', 143);
// parse $host
$a_host = parse_url($host);
if ($a_host['host']) {
$host = $a_host['host'];
$ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
if (!empty($a_host['port'])) {
$port = $a_host['port'];
}
else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) {
$port = 993;
}
}
if (!$port) {
$port = $default_port;
}
// Convert username to lowercase. If storage backend
// is case-insensitive we need to store always the same username
if ($login_lc) {
if ($login_lc == 2 || $login_lc === true) {
$username = mb_strtolower($username);
}
else if (strpos($username, '@')) {
// lowercase domain name
list($local, $domain) = explode('@', $username);
$username = $local . '@' . mb_strtolower($domain);
}
}
// Here we need IDNA ASCII
// Only rcube_contacts class is using domain names in Unicode
$host = rcube_utils::idn_to_ascii($host);
$username = rcube_utils::idn_to_ascii($username);
// user already registered?
if ($user = rcube_user::query($username, $host)) {
$username = $user->data['username'];
}
// authenticate user in IMAP
$storage = $this->rc->get_storage();
if (!$storage->connect($host, $username, $password, $port, $ssl)) {
return false;
}
// No user in database, but IMAP auth works
if (!is_object($user)) {
if ($this->rc->config->get('auto_create_user')) {
// create a new user record
$user = rcube_user::create($username, $host);
if (!$user) {
rcube::raise_error(array(
'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to create a user record",
), true, false);
return false;
}
}
else {
rcube::raise_error(array(
'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__,
'message' => "Access denied for new user $username. 'auto_create_user' is disabled",
), true, false);
return false;
}
}
+
// set session vars
$_SESSION['user_id'] = $user->ID;
$_SESSION['username'] = $user->data['username'];
$_SESSION['storage_host'] = $host;
$_SESSION['storage_port'] = $port;
$_SESSION['storage_ssl'] = $ssl;
$_SESSION['password'] = $this->rc->encrypt($password);
$this->init($user);
// force reloading of mailboxes list/data
$storage->clear_cache('mailboxes', true);
return true;
}
protected function init($user = null)
{
if ($_SESSION['user_id'] || $user) {
// overwrite config with user preferences
$this->rc->user = $user ? $user : new rcube_user($_SESSION['user_id']);
$this->rc->config->set_user_prefs((array)$this->rc->user->get_prefs());
$storage = $this->rc->get_storage();
$storage->set_charset($this->rc->config->get('default_charset', RCUBE_CHARSET));
setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8');
}
}
/**
* Configures environment
*
- * @param array $config COnfiguration
+ * @param array $config Configuration
+ * @param string $title Source identifier
*/
- public function configure($config)
+ public function configure($config, $title = null)
{
$this->config = $config;
+ // @TODO: this is currently not possible to have multiple sessions in Roundcube
+ }
+
+ /**
+ * Returns current instance title
+ *
+ * @return string Instance title (mount point)
+ */
+ public function title()
+ {
+ return '';
}
/**
* Storage driver capabilities
*
* @return array List of capabilities
*/
public function capabilities()
{
// find max filesize value
$max_filesize = parse_bytes(ini_get('upload_max_filesize'));
$max_postsize = parse_bytes(ini_get('post_max_size'));
if ($max_postsize && $max_postsize < $max_filesize) {
$max_filesize = $max_postsize;
}
$storage = $this->rc->get_storage();
$quota = $storage->get_capability('QUOTA');
return array(
file_storage::CAPS_MAX_UPLOAD => $max_filesize,
file_storage::CAPS_QUOTA => $quota,
file_storage::CAPS_LOCKS => true,
);
}
+ /**
+ * Save configuration of external driver (mount point)
+ *
+ * @param array $driver Driver data
+ *
+ * @throws Exception
+ */
+ public function driver_create($driver)
+ {
+ $drivers = $this->driver_list();
+
+ if ($drivers[$driver['title']]) {
+ throw new Exception("Driver exists", file_storage::ERROR);
+ }
+
+ $config = kolab_storage_config::get_instance();
+ $status = $config->save($driver, 'file_driver');
+
+ if (!$status) {
+ throw new Exception("Driver create failed", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Delete configuration of external driver (mount point)
+ *
+ * @param string $name Driver instance name
+ *
+ * @throws Exception
+ */
+ public function driver_delete($name)
+ {
+ $drivers = $this->driver_list();
+
+ if ($driver = $drivers[$name]) {
+ $config = kolab_storage_config::get_instance();
+ $status = $config->delete($driver['uid']);
+
+ if (!$status) {
+ throw new Exception("Driver delete failed", file_storage::ERROR);
+ }
+
+ return;
+ }
+
+ throw new Exception("Driver not found", file_storage::ERROR);
+ }
+
+ /**
+ * Return list of registered drivers (mount points)
+ *
+ * @return array List of drivers data
+ * @throws Exception
+ */
+ public function driver_list()
+ {
+ // get current relations state
+ $config = kolab_storage_config::get_instance();
+ $default = true;
+ $filter = array(
+ array('type', '=', 'file_driver'),
+ );
+
+ $drivers = $config->get_objects($filter, $default, 100);
+ $result = array();
+
+ foreach ($drivers as $driver) {
+ $result[$driver['title']] = array(
+ 'title' => $driver['title'],
+ 'driver' => $driver['driver'],
+ 'settings' => $driver['settings'],
+ // Kolab specific
+ 'uid' => $driver['uid'],
+ );
+ }
+
+ return $result;
+ }
+
+ /**
+ * Update configuration of external driver (mount point)
+ *
+ * @param string $name Driver instance name
+ * @param array $driver Driver data
+ *
+ * @throws Exception
+ */
+ public function driver_update($name, $driver)
+ {
+ $drivers = $this->driver_list();
+
+ if (!$drivers[$name]) {
+ throw new Exception("Driver not found", file_storage::ERROR);
+ }
+
+ $config = kolab_storage_config::get_instance();
+ $status = $config->save($driver, 'file_driver');
+
+ if (!$status) {
+ throw new Exception("Driver update failed", file_storage::ERROR);
+ }
+ }
+
/**
* Create a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_create($file_name, $file)
{
$exists = $this->get_file_object($file_name, $folder);
if (!empty($exists)) {
throw new Exception("Storage error. File exists.", file_storage::ERROR);
}
$object = $this->to_file_object(array(
'name' => $file_name,
'type' => $file['type'],
'path' => $file['path'],
'content' => $file['content'],
));
// save the file object in IMAP
$saved = $folder->save($object, 'file');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving object to Kolab server"),
true, false);
throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
}
}
/**
* Update a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_update($file_name, $file)
{
$file_object = $this->get_file_object($file_name, $folder);
if (empty($file_object)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$key = key($file_object['_attachments']);
$file_object['_attachments'] = array(
0 => array(
'name' => $file_name,
'path' => $file['path'],
'content' => $file['content'],
'mimetype' => $file['type'],
),
$key => false,
);
// save the file object in IMAP
$saved = $folder->save($file_object, 'file', $file_object['_msguid']);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving object to Kolab server"),
true, false);
throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
}
}
/**
* Delete a file.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_delete($file_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$deleted = $folder->delete($file);
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting object from Kolab server"),
true, false);
throw new Exception("Storage error. Deleting file failed.", file_storage::ERROR);
}
}
/**
* Return file body.
*
* @param string $file_name Name of a file (with folder path)
* @param array $params Parameters (force-download)
* @param resource $fp Print to file pointer instead (send no headers)
*
* @throws Exception
*/
public function file_get($file_name, $params = array(), $fp = null)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$file = $this->from_file_object($file);
// write to file pointer, send no headers
if ($fp) {
if ($file['size']) {
$folder->get_attachment($file['_msguid'], $file['fileid'], $file['_mailbox'], false, $fp);
}
return;
}
if (!empty($params['force-download'])) {
$disposition = 'attachment';
header("Content-Type: application/octet-stream");
// @TODO
// if ($browser->ie)
// header("Content-Type: application/force-download");
}
else {
$mimetype = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $file['type']);
$disposition = 'inline';
header("Content-Transfer-Encoding: binary");
header("Content-Type: $mimetype");
}
$filename = addcslashes($file['name'], '"');
// Workaround for nasty IE bug (#1488844)
// If Content-Disposition header contains string "attachment" e.g. in filename
// IE handles data as attachment not inline
/*
@TODO
if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) {
$filename = str_ireplace('attachment', 'attach', $filename);
}
*/
header("Content-Length: " . $file['size']);
header("Content-Disposition: $disposition; filename=\"$filename\"");
if ($file['size']) {
$folder->get_attachment($file['_msguid'], $file['fileid'], $file['_mailbox'], true);
}
}
/**
* Returns file metadata.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_info($file_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$file = $this->from_file_object($file);
return array(
'name' => $file['name'],
'size' => (int) $file['size'],
'type' => (string) $file['type'],
'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
'created' => $file['created'] ? $file['created']->format('U') : 0,
);
}
/**
* List files in a folder.
*
* @param string $folder_name Name of a folder with full path
- * @param array $params List parameters ('sort', 'reverse', 'search')
+ * @param array $params List parameters ('sort', 'reverse', 'search', 'prefix')
*
* @return array List of files (file properties array indexed by filename)
* @throws Exception
*/
public function file_list($folder_name, $params = array())
{
$filter = array(array('type', '=', 'file'));
if (!empty($params['search'])) {
foreach ($params['search'] as $idx => $value) {
switch ($idx) {
case 'name':
$filter[] = array('filename', '~', $value);
break;
case 'class':
foreach (file_utils::class2mimetypes($value) as $tag) {
$for[] = array('tags', '~', ' ' . $tag);
}
$filter[] = array($for, 'OR');
break;
}
}
}
// get files list
$folder = $this->get_folder_object($folder_name);
$files = $folder->select($filter);
$result = array();
// convert to kolab_storage files list data format
foreach ($files as $idx => $file) {
$file = $this->from_file_object($file);
if (!isset($file['name'])) {
continue;
}
- $filename = $folder_name . file_storage::SEPARATOR . $file['name'];
+ $filename = $params['prefix'] . $folder_name . file_storage::SEPARATOR . $file['name'];
$result[$filename] = array(
'name' => $file['name'],
'size' => (int) $file['size'],
'type' => (string) $file['type'],
'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
'created' => $file['created'] ? $file['created']->format('U') : 0,
);
unset($files[$idx]);
}
// @TODO: pagination, search (by filename, mimetype)
// Sorting
$sort = !empty($params['sort']) ? $params['sort'] : 'name';
$index = array();
if ($sort == 'mtime') {
$sort = 'modified';
}
if (in_array($sort, array('name', 'size', 'modified'))) {
foreach ($result as $key => $val) {
$index[$key] = $val[$sort];
}
array_multisort($index, SORT_ASC, SORT_NUMERIC, $result);
}
if ($params['reverse']) {
$result = array_reverse($result, true);
}
return $result;
}
/**
* Copy a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_copy($file_name, $new_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$new = $this->get_file_object($new_name, $new_folder);
if (!empty($new)) {
throw new Exception("Storage error. File exists.", file_storage::ERROR_FILE_EXISTS);
}
$file = $this->from_file_object($file);
// Save to temp file
// @TODO: use IMAP CATENATE extension
$temp_dir = unslashify($this->rc->config->get('temp_dir'));
$file_path = tempnam($temp_dir, 'rcmAttmnt');
$fh = fopen($file_path, 'w');
if (!$fh) {
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
if ($file['size']) {
$folder->get_attachment($file['uid'], $file['fileid'], null, false, $fh, true);
}
fclose($fh);
if (!file_exists($file_path)) {
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
// Update object
$file['_attachments'] = array(
0 => array(
'name' => $file['name'],
'path' => $file_path,
'mimetype' => $file['type'],
'size' => $file['size'],
));
$fields = array('created', 'changed', '_attachments', 'notes', 'sensitivity', 'categories', 'x-custom');
$file = array_intersect_key($file, array_combine($fields, $fields));
$saved = $new_folder->save($file, 'file');
@unlink($file_path);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error updating object on Kolab server"),
true, false);
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
}
/**
* Move (or rename) a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_move($file_name, $new_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$new = $this->get_file_object($new_name, $new_folder);
if (!empty($new)) {
throw new Exception("Storage error. File exists.", file_storage::ERROR_FILE_EXISTS);
}
// Move the file
if ($folder->name != $new_folder->name) {
$saved = $folder->move($file['uid'], $new_folder->name);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error moving object on Kolab server"),
true, false);
throw new Exception("Storage error. File move failed.", file_storage::ERROR);
}
$folder = $new_folder;
}
if ($file_name === $new_name) {
return;
}
// Update object (changing the name)
$cid = key($file['_attachments']);
$file['_attachments'][$cid]['name'] = $new_name;
$file['_attachments'][0] = $file['_attachments'][$cid];
$file['_attachments'][$cid] = false;
$saved = $folder->save($file, 'file');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error updating object on Kolab server"),
true, false);
throw new Exception("Storage error. File rename failed.", file_storage::ERROR);
}
}
/**
* Create a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_create($folder_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$success = kolab_storage::folder_create($folder_name, 'file');
if (!$success) {
throw new Exception("Storage error. Unable to create folder", file_storage::ERROR);
}
}
/**
* Delete a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_delete($folder_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$success = kolab_storage::folder_delete($folder_name);
if (!$success) {
throw new Exception("Storage error. Unable to delete folder.", file_storage::ERROR);
}
}
/**
* Move/Rename a folder.
*
* @param string $folder_name Name of a folder with full path
* @param string $new_name New name of a folder with full path
*
* @throws Exception on error
*/
public function folder_move($folder_name, $new_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$new_name = rcube_charset::convert($new_name, RCUBE_CHARSET, 'UTF7-IMAP');
$success = kolab_storage::folder_rename($folder_name, $new_name);
if (!$success) {
throw new Exception("Storage error. Unable to rename folder", file_storage::ERROR);
}
}
/**
* Returns list of folders.
*
* @return array List of folders
* @throws Exception
*/
public function folder_list()
{
$folders = kolab_storage::list_folders('', '*', 'file', false);
if (!is_array($folders)) {
throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR);
}
// create 'Files' folder in case there's no folder of type 'file'
if (empty($folders)) {
if (kolab_storage::folder_create('Files', 'file')) {
$folders[] = 'Files';
}
}
else {
$callback = function($folder) { return rcube_charset::convert($folder, 'UTF7-IMAP', RCUBE_CHARSET); };
$folders = array_map($callback, $folders);
}
return $folders;
}
/**
* Returns a list of locks
*
* This method should return all the locks for a particular URI, including
* locks that might be set on a parent URI.
*
* If child_locks is set to true, this method should also look for
* any locks in the subtree of the URI for locks.
*
* @param string $uri URI
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
* @throws Exception
*/
public function lock_list($uri, $child_locks = false)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->uri2resource($uri);
// get locks list
$list = $this->lock_db->lock_list($uri, $child_locks);
// convert back resource string into URIs
foreach ($list as $idx => $lock) {
$list[$idx]['uri'] = $this->resource2uri($lock['uri']);
}
return $list;
}
/**
* Locks a URI
*
* @param string $uri URI
* @param array $lock Lock data
* - depth: 0/'infinite'
* - scope: 'shared'/'exclusive'
* - owner: string
* - token: string
* - timeout: int
*
* @throws Exception
*/
public function lock($uri, $lock)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->uri2resource($uri);
if (!$this->lock_db->lock($uri, $lock)) {
throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
}
}
/**
* Removes a lock from a URI
*
* @param string $path URI
* @param array $lock Lock data
*
* @throws Exception
*/
public function unlock($uri, $lock)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->uri2resource($uri);
if (!$this->lock_db->unlock($uri, $lock)) {
throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR);
}
}
/**
* Return disk quota information for specified folder.
*
* @param string $folder_name Name of a folder with full path
*
* @return array Quota
* @throws Exception
*/
public function quota($folder)
{
$storage = $this->rc->get_storage();
$quota = $storage->get_quota();
$quota = $this->rc->plugins->exec_hook('quota', $quota);
unset($quota['abort']);
return $quota;
}
/**
* Get file object.
*
* @param string $file_name Name of a file (with folder path)
* @param kolab_storage_folder $folder Reference to folder object
*
* @return array File data
* @throws Exception
*/
protected function get_file_object(&$file_name, &$folder = null)
{
// extract file path and file name
$path = explode(file_storage::SEPARATOR, $file_name);
$file_name = array_pop($path);
$folder_name = implode(file_storage::SEPARATOR, $path);
if ($folder_name === '') {
throw new Exception("Missing folder name", file_storage::ERROR);
}
// get folder object
$folder = $this->get_folder_object($folder_name);
$files = $folder->select(array(
array('type', '=', 'file'),
array('filename', '=', $file_name)
));
return $files[0];
}
/**
* Get folder object.
*
* @param string $folder_name Name of a folder with full path
*
* @return kolab_storage_folder Folder object
* @throws Exception
*/
protected function get_folder_object($folder_name)
{
if ($folder_name === null || $folder_name === '') {
throw new Exception("Missing folder name", file_storage::ERROR);
}
if (empty($this->folders[$folder_name])) {
$storage = $this->rc->get_storage();
$separator = $storage->get_hierarchy_delimiter();
$folder_name = str_replace(file_storage::SEPARATOR, $separator, $folder_name);
$imap_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$folder = kolab_storage::get_folder($imap_name);
if (!$folder) {
throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
}
$this->folders[$folder_name] = $folder;
}
return $this->folders[$folder_name];
}
/**
* Simplify internal structure of the file object
*/
protected function from_file_object($file)
{
if (empty($file['_attachments'])) {
return $file;
}
$attachment = array_shift($file['_attachments']);
$file['name'] = $attachment['name'];
$file['size'] = $attachment['size'];
$file['type'] = $attachment['mimetype'];
$file['fileid'] = $attachment['id'];
unset($file['_attachments']);
return $file;
}
/**
* Convert to kolab_format internal structure of the file object
*/
protected function to_file_object($file)
{
// @TODO if path is empty and fileid exists it is an update
// get attachment body and save it in path
$file['_attachments'] = array(
0 => array(
'name' => $file['name'],
'path' => $file['path'],
'content' => $file['content'],
'mimetype' => $file['type'],
'size' => $file['size'],
));
unset($file['name']);
unset($file['size']);
unset($file['type']);
unset($file['path']);
unset($file['fileid']);
return $file;
}
protected function uri2resource($uri)
{
$storage = $this->rc->get_storage();
$namespace = $storage->get_namespace();
$separator = $storage->get_hierarchy_delimiter();
$uri = str_replace(file_storage::SEPARATOR, $separator, $uri);
$owner = $this->rc->get_user_name();
// find the owner and remove namespace prefix
foreach ($namespace as $type => $ns) {
foreach ($ns as $root) {
if (is_array($root) && $root[0] && strpos($uri, $root[0]) === 0) {
$uri = substr($uri, strlen($root[0]));
switch ($type) {
case 'shared':
// in theory there can be more than one shared root
// we add it to dummy user name, so we can revert conversion
$owner = "shared({$root[0]})";
break;
case 'other':
list($user, $uri) = explode($separator, $uri, 2);
if (strpos($user, '@') === false) {
$domain = strstr($owner, '@');
if (!empty($domain)) {
$user .= $domain;
}
}
$owner = $user;
break;
}
break 2;
}
}
}
// convert to imap charset (to be safe to store in DB)
$uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP');
return 'imap://' . urlencode($owner) . '@' . $storage->options['host'] . '/' . $uri;
}
protected function resource2uri($resource)
{
if (!preg_match('|^imap://([^@]+)@([^/]+)/(.*)$|', $resource, $matches)) {
throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR);
}
$storage = $this->rc->get_storage();
$separator = $storage->get_hierarchy_delimiter();
$owner = $this->rc->get_user_name();
$user = urldecode($matches[1]);
$uri = $matches[3];
// convert from imap charset (to be safe to store in DB)
$uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET);
// personal namespace
if ($user == $owner) {
// do nothing
// Note: that might not work if personal namespace uses e.g. INBOX/ prefix.
}
// shared namespace
else if (preg_match('/^shared\((.*)\)$/', $user, $matches)) {
$uri = $matches[1] . $uri;
}
// other users namespace
else {
$namespace = $storage->get_namespace('other');
list($local, $domain) = explode('@', $user);
// here we assume there's only one other users namespace root
$uri = $namespace[0][0] . $local . $separator . $uri;
}
$uri = str_replace($separator, file_storage::SEPARATOR, $uri);
return $uri;
}
/**
* Initializes file_locks object
*/
protected function init_lock_db()
{
if (!$this->lock_db) {
$this->lock_db = new file_locks;
}
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql
index 4f23a52..2aa046d 100644
--- a/lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql
@@ -1,175 +1,187 @@
/**
* libkolab database schema
*
- * @version 1.0
+ * @version 1.1
* @author Thomas Bruederli
* @licence GNU AGPL
**/
DROP TABLE IF EXISTS `kolab_folders`;
CREATE TABLE `kolab_folders` (
`folder_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`resource` VARCHAR(255) NOT NULL,
`type` VARCHAR(32) NOT NULL,
`synclock` INT(10) NOT NULL DEFAULT '0',
`ctag` VARCHAR(40) DEFAULT NULL,
PRIMARY KEY(`folder_id`),
INDEX `resource_type` (`resource`, `type`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache`;
DROP TABLE IF EXISTS `kolab_cache_contact`;
CREATE TABLE `kolab_cache_contact` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
+ `firstname` VARCHAR(255) NOT NULL,
+ `surname` VARCHAR(255) NOT NULL,
+ `email` VARCHAR(255) NOT NULL,
CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
- INDEX `contact_type` (`folder_id`,`type`)
+ INDEX `contact_type` (`folder_id`,`type`),
+ INDEX `contact_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_event`;
CREATE TABLE `kolab_cache_event` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_event_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- PRIMARY KEY(`folder_id`,`msguid`)
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `event_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_task`;
CREATE TABLE `kolab_cache_task` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_task_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- PRIMARY KEY(`folder_id`,`msguid`)
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `task_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_journal`;
CREATE TABLE `kolab_cache_journal` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_journal_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- PRIMARY KEY(`folder_id`,`msguid`)
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `journal_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_note`;
CREATE TABLE `kolab_cache_note` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- PRIMARY KEY(`folder_id`,`msguid`)
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `note_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_file`;
CREATE TABLE `kolab_cache_file` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`filename` varchar(255) DEFAULT NULL,
CONSTRAINT `fk_kolab_cache_file_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
- INDEX `folder_filename` (`folder_id`, `filename`)
+ INDEX `folder_filename` (`folder_id`, `filename`),
+ INDEX `file_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_configuration`;
CREATE TABLE `kolab_cache_configuration` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
CONSTRAINT `fk_kolab_cache_configuration_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
- INDEX `configuration_type` (`folder_id`,`type`)
+ INDEX `configuration_type` (`folder_id`,`type`),
+ INDEX `configuration_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_freebusy`;
CREATE TABLE `kolab_cache_freebusy` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_freebusy_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- PRIMARY KEY(`folder_id`,`msguid`)
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013100400');
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2014021000');
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013110400.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013110400.sql
new file mode 100644
index 0000000..5b7a9ef
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013110400.sql
@@ -0,0 +1 @@
+ALTER TABLE `kolab_cache_contact` CHANGE `xml` `xml` LONGTEXT NOT NULL;
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013121100.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013121100.sql
new file mode 100644
index 0000000..8cab5ef
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013121100.sql
@@ -0,0 +1,13 @@
+-- well, these deletes are really optional
+-- we can clear all caches or only contacts/events/tasks
+-- the issue we're fixing here was about contacts (Bug #2662)
+DELETE FROM `kolab_folders` WHERE `type` IN ('contact', 'event', 'task');
+
+ALTER TABLE `kolab_cache_contact` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_event` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_task` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_journal` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_note` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_file` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_configuration` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_freebusy` CHANGE `xml` `xml` LONGBLOB NOT NULL;
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014021000.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014021000.sql
new file mode 100644
index 0000000..31ce699
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014021000.sql
@@ -0,0 +1,9 @@
+ALTER TABLE `kolab_cache_contact` ADD `name` VARCHAR(255) NOT NULL,
+ ADD `firstname` VARCHAR(255) NOT NULL,
+ ADD `surname` VARCHAR(255) NOT NULL,
+ ADD `email` VARCHAR(255) NOT NULL;
+
+-- updating or clearing all contacts caches is required.
+-- either run `bin/modcache.sh update --type=contact` or execute the following query:
+-- DELETE FROM `kolab_folders` WHERE `type`='contact';
+
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014032700.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014032700.sql
new file mode 100644
index 0000000..a45fae3
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014032700.sql
@@ -0,0 +1,8 @@
+ALTER TABLE `kolab_cache_configuration` ADD INDEX `configuration_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_contact` ADD INDEX `contact_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_event` ADD INDEX `event_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_task` ADD INDEX `task_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_journal` ADD INDEX `journal_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_note` ADD INDEX `note_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_file` ADD INDEX `file_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_freebusy` ADD INDEX `freebusy_uid2msguid` (`folder_id`, `uid`, `msguid`);
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014040900.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014040900.sql
new file mode 100644
index 0000000..cfcaa9d
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014040900.sql
@@ -0,0 +1,16 @@
+ALTER TABLE `kolab_cache_contact` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_event` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_task` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_journal` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_note` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_file` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_configuration` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_freebusy` CHANGE `data` `data` LONGTEXT NOT NULL;
+
+-- rebuild cache entries for xcal objects with alarms
+DELETE FROM `kolab_cache_event` WHERE tags LIKE '% x-has-alarms %';
+DELETE FROM `kolab_cache_task` WHERE tags LIKE '% x-has-alarms %';
+
+-- force cache synchronization
+UPDATE `kolab_folders` SET ctag='' WHERE `type` IN ('event','task');
+
diff --git a/lib/drivers/kolab/plugins/libkolab/SQL/oracle.initial.sql b/lib/drivers/kolab/plugins/libkolab/SQL/oracle.initial.sql
new file mode 100644
index 0000000..2c078cb
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/oracle.initial.sql
@@ -0,0 +1,184 @@
+/**
+ * libkolab database schema
+ *
+ * @version 1.1
+ * @author Aleksander Machniak
+ * @licence GNU AGPL
+ **/
+
+
+CREATE TABLE "kolab_folders" (
+ "folder_id" number NOT NULL PRIMARY KEY,
+ "resource" VARCHAR(255) NOT NULL,
+ "type" VARCHAR(32) NOT NULL,
+ "synclock" integer DEFAULT 0 NOT NULL,
+ "ctag" VARCHAR(40) DEFAULT NULL
+);
+
+CREATE INDEX "kolab_folders_resource_idx" ON "kolab_folders" ("resource", "type");
+
+CREATE SEQUENCE "kolab_folders_seq"
+ START WITH 1 INCREMENT BY 1 NOMAXVALUE;
+
+CREATE TRIGGER "kolab_folders_seq_trig"
+BEFORE INSERT ON "kolab_folders" FOR EACH ROW
+BEGIN
+ :NEW."folder_id" := "kolab_folders_seq".nextval;
+END;
+
+
+CREATE TABLE "kolab_cache_contact" (
+ "folder_id" number NOT NULL
+ REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
+ "msguid" number NOT NULL,
+ "uid" varchar(128) NOT NULL,
+ "created" timestamp DEFAULT NULL,
+ "changed" timestamp DEFAULT NULL,
+ "data" clob NOT NULL,
+ "xml" clob NOT NULL,
+ "tags" varchar(255) DEFAULT NULL,
+ "words" clob DEFAULT NULL,
+ "type" varchar(32) NOT NULL,
+ "name" varchar(255) DEFAULT NULL,
+ "firstname" varchar(255) DEFAULT NULL,
+ "surname" varchar(255) DEFAULT NULL,
+ "email" varchar(255) DEFAULT NULL,
+ PRIMARY KEY ("folder_id", "msguid")
+);
+
+CREATE INDEX "kolab_cache_contact_type_idx" ON "kolab_cache_contact" ("folder_id", "type");
+CREATE INDEX "kolab_cache_contact_uid2msguid" ON "kolab_cache_contact" ("folder_id", "uid", "msguid");
+
+
+CREATE TABLE "kolab_cache_event" (
+ "folder_id" number NOT NULL
+ REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
+ "msguid" number NOT NULL,
+ "uid" varchar(128) NOT NULL,
+ "created" timestamp DEFAULT NULL,
+ "changed" timestamp DEFAULT NULL,
+ "data" clob NOT NULL,
+ "xml" clob NOT NULL,
+ "tags" varchar(255) DEFAULT NULL,
+ "words" clob DEFAULT NULL,
+ "dtstart" timestamp DEFAULT NULL,
+ "dtend" timestamp DEFAULT NULL,
+ PRIMARY KEY ("folder_id", "msguid")
+);
+
+CREATE INDEX "kolab_cache_event_uid2msguid" ON "kolab_cache_event" ("folder_id", "uid", "msguid");
+
+
+CREATE TABLE "kolab_cache_task" (
+ "folder_id" number NOT NULL
+ REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
+ "msguid" number NOT NULL,
+ "uid" varchar(128) NOT NULL,
+ "created" timestamp DEFAULT NULL,
+ "changed" timestamp DEFAULT NULL,
+ "data" clob NOT NULL,
+ "xml" clob NOT NULL,
+ "tags" varchar(255) DEFAULT NULL,
+ "words" clob DEFAULT NULL,
+ "dtstart" timestamp DEFAULT NULL,
+ "dtend" timestamp DEFAULT NULL,
+ PRIMARY KEY ("folder_id", "msguid")
+);
+
+CREATE INDEX "kolab_cache_task_uid2msguid" ON "kolab_cache_task" ("folder_id", "uid", "msguid");
+
+
+CREATE TABLE "kolab_cache_journal" (
+ "folder_id" number NOT NULL
+ REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
+ "msguid" number NOT NULL,
+ "uid" varchar(128) NOT NULL,
+ "created" timestamp DEFAULT NULL,
+ "changed" timestamp DEFAULT NULL,
+ "data" clob NOT NULL,
+ "xml" clob NOT NULL,
+ "tags" varchar(255) DEFAULT NULL,
+ "words" clob DEFAULT NULL,
+ "dtstart" timestamp DEFAULT NULL,
+ "dtend" timestamp DEFAULT NULL,
+ PRIMARY KEY ("folder_id", "msguid")
+);
+
+CREATE INDEX "kolab_cache_journal_uid2msguid" ON "kolab_cache_journal" ("folder_id", "uid", "msguid");
+
+
+CREATE TABLE "kolab_cache_note" (
+ "folder_id" number NOT NULL
+ REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
+ "msguid" number NOT NULL,
+ "uid" varchar(128) NOT NULL,
+ "created" timestamp DEFAULT NULL,
+ "changed" timestamp DEFAULT NULL,
+ "data" clob NOT NULL,
+ "xml" clob NOT NULL,
+ "tags" varchar(255) DEFAULT NULL,
+ "words" clob DEFAULT NULL,
+ PRIMARY KEY ("folder_id", "msguid")
+);
+
+CREATE INDEX "kolab_cache_note_uid2msguid" ON "kolab_cache_note" ("folder_id", "uid", "msguid");
+
+
+CREATE TABLE "kolab_cache_file" (
+ "folder_id" number NOT NULL
+ REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
+ "msguid" number NOT NULL,
+ "uid" varchar(128) NOT NULL,
+ "created" timestamp DEFAULT NULL,
+ "changed" timestamp DEFAULT NULL,
+ "data" clob NOT NULL,
+ "xml" clob NOT NULL,
+ "tags" varchar(255) DEFAULT NULL,
+ "words" clob DEFAULT NULL,
+ "filename" varchar(255) DEFAULT NULL,
+ PRIMARY KEY ("folder_id", "msguid")
+);
+
+CREATE INDEX "kolab_cache_file_filename" ON "kolab_cache_file" ("folder_id", "filename");
+CREATE INDEX "kolab_cache_file_uid2msguid" ON "kolab_cache_file" ("folder_id", "uid", "msguid");
+
+
+CREATE TABLE "kolab_cache_configuration" (
+ "folder_id" number NOT NULL
+ REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
+ "msguid" number NOT NULL,
+ "uid" varchar(128) NOT NULL,
+ "created" timestamp DEFAULT NULL,
+ "changed" timestamp DEFAULT NULL,
+ "data" clob NOT NULL,
+ "xml" clob NOT NULL,
+ "tags" varchar(255) DEFAULT NULL,
+ "words" clob DEFAULT NULL,
+ "type" varchar(32) NOT NULL,
+ PRIMARY KEY ("folder_id", "msguid")
+);
+
+CREATE INDEX "kolab_cache_config_type" ON "kolab_cache_configuration" ("folder_id", "type");
+CREATE INDEX "kolab_cache_config_uid2msguid" ON "kolab_cache_configuration" ("folder_id", "uid", "msguid");
+
+
+CREATE TABLE "kolab_cache_freebusy" (
+ "folder_id" number NOT NULL
+ REFERENCES "kolab_folders" ("folder_id") ON DELETE CASCADE,
+ "msguid" number NOT NULL,
+ "uid" varchar(128) NOT NULL,
+ "created" timestamp DEFAULT NULL,
+ "changed" timestamp DEFAULT NULL,
+ "data" clob NOT NULL,
+ "xml" clob NOT NULL,
+ "tags" varchar(255) DEFAULT NULL,
+ "words" clob DEFAULT NULL,
+ "dtstart" timestamp DEFAULT NULL,
+ "dtend" timestamp DEFAULT NULL,
+ PRIMARY KEY("folder_id", "msguid")
+);
+
+CREATE INDEX "kolab_cache_fb_uid2msguid" ON "kolab_cache_freebusy" ("folder_id", "uid", "msguid");
+
+
+INSERT INTO "system" ("name", "value") VALUES ('libkolab-version', '2014021000');
diff --git a/lib/drivers/kolab/plugins/libkolab/bin/modcache.sh b/lib/drivers/kolab/plugins/libkolab/bin/modcache.sh
index da6e4f8..533fefd 100755
--- a/lib/drivers/kolab/plugins/libkolab/bin/modcache.sh
+++ b/lib/drivers/kolab/plugins/libkolab/bin/modcache.sh
@@ -1,208 +1,235 @@
-#!/usr/bin/env php -d enable_dl=On
+#!/usr/bin/env php
<?php
/**
* Kolab storage cache modification script
*
* @version 3.1
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
- * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
+ * Copyright (C) 2012-2014, 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/>.
*/
define('INSTALL_PATH', realpath('.') . '/' );
ini_set('display_errors', 1);
if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
die("Execute this from the Roundcube installation dir!\n\n");
require_once INSTALL_PATH . 'program/include/clisetup.php';
function print_usage()
{
print "Usage: modcache.sh [OPTIONS] ACTION [USERNAME ARGS ...]\n";
print "Possible actions are: expunge, clear, prewarm\n";
print "-a, --all Clear/expunge all caches\n";
print "-h, --host IMAP host name\n";
print "-u, --user IMAP user name to authenticate\n";
print "-t, --type Object types to clear/expunge cache\n";
print "-l, --limit Limit the number of records to be expunged\n";
}
// read arguments
$opts = get_opt(array(
'a' => 'all',
'h' => 'host',
'u' => 'user',
'p' => 'password',
't' => 'type',
'l' => 'limit',
'v' => 'verbose',
));
$opts['username'] = !empty($opts[1]) ? $opts[1] : $opts['user'];
$action = $opts[0];
$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
// connect to database
$db = $rcmail->get_dbh();
$db->db_connect('w');
if (!$db->is_connected() || $db->is_error())
die("No DB connection\n");
+ini_set('display_errors', 1);
/*
* Script controller
*/
switch (strtolower($action)) {
/*
* Clear/expunge all cache records
*/
case 'expunge':
$folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','journal','note','task');
$folder_types_db = array_map(array($db, 'quote'), $folder_types);
$expire = strtotime(!empty($opts[2]) ? $opts[2] : 'now - 10 days');
$sql_where = "type IN (" . join(',', $folder_types_db) . ")";
if ($opts['username']) {
$sql_where .= ' AND resource LIKE ?';
}
$sql_query = "DELETE FROM %s WHERE folder_id IN (SELECT folder_id FROM kolab_folders WHERE $sql_where) AND created <= " . $db->quote(date('Y-m-d 00:00:00', $expire));
if ($opts['limit']) {
$sql_query = ' LIMIT ' . intval($opts['limit']);
}
foreach ($folder_types as $type) {
$table_name = 'kolab_cache_' . $type;
$db->query(sprintf($sql_query, $table_name), resource_prefix($opts).'%');
echo $db->affected_rows() . " records deleted from '$table_name'\n";
}
$db->query("UPDATE kolab_folders SET ctag='' WHERE $sql_where", resource_prefix($opts).'%');
break;
case 'clear':
$folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','journal','note','task');
$folder_types_db = array_map(array($db, 'quote'), $folder_types);
if ($opts['all']) {
$sql_query = "DELETE FROM kolab_folders WHERE 1";
}
else if ($opts['username']) {
$sql_query = "DELETE FROM kolab_folders WHERE type IN (" . join(',', $folder_types_db) . ") AND resource LIKE ?";
}
if ($sql_query) {
- $db->query($sql_query . $sql_add, resource_prefix($opts).'%');
+ $db->query($sql_query, resource_prefix($opts).'%');
echo $db->affected_rows() . " records deleted from 'kolab_folders'\n";
}
break;
/*
* Prewarm cache by synchronizing objects for the given user
*/
case 'prewarm':
// make sure libkolab classes are loaded
$rcmail->plugins->load_plugin('libkolab');
if (authenticate($opts)) {
$folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
foreach ($folder_types as $type) {
// sync every folder of the given type
foreach (kolab_storage::get_folders($type) as $folder) {
echo "Synching " . $folder->name . " ($type) ... ";
echo $folder->count($type) . "\n";
// also sync distribution lists in contact folders
if ($type == 'contact') {
echo "Synching " . $folder->name . " (distribution-list) ... ";
echo $folder->count('distribution-list') . "\n";
}
}
}
}
else
die("Authentication failed for " . $opts['user']);
break;
+/**
+ * Update the cache meta columns from the serialized/xml data
+ * (might be run after a schema update)
+ */
+case 'update':
+ // make sure libkolab classes are loaded
+ $rcmail->plugins->load_plugin('libkolab');
+
+ $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
+ foreach ($folder_types as $type) {
+ $class = 'kolab_storage_cache_' . $type;
+ $sql_result = $db->query("SELECT folder_id FROM kolab_folders WHERE type=? AND synclock = 0", $type);
+ while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
+ $folder = new $class;
+ $folder->select_by_id($sql_arr['folder_id']);
+ echo "Updating " . $sql_arr['folder_id'] . " ($type) ";
+ foreach ($folder->select() as $object) {
+ $object['_formatobj']->to_array(); // load data
+ $folder->save($object['_msguid'], $object, $object['_msguid']);
+ echo ".";
+ }
+ echo "done.\n";
+ }
+ }
+ break;
+
/*
* Unknown action => show usage
*/
default:
print_usage();
exit;
}
/**
* Compose cache resource URI prefix for the given user credentials
*/
function resource_prefix($opts)
{
return 'imap://' . str_replace('%', '\\%', urlencode($opts['username'])) . '@' . $opts['host'] . '/';
}
/**
* Authenticate to the IMAP server with the given user credentials
*/
function authenticate(&$opts)
{
global $rcmail;
// prompt for password
if (empty($opts['password']) && ($opts['username'] || $opts['user'])) {
$opts['password'] = prompt_silent("Password: ");
}
// simulate "login as" feature
if ($opts['user'] && $opts['user'] != $opts['username'])
$_POST['_loginas'] = $opts['username'];
else if (empty($opts['user']))
$opts['user'] = $opts['username'];
// let the kolab_auth plugin do its magic
$auth = $rcmail->plugins->exec_hook('authenticate', array(
'host' => trim($opts['host']),
'user' => trim($opts['user']),
'pass' => $opts['password'],
'cookiecheck' => false,
'valid' => !empty($opts['user']) && !empty($opts['host']),
));
if ($auth['valid']) {
$storage = $rcmail->get_storage();
if ($storage->connect($auth['host'], $auth['user'], $auth['pass'], 143, false)) {
if ($opts['verbose'])
echo "IMAP login succeeded.\n";
if (($user = rcube_user::query($opts['username'], $auth['host'])) && $user->ID)
- $rcmail->set_user($user);
+ $rcmail->user = $user;
}
else
die("Login to IMAP server failed!\n");
}
else {
die("Invalid login credentials!\n");
}
return $auth['valid'];
}
diff --git a/lib/drivers/kolab/plugins/libkolab/bin/randomcontacts.sh b/lib/drivers/kolab/plugins/libkolab/bin/randomcontacts.sh
new file mode 100755
index 0000000..e4a820c
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/bin/randomcontacts.sh
@@ -0,0 +1,181 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * Generate a number contacts with random data
+ *
+ * @version 3.1
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, 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/>.
+ */
+
+define('INSTALL_PATH', realpath('.') . '/' );
+ini_set('display_errors', 1);
+
+if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
+ die("Execute this from the Roundcube installation dir!\n\n");
+
+require_once INSTALL_PATH . 'program/include/clisetup.php';
+
+function print_usage()
+{
+ print "Usage: randomcontacts.sh [OPTIONS] USERNAME FOLDER\n";
+ print "Create random contact that for then given user in the specified folder.\n";
+ print "-n, --num Number of contacts to be created, defaults to 50\n";
+ print "-h, --host IMAP host name\n";
+ print "-p, --password IMAP user password\n";
+}
+
+// read arguments
+$opts = get_opt(array(
+ 'n' => 'num',
+ 'h' => 'host',
+ 'u' => 'user',
+ 'p' => 'pass',
+ 'v' => 'verbose',
+));
+
+$opts['username'] = !empty($opts[0]) ? $opts[0] : $opts['user'];
+$opts['folder'] = $opts[1];
+
+$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
+$rcmail->plugins->load_plugins(array('libkolab'));
+ini_set('display_errors', 1);
+
+
+if (empty($opts['host'])) {
+ $opts['host'] = $rcmail->config->get('default_host');
+ if (is_array($opts['host'])) // not unique
+ $opts['host'] = null;
+}
+
+if (empty($opts['username']) || empty($opts['folder']) || empty($opts['host'])) {
+ print_usage();
+ exit;
+}
+
+// prompt for password
+if (empty($opts['pass'])) {
+ $opts['pass'] = rcube_utils::prompt_silent("Password: ");
+}
+
+// parse $host URL
+$a_host = parse_url($opts['host']);
+if ($a_host['host']) {
+ $host = $a_host['host'];
+ $imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? TRUE : FALSE;
+ $imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
+}
+else {
+ $host = $opts['host'];
+ $imap_port = 143;
+}
+
+// instantiate IMAP class
+$IMAP = $rcmail->get_storage();
+
+// try to connect to IMAP server
+if ($IMAP->connect($host, $opts['username'], $opts['pass'], $imap_port, $imap_ssl)) {
+ print "IMAP login successful.\n";
+ $user = rcube_user::query($opts['username'], $host);
+ $rcmail->user = $user ?: new rcube_user(null, array('username' => $opts['username'], 'host' => $host));
+}
+else {
+ die("IMAP login failed for user " . $opts['username'] . " @ $host\n");
+}
+
+// get contacts folder
+$folder = kolab_storage::get_folder($opts['folder']);
+if (!$folder || empty($folder->type)) {
+ die("Invalid Address Book " . $opts['folder'] . "\n");
+}
+
+$format = new kolab_format_contact;
+
+$num = $opts['num'] ? intval($opts['num']) : 50;
+echo "Creating $num contacts in " . $folder->get_resource_uri() . "\n";
+
+for ($i=0; $i < $num; $i++) {
+ // generate random names
+ $contact = array(
+ 'surname' => random_string(rand(1,2)),
+ 'firstname' => random_string(rand(1,2)),
+ 'organization' => random_string(rand(0,2)),
+ 'profession' => random_string(rand(1,2)),
+ 'email' => array(),
+ 'phone' => array(),
+ 'address' => array(),
+ 'notes' => random_string(rand(10,200)),
+ );
+
+ // randomly add email addresses
+ $em = rand(1,3);
+ for ($e=0; $e < $em; $e++) {
+ $type = array_rand($format->emailtypes);
+ $contact['email'][] = array(
+ 'address' => strtolower(random_string(1) . '@' . random_string(1) . '.tld'),
+ 'type' => $type,
+ );
+ }
+
+ // randomly add phone numbers
+ $ph = rand(1,4);
+ for ($p=0; $p < $ph; $p++) {
+ $type = array_rand($format->phonetypes);
+ $contact['phone'][] = array(
+ 'number' => '+'.rand(2,8).rand(1,9).rand(1,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9),
+ 'type' => $type,
+ );
+ }
+
+ // randomly add addresses
+ $ad = rand(0,2);
+ for ($a=0; $a < $ad; $a++) {
+ $type = array_rand($format->addresstypes);
+ $contact['address'][] = array(
+ 'street' => random_string(rand(1,3)),
+ 'locality' => random_string(rand(1,2)),
+ 'code' => rand(1000, 89999),
+ 'country' => random_string(1),
+ 'type' => $type,
+ );
+ }
+
+ $contact['name'] = $contact['firstname'] . ' ' . $contact['surname'];
+
+ if ($folder->save($contact, 'contact')) {
+ echo ".";
+ }
+ else {
+ echo "x";
+ break; // abort on error
+ }
+}
+
+echo " done.\n";
+
+
+
+function random_string($len)
+{
+ $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a features is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform hereafter referred to without the classical prefix retains many applications, as most manufac- tured parts and many anatomical parts investigated in medical imagery contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise.");
+ for ($i = 0; $i < $len; $i++) {
+ $str .= $words[rand(0,count($words)-1)] . " ";
+ }
+
+ return rtrim($str);
+}
diff --git a/lib/drivers/kolab/plugins/libkolab/composer.json b/lib/drivers/kolab/plugins/libkolab/composer.json
new file mode 100644
index 0000000..8926037
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/composer.json
@@ -0,0 +1,30 @@
+{
+ "name": "kolab/libkolab",
+ "type": "roundcube-plugin",
+ "description": "Plugin to setup a basic environment for the interaction with a Kolab server.",
+ "homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
+ "license": "AGPLv3",
+ "version": "1.1.0",
+ "authors": [
+ {
+ "name": "Thomas Bruederli",
+ "email": "bruederli@kolabsys.com",
+ "role": "Lead"
+ },
+ {
+ "name": "Alensader Machniak",
+ "email": "machniak@kolabsys.com",
+ "role": "Developer"
+ }
+ ],
+ "repositories": [
+ {
+ "type": "composer",
+ "url": "http://plugins.roundcube.net"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0",
+ "roundcube/plugin-installer": ">=0.1.3"
+ }
+}
diff --git a/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist b/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist
index 0c612a3..79d2aa8 100644
--- a/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist
+++ b/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist
@@ -1,32 +1,61 @@
<?php
/* Configuration for libkolab */
// Enable caching of Kolab objects in local database
-$rcmail_config['kolab_cache'] = true;
+$config['kolab_cache'] = true;
// Specify format version to write Kolab objects (must be a string value!)
-$rcmail_config['kolab_format_version'] = '3.0';
+$config['kolab_format_version'] = '3.0';
// Optional override of the URL to read and trigger Free/Busy information of Kolab users
// Defaults to https://<imap-server->/freebusy
-$rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
+$config['kolab_freebusy_server'] = null;
// Enables listing of only subscribed folders. This e.g. will limit
// folders in calendar view or available addressbooks
-$rcmail_config['kolab_use_subscriptions'] = false;
+$config['kolab_use_subscriptions'] = false;
+
+// List any of 'personal','shared','other' namespaces to be excluded from groupware folder listing
+// example: array('other');
+$config['kolab_skip_namespace'] = null;
// Enables the use of displayname folder annotations as introduced in KEP:?
// for displaying resource folder names (experimental!)
-$rcmail_config['kolab_custom_display_names'] = false;
+$config['kolab_custom_display_names'] = false;
// Configuration of HTTP requests.
// See http://pear.php.net/manual/en/package.http.http-request2.config.php
// for list of supported configuration options (array keys)
-$rcmail_config['kolab_http_request'] = array();
+$config['kolab_http_request'] = array();
// When kolab_cache is enabled Roundcube's messages cache will be redundant
// when working on kolab folders. Here we can:
// 2 - bypass messages/indexes cache completely
// 1 - bypass only messages, but use index cache
-$rcmail_config['kolab_messages_cache_bypass'] = 0;
+$config['kolab_messages_cache_bypass'] = 0;
+
+// LDAP directory to find avilable users for folder sharing.
+// Either contains an array with LDAP addressbook configuration or refers to entry in $config['ldap_public'].
+// If not specified, the configuraton from 'kolab_auth_addressbook' will be used.
+$config['kolab_users_directory'] = null;
+
+// Filter to be used for resolving user folders in LDAP.
+// Defaults to the 'kolab_auth_filter' configuration option.
+$config['kolab_users_filter'] = '(&(objectclass=kolabInetOrgPerson)(|(uid=%u)(mail=%fu)))';
+
+// Which property of the LDAP user record to use for user folder mapping in IMAP.
+// Defaults to the 'kolab_auth_login' configuration option.
+$config['kolab_users_id_attrib'] = null;
+
+// Use these attributes when searching users in LDAP
+$config['kolab_users_search_attrib'] = array('cn','mail','alias');
+
+// JSON-RPC endpoint configuration of the Bonnie web service providing historic data for groupware objects
+$config['kolab_bonnie_api'] = array(
+ 'uri' => 'https://<kolab-hostname>:8080/api/rpc',
+ 'user' => 'webclient',
+ 'pass' => 'Welcome2KolabSystems',
+ 'secret' => '8431f191707fffffff00000000cccc',
+ 'debug' => true, // logs requests/responses to <log-dir>/bonnie
+);
diff --git a/lib/drivers/kolab/plugins/libkolab/js/folderlist.js b/lib/drivers/kolab/plugins/libkolab/js/folderlist.js
new file mode 100644
index 0000000..62a60ef
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/js/folderlist.js
@@ -0,0 +1,350 @@
+/**
+ * Kolab groupware folders treelist widget
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * @licstart The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * Copyright (C) 2014, 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/>.
+ *
+ * @licend The above is the entire license notice
+ * for the JavaScript code in this file.
+ */
+
+function kolab_folderlist(node, p)
+{
+ // extends treelist.js
+ rcube_treelist_widget.call(this, node, p);
+
+ // private vars
+ var me = this;
+ var search_results;
+ var search_results_widget;
+ var search_results_container;
+ var listsearch_request;
+ var search_messagebox;
+
+ var Q = rcmail.quote_html;
+
+ // render the results for folderlist search
+ function render_search_results(results)
+ {
+ if (results.length) {
+ // create treelist widget to present the search results
+ if (!search_results_widget) {
+ var list_id = (me.container.attr('id') || p.id_prefix || '0')
+ search_results_container = $('<div class="searchresults"></div>')
+ .html(p.search_title ? '<h2 class="boxtitle" id="st:' + list_id + '">' + p.search_title + '</h2>' : '')
+ .insertAfter(me.container);
+
+ search_results_widget = new rcube_treelist_widget('<ul>', {
+ id_prefix: p.id_prefix,
+ id_encode: p.id_encode,
+ id_decode: p.id_decode,
+ selectable: false
+ });
+ // copy classes from main list
+ search_results_widget.container.addClass(me.container.attr('class')).attr('aria-labelledby', 'st:' + list_id);
+
+ // register click handler on search result's checkboxes to select the given item for listing
+ search_results_widget.container
+ .appendTo(search_results_container)
+ .on('click', 'input[type=checkbox], a.subscribed, span.subscribed', function(e) {
+ var node, has_children, li = $(this).closest('li'),
+ id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '');
+ if (p.id_decode)
+ id = p.id_decode(id);
+ node = search_results_widget.get_node(id);
+ has_children = node.children && node.children.length;
+
+ e.stopPropagation();
+ e.bubbles = false;
+
+ // activate + subscribe
+ if ($(e.target).hasClass('subscribed')) {
+ search_results[id].subscribed = true;
+ $(e.target).attr('aria-checked', 'true');
+ li.children().first()
+ .toggleClass('subscribed')
+ .find('input[type=checkbox]').get(0).checked = true;
+
+ if (has_children && search_results[id].group == 'other user') {
+ li.find('ul li > div').addClass('subscribed')
+ .find('a.subscribed').attr('aria-checked', 'true');;
+ }
+ }
+ else if (!this.checked) {
+ return;
+ }
+
+ // copy item to the main list
+ add_result2list(id, li, true);
+
+ if (has_children) {
+ li.find('input[type=checkbox]').first().prop('disabled', true).prop('checked', true);
+ li.find('a.subscribed, span.subscribed').first().hide();
+ }
+ else {
+ li.remove();
+ }
+
+ // set partial subscription status
+ if (search_results[id].subscribed && search_results[id].parent && search_results[id].group == 'other') {
+ parent_subscription_status($(me.get_item(id, true)));
+ }
+
+ // set focus to cloned checkbox
+ if (rcube_event.is_keyboard(e)) {
+ $(me.get_item(id, true)).find('input[type=checkbox]').first().focus();
+ }
+ })
+ .on('click', function(e) {
+ var prop, id = String($(e.target).closest('li').attr('id')).replace(new RegExp('^'+p.id_prefix), '');
+ if (p.id_decode)
+ id = p.id_decode(id);
+
+ // forward event
+ if (prop = search_results[id]) {
+ e.data = prop;
+ if (me.triggerEvent('click-item', e) === false) {
+ e.stopPropagation();
+ return false;
+ }
+ }
+ });
+ }
+
+ // add results to list
+ for (var prop, item, i=0; i < results.length; i++) {
+ prop = results[i];
+ item = $(prop.html);
+ search_results[prop.id] = prop;
+ search_results_widget.insert({
+ id: prop.id,
+ classes: [ prop.group || '' ],
+ html: item,
+ collapsed: true,
+ virtual: prop.virtual
+ }, prop.parent);
+
+ // disable checkbox if item already exists in main list
+ if (me.get_node(prop.id) && !me.get_node(prop.id).virtual) {
+ item.find('input[type=checkbox]').first().prop('disabled', true).prop('checked', true);
+ item.find('a.subscribed, span.subscribed').hide();
+ }
+ }
+
+ search_results_container.show();
+ }
+ }
+
+ // helper method to (recursively) add a search result item to the main list widget
+ function add_result2list(id, li, active)
+ {
+ var node = search_results_widget.get_node(id),
+ prop = search_results[id],
+ parent_id = prop.parent || null,
+ has_children = node.children && node.children.length,
+ dom_node = has_children ? li.children().first().clone(true, true) : li.children().first(),
+ childs = [];
+
+ // find parent node and insert at the right place
+ if (parent_id && me.get_node(parent_id)) {
+ dom_node.children('span,a').first().html(Q(prop.editname || prop.listname));
+ }
+ else if (parent_id && search_results[parent_id]) {
+ // copy parent tree from search results
+ add_result2list(parent_id, $(search_results_widget.get_item(parent_id)), false);
+ }
+ else if (parent_id) {
+ // use full name for list display
+ dom_node.children('span,a').first().html(Q(prop.name));
+ }
+
+ // replace virtual node with a real one
+ if (me.get_node(id)) {
+ $(me.get_item(id, true)).children().first()
+ .replaceWith(dom_node)
+ .removeClass('virtual');
+ }
+ else {
+ // copy childs, too
+ if (has_children && prop.group == 'other user') {
+ for (var cid, j=0; j < node.children.length; j++) {
+ if ((cid = node.children[j].id) && search_results[cid]) {
+ childs.push(search_results_widget.get_node(cid));
+ }
+ }
+ }
+
+ // move this result item to the main list widget
+ me.insert({
+ id: id,
+ classes: [ prop.group || '' ],
+ virtual: prop.virtual,
+ html: dom_node,
+ level: node.level,
+ collapsed: true,
+ children: childs
+ }, parent_id, prop.group);
+ }
+
+ delete prop.html;
+ prop.active = active;
+ me.triggerEvent('insert-item', { id: id, data: prop, item: li });
+
+ // register childs, too
+ if (childs.length) {
+ for (var cid, j=0; j < node.children.length; j++) {
+ if ((cid = node.children[j].id) && search_results[cid]) {
+ prop = search_results[cid];
+ delete prop.html;
+ prop.active = false;
+ me.triggerEvent('insert-item', { id: cid, data: prop });
+ }
+ }
+ }
+ }
+
+ // update the given item's parent's (partial) subscription state
+ function parent_subscription_status(li)
+ {
+ var top_li = li.closest(me.container.children('li')),
+ all_childs = $('li > div:not(.treetoggle)', top_li),
+ subscribed = all_childs.filter('.subscribed').length;
+
+ if (subscribed == 0) {
+ top_li.children('div:first').removeClass('subscribed partial');
+ }
+ else {
+ top_li.children('div:first')
+ .addClass('subscribed')[subscribed < all_childs.length ? 'addClass' : 'removeClass']('partial');
+ }
+ }
+
+ // do some magic when search is performed on the widget
+ this.addEventListener('search', function(search) {
+ // hide search results
+ if (search_results_widget) {
+ search_results_container.hide();
+ search_results_widget.reset();
+ }
+ search_results = {};
+
+ if (search_messagebox)
+ rcmail.hide_message(search_messagebox);
+
+ // send search request(s) to server
+ if (search.query && search.execute) {
+ // require a minimum length for the search string
+ if (rcmail.env.autocomplete_min_length && search.query.length < rcmail.env.autocomplete_min_length && search.query != '*') {
+ search_messagebox = rcmail.display_message(
+ rcmail.get_label('autocompletechars').replace('$min', rcmail.env.autocomplete_min_length));
+ return;
+ }
+
+ if (listsearch_request) {
+ // ignore, let the currently running request finish
+ if (listsearch_request.query == search.query) {
+ return;
+ }
+ else { // cancel previous search request
+ rcmail.multi_thread_request_abort(listsearch_request.id);
+ listsearch_request = null;
+ }
+ }
+
+ var sources = p.search_sources || [ 'folders' ];
+ var reqid = rcmail.multi_thread_http_request({
+ items: sources,
+ threads: rcmail.env.autocomplete_threads || 1,
+ action: p.search_action || 'listsearch',
+ postdata: { action:'search', q:search.query, source:'%s' },
+ lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
+ onresponse: render_search_results,
+ whendone: function(data){
+ listsearch_request = null;
+ me.triggerEvent('search-complete', data);
+ }
+ });
+
+ listsearch_request = { id:reqid, query:search.query };
+ }
+ else if (!search.query && listsearch_request) {
+ rcmail.multi_thread_request_abort(listsearch_request.id);
+ listsearch_request = null;
+ }
+ });
+
+ this.container.on('click', 'a.subscribed, span.subscribed', function(e) {
+ var li = $(this).closest('li'),
+ id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+ div = li.children().first(),
+ is_subscribed;
+
+ if (me.is_search()) {
+ id = id.replace(/--xsR$/, '');
+ li = $(me.get_item(id, true));
+ div = $(div).add(li.children().first());
+ }
+
+ if (p.id_decode)
+ id = p.id_decode(id);
+
+ div.toggleClass('subscribed');
+ is_subscribed = div.hasClass('subscribed');
+ $(this).attr('aria-checked', is_subscribed ? 'true' : 'false');
+ me.triggerEvent('subscribe', { id: id, subscribed: is_subscribed, item: li });
+
+ // update subscribe state of all 'virtual user' child folders
+ if (li.hasClass('other user')) {
+ $('ul li > div', li).each(function() {
+ $(this)[is_subscribed ? 'addClass' : 'removeClass']('subscribed');
+ $('.subscribed', div).attr('aria-checked', is_subscribed ? 'true' : 'false');
+ });
+ div.removeClass('partial');
+ }
+ // propagate subscription state to parent 'virtual user' folder
+ else if (li.closest('li.other.user').length) {
+ parent_subscription_status(li);
+ }
+
+ e.stopPropagation();
+ return false;
+ });
+
+ this.container.on('click', 'a.remove', function(e) {
+ var li = $(this).closest('li'),
+ id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '');
+
+ if (me.is_search()) {
+ id = id.replace(/--xsR$/, '');
+ li = $(me.get_item(id, true));
+ }
+
+ if (p.id_decode)
+ id = p.id_decode(id);
+
+ me.triggerEvent('remove', { id: id, item: li });
+
+ e.stopPropagation();
+ return false;
+ });
+}
+
+// link prototype from base class
+kolab_folderlist.prototype = rcube_treelist_widget.prototype;
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api.php
new file mode 100644
index 0000000..23dafd8
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * Provider class for accessing historic groupware object data through the Bonnie service
+ *
+ * API Specification at https://wiki.kolabsys.com/User:Bruederli/Draft:Bonnie_Client_API
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, 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/>.
+ */
+
+class kolab_bonnie_api
+{
+ public $ready = false;
+
+ private $config = array();
+ private $client = null;
+
+
+ /**
+ * Default constructor
+ */
+ public function __construct($config)
+ {
+ $this->config = $confg;
+
+ $this->client = new kolab_bonnie_api_client($config['uri'], $config['timeout'] ?: 5, (bool)$config['debug']);
+
+ $this->client->set_secret($config['secret']);
+ $this->client->set_authentication($config['user'], $config['pass']);
+ $this->client->set_request_user(rcube::get_instance()->get_user_name());
+
+ $this->ready = !empty($config['secret']) && !empty($config['user']) && !empty($config['pass']);
+ }
+
+ /**
+ * Wrapper function for <object>.changelog() API call
+ */
+ public function changelog($type, $uid, $mailbox=null)
+ {
+ return $this->client->execute($type.'.changelog', array('uid' => $uid, 'mailbox' => $mailbox));
+ }
+
+ /**
+ * Wrapper function for <object>.diff() API call
+ */
+ public function diff($type, $uid, $rev, $mailbox=null)
+ {
+ return $this->client->execute($type.'.diff', array('uid' => $uid, 'rev' => $rev, 'mailbox' => $mailbox));
+ }
+
+ /**
+ * Wrapper function for <object>.get() API call
+ */
+ public function get($type, $uid, $rev, $mailbox=null)
+ {
+ return $this->client->execute($type.'.get', array('uid' => $uid, 'rev' => intval($rev), 'mailbox' => $mailbox));
+ }
+
+ /**
+ * Generic wrapper for direct API calls
+ */
+ public function _execute($method, $params = array())
+ {
+ return $this->client->execute($method, $params);
+ }
+
+}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api_client.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api_client.php
new file mode 100644
index 0000000..bc209f4
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api_client.php
@@ -0,0 +1,239 @@
+<?php
+
+/**
+ * JSON-RPC client class with some extra features for communicating with the Bonnie API service.
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, 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/>.
+ */
+
+class kolab_bonnie_api_client
+{
+ /**
+ * URL of the RPC endpoint
+ * @var string
+ */
+ protected $url;
+
+ /**
+ * HTTP client timeout in seconds
+ * @var integer
+ */
+ protected $timeout;
+
+ /**
+ * Debug flag
+ * @var bool
+ */
+ protected $debug;
+
+ /**
+ * Username for authentication
+ * @var string
+ */
+ protected $username;
+
+ /**
+ * Password for authentication
+ * @var string
+ */
+ protected $password;
+
+ /**
+ * Secret key for request signing
+ * @var string
+ */
+ protected $secret;
+
+ /**
+ * Default HTTP headers to send to the server
+ * @var array
+ */
+ protected $headers = array(
+ 'Connection' => 'close',
+ 'Content-Type' => 'application/json',
+ 'Accept' => 'application/json',
+ );
+
+ /**
+ * Constructor
+ *
+ * @param string $url Server URL
+ * @param integer $timeout Request timeout
+ * @param bool $debug Enabled debug logging
+ * @param array $headers Custom HTTP headers
+ */
+ public function __construct($url, $timeout = 5, $debug = false, $headers = array())
+ {
+ $this->url = $url;
+ $this->timeout = $timeout;
+ $this->debug = $debug;
+ $this->headers = array_merge($this->headers, $headers);
+ }
+
+ /**
+ * Setter for secret key for request signing
+ */
+ public function set_secret($secret)
+ {
+ $this->secret = $secret;
+ }
+
+ /**
+ * Setter for the X-Request-User header
+ */
+ public function set_request_user($username)
+ {
+ $this->headers['X-Request-User'] = $username;
+ }
+
+ /**
+ * Set authentication parameters
+ *
+ * @param string $username Username
+ * @param string $password Password
+ */
+ public function set_authentication($username, $password)
+ {
+ $this->username = $username;
+ $this->password = $password;
+ }
+
+ /**
+ * Automatic mapping of procedures
+ *
+ * @param string $method Procedure name
+ * @param array $params Procedure arguments
+ * @return mixed
+ */
+ public function __call($method, $params)
+ {
+ return $this->execute($method, $params);
+ }
+
+ /**
+ * Execute an RPC command
+ *
+ * @param string $method Procedure name
+ * @param array $params Procedure arguments
+ * @return mixed
+ */
+ public function execute($method, array $params = array())
+ {
+ $id = mt_rand();
+
+ $payload = array(
+ 'jsonrpc' => '2.0',
+ 'method' => $method,
+ 'id' => $id,
+ );
+
+ if (!empty($params)) {
+ $payload['params'] = $params;
+ }
+
+ $result = $this->send_request($payload, $method != 'system.keygen');
+
+ if (isset($result['id']) && $result['id'] == $id && array_key_exists('result', $result)) {
+ return $result['result'];
+ }
+ else if (isset($result['error'])) {
+ $this->_debug('ERROR', $result);
+ }
+
+ return null;
+ }
+
+ /**
+ * Do the HTTP request
+ *
+ * @param string $payload Data to send
+ */
+ protected function send_request($payload, $sign = true)
+ {
+ try {
+ $payload_ = json_encode($payload);
+
+ // add request signature
+ if ($sign && !empty($this->secret)) {
+ $this->headers['X-Request-Sign'] = $this->request_signature($payload_);
+ }
+ else if ($this->headers['X-Request-Sign']) {
+ unset($this->headers['X-Request-Sign']);
+ }
+
+ $this->_debug('REQUEST', $payload, $this->headers);
+ $request = libkolab::http_request($this->url, 'POST', array('timeout' => $this->timeout));
+ $request->setHeader($this->headers);
+ $request->setAuth($this->username, $this->password);
+ $request->setBody($payload_);
+
+ $response = $request->send();
+
+ if ($response->getStatus() == 200) {
+ $result = json_decode($response->getBody(), true);
+ $this->_debug('RESPONSE', $result);
+ }
+ else {
+ throw new Exception(sprintf("HTTP %d %s", $response->getStatus(), $response->getReasonPhrase()));
+ }
+ }
+ catch (Exception $e) {
+ rcube::raise_error(array(
+ 'code' => 500,
+ 'type' => 'php',
+ 'message' => "Bonnie API request failed: " . $e->getMessage(),
+ ), true);
+
+ return array('id' => $payload['id'], 'error' => $e->getMessage(), 'code' => -32000);
+ }
+
+ return is_array($result) ? $result : array();
+ }
+
+ /**
+ * Compute the hmac signature for the current event payload using
+ * the secret key configured for this API client
+ *
+ * @param string $data The request payload data
+ * @return string The request signature
+ */
+ protected function request_signature($data)
+ {
+ // TODO: get the session key with a system.keygen call
+ return hash_hmac('sha256', $this->headers['X-Request-User'] . ':' . $data, $this->secret);
+ }
+
+ /**
+ * Write debug log
+ */
+ protected function _debug(/* $message, $data1, data2, ...*/)
+ {
+ if (!$this->debug)
+ return;
+
+ $args = func_get_args();
+
+ $msg = array();
+ foreach ($args as $arg) {
+ $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
+ }
+
+ rcube::write_log('bonnie', join(";\n", $msg));
+ }
+
+}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_date_recurrence.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_date_recurrence.php
index 85ffd91..06dd331 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_date_recurrence.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_date_recurrence.php
@@ -1,135 +1,135 @@
<?php
/**
* Recurrence computation class for xcal-based Kolab format objects
*
* Utility class to compute instances of recurring events.
* It requires the libcalendaring PHP module to be installed and loaded.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
class kolab_date_recurrence
{
private /* EventCal */ $engine;
private /* kolab_format_xcal */ $object;
private /* DateTime */ $start;
private /* DateTime */ $next;
private /* cDateTime */ $cnext;
private /* DateInterval */ $duration;
/**
* Default constructor
*
* @param array The Kolab object to operate on
*/
function __construct($object)
{
$data = $object->to_array();
$this->object = $object;
$this->engine = $object->to_libcal();
$this->start = $this->next = $data['start'];
$this->cnext = kolab_format::get_datetime($this->next);
if (is_object($data['start']) && is_object($data['end']))
$this->duration = $data['start']->diff($data['end']);
else
$this->duration = new DateInterval('PT' . ($data['end'] - $data['start']) . 'S');
}
/**
* Get date/time of the next occurence of this event
*
* @param boolean Return a Unix timestamp instead of a DateTime object
* @return mixed DateTime object/unix timestamp or False if recurrence ended
*/
public function next_start($timestamp = false)
{
$time = false;
if ($this->engine && $this->next) {
if (($cnext = new cDateTime($this->engine->getNextOccurence($this->cnext))) && $cnext->isValid()) {
$next = kolab_format::php_datetime($cnext);
$time = $timestamp ? $next->format('U') : $next;
$this->cnext = $cnext;
$this->next = $next;
}
}
return $time;
}
/**
* Get the next recurring instance of this event
*
* @return mixed Array with event properties or False if recurrence ended
*/
public function next_instance()
{
if ($next_start = $this->next_start()) {
$next_end = clone $next_start;
$next_end->add($this->duration);
$next = $this->object->to_array();
$next['recurrence_id'] = $next_start->format('Y-m-d');
$next['start'] = $next_start;
$next['end'] = $next_end;
unset($next['_formatobj']);
return $next;
}
return false;
}
/**
* Get the end date of the occurence of this recurrence cycle
*
- * @return mixed Timestamp with end date of the last event or False if recurrence exceeds limit
+ * @return DateTime|bool End datetime of the last event or False if recurrence exceeds limit
*/
public function end()
{
$event = $this->object->to_array();
// recurrence end date is given
if ($event['recurrence']['UNTIL'] instanceof DateTime) {
- return $event['recurrence']['UNTIL']->format('U');
+ return $event['recurrence']['UNTIL'];
}
// let libkolab do the work
if ($this->engine && ($cend = $this->engine->getLastOccurrence()) && ($end_dt = kolab_format::php_datetime(new cDateTime($cend)))) {
- return $end_dt->format('U');
+ return $end_dt;
}
// determine a reasonable end date if none given
- if (!$event['recurrence']['COUNT']) {
+ if (!$event['recurrence']['COUNT'] && $event['end'] instanceof DateTime) {
switch ($event['recurrence']['FREQ']) {
case 'YEARLY': $intvl = 'P100Y'; break;
case 'MONTHLY': $intvl = 'P20Y'; break;
default: $intvl = 'P10Y'; break;
}
- $end_dt = clone $event['start'];
+ $end_dt = clone $event['end'];
$end_dt->add(new DateInterval($intvl));
- return $end_dt->format('U');
+ return $end_dt;
}
return false;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php
index aa88f69..ae7705c 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php
@@ -1,499 +1,583 @@
<?php
/**
* Kolab format model class wrapping libkolabxml bindings
*
* Abstract base class for different Kolab groupware objects read from/written
* to the new Kolab 3 format using the PHP bindings of libkolabxml.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
abstract class kolab_format
{
public static $timezone;
public /*abstract*/ $CTYPE;
public /*abstract*/ $CTYPEv2;
protected /*abstract*/ $objclass;
protected /*abstract*/ $read_func;
protected /*abstract*/ $write_func;
protected $obj;
protected $data;
protected $xmldata;
protected $xmlobject;
protected $formaterror;
protected $loaded = false;
protected $version = '3.0';
const KTYPE_PREFIX = 'application/x-vnd.kolab.';
- const PRODUCT_ID = 'Roundcube-libkolab-0.9';
+ const PRODUCT_ID = 'Roundcube-libkolab-1.1';
/**
* Factory method to instantiate a kolab_format object of the given type and version
*
* @param string Object type to instantiate
* @param float Format version
* @param string Cached xml data to initialize with
* @return object kolab_format
*/
public static function factory($type, $version = '3.0', $xmldata = null)
{
if (!isset(self::$timezone))
self::$timezone = new DateTimeZone('UTC');
if (!self::supports($version))
return PEAR::raiseError("No support for Kolab format version " . $version);
$type = preg_replace('/configuration\.[a-z.]+$/', 'configuration', $type);
$suffix = preg_replace('/[^a-z]+/', '', $type);
$classname = 'kolab_format_' . $suffix;
if (class_exists($classname))
return new $classname($xmldata, $version);
return PEAR::raiseError("Failed to load Kolab Format wrapper for type " . $type);
}
/**
* Determine support for the given format version
*
* @param float Format version to check
* @return boolean True if supported, False otherwise
*/
public static function supports($version)
{
if ($version == '2.0')
return class_exists('kolabobject');
// default is version 3
return class_exists('kolabformat');
}
/**
* Convert the given date/time value into a cDateTime object
*
* @param mixed Date/Time value either as unix timestamp, date string or PHP DateTime object
* @param DateTimeZone The timezone the date/time is in. Use global default if Null, local time if False
* @param boolean True of the given date has no time component
* @return object The libkolabxml date/time object
*/
public static function get_datetime($datetime, $tz = null, $dateonly = false)
{
// use timezone information from datetime of global setting
if (!$tz && $tz !== false) {
if ($datetime instanceof DateTime)
$tz = $datetime->getTimezone();
if (!$tz)
$tz = self::$timezone;
}
$result = new cDateTime();
try {
// got a unix timestamp (in UTC)
if (is_numeric($datetime)) {
$datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
if ($tz) $datetime->setTimezone($tz);
}
else if (is_string($datetime) && strlen($datetime)) {
$datetime = new DateTime($datetime, $tz ?: null);
}
}
catch (Exception $e) {}
if ($datetime instanceof DateTime) {
$result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
if (!$dateonly)
$result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
- if ($tz && $tz->getName() == 'UTC')
+ if ($tz && in_array($tz->getName(), array('UTC', 'GMT', '+00:00', 'Z')))
$result->setUTC(true);
else if ($tz !== false)
$result->setTimezone($tz->getName());
}
return $result;
}
/**
* Convert the given cDateTime into a PHP DateTime object
*
* @param object cDateTime The libkolabxml datetime object
* @return object DateTime PHP datetime instance
*/
public static function php_datetime($cdt)
{
if (!is_object($cdt) || !$cdt->isValid())
return null;
$d = new DateTime;
$d->setTimezone(self::$timezone);
try {
if ($tzs = $cdt->timezone()) {
$tz = new DateTimeZone($tzs);
$d->setTimezone($tz);
}
else if ($cdt->isUTC()) {
$d->setTimezone(new DateTimeZone('UTC'));
}
}
catch (Exception $e) { }
$d->setDate($cdt->year(), $cdt->month(), $cdt->day());
if ($cdt->isDateOnly()) {
$d->_dateonly = true;
$d->setTime(12, 0, 0); // set time to noon to avoid timezone troubles
}
else {
$d->setTime($cdt->hour(), $cdt->minute(), $cdt->second());
}
return $d;
}
/**
* Convert a libkolabxml vector to a PHP array
*
* @param object vector Object
- * @return array Indexed array contaning vector elements
+ * @return array Indexed array containing vector elements
*/
public static function vector2array($vec, $max = PHP_INT_MAX)
{
$arr = array();
for ($i=0; $i < $vec->size() && $i < $max; $i++)
$arr[] = $vec->get($i);
return $arr;
}
/**
* Build a libkolabxml vector (string) from a PHP array
*
* @param array Array with vector elements
* @return object vectors
*/
public static function array2vector($arr)
{
$vec = new vectors;
foreach ((array)$arr as $val) {
if (strlen($val))
$vec->push($val);
}
return $vec;
}
/**
* Parse the X-Kolab-Type header from MIME messages and return the object type in short form
*
* @param string X-Kolab-Type header value
* @return string Kolab object type (contact,event,task,note,etc.)
*/
public static function mime2object_type($x_kolab_type)
{
- return preg_replace('/dictionary.[a-z.]+$/', 'dictionary', substr($x_kolab_type, strlen(self::KTYPE_PREFIX)));
+ return preg_replace(
+ array('/dictionary.[a-z.]+$/', '/contact.distlist$/'),
+ array( 'dictionary', 'distribution-list'),
+ substr($x_kolab_type, strlen(self::KTYPE_PREFIX))
+ );
}
/**
* Default constructor of all kolab_format_* objects
*/
public function __construct($xmldata = null, $version = null)
{
$this->obj = new $this->objclass;
$this->xmldata = $xmldata;
if ($version)
$this->version = $version;
// use libkolab module if available
if (class_exists('kolabobject'))
$this->xmlobject = new XMLObject();
}
/**
* Check for format errors after calling kolabformat::write*()
*
* @return boolean True if there were errors, False if OK
*/
protected function format_errors()
{
$ret = $log = false;
switch (kolabformat::error()) {
case kolabformat::NoError:
$ret = false;
break;
case kolabformat::Warning:
$ret = false;
- $log = "Warning";
+ $uid = is_object($this->obj) ? $this->obj->uid() : $this->data['uid'];
+ $log = "Warning @ $uid";
break;
default:
$ret = true;
$log = "Error";
}
if ($log && !isset($this->formaterror)) {
rcube::raise_error(array(
'code' => 660,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "kolabformat $log: " . kolabformat::errorMessage(),
), true);
$this->formaterror = $ret;
}
return $ret;
}
/**
* Save the last generated UID to the object properties.
* Should be called after kolabformat::writeXXXX();
*/
protected function update_uid()
{
// get generated UID
if (!$this->data['uid']) {
if ($this->xmlobject) {
$this->data['uid'] = $this->xmlobject->getSerializedUID();
}
if (empty($this->data['uid'])) {
$this->data['uid'] = kolabformat::getSerializedUID();
}
$this->obj->setUid($this->data['uid']);
}
}
/**
* Initialize libkolabxml object with cached xml data
*/
protected function init()
{
if (!$this->loaded) {
if ($this->xmldata) {
$this->load($this->xmldata);
$this->xmldata = null;
}
$this->loaded = true;
}
}
/**
* Get constant value for libkolab's version parameter
*
* @param float Version value to convert
* @return int Constant value of either kolabobject::KolabV2 or kolabobject::KolabV3 or false if kolabobject module isn't available
*/
protected function libversion($v = null)
{
if (class_exists('kolabobject')) {
$version = $v ?: $this->version;
if ($version <= '2.0')
return kolabobject::KolabV2;
else
return kolabobject::KolabV3;
}
return false;
}
/**
* Determine the correct libkolab(xml) wrapper function for the given call
* depending on the available PHP modules
*/
protected function libfunc($func)
{
if (is_array($func) || strpos($func, '::'))
return $func;
else if (class_exists('kolabobject'))
return array($this->xmlobject, $func);
else
return 'kolabformat::' . $func;
}
/**
* Direct getter for object properties
*/
public function __get($var)
{
return $this->data[$var];
}
/**
* Load Kolab object data from the given XML block
*
* @param string XML data
* @return boolean True on success, False on failure
*/
public function load($xml)
{
$this->formaterror = null;
$read_func = $this->libfunc($this->read_func);
if (is_array($read_func))
$r = call_user_func($read_func, $xml, $this->libversion());
else
$r = call_user_func($read_func, $xml, false);
if (is_resource($r))
$this->obj = new $this->objclass($r);
else if (is_a($r, $this->objclass))
$this->obj = $r;
$this->loaded = !$this->format_errors();
}
/**
* Write object data to XML format
*
* @param float Format version to write
* @return string XML data
*/
public function write($version = null)
{
$this->formaterror = null;
$this->init();
$write_func = $this->libfunc($this->write_func);
if (is_array($write_func))
$this->xmldata = call_user_func($write_func, $this->obj, $this->libversion($version), self::PRODUCT_ID);
else
$this->xmldata = call_user_func($write_func, $this->obj, self::PRODUCT_ID);
if (!$this->format_errors())
$this->update_uid();
else
$this->xmldata = null;
return $this->xmldata;
}
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
$this->init();
if (!empty($object['uid']))
$this->obj->setUid($object['uid']);
// set some automatic values if missing
if (empty($object['created']) && method_exists($this->obj, 'setCreated')) {
$cdt = $this->obj->created();
$object['created'] = $cdt && $cdt->isValid() ? self::php_datetime($cdt) : new DateTime('now', new DateTimeZone('UTC'));
if (!$cdt || !$cdt->isValid())
$this->obj->setCreated(self::get_datetime($object['created']));
}
$object['changed'] = new DateTime('now', new DateTimeZone('UTC'));
$this->obj->setLastModified(self::get_datetime($object['changed']));
// Save custom properties of the given object
- if (isset($object['x-custom'])) {
+ if (isset($object['x-custom']) && method_exists($this->obj, 'setCustomProperties')) {
$vcustom = new vectorcs;
foreach ((array)$object['x-custom'] as $cp) {
if (is_array($cp))
$vcustom->push(new CustomProperty($cp[0], $cp[1]));
}
$this->obj->setCustomProperties($vcustom);
}
- else { // load custom properties from XML for caching (#2238)
+ // load custom properties from XML for caching (#2238) if method exists (#3125)
+ else if (method_exists($this->obj, 'customProperties')) {
$object['x-custom'] = array();
$vcustom = $this->obj->customProperties();
for ($i=0; $i < $vcustom->size(); $i++) {
$cp = $vcustom->get($i);
$object['x-custom'][] = array($cp->identifier, $cp->value);
}
}
}
/**
* Convert the Kolab object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Kolab object data as hash array
*/
public function to_array($data = array())
{
$this->init();
// read object properties into local data object
$object = array(
'uid' => $this->obj->uid(),
'changed' => self::php_datetime($this->obj->lastModified()),
);
// not all container support the created property
if (method_exists($this->obj, 'created')) {
$object['created'] = self::php_datetime($this->obj->created());
}
// read custom properties
- $vcustom = $this->obj->customProperties();
- for ($i=0; $i < $vcustom->size(); $i++) {
- $cp = $vcustom->get($i);
- $object['x-custom'][] = array($cp->identifier, $cp->value);
+ if (method_exists($this->obj, 'customProperties')) {
+ $vcustom = $this->obj->customProperties();
+ for ($i=0; $i < $vcustom->size(); $i++) {
+ $cp = $vcustom->get($i);
+ $object['x-custom'][] = array($cp->identifier, $cp->value);
+ }
}
// merge with additional data, e.g. attachments from the message
if ($data) {
foreach ($data as $idx => $value) {
if (is_array($value)) {
$object[$idx] = array_merge((array)$object[$idx], $value);
}
else {
$object[$idx] = $value;
}
}
}
return $object;
}
/**
* Object validation method to be implemented by derived classes
*/
abstract public function is_valid();
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
return array();
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
return array();
}
+
+ /**
+ * Utility function to extract object attachment data
+ *
+ * @param array Hash array reference to append attachment data into
+ */
+ public function get_attachments(&$object)
+ {
+ $this->init();
+
+ // handle attachments
+ $vattach = $this->obj->attachments();
+ for ($i=0; $i < $vattach->size(); $i++) {
+ $attach = $vattach->get($i);
+
+ // skip cid: attachments which are mime message parts handled by kolab_storage_folder
+ if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
+ $name = $attach->label();
+ $key = $name . (isset($object['_attachments'][$name]) ? '.'.$i : '');
+ $content = $attach->data();
+ $object['_attachments'][$key] = array(
+ 'id' => 'i:'.$i,
+ 'name' => $name,
+ 'mimetype' => $attach->mimetype(),
+ 'size' => strlen($content),
+ 'content' => $content,
+ );
+ }
+ else if (in_array(substr($attach->uri(), 0, 4), array('http','imap'))) {
+ $object['links'][] = $attach->uri();
+ }
+ }
+ }
+
+ /**
+ * Utility function to set attachment properties to the kolabformat object
+ *
+ * @param array Object data as hash array
+ * @param boolean True to always overwrite attachment information
+ */
+ protected function set_attachments($object, $write = true)
+ {
+ // save attachments
+ $vattach = new vectorattachment;
+ foreach ((array) $object['_attachments'] as $cid => $attr) {
+ if (empty($attr))
+ continue;
+ $attach = new Attachment;
+ $attach->setLabel((string)$attr['name']);
+ $attach->setUri('cid:' . $cid, $attr['mimetype'] ?: 'application/octet-stream');
+ if ($attach->isValid()) {
+ $vattach->push($attach);
+ $write = true;
+ }
+ else {
+ rcube::raise_error(array(
+ 'code' => 660,
+ 'type' => 'php',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ 'message' => "Invalid attributes for attachment $cid: " . var_export($attr, true),
+ ), true);
+ }
+ }
+
+ foreach ((array) $object['links'] as $link) {
+ $attach = new Attachment;
+ $attach->setUri($link, 'unknown');
+ $vattach->push($attach);
+ $write = true;
+ }
+
+ if ($write) {
+ $this->obj->setAttachments($vattach);
+ }
+ }
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php
index 5a8d3ff..17b46a7 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php
@@ -1,139 +1,248 @@
<?php
/**
* Kolab Configuration data model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
class kolab_format_configuration extends kolab_format
{
- public $CTYPE = 'application/x-vnd.kolab.configuration';
+ public $CTYPE = 'application/x-vnd.kolab.configuration';
public $CTYPEv2 = 'application/x-vnd.kolab.configuration';
- protected $objclass = 'Configuration';
- protected $read_func = 'readConfiguration';
+ protected $objclass = 'Configuration';
+ protected $read_func = 'readConfiguration';
protected $write_func = 'writeConfiguration';
private $type_map = array(
+ 'category' => Configuration::TypeCategoryColor,
'dictionary' => Configuration::TypeDictionary,
- 'category' => Configuration::TypeCategoryColor,
+ 'relation' => Configuration::TypeRelation,
+ 'snippet' => Configuration::TypeSnippet,
);
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
// set common object properties
parent::set($object);
// read type-specific properties
switch ($object['type']) {
case 'dictionary':
$dict = new Dictionary($object['language']);
$dict->setEntries(self::array2vector($object['e']));
$this->obj = new Configuration($dict);
break;
case 'category':
// TODO: implement this
$categories = new vectorcategorycolor;
$this->obj = new Configuration($categories);
break;
+
+ case 'relation':
+ $relation = new Relation(strval($object['name']), strval($object['category']));
+
+ if ($object['color']) {
+ $relation->setColor($object['color']);
+ }
+ if ($object['parent']) {
+ $relation->setParent($object['parent']);
+ }
+ if ($object['iconName']) {
+ $relation->setIconName($object['iconName']);
+ }
+ if ($object['priority'] > 0) {
+ $relation->setPriority((int) $object['priority']);
+ }
+ if (!empty($object['members'])) {
+ $relation->setMembers(self::array2vector($object['members']));
+ }
+
+ $this->obj = new Configuration($relation);
+ break;
+
+ case 'snippet':
+ $collection = new SnippetCollection($object['name']);
+ $snippets = new vectorsnippets;
+
+ foreach ((array) $object['snippets'] as $item) {
+ $snippet = new snippet($item['name'], $item['text']);
+ $snippet->setTextType(strtolower($item['type']) == 'html' ? Snippet::HTML : Snippet::Plain);
+ if ($item['shortcut']) {
+ $snippet->setShortCut($item['shortcut']);
+ }
+
+ $snippets->push($snippet);
+ }
+
+ $collection->setSnippets($snippets);
+
+ $this->obj = new Configuration($collection);
+ break;
+
default:
return false;
}
// adjust content-type string
$this->CTYPE = $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return $this->data || (is_object($this->obj) && $this->obj->isValid());
}
/**
* Convert the Configuration object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = array())
{
// return cached result
- if (!empty($this->data))
+ if (!empty($this->data)) {
return $this->data;
+ }
// read common object props into local data object
$object = parent::to_array($data);
$type_map = array_flip($this->type_map);
$object['type'] = $type_map[$this->obj->type()];
// read type-specific properties
switch ($object['type']) {
case 'dictionary':
$dict = $this->obj->dictionary();
$object['language'] = $dict->language();
$object['e'] = self::vector2array($dict->entries());
break;
case 'category':
// TODO: implement this
break;
+
+ case 'relation':
+ $relation = $this->obj->relation();
+
+ $object['name'] = $relation->name();
+ $object['category'] = $relation->type();
+ $object['color'] = $relation->color();
+ $object['parent'] = $relation->parent();
+ $object['iconName'] = $relation->iconName();
+ $object['priority'] = $relation->priority();
+ $object['members'] = self::vector2array($relation->members());
+
+ break;
+
+ case 'snippet':
+ $collection = $this->obj->snippets();
+
+ $object['name'] = $collection->name();
+ $object['snippets'] = array();
+
+ $snippets = $collection->snippets();
+ for ($i=0; $i < $snippets->size(); $i++) {
+ $snippet = $snippets->get($i);
+ $object['snippets'][] = array(
+ 'name' => $snippet->name(),
+ 'text' => $snippet->text(),
+ 'type' => $snippet->textType() == Snippet::HTML ? 'html' : 'plain',
+ 'shortcut' => $snippet->shortCut(),
+ );
+ }
+
+ break;
}
// adjust content-type string
- if ($object['type'])
+ if ($object['type']) {
$this->CTYPE = $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
+ }
$this->data = $object;
return $this->data;
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
$tags = array();
- if ($this->data['type'] == 'dictionary')
+ switch ($this->data['type']) {
+ case 'dictionary':
$tags = array($this->data['language']);
+ break;
+
+ case 'relation':
+ $tags = array('category:' . $this->data['category']);
+ break;
+ }
return $tags;
}
+ /**
+ * Callback for kolab_storage_cache to get words to index for fulltext search
+ *
+ * @return array List of words to save in cache
+ */
+ public function get_words()
+ {
+ $words = array();
+
+ foreach ((array)$this->data['members'] as $url) {
+ $member = kolab_storage_config::parse_member_url($url);
+
+ if (empty($member)) {
+ if (strpos($url, 'urn:uuid:') === 0) {
+ $words[] = substr($url, 9);
+ }
+ }
+ else if (!empty($member['params']['message-id'])) {
+ $words[] = $member['params']['message-id'];
+ }
+ }
+
+ return $words;
+ }
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_contact.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_contact.php
index 0d0bc75..806a819 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_contact.php
@@ -1,450 +1,482 @@
<?php
/**
* Kolab Contact model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
class kolab_format_contact extends kolab_format
{
public $CTYPE = 'application/vcard+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.contact';
protected $objclass = 'Contact';
protected $read_func = 'readContact';
protected $write_func = 'writeContact';
public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email:address');
public $phonetypes = array(
'home' => Telephone::Home,
'work' => Telephone::Work,
'text' => Telephone::Text,
'main' => Telephone::Voice,
'homefax' => Telephone::Fax,
'workfax' => Telephone::Fax,
'mobile' => Telephone::Cell,
'video' => Telephone::Video,
'pager' => Telephone::Pager,
'car' => Telephone::Car,
'other' => Telephone::Textphone,
);
public $emailtypes = array(
'home' => Email::Home,
'work' => Email::Work,
'other' => Email::NoType,
);
public $addresstypes = array(
'home' => Address::Home,
'work' => Address::Work,
'office' => 0,
);
private $gendermap = array(
'female' => Contact::Female,
'male' => Contact::Male,
);
private $relatedmap = array(
'manager' => Related::Manager,
'assistant' => Related::Assistant,
'spouse' => Related::Spouse,
'children' => Related::Child,
);
/**
* Default constructor
*/
function __construct($xmldata = null, $version = 3.0)
{
parent::__construct($xmldata, $version);
// complete phone types
$this->phonetypes['homefax'] |= Telephone::Home;
$this->phonetypes['workfax'] |= Telephone::Work;
}
/**
* Set contact properties to the kolabformat object
*
* @param array Contact data as hash array
*/
public function set(&$object)
{
// set common object properties
parent::set($object);
// do the hard work of setting object values
$nc = new NameComponents;
$nc->setSurnames(self::array2vector($object['surname']));
$nc->setGiven(self::array2vector($object['firstname']));
$nc->setAdditional(self::array2vector($object['middlename']));
$nc->setPrefixes(self::array2vector($object['prefix']));
$nc->setSuffixes(self::array2vector($object['suffix']));
$this->obj->setNameComponents($nc);
$this->obj->setName($object['name']);
$this->obj->setCategories(self::array2vector($object['categories']));
if (isset($object['nickname']))
$this->obj->setNickNames(self::array2vector($object['nickname']));
- if (isset($object['profession']))
- $this->obj->setTitles(self::array2vector($object['profession']));
+ if (isset($object['jobtitle']))
+ $this->obj->setTitles(self::array2vector($object['jobtitle']));
// organisation related properties (affiliation)
$org = new Affiliation;
$offices = new vectoraddress;
if ($object['organization'])
$org->setOrganisation($object['organization']);
if ($object['department'])
$org->setOrganisationalUnits(self::array2vector($object['department']));
- if ($object['jobtitle'])
- $org->setRoles(self::array2vector($object['jobtitle']));
+ if ($object['profession'])
+ $org->setRoles(self::array2vector($object['profession']));
$rels = new vectorrelated;
- if ($object['manager']) {
- foreach ((array)$object['manager'] as $manager)
- $rels->push(new Related(Related::Text, $manager, Related::Manager));
- }
- if ($object['assistant']) {
- foreach ((array)$object['assistant'] as $assistant)
- $rels->push(new Related(Related::Text, $assistant, Related::Assistant));
+ foreach (array('manager','assistant') as $field) {
+ if (!empty($object[$field])) {
+ $reltype = $this->relatedmap[$field];
+ foreach ((array)$object[$field] as $value) {
+ $rels->push(new Related(Related::Text, $value, $reltype));
+ }
+ }
}
$org->setRelateds($rels);
// im, email, url
$this->obj->setIMaddresses(self::array2vector($object['im']));
if (class_exists('vectoremail')) {
$vemails = new vectoremail;
foreach ((array)$object['email'] as $email) {
$type = $this->emailtypes[$email['type']];
$vemails->push(new Email($email['address'], intval($type)));
}
}
else {
$vemails = self::array2vector(array_map(function($v){ return $v['address']; }, $object['email']));
}
$this->obj->setEmailAddresses($vemails);
$vurls = new vectorurl;
foreach ((array)$object['website'] as $url) {
$type = $url['type'] == 'blog' ? Url::Blog : Url::NoType;
$vurls->push(new Url($url['url'], $type));
}
$this->obj->setUrls($vurls);
// addresses
$adrs = new vectoraddress;
foreach ((array)$object['address'] as $address) {
$adr = new Address;
$type = $this->addresstypes[$address['type']];
if (isset($type))
$adr->setTypes($type);
else if ($address['type'])
$adr->setLabel($address['type']);
if ($address['street'])
$adr->setStreet($address['street']);
if ($address['locality'])
$adr->setLocality($address['locality']);
if ($address['code'])
$adr->setCode($address['code']);
if ($address['region'])
$adr->setRegion($address['region']);
if ($address['country'])
$adr->setCountry($address['country']);
if ($address['type'] == 'office')
$offices->push($adr);
else
$adrs->push($adr);
}
$this->obj->setAddresses($adrs);
$org->setAddresses($offices);
// add org affiliation after addresses are set
$orgs = new vectoraffiliation;
$orgs->push($org);
$this->obj->setAffiliations($orgs);
// telephones
$tels = new vectortelephone;
foreach ((array)$object['phone'] as $phone) {
$tel = new Telephone;
if (isset($this->phonetypes[$phone['type']]))
$tel->setTypes($this->phonetypes[$phone['type']]);
$tel->setNumber($phone['number']);
$tels->push($tel);
}
$this->obj->setTelephones($tels);
if (isset($object['gender']))
$this->obj->setGender($this->gendermap[$object['gender']] ? $this->gendermap[$object['gender']] : Contact::NotSet);
if (isset($object['notes']))
$this->obj->setNote($object['notes']);
if (isset($object['freebusyurl']))
$this->obj->setFreeBusyUrl($object['freebusyurl']);
+ if (isset($object['lang']))
+ $this->obj->setLanguages(self::array2vector($object['lang']));
if (isset($object['birthday']))
$this->obj->setBDay(self::get_datetime($object['birthday'], false, true));
if (isset($object['anniversary']))
$this->obj->setAnniversary(self::get_datetime($object['anniversary'], false, true));
if (!empty($object['photo'])) {
if ($type = rcube_mime::image_content_type($object['photo']))
$this->obj->setPhoto($object['photo'], $type);
}
else if (isset($object['photo']))
$this->obj->setPhoto('','');
else if ($this->obj->photoMimetype()) // load saved photo for caching
$object['photo'] = $this->obj->photo();
// spouse and children are relateds
$rels = new vectorrelated;
- if ($object['spouse']) {
- $rels->push(new Related(Related::Text, $object['spouse'], Related::Spouse));
+ foreach (array('spouse','children') as $field) {
+ if (!empty($object[$field])) {
+ $reltype = $this->relatedmap[$field];
+ foreach ((array)$object[$field] as $value) {
+ $rels->push(new Related(Related::Text, $value, $reltype));
+ }
+ }
}
- if ($object['children']) {
- foreach ((array)$object['children'] as $child)
- $rels->push(new Related(Related::Text, $child, Related::Child));
+ // add other relateds
+ if (is_array($object['related'])) {
+ foreach ($object['related'] as $value) {
+ $rels->push(new Related(Related::Text, $value));
+ }
}
$this->obj->setRelateds($rels);
// insert/replace crypto keys
$pgp_index = $pkcs7_index = -1;
$keys = $this->obj->keys();
for ($i=0; $i < $keys->size(); $i++) {
$key = $keys->get($i);
if ($pgp_index < 0 && $key->type() == Key::PGP)
$pgp_index = $i;
else if ($pkcs7_index < 0 && $key->type() == Key::PKCS7_MIME)
$pkcs7_index = $i;
}
$pgpkey = $object['pgppublickey'] ? new Key($object['pgppublickey'], Key::PGP) : new Key();
$pkcs7key = $object['pkcs7publickey'] ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
if ($pgp_index >= 0)
$keys->set($pgp_index, $pgpkey);
else if (!empty($object['pgppublickey']))
$keys->push($pgpkey);
if ($pkcs7_index >= 0)
$keys->set($pkcs7_index, $pkcs7key);
else if (!empty($object['pkcs7publickey']))
$keys->push($pkcs7key);
$this->obj->setKeys($keys);
// TODO: handle language, gpslocation, etc.
// set type property for proper caching
$object['_type'] = 'contact';
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/));
}
/**
* Convert the Contact object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Contact data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common object props into local data object
$object = parent::to_array($data);
$object['name'] = $this->obj->name();
$nc = $this->obj->nameComponents();
$object['surname'] = join(' ', self::vector2array($nc->surnames()));
$object['firstname'] = join(' ', self::vector2array($nc->given()));
$object['middlename'] = join(' ', self::vector2array($nc->additional()));
$object['prefix'] = join(' ', self::vector2array($nc->prefixes()));
$object['suffix'] = join(' ', self::vector2array($nc->suffixes()));
$object['nickname'] = join(' ', self::vector2array($this->obj->nickNames()));
- $object['profession'] = join(' ', self::vector2array($this->obj->titles()));
+ $object['jobtitle'] = join(' ', self::vector2array($this->obj->titles()));
$object['categories'] = self::vector2array($this->obj->categories());
// organisation related properties (affiliation)
$orgs = $this->obj->affiliations();
if ($orgs->size()) {
$org = $orgs->get(0);
$object['organization'] = $org->organisation();
- $object['jobtitle'] = join(' ', self::vector2array($org->roles()));
+ $object['profession'] = join(' ', self::vector2array($org->roles()));
$object['department'] = join(' ', self::vector2array($org->organisationalUnits()));
$this->read_relateds($org->relateds(), $object);
}
$object['im'] = self::vector2array($this->obj->imAddresses());
$emails = $this->obj->emailAddresses();
if ($emails instanceof vectoremail) {
$emailtypes = array_flip($this->emailtypes);
for ($i=0; $i < $emails->size(); $i++) {
$email = $emails->get($i);
$object['email'][] = array('address' => $email->address(), 'type' => $emailtypes[$email->types()]);
}
}
else {
$object['email'] = self::vector2array($emails);
}
$urls = $this->obj->urls();
for ($i=0; $i < $urls->size(); $i++) {
$url = $urls->get($i);
$subtype = $url->type() == Url::Blog ? 'blog' : 'homepage';
$object['website'][] = array('url' => $url->url(), 'type' => $subtype);
}
// addresses
$this->read_addresses($this->obj->addresses(), $object);
if ($org && ($offices = $org->addresses()))
$this->read_addresses($offices, $object, 'office');
// telehones
$tels = $this->obj->telephones();
$teltypes = array_flip($this->phonetypes);
for ($i=0; $i < $tels->size(); $i++) {
$tel = $tels->get($i);
$object['phone'][] = array('number' => $tel->number(), 'type' => $teltypes[$tel->types()]);
}
$object['notes'] = $this->obj->note();
$object['freebusyurl'] = $this->obj->freeBusyUrl();
+ $object['lang'] = self::vector2array($this->obj->languages());
if ($bday = self::php_datetime($this->obj->bDay()))
- $object['birthday'] = $bday->format('c');
+ $object['birthday'] = $bday;
if ($anniversary = self::php_datetime($this->obj->anniversary()))
- $object['anniversary'] = $anniversary->format('c');
+ $object['anniversary'] = $anniversary;
$gendermap = array_flip($this->gendermap);
if (($g = $this->obj->gender()) && $gendermap[$g])
$object['gender'] = $gendermap[$g];
if ($this->obj->photoMimetype())
$object['photo'] = $this->obj->photo();
else if ($this->xmlobject && ($photo_name = $this->xmlobject->pictureAttachmentName()))
$object['photo'] = $photo_name;
// relateds -> spouse, children
- $this->read_relateds($this->obj->relateds(), $object);
+ $this->read_relateds($this->obj->relateds(), $object, 'related');
// crypto settings: currently only key values are supported
$keys = $this->obj->keys();
for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
$key = $keys->get($i);
if ($key->type() == Key::PGP)
$object['pgppublickey'] = $key->key();
else if ($key->type() == Key::PKCS7_MIME)
$object['pkcs7publickey'] = $key->key();
}
$this->data = $object;
return $this->data;
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
$data = '';
foreach (self::$fulltext_cols as $colname) {
list($col, $field) = explode(':', $colname);
if ($field) {
$a = array();
foreach ((array)$this->data[$col] as $attr)
$a[] = $attr[$field];
$val = join(' ', $a);
}
else {
$val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
}
if (strlen($val))
$data .= $val . ' ';
}
return array_unique(rcube_utils::normalize_string($data, true));
}
+ /**
+ * Callback for kolab_storage_cache to get object specific tags to cache
+ *
+ * @return array List of tags to save in cache
+ */
+ public function get_tags()
+ {
+ $tags = array();
+
+ if (!empty($this->data['birthday'])) {
+ $tags[] = 'x-has-birthday';
+ }
+
+ return $tags;
+ }
+
/**
* Helper method to copy contents of an Address vector to the contact data object
*/
private function read_addresses($addresses, &$object, $type = null)
{
$adrtypes = array_flip($this->addresstypes);
for ($i=0; $i < $addresses->size(); $i++) {
$adr = $addresses->get($i);
$object['address'][] = array(
'type' => $type ? $type : ($adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : ''), /*$adr->label()),*/
'street' => $adr->street(),
'code' => $adr->code(),
'locality' => $adr->locality(),
'region' => $adr->region(),
'country' => $adr->country()
);
}
}
/**
* Helper method to map contents of a Related vector to the contact data object
*/
- private function read_relateds($rels, &$object)
+ private function read_relateds($rels, &$object, $catchall = null)
{
$typemap = array_flip($this->relatedmap);
for ($i=0; $i < $rels->size(); $i++) {
$rel = $rels->get($i);
if ($rel->type() != Related::Text) // we can't handle UID relations yet
continue;
+ $known = false;
$types = $rel->relationTypes();
foreach ($typemap as $t => $field) {
if ($types & $t) {
$object[$field][] = $rel->text();
+ $known = true;
break;
}
}
+
+ if (!$known && $catchall) {
+ $object[$catchall][] = $rel->text();
+ }
}
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php
index 46dda01..88c6f7b 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -1,113 +1,125 @@
<?php
/**
* Kolab Distribution List model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
class kolab_format_distributionlist extends kolab_format
{
public $CTYPE = 'application/vcard+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.distribution-list';
protected $objclass = 'DistList';
protected $read_func = 'readDistlist';
protected $write_func = 'writeDistlist';
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
// set common object properties
parent::set($object);
$this->obj->setName($object['name']);
+ $seen = array();
$members = new vectorcontactref;
- foreach ((array)$object['member'] as $member) {
- if ($member['uid'])
+ foreach ((array)$object['member'] as $i => $member) {
+ if ($member['uid']) {
+ $key = 'uid:' . $member['uid'];
$m = new ContactReference(ContactReference::UidReference, $member['uid']);
- else if ($member['email'])
+ }
+ else if ($member['email']) {
+ $key = 'mailto:' . $member['email'];
$m = new ContactReference(ContactReference::EmailReference, $member['email']);
- else
+ $m->setName($member['name']);
+ }
+ else {
continue;
-
- $m->setName($member['name']);
- $members->push($m);
+ }
+
+ if (!$seen[$key]++) {
+ $members->push($m);
+ }
+ else {
+ // remove dupes for caching
+ unset($object['member'][$i]);
+ }
}
$this->obj->setMembers($members);
// set type property for proper caching
$object['_type'] = 'distribution-list';
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
* Convert the Distlist object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Distribution list data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common object props into local data object
$object = parent::to_array($data);
// add object properties
$object += array(
'name' => $this->obj->name(),
'member' => array(),
'_type' => 'distribution-list',
);
$members = $this->obj->members();
for ($i=0; $i < $members->size(); $i++) {
$member = $members->get($i);
// if ($member->type() == ContactReference::UidReference && ($uid = $member->uid()))
$object['member'][] = array(
'uid' => $member->uid(),
'email' => $member->email(),
'name' => $member->name(),
);
}
$this->data = $object;
return $this->data;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php
index 9be9bdf..c233f44 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php
@@ -1,234 +1,236 @@
<?php
/**
* Kolab Event model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
class kolab_format_event extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.event';
+ public $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+
protected $objclass = 'Event';
protected $read_func = 'readEvent';
protected $write_func = 'writeEvent';
/**
* Default constructor
*/
function __construct($data = null, $version = 3.0)
{
parent::__construct(is_string($data) ? $data : null, $version);
// got an Event object as argument
if (is_object($data) && is_a($data, $this->objclass)) {
$this->obj = $data;
$this->loaded = true;
}
}
/**
* Clones into an instance of libcalendaring's extended EventCal class
*
* @return mixed EventCal object or false on failure
*/
public function to_libcal()
{
static $error_logged = false;
if (class_exists('kolabcalendaring')) {
return new EventCal($this->obj);
}
else if (!$error_logged) {
$error_logged = true;
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabcalendaring module not found"
), true);
}
return false;
}
/**
* Set event properties to the kolabformat object
*
* @param array Event data as hash array
*/
public function set(&$object)
{
// set common xcal properties
parent::set($object);
// do the hard work of setting object values
$this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
$this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
$this->obj->setTransparency($object['free_busy'] == 'free');
$status = kolabformat::StatusUndefined;
if ($object['free_busy'] == 'tentative')
$status = kolabformat::StatusTentative;
if ($object['cancelled'])
$status = kolabformat::StatusCancelled;
+ else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
+ $status = $this->status_map[$object['status']];
$this->obj->setStatus($status);
// save recurrence exceptions
- if ($object['recurrence']['EXCEPTIONS']) {
+ if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
$vexceptions = new vectorevent;
foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
$exevent = new kolab_format_event;
$exevent->set($this->compact_exception($exception, $object)); // only save differing values
$exevent->obj->setRecurrenceID(self::get_datetime($exception['start'], null, true), (bool)$exception['thisandfuture']);
$vexceptions->push($exevent->obj);
}
$this->obj->setExceptions($vexceptions);
}
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return !$this->formaterror && (($this->data && !empty($this->data['start']) && !empty($this->data['end'])) ||
(is_object($this->obj) && $this->obj->isValid() && $this->obj->uid()));
}
/**
* Convert the Event object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Event data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common xcal props
$object = parent::to_array($data);
// read object properties
$object += array(
'end' => self::php_datetime($this->obj->end()),
'allday' => $this->obj->start()->isDateOnly(),
'free_busy' => $this->obj->transparency() ? 'free' : 'busy', // TODO: transparency is only boolean
'attendees' => array(),
);
// derive event end from duration (#1916)
if (!$object['end'] && $object['start'] && ($duration = $this->obj->duration()) && $duration->isValid()) {
$interval = new DateInterval('PT0S');
$interval->d = $duration->weeks() * 7 + $duration->days();
$interval->h = $duration->hours();
$interval->i = $duration->minutes();
$interval->s = $duration->seconds();
$object['end'] = clone $object['start'];
$object['end']->add($interval);
}
// organizer is part of the attendees list in Roundcube
if ($object['organizer']) {
$object['organizer']['role'] = 'ORGANIZER';
array_unshift($object['attendees'], $object['organizer']);
}
// status defines different event properties...
$status = $this->obj->status();
if ($status == kolabformat::StatusTentative)
$object['free_busy'] = 'tentative';
else if ($status == kolabformat::StatusCancelled)
$object['cancelled'] = true;
+ // this is an exception object
+ if ($this->obj->recurrenceID()->isValid()) {
+ $object['thisandfuture'] = $this->obj->thisAndFuture();
+ }
// read exception event objects
- if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
+ else if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
+ $recurrence_exceptions = array();
for ($i=0; $i < $exceptions->size(); $i++) {
if (($exobj = $exceptions->get($i))) {
$exception = new kolab_format_event($exobj);
if ($exception->is_valid()) {
- $object['recurrence']['EXCEPTIONS'][] = $this->expand_exception($exception->to_array(), $object);
+ $recurrence_exceptions[] = $this->expand_exception($exception->to_array(), $object);
}
}
}
- }
- // this is an exception object
- else if ($this->obj->recurrenceID()->isValid()) {
- $object['thisandfuture'] = $this->obj->thisAndFuture();
+ $object['recurrence']['EXCEPTIONS'] = $recurrence_exceptions;
}
return $this->data = $object;
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
- $tags = array();
+ $tags = parent::get_tags();
foreach ((array)$this->data['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
- if (!empty($this->data['alarms'])) {
- $tags[] = 'x-has-alarms';
- }
-
return $tags;
}
/**
* Remove some attributes from the exception container
*/
private function compact_exception($exception, $master)
{
$forbidden = array('recurrence','organizer','attendees','sequence');
foreach ($forbidden as $prop) {
if (array_key_exists($prop, $exception)) {
unset($exception[$prop]);
}
}
return $exception;
}
/**
* Copy attributes not specified by the exception from the master event
*/
private function expand_exception($exception, $master)
{
foreach ($master as $prop => $value) {
if (empty($exception[$prop]) && !empty($value))
$exception[$prop] = $value;
}
return $exception;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_file.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_file.php
index 5f73bf1..34c0ca6 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_file.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_file.php
@@ -1,156 +1,156 @@
<?php
/**
* Kolab File model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
class kolab_format_file extends kolab_format
{
- public $CTYPE = 'application/x-vnd.kolab.file';
+ public $CTYPE = 'application/vnd.kolab+xml';
protected $objclass = 'File';
protected $read_func = 'kolabformat::readKolabFile';
protected $write_func = 'kolabformat::writeKolabFile';
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
'private' => kolabformat::ClassPrivate,
'confidential' => kolabformat::ClassConfidential,
);
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
// set common object properties
parent::set($object);
$this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
$this->obj->setCategories(self::array2vector($object['categories']));
if (isset($object['notes'])) {
$this->obj->setNote($object['notes']);
}
// Add file attachment
if (!empty($object['_attachments'])) {
$cid = key($object['_attachments']);
$attach_attr = $object['_attachments'][$cid];
$attach = new Attachment;
$attach->setLabel((string)$attach_attr['name']);
$attach->setUri('cid:' . $cid, $attach_attr['mimetype']);
$this->obj->setFile($attach);
// make sure size is set, so object saved in cache contains this info
if (!isset($attach_attr['size'])) {
$size = 0;
if (!empty($attach_attr['content'])) {
if (is_resource($attach_attr['content'])) {
$stat = fstat($attach_attr['content']);
$size = $stat ? $stat['size'] : 0;
}
else {
$size = strlen($attach_attr['content']);
}
}
else if (isset($attach_attr['path'])) {
$size = @filesize($attach_attr['path']);
}
$object['_attachments'][$cid]['size'] = $size;
}
}
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
* Check if object's data validity
*/
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
* Convert the Configuration object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data)) {
return $this->data;
}
// read common object props into local data object
$object = parent::to_array($data);
$sensitivity_map = array_flip($this->sensitivity_map);
// read object properties
$object += array(
'sensitivity' => $sensitivity_map[$this->obj->classification()],
'categories' => self::vector2array($this->obj->categories()),
'notes' => $this->obj->note(),
);
return $this->data = $object;
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
$tags = array();
foreach ((array)$this->data['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
// Add file mimetype to tags
if (!empty($this->data['_attachments'])) {
reset($this->data['_attachments']);
$key = key($this->data['_attachments']);
$attachment = $this->data['_attachments'][$key];
if ($attachment['mimetype']) {
$tags[] = $attachment['mimetype'];
}
}
return $tags;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php
index 04a8421..bca5156 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php
@@ -1,82 +1,153 @@
<?php
/**
* Kolab Note model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
class kolab_format_note extends kolab_format
{
- public $CTYPE = 'application/x-vnd.kolab.note';
+ public $CTYPE = 'application/vnd.kolab+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.note';
+ public static $fulltext_cols = array('title', 'description', 'categories');
+
protected $objclass = 'Note';
protected $read_func = 'readNote';
protected $write_func = 'writeNote';
+ protected $sensitivity_map = array(
+ 'public' => kolabformat::ClassPublic,
+ 'private' => kolabformat::ClassPrivate,
+ 'confidential' => kolabformat::ClassConfidential,
+ );
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
// set common object properties
parent::set($object);
- // TODO: set object propeties
+ $this->obj->setSummary($object['title']);
+ $this->obj->setDescription($object['description']);
+ $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
+ $this->obj->setCategories(self::array2vector($object['categories']));
+
+ $this->set_attachments($object);
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
* Convert the Configuration object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common object props into local data object
$object = parent::to_array($data);
- // TODO: read object properties
+ $sensitivity_map = array_flip($this->sensitivity_map);
- $this->data = $object;
- return $this->data;
+ // read object properties
+ $object += array(
+ 'sensitivity' => $sensitivity_map[$this->obj->classification()],
+ 'categories' => self::vector2array($this->obj->categories()),
+ 'title' => $this->obj->summary(),
+ 'description' => $this->obj->description(),
+ );
+
+ $this->get_attachments($object);
+
+ return $this->data = $object;
+ }
+
+ /**
+ * Callback for kolab_storage_cache to get object specific tags to cache
+ *
+ * @return array List of tags to save in cache
+ */
+ public function get_tags()
+ {
+ $tags = array();
+
+ foreach ((array)$this->data['categories'] as $cat) {
+ $tags[] = rcube_utils::normalize_string($cat);
+ }
+
+ // add tag for message references
+ foreach ((array)$this->data['links'] as $link) {
+ $url = parse_url($link);
+ if ($url['scheme'] == 'imap') {
+ parse_str($url['query'], $param);
+ $tags[] = 'ref:' . trim($param['message-id'] ?: urldecode($url['fragment']), '<> ');
+ }
+ }
+
+ return $tags;
+ }
+
+ /**
+ * Callback for kolab_storage_cache to get words to index for fulltext search
+ *
+ * @return array List of words to save in cache
+ */
+ public function get_words()
+ {
+ $data = '';
+ foreach (self::$fulltext_cols as $col) {
+ // convert HTML content to plain text
+ if ($col == 'description' && preg_match('/<(html|body)(\s[a-z]|>)/', $this->data[$col], $m) && strpos($this->data[$col], '</'.$m[1].'>')) {
+ $converter = new rcube_html2text($this->data[$col], false, false, 0);
+ $val = $converter->get_text();
+ }
+ else {
+ $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
+ }
+
+ if (strlen($val))
+ $data .= $val . ' ';
+ }
+
+ return array_filter(array_unique(rcube_utils::normalize_string($data, true)));
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_task.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_task.php
index a15cb0b..52744d4 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_task.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_task.php
@@ -1,125 +1,129 @@
<?php
/**
* Kolab Task (ToDo) model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
class kolab_format_task extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.task';
+ public $scheduling_properties = array('start', 'due', 'summary', 'status');
+
protected $objclass = 'Todo';
protected $read_func = 'readTodo';
protected $write_func = 'writeTodo';
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
// set common xcal properties
parent::set($object);
$this->obj->setPercentComplete(intval($object['complete']));
- if (isset($object['start']))
- $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
+ $status = kolabformat::StatusUndefined;
+ if ($object['complete'] == 100 && !array_key_exists('status', $object))
+ $status = kolabformat::StatusCompleted;
+ else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
+ $status = $this->status_map[$object['status']];
+ $this->obj->setStatus($status);
+ $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
$this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly));
$related = new vectors;
if (!empty($object['parent_id']))
$related->push($object['parent_id']);
$this->obj->setRelatedTo($related);
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
* Convert the Configuration object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common xcal props
$object = parent::to_array($data);
$object['complete'] = intval($this->obj->percentComplete());
// if due date is set
if ($due = $this->obj->due())
$object['due'] = self::php_datetime($due);
// related-to points to parent task; we only support one relation
$related = self::vector2array($this->obj->relatedTo());
if (count($related))
$object['parent_id'] = $related[0];
// TODO: map more properties
$this->data = $object;
return $this->data;
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
- $tags = array();
+ $tags = parent::get_tags();
- if ($this->data['status'] == 'COMPLETED' || $this->data['complete'] == 100)
+ if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status'])))
$tags[] = 'x-complete';
if ($this->data['priority'] == 1)
$tags[] = 'x-flagged';
- if (!empty($this->data['alarms']))
- $tags[] = 'x-has-alarms';
-
if ($this->data['parent_id'])
$tags[] = 'x-parent:' . $this->data['parent_id'];
return $tags;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php
index 500dfa2..08f27d0 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php
@@ -1,452 +1,627 @@
<?php
/**
* Xcal based Kolab format class wrapping libkolabxml bindings
*
* Base class for xcal-based Kolab groupware objects such as event, todo, journal
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
abstract class kolab_format_xcal extends kolab_format
{
public $CTYPE = 'application/calendar+xml';
public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
+ public $scheduling_properties = array('start', 'end', 'location');
+
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
'private' => kolabformat::ClassPrivate,
'confidential' => kolabformat::ClassConfidential,
);
protected $role_map = array(
'REQ-PARTICIPANT' => kolabformat::Required,
'OPT-PARTICIPANT' => kolabformat::Optional,
'NON-PARTICIPANT' => kolabformat::NonParticipant,
'CHAIR' => kolabformat::Chair,
);
protected $cutype_map = array(
'INDIVIDUAL' => kolabformat::CutypeIndividual,
'GROUP' => kolabformat::CutypeGroup,
'ROOM' => kolabformat::CutypeRoom,
'RESOURCE' => kolabformat::CutypeResource,
'UNKNOWN' => kolabformat::CutypeUnknown,
);
protected $rrule_type_map = array(
'MINUTELY' => RecurrenceRule::Minutely,
'HOURLY' => RecurrenceRule::Hourly,
'DAILY' => RecurrenceRule::Daily,
'WEEKLY' => RecurrenceRule::Weekly,
'MONTHLY' => RecurrenceRule::Monthly,
'YEARLY' => RecurrenceRule::Yearly,
);
protected $weekday_map = array(
'MO' => kolabformat::Monday,
'TU' => kolabformat::Tuesday,
'WE' => kolabformat::Wednesday,
'TH' => kolabformat::Thursday,
'FR' => kolabformat::Friday,
'SA' => kolabformat::Saturday,
'SU' => kolabformat::Sunday,
);
protected $alarm_type_map = array(
'DISPLAY' => Alarm::DisplayAlarm,
'EMAIL' => Alarm::EMailAlarm,
'AUDIO' => Alarm::AudioAlarm,
);
- private $status_map = array(
+ protected $status_map = array(
'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
'IN-PROCESS' => kolabformat::StatusInProcess,
'COMPLETED' => kolabformat::StatusCompleted,
'CANCELLED' => kolabformat::StatusCancelled,
+ 'TENTATIVE' => kolabformat::StatusTentative,
+ 'CONFIRMED' => kolabformat::StatusConfirmed,
+ 'DRAFT' => kolabformat::StatusDraft,
+ 'FINAL' => kolabformat::StatusFinal,
);
protected $part_status_map = array(
'UNKNOWN' => kolabformat::PartNeedsAction,
'NEEDS-ACTION' => kolabformat::PartNeedsAction,
'TENTATIVE' => kolabformat::PartTentative,
'ACCEPTED' => kolabformat::PartAccepted,
'DECLINED' => kolabformat::PartDeclined,
'DELEGATED' => kolabformat::PartDelegated,
);
/**
* Convert common xcard properties into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Object data as hash array
*/
public function to_array($data = array())
{
// read common object props
$object = parent::to_array($data);
$status_map = array_flip($this->status_map);
$sensitivity_map = array_flip($this->sensitivity_map);
$object += array(
'sequence' => intval($this->obj->sequence()),
'title' => $this->obj->summary(),
'location' => $this->obj->location(),
'description' => $this->obj->description(),
'url' => $this->obj->url(),
'status' => $status_map[$this->obj->status()],
'sensitivity' => $sensitivity_map[$this->obj->classification()],
'priority' => $this->obj->priority(),
'categories' => self::vector2array($this->obj->categories()),
'start' => self::php_datetime($this->obj->start()),
);
+ if (method_exists($this->obj, 'comment')) {
+ $object['comment'] = $this->obj->comment();
+ }
+
// read organizer and attendees
if (($organizer = $this->obj->organizer()) && ($organizer->email() || $organizer->name())) {
$object['organizer'] = array(
'email' => $organizer->email(),
'name' => $organizer->name(),
);
}
$role_map = array_flip($this->role_map);
$cutype_map = array_flip($this->cutype_map);
$part_status_map = array_flip($this->part_status_map);
$attvec = $this->obj->attendees();
for ($i=0; $i < $attvec->size(); $i++) {
$attendee = $attvec->get($i);
$cr = $attendee->contact();
if ($cr->email() != $object['organizer']['email']) {
+ $delegators = $delegatees = array();
+ $vdelegators = $attendee->delegatedFrom();
+ for ($j=0; $j < $vdelegators->size(); $j++) {
+ $delegators[] = $vdelegators->get($j)->email();
+ }
+ $vdelegatees = $attendee->delegatedTo();
+ for ($j=0; $j < $vdelegatees->size(); $j++) {
+ $delegatees[] = $vdelegatees->get($j)->email();
+ }
+
$object['attendees'][] = array(
'role' => $role_map[$attendee->role()],
'cutype' => $cutype_map[$attendee->cutype()],
'status' => $part_status_map[$attendee->partStat()],
'rsvp' => $attendee->rsvp(),
'email' => $cr->email(),
'name' => $cr->name(),
+ 'delegated-from' => $delegators,
+ 'delegated-to' => $delegatees,
);
}
}
// read recurrence rule
if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
$rrule_type_map = array_flip($this->rrule_type_map);
$object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
if ($intvl = $rr->interval())
$object['recurrence']['INTERVAL'] = $intvl;
if (($count = $rr->count()) && $count > 0) {
$object['recurrence']['COUNT'] = $count;
}
else if ($until = self::php_datetime($rr->end())) {
$until->setTime($object['start']->format('G'), $object['start']->format('i'), 0);
$object['recurrence']['UNTIL'] = $until;
}
if (($byday = $rr->byday()) && $byday->size()) {
$weekday_map = array_flip($this->weekday_map);
$weekdays = array();
for ($i=0; $i < $byday->size(); $i++) {
$daypos = $byday->get($i);
$prefix = $daypos->occurence();
$weekdays[] = ($prefix ? $prefix : '') . $weekday_map[$daypos->weekday()];
}
$object['recurrence']['BYDAY'] = join(',', $weekdays);
}
if (($bymday = $rr->bymonthday()) && $bymday->size()) {
$object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
}
if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
$object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
}
if ($exdates = $this->obj->exceptionDates()) {
for ($i=0; $i < $exdates->size(); $i++) {
if ($exdate = self::php_datetime($exdates->get($i)))
$object['recurrence']['EXDATE'][] = $exdate;
}
}
}
+ if ($rdates = $this->obj->recurrenceDates()) {
+ for ($i=0; $i < $rdates->size(); $i++) {
+ if ($rdate = self::php_datetime($rdates->get($i)))
+ $object['recurrence']['RDATE'][] = $rdate;
+ }
+ }
+
// read alarm
$valarms = $this->obj->alarms();
$alarm_types = array_flip($this->alarm_type_map);
+ $object['valarms'] = array();
for ($i=0; $i < $valarms->size(); $i++) {
$alarm = $valarms->get($i);
$type = $alarm_types[$alarm->type()];
- if ($type == 'DISPLAY' || $type == 'EMAIL') { // only DISPLAY and EMAIL alarms are supported
+ if ($type == 'DISPLAY' || $type == 'EMAIL' || $type == 'AUDIO') { // only some alarms are supported
+ $valarm = array(
+ 'action' => $type,
+ 'summary' => $alarm->summary(),
+ 'description' => $alarm->description(),
+ );
+
+ if ($type == 'EMAIL') {
+ $valarm['attendees'] = array();
+ $attvec = $alarm->attendees();
+ for ($j=0; $j < $attvec->size(); $j++) {
+ $cr = $attvec->get($j);
+ $valarm['attendees'][] = $cr->email();
+ }
+ }
+ else if ($type == 'AUDIO') {
+ $attach = $alarm->audioFile();
+ $valarm['uri'] = $attach->uri();
+ }
+
if ($start = self::php_datetime($alarm->start())) {
$object['alarms'] = '@' . $start->format('U');
+ $valarm['trigger'] = $start;
}
else if ($offset = $alarm->relativeStart()) {
- $value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+ $prefix = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+ $value = $time = '';
if ($w = $offset->weeks()) $value .= $w . 'W';
else if ($d = $offset->days()) $value .= $d . 'D';
- else if ($h = $offset->hours()) $value .= $h . 'H';
- else if ($m = $offset->minutes()) $value .= $m . 'M';
- else if ($s = $offset->seconds()) $value .= $s . 'S';
- else continue;
+ else if ($h = $offset->hours()) $time .= $h . 'H';
+ else if ($m = $offset->minutes()) $time .= $m . 'M';
+ else if ($s = $offset->seconds()) $time .= $s . 'S';
+
+ // assume 'at event time'
+ if (empty($value) && empty($time)) {
+ $prefix = '';
+ $time = '0S';
+ }
- $object['alarms'] = $value;
+ $object['alarms'] = $prefix . $value . $time;
+ $valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : '');
}
- $object['alarms'] .= ':' . $type;
- break;
- }
- }
- // handle attachments
- $vattach = $this->obj->attachments();
- for ($i=0; $i < $vattach->size(); $i++) {
- $attach = $vattach->get($i);
-
- // skip cid: attachments which are mime message parts handled by kolab_storage_folder
- if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
- $name = $attach->label();
- $content = $attach->data();
- $object['_attachments'][$name] = array(
- 'name' => $name,
- 'mimetype' => $attach->mimetype(),
- 'size' => strlen($content),
- 'content' => $content,
- );
- }
- else if (substr($attach->uri(), 0, 4) == 'http') {
- $object['links'][] = $attach->uri();
+ // read alarm duration and repeat properties
+ if (($duration = $alarm->duration()) && $duration->isValid()) {
+ $value = $time = '';
+ if ($w = $duration->weeks()) $value .= $w . 'W';
+ else if ($d = $duration->days()) $value .= $d . 'D';
+ else if ($h = $duration->hours()) $time .= $h . 'H';
+ else if ($m = $duration->minutes()) $time .= $m . 'M';
+ else if ($s = $duration->seconds()) $time .= $s . 'S';
+ $valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : '');
+ $valarm['repeat'] = $alarm->numrepeat();
+ }
+
+ $object['alarms'] .= ':' . $type; // legacy property
+ $object['valarms'][] = array_filter($valarm);
}
}
+ $this->get_attachments($object);
+
return $object;
}
/**
* Set common xcal properties to the kolabformat object
*
* @param array Event data as hash array
*/
public function set(&$object)
{
$this->init();
$is_new = !$this->obj->uid();
+ $old_sequence = $this->obj->sequence();
+ $reschedule = $is_new;
// set common object properties
parent::set($object);
- // increment sequence on updates
- if (empty($object['sequence']))
- $object['sequence'] = !$is_new ? $this->obj->sequence()+1 : 0;
- $this->obj->setSequence($object['sequence']);
+ // set sequence value
+ if (!isset($object['sequence'])) {
+ if ($is_new) {
+ $object['sequence'] = 0;
+ }
+ else {
+ $object['sequence'] = $old_sequence;
+ $old = $this->data['uid'] ? $this->data : $this->to_array();
+
+ // increment sequence when updating properties relevant for scheduling.
+ // RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
+ // TODO: make the list of properties considered 'significant' for scheduling configurable
+ foreach ($this->scheduling_properties as $prop) {
+ $a = $old[$prop];
+ $b = $object[$prop];
+ if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
+ $a = $a->format('Y-m-d');
+ $b = $b->format('Y-m-d');
+ }
+ if ($a != $b) {
+ $object['sequence']++;
+ break;
+ }
+ }
+ }
+ }
+ $this->obj->setSequence(intval($object['sequence']));
+
+ if ($object['sequence'] > $old_sequence) {
+ $reschedule = true;
+ }
$this->obj->setSummary($object['title']);
$this->obj->setLocation($object['location']);
$this->obj->setDescription($object['description']);
$this->obj->setPriority($object['priority']);
$this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
$this->obj->setCategories(self::array2vector($object['categories']));
$this->obj->setUrl(strval($object['url']));
+ if (method_exists($this->obj, 'setComment')) {
+ $this->obj->setComment($object['comment']);
+ }
+
// process event attendees
$attendees = new vectorattendee;
- foreach ((array)$object['attendees'] as $attendee) {
+ foreach ((array)$object['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$object['organizer'] = $attendee;
}
else if ($attendee['email'] != $object['organizer']['email']) {
$cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
$cr->setName($attendee['name']);
$att = new Attendee;
$att->setContact($cr);
$att->setPartStat($this->part_status_map[$attendee['status']]);
$att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
$att->setCutype($this->cutype_map[$attendee['cutype']] ? $this->cutype_map[$attendee['cutype']] : kolabformat::CutypeIndividual);
- $att->setRSVP((bool)$attendee['rsvp']);
+ $att->setRSVP((bool)$attendee['rsvp'] || $reschedule);
+
+ $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] || $reschedule;
+
+ if (!empty($attendee['delegated-from'])) {
+ $vdelegators = new vectorcontactref;
+ foreach ((array)$attendee['delegated-from'] as $delegator) {
+ $vdelegators->push(new ContactReference(ContactReference::EmailReference, $delegator));
+ }
+ $att->setDelegatedFrom($vdelegators);
+ }
+ if (!empty($attendee['delegated-to'])) {
+ $vdelegatees = new vectorcontactref;
+ foreach ((array)$attendee['delegated-to'] as $delegatee) {
+ $vdelegatees->push(new ContactReference(ContactReference::EmailReference, $delegatee));
+ }
+ $att->setDelegatedTo($vdelegatees);
+ }
if ($att->isValid()) {
$attendees->push($att);
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid event attendee: " . json_encode($attendee),
), true);
}
}
}
$this->obj->setAttendees($attendees);
if ($object['organizer']) {
$organizer = new ContactReference(ContactReference::EmailReference, $object['organizer']['email']);
$organizer->setName($object['organizer']['name']);
$this->obj->setOrganizer($organizer);
}
// save recurrence rule
$rr = new RecurrenceRule;
$rr->setFrequency(RecurrenceRule::FreqNone);
- if ($object['recurrence']) {
+ if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) {
$rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
if ($object['recurrence']['INTERVAL'])
$rr->setInterval(intval($object['recurrence']['INTERVAL']));
if ($object['recurrence']['BYDAY']) {
$byday = new vectordaypos;
foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
$occurrence = 0;
if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
$occurrence = intval($m[1]);
$day = $m[2];
}
if (isset($this->weekday_map[$day]))
$byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
}
$rr->setByday($byday);
}
if ($object['recurrence']['BYMONTHDAY']) {
$bymday = new vectori;
foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
$bymday->push(intval($day));
$rr->setBymonthday($bymday);
}
if ($object['recurrence']['BYMONTH']) {
$bymonth = new vectori;
foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
$bymonth->push(intval($month));
$rr->setBymonth($bymonth);
}
if ($object['recurrence']['COUNT'])
$rr->setCount(intval($object['recurrence']['COUNT']));
else if ($object['recurrence']['UNTIL'])
$rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true));
if ($rr->isValid()) {
// add exception dates (only if recurrence rule is valid)
$exdates = new vectordatetime;
foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
$exdates->push(self::get_datetime($exdate, null, true));
$this->obj->setExceptionDates($exdates);
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
), true);
}
}
$this->obj->setRecurrenceRule($rr);
+ // save recurrence dates (aka RDATE)
+ if (!empty($object['recurrence']['RDATE'])) {
+ $rdates = new vectordatetime;
+ foreach ((array)$object['recurrence']['RDATE'] as $rdate)
+ $rdates->push(self::get_datetime($rdate, null, true));
+ $this->obj->setRecurrenceDates($rdates);
+ }
+
// save alarm
$valarms = new vectoralarm;
- if ($object['alarms']) {
+ if ($object['valarms']) {
+ foreach ($object['valarms'] as $valarm) {
+ if (!array_key_exists($valarm['action'], $this->alarm_type_map)) {
+ continue; // skip unknown alarm types
+ }
+
+ if ($valarm['action'] == 'EMAIL') {
+ $recipients = new vectorcontactref;
+ foreach (($valarm['attendees'] ?: array($object['_owner'])) as $email) {
+ $recipients->push(new ContactReference(ContactReference::EmailReference, $email));
+ }
+ $alarm = new Alarm(
+ strval($valarm['summary'] ?: $object['title']),
+ strval($valarm['description'] ?: $object['description']),
+ $recipients
+ );
+ }
+ else if ($valarm['action'] == 'AUDIO') {
+ $attach = new Attachment;
+ $attach->setUri($valarm['uri'] ?: 'null', 'unknown');
+ $alarm = new Alarm($attach);
+ }
+ else {
+ // action == DISPLAY
+ $alarm = new Alarm(strval($valarm['summary'] ?: $object['title']));
+ }
+
+ if (is_object($valarm['trigger']) && $valarm['trigger'] instanceof DateTime) {
+ $alarm->setStart(self::get_datetime($valarm['trigger'], new DateTimeZone('UTC')));
+ }
+ else {
+ try {
+ $prefix = $valarm['trigger'][0];
+ $period = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger']));
+ $duration = new Duration($period->d, $period->h, $period->i, $period->s, $prefix == '-');
+ }
+ catch (Exception $e) {
+ // skip alarm with invalid trigger values
+ rcube::raise_error($e, true);
+ continue;
+ }
+
+ $alarm->setRelativeStart($duration, $prefix == '-' ? kolabformat::Start : kolabformat::End);
+ }
+
+ if ($valarm['duration']) {
+ try {
+ $d = new DateInterval($valarm['duration']);
+ $duration = new Duration($d->d, $d->h, $d->i, $d->s);
+ $alarm->setDuration($duration, intval($valarm['repeat']));
+ }
+ catch (Exception $e) {
+ // ignore
+ }
+ }
+
+ $valarms->push($alarm);
+ }
+ }
+ // legacy support
+ else if ($object['alarms']) {
list($offset, $type) = explode(":", $object['alarms']);
if ($type == 'EMAIL' && !empty($object['_owner'])) { // email alarms implicitly go to event owner
$recipients = new vectorcontactref;
$recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
$alarm = new Alarm($object['title'], strval($object['description']), $recipients);
}
else { // default: display alarm
$alarm = new Alarm($object['title']);
}
if (preg_match('/^@(\d+)/', $offset, $d)) {
$alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
}
else if (preg_match('/^([-+]?)P?T?(\d+)([SMHDW])/', $offset, $d)) {
$days = $hours = $minutes = $seconds = 0;
switch ($d[3]) {
case 'W': $days = 7*intval($d[2]); break;
case 'D': $days = intval($d[2]); break;
case 'H': $hours = intval($d[2]); break;
case 'M': $minutes = intval($d[2]); break;
case 'S': $seconds = intval($d[2]); break;
}
$alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
}
$valarms->push($alarm);
}
$this->obj->setAlarms($valarms);
- // save attachments
- $vattach = new vectorattachment;
- foreach ((array)$object['_attachments'] as $cid => $attr) {
- if (empty($attr))
- continue;
- $attach = new Attachment;
- $attach->setLabel((string)$attr['name']);
- $attach->setUri('cid:' . $cid, $attr['mimetype']);
- $vattach->push($attach);
- }
-
- foreach ((array)$object['links'] as $link) {
- $attach = new Attachment;
- $attach->setUri($link, 'unknown');
- $vattach->push($attach);
- }
-
- $this->obj->setAttachments($vattach);
+ $this->set_attachments($object);
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
$data = '';
foreach (self::$fulltext_cols as $colname) {
list($col, $field) = explode(':', $colname);
if ($field) {
$a = array();
foreach ((array)$this->data[$col] as $attr)
$a[] = $attr[$field];
$val = join(' ', $a);
}
else {
$val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
}
if (strlen($val))
$data .= $val . ' ';
}
return array_unique(rcube_utils::normalize_string($data, true));
}
+ /**
+ * Callback for kolab_storage_cache to get object specific tags to cache
+ *
+ * @return array List of tags to save in cache
+ */
+ public function get_tags()
+ {
+ $tags = array();
+
+ if (!empty($this->data['valarms'])) {
+ $tags[] = 'x-has-alarms';
+ }
+
+ // create tags reflecting participant status
+ if (is_array($this->data['attendees'])) {
+ foreach ($this->data['attendees'] as $attendee) {
+ if (!empty($attendee['email']) && !empty($attendee['status']))
+ $tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
+ }
+ }
+
+ return $tags;
+ }
}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php
index 5f8b9c6..dfd1887 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php
@@ -1,1049 +1,1567 @@
<?php
/**
* Kolab storage class providing static methods to access groupware objects on a Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
- * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
+ * Copyright (C) 2012-2014, 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/>.
*/
class kolab_storage
{
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
const NAME_KEY_SHARED = '/shared/vendor/kolab/displayname';
const NAME_KEY_PRIVATE = '/private/vendor/kolab/displayname';
const UID_KEY_SHARED = '/shared/vendor/kolab/uniqueid';
const UID_KEY_PRIVATE = '/private/vendor/kolab/uniqueid';
const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
public static $version = '3.0';
public static $last_error;
+ public static $encode_ids = false;
private static $ready = false;
+ private static $with_tempsubs = true;
private static $subscriptions;
+ private static $typedata = array();
private static $states;
private static $config;
private static $imap;
+ private static $ldap;
// Default folder names
private static $default_folders = array(
'event' => 'Calendar',
'contact' => 'Contacts',
'task' => 'Tasks',
'note' => 'Notes',
'file' => 'Files',
'configuration' => 'Configuration',
'journal' => 'Journal',
'mail.inbox' => 'INBOX',
'mail.drafts' => 'Drafts',
'mail.sentitems' => 'Sent',
'mail.wastebasket' => 'Trash',
'mail.outbox' => 'Outbox',
'mail.junkemail' => 'Junk',
);
/**
* Setup the environment needed by the libs
*/
public static function setup()
{
if (self::$ready)
return true;
$rcmail = rcube::get_instance();
self::$config = $rcmail->config;
self::$version = strval($rcmail->config->get('kolab_format_version', self::$version));
self::$imap = $rcmail->get_storage();
self::$ready = class_exists('kolabformat') &&
(self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
if (self::$ready) {
// set imap options
self::$imap->set_options(array(
'skip_deleted' => true,
'threading' => false,
));
- self::$imap->set_pagesize(9999);
}
else if (!class_exists('kolabformat')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabformat module not found"
), true);
}
else {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "IMAP server doesn't support METADATA or ANNOTATEMORE"
), true);
}
return self::$ready;
}
+ /**
+ * Initializes LDAP object to resolve Kolab users
+ */
+ public static function ldap()
+ {
+ if (self::$ldap) {
+ return self::$ldap;
+ }
+
+ self::setup();
+
+ $config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook'));
+
+ if (!is_array($config)) {
+ $ldap_config = (array)self::$config->get('ldap_public');
+ $config = $ldap_config[$config];
+ }
+
+ if (empty($config)) {
+ return null;
+ }
+
+ // overwrite filter option
+ if ($filter = self::$config->get('kolab_users_filter')) {
+ self::$config->set('kolab_auth_filter', $filter);
+ }
+
+ // re-use the LDAP wrapper class from kolab_auth plugin
+ require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php';
+
+ self::$ldap = new kolab_auth_ldap($config);
+
+ return self::$ldap;
+ }
+
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type, $subscribed = null)
{
$folders = $folderdata = array();
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
$folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Getter for the storage folder for the given type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @return object kolab_storage_folder The folder object
*/
public static function get_default_folder($type)
{
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
return new kolab_storage_folder($foldername, $folderdata[$foldername]);
}
}
return null;
}
/**
* Getter for a specific storage folder
*
* @param string IMAP folder to access (UTF7-IMAP)
* @return object kolab_storage_folder The folder object
*/
public static function get_folder($folder)
{
return self::setup() ? new kolab_storage_folder($folder) : null;
}
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
*
* @param string Object UID
* @param string Object type (contact,event,task,journal,file,note,configuration)
* @return array The Kolab object represented as hash array or false if not found
*/
public static function get_object($uid, $type)
{
self::setup();
$folder = null;
foreach ((array)self::list_folders('', '*', $type) as $foldername) {
if (!$folder)
$folder = new kolab_storage_folder($foldername);
else
$folder->set_folder($foldername);
if ($object = $folder->get_object($uid, '*'))
return $object;
}
return false;
}
-
/**
+ * Execute cross-folder searches with the given query.
*
+ * @param array Pseudo-SQL query as list of filter parameter triplets
+ * @param string Object type (contact,event,task,journal,file,note,configuration)
+ * @return array List of Kolab data objects (each represented as hash array)
+ * @see kolab_storage_format::select()
*/
- public static function get_freebusy_server()
+ public static function select($query, $type)
{
- return unslashify(self::$config->get('kolab_freebusy_server', 'https://' . $_SESSION['imap_host'] . '/freebusy'));
+ self::setup();
+ $folder = null;
+ $result = array();
+
+ foreach ((array)self::list_folders('', '*', $type) as $foldername) {
+ if (!$folder)
+ $folder = new kolab_storage_folder($foldername);
+ else
+ $folder->set_folder($foldername);
+
+ foreach ($folder->select($query, '*') as $object) {
+ $result[] = $object;
+ }
+ }
+
+ return $result;
}
+ /**
+ * Returns Free-busy server URL
+ */
+ public static function get_freebusy_server()
+ {
+ $url = 'https://' . $_SESSION['imap_host'] . '/freebusy';
+ $url = self::$config->get('kolab_freebusy_server', $url);
+ $url = rcube_utils::resolve_url($url);
+
+ return unslashify($url);
+ }
/**
* Compose an URL to query the free/busy status for the given user
*/
public static function get_freebusy_url($email)
{
return self::get_freebusy_server() . '/' . $email . '.ifb';
}
-
/**
* Creates folder ID from folder name
*
- * @param string $folder Folder name (UTF7-IMAP)
- *
+ * @param string $folder Folder name (UTF7-IMAP)
+ * @param boolean $enc Use lossless encoding
* @return string Folder ID string
*/
- public static function folder_id($folder)
+ public static function folder_id($folder, $enc = null)
+ {
+ return $enc == true || ($enc === null && self::$encode_ids) ?
+ self::id_encode($folder) :
+ asciiwords(strtr($folder, '/.-', '___'));
+ }
+
+ /**
+ * Encode the given ID to a safe ascii representation
+ *
+ * @param string $id Arbitrary identifier string
+ *
+ * @return string Ascii representation
+ */
+ public static function id_encode($id)
{
- return asciiwords(strtr($folder, '/.-', '___'));
+ return rtrim(strtr(base64_encode($id), '+/', '-_'), '=');
+ }
+
+ /**
+ * Convert the given identifier back to it's raw value
+ *
+ * @param string $id Ascii identifier
+ * @return string Raw identifier string
+ */
+ public static function id_decode($id)
+ {
+ return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
+ }
+
+ /**
+ * Return the (first) path of the requested IMAP namespace
+ *
+ * @param string Namespace name (personal, shared, other)
+ * @return string IMAP root path for that namespace
+ */
+ public static function namespace_root($name)
+ {
+ foreach ((array)self::$imap->get_namespace($name) as $paths) {
+ if (strlen($paths[0]) > 1) {
+ return $paths[0];
+ }
+ }
+
+ return '';
}
/**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_delete($name)
{
// clear cached entries first
if ($folder = self::get_folder($name))
$folder->cache->purge();
+ $rcmail = rcube::get_instance();
+ $plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name));
+
$success = self::$imap->delete_folder($name);
self::$last_error = self::$imap->get_error_str();
return $success;
}
/**
* Creates IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $subscribed Sets folder subscription
* @param bool $active Sets folder state (client-side subscription)
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type = null, $subscribed = false, $active = false)
{
self::setup();
+ $rcmail = rcube::get_instance();
+ $plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array(
+ 'name' => $name,
+ 'subscribe' => $subscribed,
+ )));
+
if ($saved = self::$imap->create_folder($name, $subscribed)) {
// set metadata for folder type
if ($type) {
$saved = self::set_folder_type($name, $type);
// revert if metadata could not be set
if (!$saved) {
self::$imap->delete_folder($name);
}
// activate folder
else if ($active) {
self::set_state($name, true);
}
}
}
if ($saved) {
return true;
}
self::$last_error = self::$imap->get_error_str();
return false;
}
/**
* Renames IMAP folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_rename($oldname, $newname)
{
self::setup();
+ $rcmail = rcube::get_instance();
+ $plugin = $rcmail->plugins->exec_hook('folder_rename', array(
+ 'oldname' => $oldname, 'newname' => $newname));
+
$oldfolder = self::get_folder($oldname);
$active = self::folder_is_active($oldname);
$success = self::$imap->rename_folder($oldname, $newname);
self::$last_error = self::$imap->get_error_str();
// pass active state to new folder name
if ($success && $active) {
self::set_state($oldnam, false);
self::set_state($newname, true);
}
// assign existing cache entries to new resource uri
if ($success && $oldfolder) {
$oldfolder->cache->rename($newname);
}
return $success;
}
/**
* Rename or Create a new IMAP folder.
*
* Does additional checks for permissions and folder name restrictions
*
* @param array Hash array with folder properties and metadata
* - name: Folder name
* - oldname: Old folder name when changed
* - parent: Parent folder to create the new one in
* - type: Folder type to create
* - subscribed: Subscribed flag (IMAP subscription)
* - active: Activation flag (client-side subscription)
* @return mixed New folder name or False on failure
*/
public static function folder_update(&$prop)
{
self::setup();
$folder = rcube_charset::convert($prop['name'], RCUBE_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$delimiter = self::$imap->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = self::$imap->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
self::$last_error = 'cannotbeempty';
return false;
}
else if (strlen($folder) > 128) {
self::$last_error = 'nametoolong';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $char) !== false) {
self::$last_error = 'forbiddencharacter';
return false;
}
}
}
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$folder = self::$imap->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$parent_opts = self::$imap->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
self::$last_error = 'No permission to create folder';
return false;
}
}
// update the folder name
if (strlen($oldfolder)) {
if ($oldfolder != $folder) {
$result = self::folder_rename($oldfolder, $folder);
}
else
$result = true;
}
// create new folder
else {
$result = self::folder_create($folder, $prop['type'], $prop['subscribed'], $prop['active']);
}
if ($result) {
self::set_folder_props($folder, $prop);
}
return $result ? $folder : false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns=null)
{
self::setup();
// find custom display name in folder METADATA
if ($name = self::custom_displayname($folder)) {
return $name;
}
$found = false;
$namespace = self::$imap->get_namespace();
if (!empty($namespace['shared'])) {
foreach ($namespace['shared'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
$prefix = '';
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
$found = true;
$folder_ns = 'shared';
break;
}
}
}
if (!$found && !empty($namespace['other'])) {
foreach ($namespace['other'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
// get username
$pos = strpos($folder, $delim);
if ($pos) {
- $prefix = '('.substr($folder, 0, $pos).') ';
+ $prefix = '('.substr($folder, 0, $pos).')';
$folder = substr($folder, $pos+1);
}
else {
$prefix = '('.$folder.')';
$folder = '';
}
+
$found = true;
$folder_ns = 'other';
break;
}
}
}
if (!$found && !empty($namespace['personal'])) {
foreach ($namespace['personal'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$prefix = '';
$delim = $ns[1];
$found = true;
break;
}
}
}
if (empty($delim))
$delim = self::$imap->get_hierarchy_delimiter();
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$folder = html::quote($folder);
$folder = str_replace(html::quote($delim), ' &raquo; ', $folder);
if ($prefix)
- $folder = html::quote($prefix) . ' ' . $folder;
+ $folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
if (!$folder_ns)
$folder_ns = 'personal';
return $folder;
}
/**
* Get custom display name (saved in metadata) for the given folder
*/
public static function custom_displayname($folder)
{
// find custom display name in folder METADATA
if (self::$config->get('kolab_custom_display_names', true)) {
$metadata = self::$imap->get_metadata($folder, array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
if (($name = $metadata[$folder][self::NAME_KEY_PRIVATE]) || ($name = $metadata[$folder][self::NAME_KEY_SHARED])) {
return $name;
}
}
return false;
}
/**
- * Helper method to generate a truncated folder name to display
+ * Helper method to generate a truncated folder name to display.
+ * Note: $origname is a string returned by self::object_name()
*/
public static function folder_displayname($origname, &$names)
{
$name = $origname;
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i] . ' &raquo; ') === 0) {
$length = strlen($names[$i] . ' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
- $name = str_repeat('&nbsp;&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
+ $diff = 1;
+
+ // check if prefix folder is in other users namespace
+ for ($n = count($names)-1; $n >= 0; $n--) {
+ if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
+ $diff = 0;
+ break;
+ }
+ }
+
+ $name = str_repeat('&nbsp;&nbsp;&nbsp;', $count - $diff) . '&raquo; ' . substr($name, $length);
+ break;
+ }
+ // other users namespace and parent folder exists
+ else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
+ $length = strlen('(' . $names[$i] . ') ');
+ $prefix = substr($name, 0, $length);
+ $count = count(explode(' &raquo; ', $prefix));
+ $name = str_repeat('&nbsp;&nbsp;&nbsp;', $count) . '&raquo; ' . substr($name, $length);
break;
}
}
+
$names[] = $origname;
return $name;
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public static function folder_selector($type, $attrs, $current = '')
{
- // get all folders of specified type
- $folders = self::get_folders($type, false);
+ // get all folders of specified type (sorted)
+ $folders = self::get_folders($type, true);
$delim = self::$imap->get_hierarchy_delimiter();
$names = array();
$len = strlen($current);
if ($len && ($rpos = strrpos($current, $delim))) {
$parent = substr($current, 0, $rpos);
$p_len = strlen($parent);
}
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
+
// skip current folder and it's subfolders
- if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
- continue;
+ if ($len) {
+ if ($name == $current) {
+ // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
+ if ($p_len && !isset($names[$parent])) {
+ $names[$parent] = self::object_name($parent);
+ }
+ continue;
+ }
+ if (strpos($name, $current.$delim) === 0) {
+ continue;
+ }
}
// always show the parent of current folder
- if ($p_len && $name == $parent) { }
+ if ($p_len && $name == $parent) {
+ }
// skip folders where user have no rights to create subfolders
else if ($c_folder->get_owner() != $_SESSION['username']) {
$rights = $c_folder->get_myrights();
if (!preg_match('/[ck]/', $rights)) {
continue;
}
}
$names[$name] = self::object_name($name);
}
- // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
- if ($p_len && !isset($names[$parent])) {
- $names[$parent] = self::object_name($parent);
- }
-
- // Sort folders list
- asort($names, SORT_LOCALE_STRING);
-
// Build SELECT field of parent folder
$attrs['is_escaped'] = true;
$select = new html_select($attrs);
$select->add('---', '');
$listnames = array();
foreach (array_keys($names) as $imap_name) {
$name = $origname = $names[$imap_name];
// find folder prefix to truncate
for ($i = count($listnames)-1; $i >= 0; $i--) {
if (strpos($name, $listnames[$i].' &raquo; ') === 0) {
$length = strlen($listnames[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$listnames[] = $origname;
$select->add($name, $imap_name);
}
return $select;
}
/**
* Returns a list of folder names
*
* @param string Optional root folder
* @param string Optional name pattern
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
{
if (!self::setup()) {
return null;
}
// use IMAP subscriptions
if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) {
$subscribed = true;
}
if (!$filter) {
// Get ALL folders list, standard way
if ($subscribed) {
- return self::$imap->list_folders_subscribed($root, $mbox);
+ $folders = self::$imap->list_folders_subscribed($root, $mbox);
+ // add temporarily subscribed folders
+ if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
+ $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
+ }
}
else {
- return self::$imap->list_folders($root, $mbox);
+ $folders = self::_imap_list_folders($root, $mbox);
}
- }
+ return $folders;
+ }
$prefix = $root . $mbox;
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
- // get folders types
- $folderdata = self::folders_typedata($prefix);
+ // get folders types for all folders
+ if (!$subscribed || $prefix == '*' || !self::$config->get('kolab_skip_namespace')) {
+ $folderdata = self::folders_typedata($prefix);
+ }
+ else {
+ // fetch folder types for the effective list of (subscribed) folders when post-filtering
+ $folderdata = array();
+ }
if (!is_array($folderdata)) {
return array();
}
// In some conditions we can skip LIST command (?)
if (!$subscribed && $filter != 'mail' && $prefix == '*') {
foreach ($folderdata as $folder => $type) {
if (!preg_match($regexp, $type)) {
unset($folderdata[$folder]);
}
}
- return array_keys($folderdata);
+
+ return self::$imap->sort_folder_list(array_keys($folderdata), true);
}
// Get folders list
if ($subscribed) {
$folders = self::$imap->list_folders_subscribed($root, $mbox);
+
+ // add temporarily subscribed folders
+ if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
+ $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
+ }
}
else {
- $folders = self::$imap->list_folders($root, $mbox);
+ $folders = self::_imap_list_folders($root, $mbox);
}
// In case of an error, return empty list (?)
if (!is_array($folders)) {
return array();
}
// Filter folders list
foreach ($folders as $idx => $folder) {
+ // lookup folder type
+ if (!array_key_exists($folder, $folderdata)) {
+ $folderdata[$folder] = self::folder_type($folder);
+ }
+
$type = $folderdata[$folder];
if ($filter == 'mail' && empty($type)) {
continue;
}
if (empty($type) || !preg_match($regexp, $type)) {
unset($folders[$idx]);
}
}
return $folders;
}
+ /**
+ * Wrapper for rcube_imap::list_folders() with optional post-filtering
+ */
+ protected static function _imap_list_folders($root, $mbox)
+ {
+ $postfilter = null;
+
+ // compose a post-filter expression for the excluded namespaces
+ if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
+ $excludes = array();
+ foreach ((array)$skip_ns as $ns) {
+ if ($ns_root = self::namespace_root($ns)) {
+ $excludes[] = $ns_root;
+ }
+ }
+
+ if (count($excludes)) {
+ $postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!';
+ }
+ }
+
+ // use normal LIST command to return all folders, it's fast enough
+ $folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter));
+
+ if (!empty($postfilter)) {
+ $folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); });
+ $folders = self::$imap->sort_folder_list($folders);
+ }
+
+ return $folders;
+ }
+
+
+ /**
+ * Search for shared or otherwise not listed groupware folders the user has access
+ *
+ * @param string Folder type of folders to search for
+ * @param string Search string
+ * @param array Namespace(s) to exclude results from
+ *
+ * @return array List of matching kolab_storage_folder objects
+ */
+ public static function search_folders($type, $query, $exclude_ns = array())
+ {
+ if (!self::setup()) {
+ return array();
+ }
+
+ $folders = array();
+ $query = str_replace('*', '', $query);
+
+ // find unsubscribed IMAP folders of the given type
+ foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
+ // FIXME: only consider the last part of the folder path for searching?
+ $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
+ if (($query == '' || strpos($realname, $query) !== false) &&
+ !self::folder_is_subscribed($foldername, true) &&
+ !in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
+ ) {
+ $folders[] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ }
+ }
+
+ return $folders;
+ }
+
/**
* Sort the given list of kolab folders by namespace/name
*
* @param array List of kolab_storage_folder objects
* @return array Sorted list of folders
*/
public static function sort_folders($folders)
{
- $pad = ' ';
+ $pad = ' ';
+ $out = array();
$nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
+
foreach ($folders as $folder) {
$folders[$folder->name] = $folder;
$ns = $folder->get_namespace();
$nsnames[$ns][$folder->name] = strtolower(html_entity_decode(self::object_name($folder->name, $ns), ENT_COMPAT, RCUBE_CHARSET)) . $pad; // decode &raquo;
}
- $names = array();
- foreach ($nsnames as $ns => $dummy) {
+ // $folders is a result of get_folders() we can assume folders were already sorted
+ foreach (array_keys($nsnames) as $ns) {
asort($nsnames[$ns], SORT_LOCALE_STRING);
- $names += $nsnames[$ns];
+ foreach (array_keys($nsnames[$ns]) as $utf7name) {
+ $out[] = $folders[$utf7name];
+ }
}
- $out = array();
- foreach ($names as $utf7name => $name) {
- $out[] = $folders[$utf7name];
+ return $out;
+ }
+
+
+ /**
+ * Check the folder tree and add the missing parents as virtual folders
+ *
+ * @param array $folders Folders list
+ * @param object $tree Reference to the root node of the folder tree
+ *
+ * @return array Flat folders list
+ */
+ public static function folder_hierarchy($folders, &$tree = null)
+ {
+ $_folders = array();
+ $delim = self::$imap->get_hierarchy_delimiter();
+ $other_ns = rtrim(self::namespace_root('other'), $delim);
+ $tree = new kolab_storage_folder_virtual('', '<root>', ''); // create tree root
+ $refs = array('' => $tree);
+
+ foreach ($folders as $idx => $folder) {
+ $path = explode($delim, $folder->name);
+ array_pop($path);
+ $folder->parent = join($delim, $path);
+ $folder->children = array(); // reset list
+
+ // skip top folders or ones with a custom displayname
+ if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
+ $tree->children[] = $folder;
+ }
+ else {
+ $parents = array();
+ $depth = $folder->get_namespace() == 'personal' ? 1 : 2;
+
+ while (count($path) >= $depth && ($parent = join($delim, $path))) {
+ array_pop($path);
+ $parent_parent = join($delim, $path);
+ if (!$refs[$parent]) {
+ if ($folder->type && self::folder_type($parent) == $folder->type) {
+ $refs[$parent] = new kolab_storage_folder($parent, $folder->type);
+ $refs[$parent]->parent = $parent_parent;
+ }
+ else if ($parent_parent == $other_ns) {
+ $refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
+ }
+ else {
+ $name = kolab_storage::object_name($parent, $folder->get_namespace());
+ $refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
+ }
+ $parents[] = $refs[$parent];
+ }
+ }
+
+ if (!empty($parents)) {
+ $parents = array_reverse($parents);
+ foreach ($parents as $parent) {
+ $parent_node = $refs[$parent->parent] ?: $tree;
+ $parent_node->children[] = $parent;
+ $_folders[] = $parent;
+ }
+ }
+
+ $parent_node = $refs[$folder->parent] ?: $tree;
+ $parent_node->children[] = $folder;
+ }
+
+ $refs[$folder->name] = $folder;
+ $_folders[] = $folder;
+ unset($folders[$idx]);
}
- return $out;
+ return $_folders;
}
/**
* Returns folder types indexed by folder name
*
* @param string $prefix Folder prefix (Default '*' for all folders)
*
* @return array|bool List of folders, False on failure
*/
public static function folders_typedata($prefix = '*')
{
if (!self::setup()) {
return false;
}
- $folderdata = self::$imap->get_metadata($prefix, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
+ // return cached result
+ if (is_array(self::$typedata[$prefix])) {
+ return self::$typedata[$prefix];
+ }
+
+ $type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
+
+ // fetch metadata from *some* folders only
+ if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
+ $delimiter = self::$imap->get_hierarchy_delimiter();
+ $folderdata = $blacklist = array();
+ foreach ((array)$skip_ns as $ns) {
+ if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
+ $blacklist[] = $ns_root;
+ }
+ }
+ foreach (array('personal','other','shared') as $ns) {
+ if (!in_array($ns, (array)$skip_ns)) {
+ $ns_root = rtrim(self::namespace_root($ns), $delimiter);
+
+ // list top-level folders and their childs one by one
+ // GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
+ if ($ns_root == '') {
+ foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
+ if (!in_array($folder, $blacklist)) {
+ $folderdata[$folder] = $metadata;
+ $opts = self::$imap->folder_attributes($folder);
+ if (!in_array('\\HasNoChildren', $opts) && ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
+ $folderdata += $data;
+ }
+ }
+ }
+ }
+ else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
+ $folderdata += $data;
+ }
+ }
+ }
+ }
+ else {
+ $folderdata = self::$imap->get_metadata($prefix, $type_keys);
+ }
if (!is_array($folderdata)) {
return false;
}
- return array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+ // keep list in memory
+ self::$typedata[$prefix] = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+
+ return self::$typedata[$prefix];
}
/**
* Callback for array_map to select the correct annotation value
*/
public static function folder_select_metadata($types)
{
if (!empty($types[self::CTYPE_KEY_PRIVATE])) {
return $types[self::CTYPE_KEY_PRIVATE];
}
else if (!empty($types[self::CTYPE_KEY])) {
list($ctype, $suffix) = explode('.', $types[self::CTYPE_KEY]);
return $ctype;
}
return null;
}
/**
* Returns type of IMAP folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
public static function folder_type($folder)
{
self::setup();
+ // return in-memory cached result
+ foreach (self::$typedata as $typedata) {
+ if (array_key_exists($folder, $typedata)) {
+ return $typedata[$folder];
+ }
+ }
+
$metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($metadata)) {
return null;
}
if (!empty($metadata[$folder])) {
return self::folder_select_metadata($metadata[$folder]);
}
return 'mail';
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return boolean True on success
*/
public static function set_folder_type($folder, $type='mail')
{
self::setup();
list($ctype, $subtype) = explode('.', $type);
$success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
if (!$success) // fallback: only set private annotation
$success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
return $success;
}
/**
* Check subscription status of this folder
*
* @param string $folder Folder name
+ * @param boolean $temp Include temporary/session subscriptions
*
* @return boolean True if subscribed, false if not
*/
- public static function folder_is_subscribed($folder)
+ public static function folder_is_subscribed($folder, $temp = false)
{
if (self::$subscriptions === null) {
self::setup();
+ self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
+ self::$with_tempsubs = true;
}
- return in_array($folder, self::$subscriptions);
+ return in_array($folder, self::$subscriptions) ||
+ ($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
+ * @param boolean $temp Only subscribe temporarily for the current session
*
* @return True on success, false on error
*/
- public static function folder_subscribe($folder)
+ public static function folder_subscribe($folder, $temp = false)
{
self::setup();
- if (self::$imap->subscribe($folder)) {
+ // temporary/session subscription
+ if ($temp) {
+ if (self::folder_is_subscribed($folder)) {
+ return true;
+ }
+ else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) {
+ $_SESSION['kolab_subscribed_folders'][] = $folder;
+ return true;
+ }
+ }
+ else if (self::$imap->subscribe($folder)) {
self::$subscriptions === null;
return true;
}
return false;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
+ * @param boolean $temp Only remove temporary subscription
*
* @return True on success, false on error
*/
- public static function folder_unsubscribe($folder)
+ public static function folder_unsubscribe($folder, $temp = false)
{
self::setup();
- if (self::$imap->unsubscribe($folder)) {
+ // temporary/session subscription
+ if ($temp) {
+ if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
+ unset($_SESSION['kolab_subscribed_folders'][$i]);
+ }
+ return true;
+ }
+ else if (self::$imap->unsubscribe($folder)) {
self::$subscriptions === null;
return true;
}
return false;
}
/**
* Check activation status of this folder
*
* @param string $folder Folder name
*
* @return boolean True if active, false if not
*/
public static function folder_is_active($folder)
{
$active_folders = self::get_states();
return in_array($folder, $active_folders);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_activate($folder)
{
+ // activation implies temporary subscription
+ self::folder_subscribe($folder, true);
return self::set_state($folder, true);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_deactivate($folder)
{
+ // remove from temp subscriptions, really?
+ self::folder_unsubscribe($folder, true);
+
return self::set_state($folder, false);
}
/**
* Return list of active folders
*/
private static function get_states()
{
if (self::$states !== null) {
return self::$states;
}
$rcube = rcube::get_instance();
$folders = $rcube->config->get('kolab_active_folders');
if ($folders !== null) {
self::$states = !empty($folders) ? explode('**', $folders) : array();
}
// for backward-compatibility copy server-side subscriptions to activation states
else {
self::setup();
if (self::$subscriptions === null) {
+ self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
+ self::$with_tempsubs = true;
}
self::$states = self::$subscriptions;
$folders = implode(self::$states, '**');
$rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
return self::$states;
}
/**
* Update list of active folders
*/
private static function set_state($folder, $state)
{
self::get_states();
// update in-memory list
$idx = array_search($folder, self::$states);
if ($state && $idx === false) {
self::$states[] = $folder;
}
else if (!$state && $idx !== false) {
unset(self::$states[$idx]);
}
// update user preferences
$folders = implode(self::$states, '**');
$rcube = rcube::get_instance();
return $rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
/**
* Creates default folder of specified type
* To be run when none of subscribed folders (of specified type) is found
*
* @param string $type Folder type
* @param string $props Folder properties (color, etc)
*
* @return string Folder name
*/
public static function create_default_folder($type, $props = array())
{
if (!self::setup()) {
return;
}
$folders = self::$imap->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE));
// from kolab_folders config
$folder_type = strpos($type, '.') ? str_replace('.', '_', $type) : $type . '_default';
$default_name = self::$config->get('kolab_folders_' . $folder_type);
$folder_type = str_replace('_', '.', $folder_type);
// check if we have any folder in personal namespace
// folder(s) may exist but not subscribed
- foreach ($folders as $f => $data) {
+ foreach ((array)$folders as $f => $data) {
if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
$folder = $f;
break;
}
}
if (!$folder) {
if (!$default_name) {
$default_name = self::$default_folders[$type];
}
if (!$default_name) {
return;
}
$folder = rcube_charset::convert($default_name, RCUBE_CHARSET, 'UTF7-IMAP');
$prefix = self::$imap->get_namespace('prefix');
// add personal namespace prefix if needed
if ($prefix && strpos($folder, $prefix) !== 0 && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
if (!self::$imap->folder_exists($folder)) {
if (!self::$imap->create_folder($folder)) {
return;
}
}
self::set_folder_type($folder, $folder_type);
}
self::folder_subscribe($folder);
if ($props['active']) {
self::set_state($folder, true);
}
if (!empty($props)) {
self::set_folder_props($folder, $props);
}
return $folder;
}
/**
* Sets folder metadata properties
*
* @param string $folder Folder name
* @param array $prop Folder properties
*/
public static function set_folder_props($folder, &$prop)
{
if (!self::setup()) {
return;
}
// TODO: also save 'showalarams' and other properties here
$ns = self::$imap->folder_namespace($folder);
$supported = array(
'color' => array(self::COLOR_KEY_SHARED, self::COLOR_KEY_PRIVATE),
'displayname' => array(self::NAME_KEY_SHARED, self::NAME_KEY_PRIVATE),
);
foreach ($supported as $key => $metakeys) {
if (array_key_exists($key, $prop)) {
$meta_saved = false;
if ($ns == 'personal') // save in shared namespace for personal folders
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key]));
if (!$meta_saved) // try in private namespace
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key]));
if ($meta_saved)
unset($prop[$key]); // unsetting will prevent fallback to local user prefs
}
}
}
+
+ /**
+ *
+ * @param mixed $query Search value (or array of field => value pairs)
+ * @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
+ * @param array $required List of fields that shall ot be empty
+ * @param int $limit Maximum number of records
+ * @param int $count Returns the number of records found
+ *
+ * @return array List or false on error
+ */
+ public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
+ {
+ $query = str_replace('*', '', $query);
+
+ // requires a working LDAP setup
+ if (!self::ldap() || strlen($query) == 0) {
+ return array();
+ }
+
+ // search users using the configured attributes
+ $results = self::$ldap->dosearch(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit, $count);
+
+ // exclude myself
+ if ($_SESSION['kolab_dn']) {
+ unset($results[$_SESSION['kolab_dn']]);
+ }
+
+ // resolve to IMAP folder name
+ $root = self::namespace_root('other');
+ $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
+
+ array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
+ list($localpart, $domain) = explode('@', $user[$user_attrib]);
+ $user['kolabtargetfolder'] = $root . $localpart;
+ });
+
+ return $results;
+ }
+
+
+ /**
+ * Returns a list of IMAP folders shared by the given user
+ *
+ * @param array User entry from LDAP
+ * @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
+ * @param boolean Return subscribed folders only (null to use configured subscription mode)
+ * @param array Will be filled with folder-types data
+ *
+ * @return array List of folders
+ */
+ public static function list_user_folders($user, $type, $subscribed = null, &$folderdata = array())
+ {
+ self::setup();
+
+ $folders = array();
+
+ // use localpart of user attribute as root for folder listing
+ $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
+ if (!empty($user[$user_attrib])) {
+ list($mbox) = explode('@', $user[$user_attrib]);
+
+ $delimiter = self::$imap->get_hierarchy_delimiter();
+ $other_ns = self::namespace_root('other');
+ $folders = self::list_folders($other_ns . $mbox . $delimiter, '*', $type, $subscribed, $folderdata);
+ }
+
+ return $folders;
+ }
+
+
+ /**
+ * Get a list of (virtual) top-level folders from the other users namespace
+ *
+ * @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
+ * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
+ *
+ * @return array List of kolab_storage_folder_user objects
+ */
+ public static function get_user_folders($type, $subscribed)
+ {
+ $folders = $folderdata = array();
+
+ if (self::setup()) {
+ $delimiter = self::$imap->get_hierarchy_delimiter();
+ $other_ns = rtrim(self::namespace_root('other'), $delimiter);
+ $path_len = count(explode($delimiter, $other_ns));
+
+ foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
+ if ($foldername == 'INBOX') // skip INBOX which is added by default
+ continue;
+
+ $path = explode($delimiter, $foldername);
+
+ // compare folder type if a subfolder is listed
+ if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) {
+ continue;
+ }
+
+ // truncate folder path to top-level folders of the 'other' namespace
+ $foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
+
+ if (!$folders[$foldername]) {
+ $folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
+ }
+ }
+
+ // for every (subscribed) user folder, list all (unsubscribed) subfolders
+ foreach ($folders as $userfolder) {
+ foreach ((array)self::list_folders($userfolder->name . $delimiter, '*', $type, false, $folderdata) as $foldername) {
+ if (!$folders[$foldername]) {
+ $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ $userfolder->children[] = $folders[$foldername];
+ }
+ }
+ }
+ }
+
+ return $folders;
+ }
+
+
+ /**
+ * Handler for user_delete plugin hooks
+ *
+ * Remove all cache data from the local database related to the given user.
+ */
+ public static function delete_user_folders($args)
+ {
+ $db = rcmail::get_instance()->get_dbh();
+ $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
+ $db->query("DELETE FROM " . $db->table_name('kolab_folders', true) . " WHERE `resource` LIKE ?", $prefix);
+ }
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php
index 651dc18..4f09e0f 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php
@@ -1,895 +1,1061 @@
<?php
/**
* Kolab storage cache class providing a local caching layer for Kolab groupware objects.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-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/>.
*/
class kolab_storage_cache
{
+ const DB_DATE_FORMAT = 'Y-m-d H:i:s';
+
protected $db;
protected $imap;
protected $folder;
protected $uid2msg;
protected $objects;
- protected $index = array();
protected $metadata = array();
protected $folder_id;
protected $resource_uri;
protected $enabled = true;
protected $synched = false;
protected $synclock = false;
protected $ready = false;
protected $cache_table;
protected $folders_table;
protected $max_sql_packet;
protected $max_sync_lock_time = 600;
protected $binary_items = array();
protected $extra_cols = array();
+ protected $order_by = null;
+ protected $limit = null;
/**
* Factory constructor
*/
public static function factory(kolab_storage_folder $storage_folder)
{
$subclass = 'kolab_storage_cache_' . $storage_folder->type;
if (class_exists($subclass)) {
return new $subclass($storage_folder);
}
else {
rcube::raise_error(array(
'code' => 900,
'type' => 'php',
- 'message' => "No kolab_storage_cache class found for folder of type " . $storage_folder->type
+ 'message' => "No kolab_storage_cache class found for folder '$storage_folder->name' of type '$storage_folder->type'"
), true);
+
+ return new kolab_storage_cache($storage_folder);
}
}
/**
* Default constructor
*/
public function __construct(kolab_storage_folder $storage_folder = null)
{
$rcmail = rcube::get_instance();
$this->db = $rcmail->get_dbh();
$this->imap = $rcmail->get_storage();
$this->enabled = $rcmail->config->get('kolab_cache', false);
if ($this->enabled) {
// always read folder cache and lock state from DB master
$this->db->set_table_dsn('kolab_folders', 'w');
// remove sync-lock on script termination
$rcmail->add_shutdown_function(array($this, '_sync_unlock'));
}
if ($storage_folder)
$this->set_folder($storage_folder);
}
+ /**
+ * Direct access to cache by folder_id
+ * (only for internal use)
+ */
+ public function select_by_id($folder_id)
+ {
+ $folders_table = $this->db->table_name('kolab_folders', true);
+ $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM $folders_table WHERE `folder_id` = ?", $folder_id));
+ if ($sql_arr) {
+ $this->metadata = $sql_arr;
+ $this->folder_id = $sql_arr['folder_id'];
+ $this->folder = new StdClass;
+ $this->folder->type = $sql_arr['type'];
+ $this->resource_uri = $sql_arr['resource'];
+ $this->cache_table = $this->db->table_name('kolab_cache_' . $sql_arr['type']);
+ $this->ready = true;
+ }
+ }
/**
* Connect cache with a storage folder
*
* @param kolab_storage_folder The storage folder instance to connect with
*/
public function set_folder(kolab_storage_folder $storage_folder)
{
$this->folder = $storage_folder;
if (empty($this->folder->name)) {
$this->ready = false;
return;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = $this->folder->get_resource_uri();
$this->folders_table = $this->db->table_name('kolab_folders');
$this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
- $this->ready = $this->enabled;
+ $this->ready = $this->enabled && !empty($this->folder->type);
$this->folder_id = null;
}
/**
* Returns true if this cache supports query by type
*/
public function has_type_col()
{
return in_array('type', $this->extra_cols);
}
+ /**
+ * Getter for the numeric ID used in cache tables
+ */
+ public function get_folder_id()
+ {
+ $this->_read_folder_data();
+ return $this->folder_id;
+ }
+
/**
* Synchronize local cache data with remote
*/
public function synchronize()
{
// only sync once per request cycle
if ($this->synched)
return;
// increase time limit
@set_time_limit($this->max_sync_lock_time);
- // read cached folder metadata
- $this->_read_folder_data();
+ if (!$this->ready) {
+ // kolab cache is disabled, synchronize IMAP mailbox cache only
+ $this->imap->folder_sync($this->folder->name);
+ }
+ else {
+ // read cached folder metadata
+ $this->_read_folder_data();
- // check cache status hash first ($this->metadata is set in _read_folder_data())
- if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
+ // check cache status hash first ($this->metadata is set in _read_folder_data())
+ if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
+ // lock synchronization for this folder or wait if locked
+ $this->_sync_lock();
- // lock synchronization for this folder or wait if locked
- $this->_sync_lock();
+ // disable messages cache if configured to do so
+ $this->bypass(true);
- // disable messages cache if configured to do so
- $this->bypass(true);
+ // synchronize IMAP mailbox cache
+ $this->imap->folder_sync($this->folder->name);
- // synchronize IMAP mailbox cache
- $this->imap->folder_sync($this->folder->name);
+ // compare IMAP index with object cache index
+ $imap_index = $this->imap->index($this->folder->name, null, null, true, true);
- // compare IMAP index with object cache index
- $imap_index = $this->imap->index($this->folder->name);
- $this->index = $imap_index->get();
+ // determine objects to fetch or to invalidate
+ if (!$imap_index->is_error()) {
+ $imap_index = $imap_index->get();
- // determine objects to fetch or to invalidate
- if ($this->ready) {
- // read cache index
- $sql_result = $this->db->query(
- "SELECT msguid, uid FROM $this->cache_table WHERE folder_id=?",
- $this->folder_id
- );
+ // read cache index
+ $sql_result = $this->db->query(
+ "SELECT `msguid`, `uid` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
+ $this->folder_id
+ );
- $old_index = array();
- while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $old_index[] = $sql_arr['msguid'];
- $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
- }
+ $old_index = array();
+ while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $old_index[] = $sql_arr['msguid'];
+ $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+ }
- // fetch new objects from imap
- foreach (array_diff($this->index, $old_index) as $msguid) {
- if ($object = $this->folder->read_object($msguid, '*')) {
- $this->_extended_insert($msguid, $object);
+ // fetch new objects from imap
+ foreach (array_diff($imap_index, $old_index) as $msguid) {
+ if ($object = $this->folder->read_object($msguid, '*')) {
+ $this->_extended_insert($msguid, $object);
+ }
+ }
+ $this->_extended_insert(0, null);
+
+ // delete invalid entries from local DB
+ $del_index = array_diff($old_index, $imap_index);
+ if (!empty($del_index)) {
+ $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
+ $this->db->query(
+ "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` IN ($quoted_ids)",
+ $this->folder_id
+ );
}
- }
- $this->_extended_insert(0, null);
-
- // delete invalid entries from local DB
- $del_index = array_diff($old_index, $this->index);
- if (!empty($del_index)) {
- $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
- $this->db->query(
- "DELETE FROM $this->cache_table WHERE folder_id=? AND msguid IN ($quoted_ids)",
- $this->folder_id
- );
- }
- // update ctag value (will be written to database in _sync_unlock())
- $this->metadata['ctag'] = $this->folder->get_ctag();
- }
+ // update ctag value (will be written to database in _sync_unlock())
+ $this->metadata['ctag'] = $this->folder->get_ctag();
+ }
- $this->bypass(false);
+ $this->bypass(false);
- // remove lock
- $this->_sync_unlock();
+ // remove lock
+ $this->_sync_unlock();
+ }
}
$this->synched = time();
}
/**
* Read a single entry from cache or from IMAP directly
*
* @param string Related IMAP message UID
* @param string Object type to read
* @param string IMAP folder name the entry relates to
* @param array Hash array with object properties or null if not found
*/
public function get($msguid, $type = null, $foldername = null)
{
// delegate to another cache instance
if ($foldername && $foldername != $this->folder->name) {
return kolab_storage::get_folder($foldername)->cache->get($msguid, $type);
}
// load object if not in memory
if (!isset($this->objects[$msguid])) {
if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
- "SELECT * FROM $this->cache_table ".
- "WHERE folder_id=? AND msguid=?",
+ "SELECT * FROM `{$this->cache_table}` ".
+ "WHERE `folder_id` = ? AND `msguid` = ?",
$this->folder_id,
$msguid
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $this->objects[$msguid] = $this->_unserialize($sql_arr);
+ $this->objects = array($msguid => $this->_unserialize($sql_arr)); // store only this object in memory (#2827)
}
}
// fetch from IMAP if not present in cache
if (empty($this->objects[$msguid])) {
- $result = $this->_fetch(array($msguid), $type, $foldername);
- $this->objects[$msguid] = $result[0];
+ if ($object = $this->folder->read_object($msguid, $type ?: '*', $foldername)) {
+ $this->objects = array($msguid => $object);
+ $this->set($msguid, $object);
+ }
}
}
return $this->objects[$msguid];
}
/**
* Insert/Update a cache entry
*
* @param string Related IMAP message UID
* @param mixed Hash array with object properties to save or false to delete the cache entry
* @param string IMAP folder name the entry relates to
*/
public function set($msguid, $object, $foldername = null)
{
if (!$msguid) {
return;
}
// delegate to another cache instance
if ($foldername && $foldername != $this->folder->name) {
kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
return;
}
// remove old entry
if ($this->ready) {
$this->_read_folder_data();
- $this->db->query("DELETE FROM $this->cache_table WHERE folder_id=? AND msguid=?",
+ $this->db->query("DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` = ?",
$this->folder_id, $msguid);
}
if ($object) {
// insert new object data...
- $this->insert($msguid, $object);
+ $this->save($msguid, $object);
}
else {
// ...or set in-memory cache to false
$this->objects[$msguid] = $object;
}
}
/**
- * Insert a cache entry
+ * Insert (or update) a cache entry
*
- * @param string Related IMAP message UID
+ * @param int Related IMAP message UID
* @param mixed Hash array with object properties to save or false to delete the cache entry
+ * @param int Optional old message UID (for update)
*/
- public function insert($msguid, $object)
+ public function save($msguid, $object, $olduid = null)
{
// write to cache
if ($this->ready) {
$this->_read_folder_data();
$sql_data = $this->_serialize($object);
+ $sql_data['folder_id'] = $this->folder_id;
+ $sql_data['msguid'] = $msguid;
+ $sql_data['uid'] = $object['uid'];
- $extra_cols = $this->extra_cols ? ', ' . join(', ', $this->extra_cols) : '';
- $extra_fields = $this->extra_cols ? str_repeat(', ?', count($this->extra_cols)) : '';
+ $args = array();
+ $cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'xml', 'tags', 'words');
+ $cols = array_merge($cols, $this->extra_cols);
- $args = array(
- "INSERT INTO $this->cache_table ".
- " (folder_id, msguid, uid, created, changed, data, xml, tags, words $extra_cols)".
- " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_fields)",
- $this->folder_id,
- $msguid,
- $object['uid'],
- $sql_data['changed'],
- $sql_data['data'],
- $sql_data['xml'],
- $sql_data['tags'],
- $sql_data['words'],
- );
+ foreach ($cols as $idx => $col) {
+ $cols[$idx] = $this->db->quote_identifier($col);
+ $args[] = $sql_data[$col];
+ }
- foreach ($this->extra_cols as $col) {
- $args[] = $sql_data[$col];
+ if ($olduid) {
+ foreach ($cols as $idx => $col) {
+ $cols[$idx] = "$col = ?";
+ }
+
+ $query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
+ . " WHERE `folder_id` = ? AND `msguid` = ?";
+ $args[] = $this->folder_id;
+ $args[] = $olduid;
+ }
+ else {
+ $query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
+ . ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
}
- $result = call_user_func_array(array($this->db, 'query'), $args);
+ $result = $this->db->query($query, $args);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "Failed to write to kolab cache"
), true);
}
}
// keep a copy in memory for fast access
$this->objects = array($msguid => $object);
$this->uid2msg = array($object['uid'] => $msguid);
}
/**
* Move an existing cache entry to a new resource
*
* @param string Entry's IMAP message UID
* @param string Entry's Object UID
* @param string Target IMAP folder to move it to
*/
public function move($msguid, $uid, $target_folder)
{
- $target = kolab_storage::get_folder($target_folder);
+ if ($this->ready) {
+ $target = kolab_storage::get_folder($target_folder);
- // resolve new message UID in target folder
- if ($new_msguid = $target->cache->uid2msguid($uid)) {
- $this->_read_folder_data();
+ // resolve new message UID in target folder
+ if ($new_msguid = $target->cache->uid2msguid($uid)) {
+ $this->_read_folder_data();
- $this->db->query(
- "UPDATE $this->cache_table SET folder_id=?, msguid=? ".
- "WHERE folder_id=? AND msguid=?",
- $target->folder_id,
- $new_msguid,
- $this->folder_id,
- $msguid
- );
+ $this->db->query(
+ "UPDATE `{$this->cache_table}` SET `folder_id` = ?, `msguid` = ? ".
+ "WHERE `folder_id` = ? AND `msguid` = ?",
+ $target->cache->get_folder_id(),
+ $new_msguid,
+ $this->folder_id,
+ $msguid
+ );
+
+ $result = $this->db->affected_rows();
+ }
}
- else {
+
+ if (empty($result)) {
// just clear cache entry
$this->set($msguid, false);
}
unset($this->uid2msg[$uid]);
}
/**
* Remove all objects from local cache
*/
public function purge($type = null)
{
+ if (!$this->ready) {
+ return true;
+ }
+
$this->_read_folder_data();
$result = $this->db->query(
- "DELETE FROM $this->cache_table WHERE folder_id=?".
+ "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ?",
$this->folder_id
);
+
return $this->db->affected_rows($result);
}
/**
* Update resource URI for existing cache entries
*
* @param string Target IMAP folder to move it to
*/
public function rename($new_folder)
{
+ if (!$this->ready) {
+ return;
+ }
+
$target = kolab_storage::get_folder($new_folder);
// resolve new message UID in target folder
$this->db->query(
- "UPDATE $this->folders_table SET resource=? ".
- "WHERE resource=?",
+ "UPDATE `{$this->folders_table}` SET `resource` = ? ".
+ "WHERE `resource` = ?",
$target->get_resource_uri(),
$this->resource_uri
);
}
/**
* Select Kolab objects filtered by the given query
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* triplet: array('<colname>', '<comparator>', '<value>')
* @param boolean Set true to only return UIDs instead of complete objects
* @return array List of Kolab data objects (each represented as hash array) or UIDs
*/
public function select($query = array(), $uids = false)
{
- $result = array();
+ $result = $uids ? array() : new kolab_storage_dataset($this);
// read from local cache DB (assume it to be synchronized)
if ($this->ready) {
$this->_read_folder_data();
- $sql_result = $this->db->query(
- "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM $this->cache_table ".
- "WHERE folder_id=? " . $this->_sql_where($query),
- $this->folder_id
- );
+ // fetch full object data on one query if a small result set is expected
+ $fetchall = !$uids && ($this->limit ? $this->limit[0] : $this->count($query)) < 500;
+ $sql_query = "SELECT " . ($fetchall ? '*' : '`msguid` AS `_msguid`, `uid`') . " FROM `{$this->cache_table}` ".
+ "WHERE `folder_id` = ? " . $this->_sql_where($query);
+ if (!empty($this->order_by)) {
+ $sql_query .= ' ORDER BY ' . $this->order_by;
+ }
+ $sql_result = $this->limit ?
+ $this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
+ $this->db->query($sql_query, $this->folder_id);
+
+ if ($this->db->is_error($sql_result)) {
+ if ($uids) {
+ return null;
+ }
+ $result->set_error(true);
+ return $result;
+ }
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if ($uids) {
- $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+ $this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid'];
$result[] = $sql_arr['uid'];
}
- else if ($object = $this->_unserialize($sql_arr)) {
+ else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
$result[] = $object;
}
+ else if (!$fetchall) {
+ // only add msguid to dataset index
+ $result[] = $sql_arr;
+ }
}
}
+ // use IMAP
else {
- // extract object type from query parameter
$filter = $this->_query2assoc($query);
- // use 'list' for folder's default objects
- if ($filter['type'] == $this->type) {
- $index = $this->index;
- }
- else { // search by object type
+ if ($filter['type']) {
$search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
- $index = $this->imap->search_once($this->folder->name, $search)->get();
+ $index = $this->imap->search_once($this->folder->name, $search);
+ }
+ else {
+ $index = $this->imap->index($this->folder->name, null, null, true, true);
+ }
+
+ if ($index->is_error()) {
+ if ($uids) {
+ return null;
+ }
+ $result->set_error(true);
+ return $result;
}
- // fetch all messages in $index from IMAP
- $result = $uids ? $this->_fetch_uids($index, $filter['type']) : $this->_fetch($index, $filter['type']);
+ $index = $index->get();
+ $result = $uids ? $index : $this->_fetch($index, $filter['type']);
// TODO: post-filter result according to query
}
// We don't want to cache big results in-memory, however
// if we select only one object here, there's a big chance we will need it later
if (!$uids && count($result) == 1) {
if ($msguid = $result[0]['_msguid']) {
$this->uid2msg[$result[0]['uid']] = $msguid;
- $this->objects[$msguid] = $result[0];
+ $this->objects = array($msguid => $result[0]);
}
}
return $result;
}
/**
* Get number of objects mathing the given query
*
* @param array $query Pseudo-SQL query as list of filter parameter triplets
* @return integer The number of objects of the given type
*/
public function count($query = array())
{
- $count = 0;
-
- // cache is in sync, we can count records in local DB
- if ($this->synched) {
+ // read from local cache DB (assume it to be synchronized)
+ if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
- "SELECT COUNT(*) AS numrows FROM $this->cache_table ".
- "WHERE folder_id=? " . $this->_sql_where($query),
+ "SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
+ "WHERE `folder_id` = ?" . $this->_sql_where($query),
$this->folder_id
);
+ if ($this->db->is_error($sql_result)) {
+ return null;
+ }
+
$sql_arr = $this->db->fetch_assoc($sql_result);
- $count = intval($sql_arr['numrows']);
+ $count = intval($sql_arr['numrows']);
}
+ // use IMAP
else {
- // search IMAP by object type
$filter = $this->_query2assoc($query);
- $ctype = kolab_format::KTYPE_PREFIX . $filter['type'];
- $index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
+
+ if ($filter['type']) {
+ $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
+ $index = $this->imap->search_once($this->folder->name, $search);
+ }
+ else {
+ $index = $this->imap->index($this->folder->name, null, null, true, true);
+ }
+
+ if ($index->is_error()) {
+ return null;
+ }
+
+ // TODO: post-filter result according to query
+
$count = $index->count();
}
return $count;
}
+ /**
+ * Define ORDER BY clause for cache queries
+ */
+ public function set_order_by($sortcols)
+ {
+ if (!empty($sortcols)) {
+ $this->order_by = '`' . join('`, `', (array)$sortcols) . '`';
+ }
+ else {
+ $this->order_by = null;
+ }
+ }
+
+ /**
+ * Define LIMIT clause for cache queries
+ */
+ public function set_limit($length, $offset = 0)
+ {
+ $this->limit = array($length, $offset);
+ }
/**
* Helper method to compose a valid SQL query from pseudo filter triplets
*/
protected function _sql_where($query)
{
$sql_where = '';
foreach ((array) $query as $param) {
if (is_array($param[0])) {
$subq = array();
foreach ($param[0] as $q) {
$subq[] = preg_replace('/^\s*AND\s+/i', '', $this->_sql_where(array($q)));
}
if (!empty($subq)) {
$sql_where .= ' AND (' . implode($param[1] == 'OR' ? ' OR ' : ' AND ', $subq) . ')';
}
continue;
}
else if ($param[1] == '=' && is_array($param[2])) {
$qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')';
$param[1] = 'IN';
}
else if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') {
$not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : '';
$param[1] = $not . 'LIKE';
$qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
}
else if ($param[0] == 'tags') {
- $param[1] = 'LIKE';
+ $param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE';
$qvalue = $this->db->quote('% '.$param[2].' %');
}
else {
$qvalue = $this->db->quote($param[2]);
}
$sql_where .= sprintf(' AND %s %s %s',
$this->db->quote_identifier($param[0]),
$param[1],
$qvalue
);
}
return $sql_where;
}
/**
* Helper method to convert the given pseudo-query triplets into
* an associative filter array with 'equals' values only
*/
protected function _query2assoc($query)
{
// extract object type from query parameter
$filter = array();
foreach ($query as $param) {
if ($param[1] == '=')
$filter[$param[0]] = $param[2];
}
return $filter;
}
/**
* Fetch messages from IMAP
*
* @param array List of message UIDs to fetch
* @param string Requested object type or * for all
* @param string IMAP folder to read from
* @return array List of parsed Kolab objects
*/
protected function _fetch($index, $type = null, $folder = null)
{
- $results = array();
+ $results = new kolab_storage_dataset($this);
foreach ((array)$index as $msguid) {
if ($object = $this->folder->read_object($msguid, $type, $folder)) {
$results[] = $object;
$this->set($msguid, $object);
}
}
return $results;
}
-
- /**
- * Fetch object UIDs (aka message subjects) from IMAP
- *
- * @param array List of message UIDs to fetch
- * @param string Requested object type or * for all
- * @param string IMAP folder to read from
- * @return array List of parsed Kolab objects
- */
- protected function _fetch_uids($index, $type = null)
- {
- if (!$type)
- $type = $this->folder->type;
-
- $this->bypass(true);
-
- $results = array();
- $headers = $this->imap->fetch_headers($this->folder->name, $index, false);
-
- $this->bypass(false);
-
- foreach ((array)$headers as $msguid => $headers) {
- $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
-
- // check object type header and abort on mismatch
- if ($type != '*' && $object_type != $type)
- return false;
-
- $uid = $headers->subject;
- $this->uid2msg[$uid] = $msguid;
- $results[] = $uid;
- }
-
- return $results;
- }
-
-
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*/
protected function _serialize($object)
{
$sql_data = array('changed' => null, 'xml' => '', 'tags' => '', 'words' => '');
if ($object['changed']) {
$sql_data['changed'] = date('Y-m-d H:i:s', is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']);
}
if ($object['_formatobj']) {
$sql_data['xml'] = preg_replace('!(</?[a-z0-9:-]+>)[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write(3.0));
$sql_data['tags'] = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' '; // pad with spaces for strict/prefix search
$sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' ';
}
// extract object data
$data = array();
foreach ($object as $key => $val) {
// skip empty properties
if ($val === "" || $val === null) {
continue;
}
// mark binary data to be extracted from xml on unserialize()
if (isset($this->binary_items[$key])) {
$data[$key] = true;
}
else if ($key[0] != '_') {
$data[$key] = $val;
}
else if ($key == '_attachments') {
foreach ($val as $k => $att) {
unset($att['content'], $att['path']);
if ($att['id'])
$data[$key][$k] = $att;
}
}
}
- $sql_data['data'] = serialize($data);
+ // use base64 encoding (Bug #1912, #2662)
+ $sql_data['data'] = base64_encode(serialize($data));
+
return $sql_data;
}
/**
* Helper method to turn stored cache data into a valid storage object
*/
protected function _unserialize($sql_arr)
{
+ // check if data is a base64-encoded string, for backward compat.
+ if (strpos(substr($sql_arr['data'], 0, 64), ':') === false) {
+ $sql_arr['data'] = base64_decode($sql_arr['data']);
+ }
+
$object = unserialize($sql_arr['data']);
+ // de-serialization failed
+ if ($object === false) {
+ rcube::raise_error(array(
+ 'code' => 900, 'type' => 'php',
+ 'message' => "Malformed data for {$this->resource_uri}/{$sql_arr['msguid']} object."
+ ), true);
+
+ return null;
+ }
+
// decode binary properties
foreach ($this->binary_items as $key => $regexp) {
if (!empty($object[$key]) && preg_match($regexp, $sql_arr['xml'], $m)) {
$object[$key] = base64_decode($m[1]);
}
}
+ $object_type = $sql_arr['type'] ?: $this->folder->type;
+ $format_type = $this->folder->type == 'configuration' ? 'configuration' : $object_type;
+
// add meta data
- $object['_type'] = $sql_arr['type'] ?: $this->folder->type;
- $object['_msguid'] = $sql_arr['msguid'];
- $object['_mailbox'] = $this->folder->name;
- $object['_size'] = strlen($sql_arr['xml']);
- $object['_formatobj'] = kolab_format::factory($object['_type'], 3.0, $sql_arr['xml']);
+ $object['_type'] = $object_type;
+ $object['_msguid'] = $sql_arr['msguid'];
+ $object['_mailbox'] = $this->folder->name;
+ $object['_size'] = strlen($sql_arr['xml']);
+ $object['_formatobj'] = kolab_format::factory($format_type, 3.0, $sql_arr['xml']);
return $object;
}
/**
* Write records into cache using extended inserts to reduce the number of queries to be executed
*
* @param int Message UID. Set 0 to commit buffered inserts
* @param array Kolab object to cache
*/
protected function _extended_insert($msguid, $object)
{
static $buffer = '';
$line = '';
if ($object) {
$sql_data = $this->_serialize($object);
+
+ // Skip multifolder insert for Oracle, we can't put long data inline
+ if ($this->db->db_provider == 'oracle') {
+ $extra_cols = '';
+ if ($this->extra_cols) {
+ $extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols);
+ $extra_cols = ', ' . join(', ', $extra_cols);
+ $extra_args = str_repeat(', ?', count($this->extra_cols));
+ }
+
+ $params = array($this->folder_id, $msguid, $object['uid'], $sql_data['changed'],
+ $sql_data['data'], $sql_data['xml'], $sql_data['tags'], $sql_data['words']);
+
+ foreach ($this->extra_cols as $col) {
+ $params[] = $sql_data[$col];
+ }
+
+ $result = $this->db->query(
+ "INSERT INTO `{$this->cache_table}` "
+ . " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)"
+ . " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_args)",
+ $params
+ );
+
+ if (!$this->db->affected_rows($result)) {
+ rcube::raise_error(array(
+ 'code' => 900, 'type' => 'php',
+ 'message' => "Failed to write to kolab cache"
+ ), true);
+ }
+
+ return;
+ }
+
$values = array(
$this->db->quote($this->folder_id),
$this->db->quote($msguid),
$this->db->quote($object['uid']),
$this->db->now(),
$this->db->quote($sql_data['changed']),
$this->db->quote($sql_data['data']),
$this->db->quote($sql_data['xml']),
$this->db->quote($sql_data['tags']),
$this->db->quote($sql_data['words']),
);
foreach ($this->extra_cols as $col) {
$values[] = $this->db->quote($sql_data[$col]);
}
$line = '(' . join(',', $values) . ')';
}
if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
- $extra_cols = $this->extra_cols ? ', ' . join(', ', $this->extra_cols) : '';
+ $extra_cols = '';
+ if ($this->extra_cols) {
+ $extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols);
+ $extra_cols = ', ' . join(', ', $extra_cols);
+ }
+
$result = $this->db->query(
- "INSERT INTO $this->cache_table ".
- " (folder_id, msguid, uid, created, changed, data, xml, tags, words $extra_cols)".
+ "INSERT INTO `{$this->cache_table}` ".
+ " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)".
" VALUES $buffer"
);
+
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "Failed to write to kolab cache"
), true);
}
$buffer = '';
}
$buffer .= ($buffer ? ',' : '') . $line;
}
/**
* Returns max_allowed_packet from mysql config
*/
protected function max_sql_packet()
{
if (!$this->max_sql_packet) {
// mysql limit or max 4 MB
$value = $this->db->get_variable('max_allowed_packet', 1048500);
$this->max_sql_packet = min($value, 4*1024*1024) - 2000;
}
return $this->max_sql_packet;
}
/**
* Read this folder's ID and cache metadata
*/
protected function _read_folder_data()
{
// already done
- if (!empty($this->folder_id))
+ if (!empty($this->folder_id) || !$this->ready)
return;
- $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT folder_id, synclock, ctag FROM $this->folders_table WHERE resource=?", $this->resource_uri));
+ $sql_arr = $this->db->fetch_assoc($this->db->query(
+ "SELECT `folder_id`, `synclock`, `ctag`"
+ . " FROM `{$this->folders_table}` WHERE `resource` = ?",
+ $this->resource_uri
+ ));
+
if ($sql_arr) {
$this->metadata = $sql_arr;
$this->folder_id = $sql_arr['folder_id'];
}
else {
- $this->db->query("INSERT INTO $this->folders_table (resource, type) VALUES (?, ?)", $this->resource_uri, $this->folder->type);
+ $this->db->query("INSERT INTO `{$this->folders_table}` (`resource`, `type`)"
+ . " VALUES (?, ?)", $this->resource_uri, $this->folder->type);
+
$this->folder_id = $this->db->insert_id('kolab_folders');
$this->metadata = array();
}
}
/**
* Check lock record for this folder and wait if locked or set lock
*/
protected function _sync_lock()
{
if (!$this->ready)
return;
$this->_read_folder_data();
- $sql_query = "SELECT synclock, ctag FROM $this->folders_table WHERE folder_id=?";
+ $sql_query = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?";
// abort if database is not set-up
if ($this->db->is_error()) {
$this->ready = false;
return;
}
$this->synclock = true;
// wait if locked (expire locks after 10 minutes)
while ($this->metadata && intval($this->metadata['synclock']) > 0 && $this->metadata['synclock'] + $this->max_sync_lock_time > time()) {
usleep(500000);
$this->metadata = $this->db->fetch_assoc($this->db->query($sql_query, $this->folder_id));
}
// set lock
- $this->db->query("UPDATE $this->folders_table SET synclock = ? WHERE folder_id = ?", time(), $this->folder_id);
+ $this->db->query("UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ?", time(), $this->folder_id);
}
/**
* Remove lock for this folder
*/
public function _sync_unlock()
{
if (!$this->ready || !$this->synclock)
return;
$this->db->query(
- "UPDATE $this->folders_table SET synclock = 0, ctag = ? WHERE folder_id = ?",
+ "UPDATE `{$this->folders_table}` SET `synclock` = 0, `ctag` = ? WHERE `folder_id` = ?",
$this->metadata['ctag'],
$this->folder_id
);
$this->synclock = false;
}
/**
* Resolve an object UID into an IMAP message UID
*
* @param string Kolab object UID
* @param boolean Include deleted objects
* @return int The resolved IMAP message UID
*/
public function uid2msguid($uid, $deleted = false)
{
+ // query local database if available
+ if (!isset($this->uid2msg[$uid]) && $this->ready) {
+ $this->_read_folder_data();
+
+ $sql_result = $this->db->query(
+ "SELECT `msguid` FROM `{$this->cache_table}` ".
+ "WHERE `folder_id` = ? AND `uid` = ? ORDER BY `msguid` DESC",
+ $this->folder_id,
+ $uid
+ );
+
+ if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $this->uid2msg[$uid] = $sql_arr['msguid'];
+ }
+ }
+
if (!isset($this->uid2msg[$uid])) {
// use IMAP SEARCH to get the right message
$index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') .
'HEADER SUBJECT ' . rcube_imap_generic::escape($uid));
$results = $index->get();
- $this->uid2msg[$uid] = $results[0];
+ $this->uid2msg[$uid] = end($results);
}
return $this->uid2msg[$uid];
}
/**
* Getter for protected member variables
*/
public function __get($name)
{
if ($name == 'folder_id') {
$this->_read_folder_data();
}
return $this->$name;
}
/**
* Bypass Roundcube messages cache.
* Roundcube cache duplicates information already stored in kolab_cache.
*
* @param bool $disable True disables, False enables messages cache
*/
public function bypass($disable = false)
{
// if kolab cache is disabled do nothing
if (!$this->enabled) {
return;
}
static $messages_cache, $cache_bypass;
if ($messages_cache === null) {
$rcmail = rcube::get_instance();
$messages_cache = (bool) $rcmail->config->get('messages_cache');
$cache_bypass = (int) $rcmail->config->get('kolab_messages_cache_bypass');
}
if ($messages_cache) {
// handle recurrent (multilevel) bypass() calls
if ($disable) {
$this->cache_bypassed += 1;
if ($this->cache_bypassed > 1) {
return;
}
}
else {
$this->cache_bypassed -= 1;
if ($this->cache_bypassed > 0) {
return;
}
}
switch ($cache_bypass) {
case 2:
// Disable messages cache completely
$this->imap->set_messages_caching(!$disable);
break;
case 1:
// We'll disable messages cache, but keep index cache.
// Default mode is both (MODE_INDEX | MODE_MESSAGE)
$mode = rcube_imap_cache::MODE_INDEX;
if (!$disable) {
$mode |= rcube_imap_cache::MODE_MESSAGE;
}
$this->imap->set_messages_caching(true, $mode);
}
}
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php
index 8380aa8..c3c7ac4 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php
@@ -1,40 +1,88 @@
<?php
/**
* Kolab storage cache class for configuration objects
*
* @author Thomas Bruederli <bruederli@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/>.
*/
class kolab_storage_cache_configuration extends kolab_storage_cache
{
protected $extra_cols = array('type');
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*
* @override
*/
protected function _serialize($object)
{
$sql_data = parent::_serialize($object);
$sql_data['type'] = $object['type'];
return $sql_data;
}
-}
\ No newline at end of file
+
+ /**
+ * Select Kolab objects filtered by the given query
+ *
+ * @param array Pseudo-SQL query as list of filter parameter triplets
+ * @param boolean Set true to only return UIDs instead of complete objects
+ * @return array List of Kolab data objects (each represented as hash array) or UIDs
+ */
+ public function select($query = array(), $uids = false)
+ {
+ // modify query for IMAP search: query param 'type' is actually a subtype
+ if (!$this->ready) {
+ foreach ($query as $i => $tuple) {
+ if ($tuple[0] == 'type') {
+ $tuple[2] = 'configuration.' . $tuple[2];
+ $query[$i] = $tuple;
+ }
+ }
+ }
+
+ return parent::select($query, $uids);
+ }
+
+ /**
+ * Helper method to compose a valid SQL query from pseudo filter triplets
+ */
+ protected function _sql_where($query)
+ {
+ if (is_array($query)) {
+ foreach ($query as $idx => $param) {
+ // convert category filter
+ if ($param[0] == 'category') {
+ $param[2] = array_map(function($n) { return 'category:' . $n; }, (array) $param[2]);
+
+ $query[$idx][0] = 'tags';
+ $query[$idx][2] = count($param[2]) > 1 ? $param[2] : $param[2][0];
+ }
+ // convert member filter (we support only = operator with single value)
+ else if ($param[0] == 'member') {
+ $query[$idx][0] = 'words';
+ $query[$idx][1] = '~';
+ $query[$idx][2] = '^' . $param[2] . '$';
+ }
+ }
+ }
+
+ return parent::_sql_where($query);
+ }
+}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_contact.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_contact.php
index e17923d..9666a39 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_contact.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_contact.php
@@ -1,45 +1,59 @@
<?php
/**
* Kolab storage cache class for contact objects
*
* @author Thomas Bruederli <bruederli@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/>.
*/
class kolab_storage_cache_contact extends kolab_storage_cache
{
- protected $extra_cols = array('type');
+ protected $extra_cols = array('type','name','firstname','surname','email');
protected $binary_items = array(
'photo' => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
'pgppublickey' => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></key>|i',
'pkcs7publickey' => '|<key><uri>date:application/pkcs7-mime;base64,([^<]+)</uri></key>|i',
);
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*
* @override
*/
protected function _serialize($object)
{
$sql_data = parent::_serialize($object);
$sql_data['type'] = $object['_type'];
+ // columns for sorting
+ $sql_data['name'] = rcube_charset::clean($object['name'] . $object['prefix']);
+ $sql_data['firstname'] = rcube_charset::clean($object['firstname'] . $object['middlename'] . $object['surname']);
+ $sql_data['surname'] = rcube_charset::clean($object['surname'] . $object['firstname'] . $object['middlename']);
+ $sql_data['email'] = rcube_charset::clean(is_array($object['email']) ? $object['email'][0] : $object['email']);
+
+ if (is_array($sql_data['email'])) {
+ $sql_data['email'] = $sql_data['email']['address'];
+ }
+ // avoid value being null
+ if (empty($sql_data['email'])) {
+ $sql_data['email'] = '';
+ }
+
return $sql_data;
}
}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php
index 876c3b4..5fc44cd 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php
@@ -1,49 +1,49 @@
<?php
/**
* Kolab storage cache class for calendar event objects
*
* @author Thomas Bruederli <bruederli@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/>.
*/
class kolab_storage_cache_event extends kolab_storage_cache
{
protected $extra_cols = array('dtstart','dtend');
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*
* @override
*/
protected function _serialize($object)
{
$sql_data = parent::_serialize($object);
- // database runs in server's timezone so using date() is what we want
- $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
- $sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['end']) ? $object['end']->format('U') : $object['end']);
+ $sql_data['dtstart'] = is_object($object['start']) ? $object['start']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['start']);
+ $sql_data['dtend'] = is_object($object['end']) ? $object['end']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['end']);
// extend date range for recurring events
if ($object['recurrence'] && $object['_formatobj']) {
$recurrence = new kolab_date_recurrence($object['_formatobj']);
- $sql_data['dtend'] = date('Y-m-d 23:59:59', $recurrence->end() ?: strtotime('now +10 years'));
+ $dtend = $recurrence->end() ?: new DateTime('now +10 years');
+ $sql_data['dtend'] = $dtend->format(self::DB_DATE_FORMAT);
}
return $sql_data;
}
}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_task.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_task.php
index a1953f6..7bf5c79 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_task.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_task.php
@@ -1,44 +1,44 @@
<?php
/**
* Kolab storage cache class for task objects
*
* @author Thomas Bruederli <bruederli@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/>.
*/
class kolab_storage_cache_task extends kolab_storage_cache
{
protected $extra_cols = array('dtstart','dtend');
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*
* @override
*/
protected function _serialize($object)
{
$sql_data = parent::_serialize($object) + array('dtstart' => null, 'dtend' => null);
if ($object['start'])
- $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
+ $sql_data['dtstart'] = is_object($object['start']) ? $object['start']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['start']);
if ($object['due'])
- $sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['due']) ? $object['due']->format('U') : $object['due']);
+ $sql_data['dtend'] = is_object($object['due']) ? $object['due']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['due']);
return $sql_data;
}
}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php
new file mode 100644
index 0000000..8a0fab5
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php
@@ -0,0 +1,728 @@
+<?php
+
+/**
+ * Kolab storage class providing access to configuration objects on a Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2012-2014, 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/>.
+ */
+
+class kolab_storage_config
+{
+ const FOLDER_TYPE = 'configuration';
+
+
+ /**
+ * Singleton instace of kolab_storage_config
+ *
+ * @var kolab_storage_config
+ */
+ static protected $instance;
+
+ private $folders;
+ private $default;
+ private $enabled;
+
+
+ /**
+ * This implements the 'singleton' design pattern
+ *
+ * @return kolab_storage_config The one and only instance
+ */
+ static function get_instance()
+ {
+ if (!self::$instance) {
+ self::$instance = new kolab_storage_config();
+ }
+
+ return self::$instance;
+ }
+
+ /**
+ * Private constructor (finds default configuration folder as a config source)
+ */
+ private function __construct()
+ {
+ // get all configuration folders
+ $this->folders = kolab_storage::get_folders(self::FOLDER_TYPE, false);
+
+ foreach ($this->folders as $folder) {
+ if ($folder->default) {
+ $this->default = $folder;
+ break;
+ }
+ }
+
+ // if no folder is set as default, choose the first one
+ if (!$this->default) {
+ $this->default = reset($this->folders);
+ }
+
+ // attempt to create a default folder if it does not exist
+ if (!$this->default) {
+ $folder_name = 'Configuration';
+ $folder_type = self::FOLDER_TYPE . '.default';
+
+ if (kolab_storage::folder_create($folder_name, $folder_type, true)) {
+ $this->default = new kolab_storage_folder($folder_name, $folder_type);
+ }
+ }
+
+ // check if configuration folder exist
+ if ($this->default && $this->default->name) {
+ $this->enabled = true;
+ }
+ }
+
+ /**
+ * Check wether any configuration storage (folder) exists
+ *
+ * @return bool
+ */
+ public function is_enabled()
+ {
+ return $this->enabled;
+ }
+
+ /**
+ * Get configuration objects
+ *
+ * @param array $filter Search filter
+ * @param bool $default Enable to get objects only from default folder
+ * @param int $limit Max. number of records (per-folder)
+ *
+ * @return array List of objects
+ */
+ public function get_objects($filter = array(), $default = false, $limit = 0)
+ {
+ $list = array();
+
+ foreach ($this->folders as $folder) {
+ // we only want to read from default folder
+ if ($default && !$folder->default) {
+ continue;
+ }
+
+ // for better performance it's good to assume max. number of records
+ if ($limit) {
+ $folder->set_order_and_limit(null, $limit);
+ }
+
+ foreach ($folder->select($filter) as $object) {
+ unset($object['_formatobj']);
+ $list[] = $object;
+ }
+ }
+
+ return $list;
+ }
+
+ /**
+ * Get configuration object
+ *
+ * @param string $uid Object UID
+ * @param bool $default Enable to get objects only from default folder
+ *
+ * @return array Object data
+ */
+ public function get_object($uid, $default = false)
+ {
+ foreach ($this->folders as $folder) {
+ // we only want to read from default folder
+ if ($default && !$folder->default) {
+ continue;
+ }
+
+ if ($object = $folder->get_object($uid)) {
+ return $object;
+ }
+ }
+ }
+
+ /**
+ * Create/update configuration object
+ *
+ * @param array $object Object data
+ * @param string $type Object type
+ *
+ * @return bool True on success, False on failure
+ */
+ public function save(&$object, $type)
+ {
+ if (!$this->enabled) {
+ return false;
+ }
+
+ $folder = $this->find_folder($object);
+
+ if ($type) {
+ $object['type'] = $type;
+ }
+
+ return $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']);
+ }
+
+ /**
+ * Remove configuration object
+ *
+ * @param string $uid Object UID
+ *
+ * @return bool True on success, False on failure
+ */
+ public function delete($uid)
+ {
+ if (!$this->enabled) {
+ return false;
+ }
+
+ // fetch the object to find folder
+ $object = $this->get_object($uid);
+
+ if (!$object) {
+ return false;
+ }
+
+ $folder = $this->find_folder($object);
+
+ return $folder->delete($uid);
+ }
+
+ /**
+ * Find folder
+ */
+ public function find_folder($object = array())
+ {
+ // find folder object
+ if ($object['_mailbox']) {
+ foreach ($this->folders as $folder) {
+ if ($folder->name == $object['_mailbox']) {
+ break;
+ }
+ }
+ }
+ else {
+ $folder = $this->default;
+ }
+
+ return $folder;
+ }
+
+ /**
+ * Builds relation member URI
+ *
+ * @param string|array Object UUID or Message folder, UID, Search headers (Message-Id, Date)
+ *
+ * @return string $url Member URI
+ */
+ public static function build_member_url($params)
+ {
+ // param is object UUID
+ if (is_string($params) && !empty($params)) {
+ return 'urn:uuid:' . $params;
+ }
+
+ if (empty($params) || !strlen($params['folder'])) {
+ return null;
+ }
+
+ $rcube = rcube::get_instance();
+ $storage = $rcube->get_storage();
+
+ // modify folder spec. according to namespace
+ $folder = $params['folder'];
+ $ns = $storage->folder_namespace($folder);
+
+ if ($ns == 'shared') {
+ // Note: this assumes there's only one shared namespace root
+ if ($ns = $storage->get_namespace('shared')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = 'shared' . substr($folder, strlen($prefix));
+ }
+ }
+ }
+ else {
+ if ($ns == 'other') {
+ // Note: this assumes there's only one other users namespace root
+ if ($ns = $storage->get_namespace('shared')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = 'user' . substr($folder, strlen($prefix));
+ }
+ }
+ }
+ else {
+ $folder = 'user' . '/' . $rcube->get_user_name() . '/' . $folder;
+ }
+ }
+
+ $folder = implode('/', array_map('rawurlencode', explode('/', $folder)));
+
+ // build URI
+ $url = 'imap:///' . $folder;
+
+ // UID is optional here because sometimes we want
+ // to build just a member uri prefix
+ if ($params['uid']) {
+ $url .= '/' . $params['uid'];
+ }
+
+ unset($params['folder']);
+ unset($params['uid']);
+
+ if (!empty($params)) {
+ $url .= '?' . http_build_query($params, '', '&');
+ }
+
+ return $url;
+ }
+
+ /**
+ * Parses relation member string
+ *
+ * @param string $url Member URI
+ *
+ * @return array Message folder, UID, Search headers (Message-Id, Date)
+ */
+ public static function parse_member_url($url)
+ {
+ // Look for IMAP URI:
+ // imap:///(user/username@domain|shared)/<folder>/<UID>?<search_params>
+ if (strpos($url, 'imap:///') === 0) {
+ $rcube = rcube::get_instance();
+ $storage = $rcube->get_storage();
+
+ // parse_url does not work with imap:/// prefix
+ $url = parse_url(substr($url, 8));
+ $path = explode('/', $url['path']);
+ parse_str($url['query'], $params);
+
+ $uid = array_pop($path);
+ $ns = array_shift($path);
+ $path = array_map('rawurldecode', $path);
+
+ // resolve folder name
+ if ($ns == 'shared') {
+ $folder = implode('/', $path);
+ // Note: this assumes there's only one shared namespace root
+ if ($ns = $storage->get_namespace('shared')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = $prefix . '/' . $folder;
+ }
+ }
+ }
+ else if ($ns == 'user') {
+ $username = array_shift($path);
+ $folder = implode('/', $path);
+
+ if ($username != $rcube->get_user_name()) {
+ // Note: this assumes there's only one other users namespace root
+ if ($ns = $storage->get_namespace('other')) {
+ if ($prefix = $ns[0][0]) {
+ $folder = $prefix . '/' . $username . '/' . $folder;
+ }
+ }
+ }
+ else if (!strlen($folder)) {
+ $folder = 'INBOX';
+ }
+ }
+ else {
+ return;
+ }
+
+ return array(
+ 'folder' => $folder,
+ 'uid' => $uid,
+ 'params' => $params,
+ );
+ }
+ }
+
+ /**
+ * Build array of member URIs from set of messages
+ *
+ * @param string $folder Folder name
+ * @param array $messages Array of rcube_message objects
+ *
+ * @return array List of members (IMAP URIs)
+ */
+ public static function build_members($folder, $messages)
+ {
+ $members = array();
+
+ foreach ((array) $messages as $msg) {
+ $params = array(
+ 'folder' => $folder,
+ 'uid' => $msg->uid,
+ );
+
+ // add search parameters:
+ // we don't want to build "invalid" searches e.g. that
+ // will return false positives (more or wrong messages)
+ if (($messageid = $msg->get('message-id', false)) && ($date = $msg->get('date', false))) {
+ $params['message-id'] = $messageid;
+ $params['date'] = $date;
+
+ if ($subject = $msg->get('subject', false)) {
+ $params['subject'] = substr($subject, 0, 256);
+ }
+ }
+
+ $members[] = self::build_member_url($params);
+ }
+
+ return $members;
+ }
+
+ /**
+ * Resolve/validate/update members (which are IMAP URIs) of relation object.
+ *
+ * @param array $tag Tag object
+ * @param bool $force Force members list update
+ *
+ * @return array Folder/UIDs list
+ */
+ public static function resolve_members(&$tag, $force = true)
+ {
+ $result = array();
+
+ foreach ((array) $tag['members'] as $member) {
+ // IMAP URI members
+ if ($url = self::parse_member_url($member)) {
+ $folder = $url['folder'];
+
+ if (!$force) {
+ $result[$folder][] = $url['uid'];
+ }
+ else {
+ $result[$folder]['uid'][] = $url['uid'];
+ $result[$folder]['params'][] = $url['params'];
+ $result[$folder]['member'][] = $member;
+ }
+ }
+ }
+
+ if (empty($result) || !$force) {
+ return $result;
+ }
+
+ $rcube = rcube::get_instance();
+ $storage = $rcube->get_storage();
+ $search = array();
+ $missing = array();
+
+ // first we search messages by Folder+UID
+ foreach ($result as $folder => $data) {
+ // @FIXME: maybe better use index() which is cached?
+ // @TODO: consider skip_deleted option
+ $index = $storage->search_once($folder, 'UID ' . rcube_imap_generic::compressMessageSet($data['uid']));
+ $uids = $index->get();
+
+ // messages that were not found need to be searched by search parameters
+ $not_found = array_diff($data['uid'], $uids);
+ if (!empty($not_found)) {
+ foreach ($not_found as $uid) {
+ $idx = array_search($uid, $data['uid']);
+
+ if ($p = $data['params'][$idx]) {
+ $search[] = $p;
+ }
+
+ $missing[] = $result[$folder]['member'][$idx];
+
+ unset($result[$folder]['uid'][$idx]);
+ unset($result[$folder]['params'][$idx]);
+ unset($result[$folder]['member'][$idx]);
+ }
+ }
+
+ $result[$folder] = $uids;
+ }
+
+ // search in all subscribed mail folders using search parameters
+ if (!empty($search)) {
+ // remove not found members from the members list
+ $tag['members'] = array_diff($tag['members'], $missing);
+
+ // get subscribed folders
+ $folders = $storage->list_folders_subscribed('', '*', 'mail', null, true);
+
+ // @TODO: do this search in chunks (for e.g. 10 messages)?
+ $search_str = '';
+
+ foreach ($search as $p) {
+ $search_params = array();
+ foreach ($p as $key => $val) {
+ $key = strtoupper($key);
+ // don't search by subject, we don't want false-positives
+ if ($key != 'SUBJECT') {
+ $search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
+ }
+ }
+
+ $search_str .= ' (' . implode(' ', $search_params) . ')';
+ }
+
+ $search_str = trim(str_repeat(' OR', count($search)-1) . $search_str);
+
+ // search
+ $search = $storage->search_once($folders, $search_str);
+
+ // handle search result
+ $folders = (array) $search->get_parameters('MAILBOX');
+
+ foreach ($folders as $folder) {
+ $set = $search->get_set($folder);
+ $uids = $set->get();
+
+ if (!empty($uids)) {
+ $msgs = $storage->fetch_headers($folder, $uids, false);
+ $members = self::build_members($folder, $msgs);
+
+ // merge new members into the tag members list
+ $tag['members'] = array_merge($tag['members'], $members);
+
+ // add UIDs into the result
+ $result[$folder] = array_unique(array_merge((array)$result[$folder], $uids));
+ }
+ }
+
+ // update tag object with new members list
+ $tag['members'] = array_unique($tag['members']);
+ kolab_storage_config::get_instance()->save($tag, 'relation', false);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Assign tags to kolab objects
+ *
+ * @param array $records List of kolab objects
+ *
+ * @return array List of tags
+ */
+ public function apply_tags(&$records)
+ {
+ // first convert categories into tags
+ foreach ($records as $i => $rec) {
+ if (!empty($rec['categories'])) {
+ $folder = new kolab_storage_folder($rec['_mailbox']);
+ if ($object = $folder->get_object($rec['uid'])) {
+ $tags = $rec['categories'];
+
+ unset($object['categories']);
+ unset($records[$i]['categories']);
+
+ $this->save_tags($rec['uid'], $tags);
+ $folder->save($object, $rec['_type'], $rec['uid']);
+ }
+ }
+ }
+
+ $tags = array();
+
+ // assign tags to objects
+ foreach ($this->get_tags() as $tag) {
+ foreach ($records as $idx => $rec) {
+ $uid = self::build_member_url($rec['uid']);
+ if (in_array($uid, (array) $tag['members'])) {
+ $records[$idx]['tags'][] = $tag['name'];
+ }
+ }
+
+ $tags[] = $tag['name'];
+ }
+
+ $tags = array_unique($tags);
+
+ return $tags;
+ }
+
+ /**
+ * Update object tags
+ *
+ * @param string $uid Kolab object UID
+ * @param array $tags List of tag names
+ */
+ public function save_tags($uid, $tags)
+ {
+ $url = self::build_member_url($uid);
+ $relations = $this->get_tags();
+
+ foreach ($relations as $idx => $relation) {
+ $selected = !empty($tags) && in_array($relation['name'], $tags);
+ $found = !empty($relation['members']) && in_array($url, $relation['members']);
+ $update = false;
+
+ // remove member from the relation
+ if ($found && !$selected) {
+ $relation['members'] = array_diff($relation['members'], (array) $url);
+ $update = true;
+ }
+ // add member to the relation
+ else if (!$found && $selected) {
+ $relation['members'][] = $url;
+ $update = true;
+ }
+
+ if ($update) {
+ if ($this->save($relation, 'relation')) {
+ $this->tags[$idx] = $relation; // update in-memory cache
+ }
+ }
+
+ if ($selected) {
+ $tags = array_diff($tags, (array)$relation['name']);
+ }
+ }
+
+ // create new relations
+ if (!empty($tags)) {
+ foreach ($tags as $tag) {
+ $relation = array(
+ 'name' => $tag,
+ 'members' => (array) $url,
+ 'category' => 'tag',
+ );
+
+ if ($this->save($relation, 'relation')) {
+ $this->tags[] = $relation; // update in-memory cache
+ }
+ }
+ }
+ }
+
+ /**
+ * Get tags (all or referring to specified object)
+ *
+ * @param string $uid Optional object UID
+ *
+ * @return array List of Relation objects
+ */
+ public function get_tags($uid = '*')
+ {
+ if (!isset($this->tags)) {
+ $default = true;
+ $filter = array(
+ array('type', '=', 'relation'),
+ array('category', '=', 'tag')
+ );
+
+ // use faster method
+ if ($uid && $uid != '*') {
+ $filter[] = array('member', '=', $uid);
+ $tags = $this->get_objects($filter, $default);
+ }
+ else {
+ $this->tags = $tags = $this->get_objects($filter, $default);
+ }
+ }
+ else {
+ $tags = $this->tags;
+ }
+
+ if ($uid === '*') {
+ return $tags;
+ }
+
+ $result = array();
+ $search = self::build_member_url($uid);
+
+ foreach ($tags as $tag) {
+ if (in_array($search, (array) $tag['members'])) {
+ $result[] = $tag;
+ }
+ }
+
+ return $result;
+ }
+
+ /**
+ * Find kolab objects assigned to specified e-mail message
+ *
+ * @param rcube_message $message E-mail message
+ * @param string $folder Folder name
+ * @param string $type Result objects type
+ *
+ * @return array List of kolab objects
+ */
+ public function get_message_relations($message, $folder, $type)
+ {
+ $result = array();
+ $uids = array();
+ $default = true;
+ $uri = self::get_message_uri($message, $folder);
+ $filter = array(
+ array('type', '=', 'relation'),
+ array('category', '=', 'generic'),
+ // @TODO: what if Message-Id (and Date) does not exist?
+ array('member', '=', $message->get('message-id', false)),
+ );
+
+ // get UIDs of assigned notes
+ foreach ($this->get_objects($filter, $default) as $relation) {
+ // we don't need to update members if the URI is found
+ if (in_array($uri, $relation['members'])) {
+ // update members...
+ $messages = kolab_storage_config::resolve_members($relation);
+ // ...and check again
+ if (empty($messages[$folder]) || !in_array($message->uid, $messages[$folder])) {
+ continue;
+ }
+ }
+
+ // find note UID(s)
+ foreach ($relation['members'] as $member) {
+ if (strpos($member, 'urn:uuid:') === 0) {
+ $uids[] = substr($member, 9);
+ }
+ }
+ }
+
+ // get kolab objects of specified type
+ if (!empty($uids)) {
+ $query = array(array('uid', '=', array_unique($uids)));
+ $result = kolab_storage::select($query, $type);
+ }
+
+ return $result;
+ }
+
+ /**
+ * Build a URI representing the given message reference
+ */
+ public static function get_message_uri($headers, $folder)
+ {
+ $params = array(
+ 'folder' => $headers->folder ?: $folder,
+ 'uid' => $headers->uid,
+ );
+
+ if (($messageid = $headers->get('message-id', false)) && ($date = $headers->get('date', false))) {
+ $params['message-id'] = $messageid;
+ $params['date'] = $date;
+
+ if ($subject = $headers->get('subject')) {
+ $params['subject'] = $subject;
+ }
+ }
+
+ return self::build_member_url($params);
+ }
+}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_dataset.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_dataset.php
new file mode 100644
index 0000000..9ddf3f9
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_dataset.php
@@ -0,0 +1,154 @@
+<?php
+
+/**
+ * Dataset class providing the results of a select operation on a kolab_storage_folder.
+ *
+ * Can be used as a normal array as well as an iterator in foreach() loops.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, 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/>.
+ */
+
+class kolab_storage_dataset implements Iterator, ArrayAccess, Countable
+{
+ private $cache; // kolab_storage_cache instance to use for fetching data
+ private $memlimit = 0;
+ private $buffer = false;
+ private $index = array();
+ private $data = array();
+ private $iteratorkey = 0;
+ private $error = null;
+
+ /**
+ * Default constructor
+ *
+ * @param object kolab_storage_cache instance to be used for fetching objects upon access
+ */
+ public function __construct($cache)
+ {
+ $this->cache = $cache;
+
+ // enable in-memory buffering up until 1/5 of the available memory
+ if (function_exists('memory_get_usage')) {
+ $this->memlimit = parse_bytes(ini_get('memory_limit')) / 5;
+ $this->buffer = true;
+ }
+ }
+
+ /**
+ * Return error state
+ */
+ public function is_error()
+ {
+ return !empty($this->error);
+ }
+
+ /**
+ * Set error state
+ */
+ public function set_error($err)
+ {
+ $this->error = $err;
+ }
+
+
+ /*** Implement PHP Countable interface ***/
+
+ public function count()
+ {
+ return count($this->index);
+ }
+
+
+ /*** Implement PHP ArrayAccess interface ***/
+
+ public function offsetSet($offset, $value)
+ {
+ $uid = $value['_msguid'];
+
+ if (is_null($offset)) {
+ $offset = count($this->index);
+ $this->index[] = $uid;
+ }
+ else {
+ $this->index[$offset] = $uid;
+ }
+
+ // keep full payload data in memory if possible
+ if ($this->memlimit && $this->buffer && isset($value['_mailbox'])) {
+ $this->data[$offset] = $value;
+
+ // check memory usage and stop buffering
+ if ($offset % 10 == 0) {
+ $this->buffer = memory_get_usage() < $this->memlimit;
+ }
+ }
+ }
+
+ public function offsetExists($offset)
+ {
+ return isset($this->index[$offset]);
+ }
+
+ public function offsetUnset($offset)
+ {
+ unset($this->index[$offset]);
+ }
+
+ public function offsetGet($offset)
+ {
+ if (isset($this->data[$offset])) {
+ return $this->data[$offset];
+ }
+ else if ($msguid = $this->index[$offset]) {
+ return $this->cache->get($msguid);
+ }
+
+ return null;
+ }
+
+
+ /*** Implement PHP Iterator interface ***/
+
+ public function current()
+ {
+ return $this->offsetGet($this->iteratorkey);
+ }
+
+ public function key()
+ {
+ return $this->iteratorkey;
+ }
+
+ public function next()
+ {
+ $this->iteratorkey++;
+ return $this->valid();
+ }
+
+ public function rewind()
+ {
+ $this->iteratorkey = 0;
+ }
+
+ public function valid()
+ {
+ return !empty($this->index[$this->iteratorkey]);
+ }
+
+}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php
index aabc130..2b25826 100644
--- a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php
@@ -1,1191 +1,1074 @@
<?php
/**
* The kolab_storage_folder class represents an IMAP folder on the Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012-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/>.
*/
-class kolab_storage_folder
+class kolab_storage_folder extends kolab_storage_folder_api
{
- /**
- * The folder name.
- * @var string
- */
- public $name;
-
- /**
- * The type of this folder.
- * @var string
- */
- public $type;
-
- /**
- * Is this folder set to be the default for its type
- * @var boolean
- */
- public $default = false;
-
/**
* The kolab_storage_cache instance for caching operations
* @var object
*/
public $cache;
private $type_annotation;
- private $namespace;
- private $imap;
- private $info;
- private $idata;
- private $owner;
private $resource_uri;
/**
* Default constructor
*/
function __construct($name, $type = null)
{
- $this->imap = rcube::get_instance()->get_storage();
+ parent::__construct($name);
$this->imap->set_options(array('skip_deleted' => true));
$this->set_folder($name, $type);
}
/**
* Set the IMAP folder this instance connects to
*
* @param string The folder name/path
* @param string Optional folder type if known
*/
public function set_folder($name, $type = null)
{
$this->type_annotation = $type ? $type : kolab_storage::folder_type($name);
$oldtype = $this->type;
list($this->type, $suffix) = explode('.', $this->type_annotation);
$this->default = $suffix == 'default';
+ $this->subtype = $this->default ? '' : $suffix;
$this->name = $name;
- $this->resource_uri = null;
+ $this->id = kolab_storage::folder_id($name);
+
+ // reset cached object properties
+ $this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null;
// get a new cache instance of folder type changed
if (!$this->cache || $type != $oldtype)
$this->cache = kolab_storage_cache::factory($this);
$this->imap->set_folder($this->name);
$this->cache->set_folder($this);
}
- /**
- *
- */
- public function get_folder_info()
- {
- if (!isset($this->info))
- $this->info = $this->imap->folder_info($this->name);
-
- return $this->info;
- }
-
- /**
- * Make IMAP folder data available for this folder
- */
- public function get_imap_data()
- {
- if (!isset($this->idata))
- $this->idata = $this->imap->folder_data($this->name);
-
- return $this->idata;
- }
-
- /**
- * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
- *
- * @param array List of metadata keys to read
- * @return array Metadata entry-value hash array on success, NULL on error
- */
- public function get_metadata($keys)
- {
- $metadata = $this->imap->get_metadata($this->name, (array)$keys);
- return $metadata[$this->name];
- }
-
-
- /**
- * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
- *
- * @param array $entries Entry-value array (use NULL value as NIL)
- * @return boolean True on success, False on failure
- */
- public function set_metadata($entries)
- {
- return $this->imap->set_metadata($this->name, $entries);
- }
-
-
- /**
- * Returns the owner of the folder.
- *
- * @return string The owner of this folder.
- */
- public function get_owner()
- {
- // return cached value
- if (isset($this->owner))
- return $this->owner;
-
- $info = $this->get_folder_info();
- $rcmail = rcube::get_instance();
-
- switch ($info['namespace']) {
- case 'personal':
- $this->owner = $rcmail->get_user_name();
- break;
-
- case 'shared':
- $this->owner = 'anonymous';
- break;
-
- default:
- list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
- if (strpos($user, '@') === false) {
- $domain = strstr($rcmail->get_user_name(), '@');
- if (!empty($domain))
- $user .= $domain;
- }
- $this->owner = $user;
- break;
- }
-
- return $this->owner;
- }
-
-
- /**
- * Getter for the name of the namespace to which the IMAP folder belongs
- *
- * @return string Name of the namespace (personal, other, shared)
- */
- public function get_namespace()
- {
- if (!isset($this->namespace))
- $this->namespace = $this->imap->folder_namespace($this->name);
- return $this->namespace;
- }
-
-
- /**
- * Get IMAP ACL information for this folder
- *
- * @return string Permissions as string
- */
- public function get_myrights()
- {
- $rights = $this->info['rights'];
-
- if (!is_array($rights))
- $rights = $this->imap->my_rights($this->name);
-
- return join('', (array)$rights);
- }
-
-
- /**
- * Get the display name value of this folder
- *
- * @return string Folder name
- */
- public function get_name()
- {
- return kolab_storage::object_name($this->name, $this->namespace);
- }
-
-
- /**
- * Get the color value stored in metadata
- *
- * @param string Default color value to return if not set
- * @return mixed Color value from IMAP metadata or $default is not set
- */
- public function get_color($default = null)
- {
- // color is defined in folder METADATA
- $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
- if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
- return $color;
- }
-
- return $default;
- }
-
-
/**
* Compose a unique resource URI for this IMAP folder
*/
public function get_resource_uri()
{
if (!empty($this->resource_uri))
return $this->resource_uri;
// strip namespace prefix from folder name
$ns = $this->get_namespace();
$nsdata = $this->imap->get_namespace($ns);
if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) {
$subpath = substr($this->name, strlen($nsdata[0][0]));
if ($ns == 'other') {
list($user, $suffix) = explode($nsdata[0][1], $subpath, 2);
$subpath = $suffix;
}
}
else {
$subpath = $this->name;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = 'imap://' . urlencode($this->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
return $this->resource_uri;
}
/**
* Helper method to extract folder UID metadata
*
* @return string Folder's UID
*/
public function get_uid()
{
// UID is defined in folder METADATA
$metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_CYRUS);
$metadata = $this->get_metadata($metakeys);
foreach ($metakeys as $key) {
if (($uid = $metadata[$key])) {
return $uid;
}
}
// generate a folder UID and set it to IMAP
- $uid = rtrim(chunk_split(md5($this->name . $this->get_owner()), 12, '-'), '-');
- $this->set_uid($uid);
+ $uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-');
+ if ($this->set_uid($uid)) {
+ return $uid;
+ }
- return $uid;
+ // create hash from folder name if we can't write the UID metadata
+ return md5($this->name . $this->get_owner());
}
/**
* Helper method to set an UID value to the given IMAP folder instance
*
* @param string Folder's UID
* @return boolean True on succes, False on failure
*/
public function set_uid($uid)
{
if (!($success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid)))) {
$success = $this->set_metadata(array(kolab_storage::UID_KEY_PRIVATE => $uid));
}
return $success;
}
/**
* Compose a folder Etag identifier
*/
public function get_ctag()
{
$fdata = $this->get_imap_data();
return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']);
}
/**
* Check activation status of this folder
*
* @return boolean True if enabled, false if not
*/
public function is_active()
{
return kolab_storage::folder_is_active($this->name);
}
/**
* Change activation status of this folder
*
* @param boolean The desired subscription status: true = active, false = not active
*
* @return True on success, false on error
*/
public function activate($active)
{
return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
}
/**
* Check subscription status of this folder
*
* @return boolean True if subscribed, false if not
*/
public function is_subscribed()
{
return kolab_storage::folder_is_subscribed($this->name);
}
/**
* Change subscription status of this folder
*
* @param boolean The desired subscription status: true = subscribed, false = not subscribed
*
* @return True on success, false on error
*/
public function subscribe($subscribed)
{
return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
}
/**
* Get number of objects stored in this folder
*
* @param mixed Pseudo-SQL query as list of filter parameter triplets
* or string with object type (e.g. contact, event, todo, journal, note, configuration)
* @return integer The number of objects of the given type
* @see self::select()
*/
public function count($query = null)
{
// synchronize cache first
$this->cache->synchronize();
return $this->cache->count($this->_prepare_query($query));
}
/**
* List all Kolab objects of the given type
*
* @param string $type Object type (e.g. contact, event, todo, journal, note, configuration)
* @return array List of Kolab data objects (each represented as hash array)
*/
public function get_objects($type = null)
{
if (!$type) $type = $this->type;
// synchronize caches
$this->cache->synchronize();
// fetch objects from cache
return $this->cache->select($this->_prepare_query($type));
}
/**
* Select *some* Kolab objects matching the given query
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* triplet: array('<colname>', '<comparator>', '<value>')
* @return array List of Kolab data objects (each represented as hash array)
*/
public function select($query = array())
{
// check query argument
if (empty($query))
return $this->get_objects();
// synchronize caches
$this->cache->synchronize();
// fetch objects from cache
return $this->cache->select($this->_prepare_query($query));
}
/**
* Getter for object UIDs only
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @return array List of Kolab object UIDs
*/
public function get_uids($query = array())
{
// synchronize caches
$this->cache->synchronize();
// fetch UIDs from cache
return $this->cache->select($this->_prepare_query($query), true);
}
+ /**
+ * Setter for ORDER BY and LIMIT parameters for cache queries
+ *
+ * @param array List of columns to order by
+ * @param integer Limit result set to this length
+ * @param integer Offset row
+ */
+ public function set_order_and_limit($sortcols, $length = null, $offset = 0)
+ {
+ $this->cache->set_order_by($sortcols);
+
+ if ($length !== null) {
+ $this->cache->set_limit($length, $offset);
+ }
+ }
/**
* Helper method to sanitize query arguments
*/
private function _prepare_query($query)
{
// string equals type query
// FIXME: should not be called this way!
if (is_string($query)) {
return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array();
}
foreach ((array)$query as $i => $param) {
if ($param[0] == 'type' && !$this->cache->has_type_col()) {
unset($query[$i]);
}
else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) {
if (is_object($param[2]) && is_a($param[2], 'DateTime'))
$param[2] = $param[2]->format('U');
if (is_numeric($param[2]))
$query[$i][2] = date('Y-m-d H:i:s', $param[2]);
}
}
return $query;
}
/**
* Getter for a single Kolab object, identified by its UID
*
* @param string $uid Object UID
* @param string $type Object type (e.g. contact, event, todo, journal, note, configuration)
* Defaults to folder type
*
* @return array The Kolab object represented as hash array
*/
public function get_object($uid, $type = null)
{
// synchronize caches
$this->cache->synchronize();
$msguid = $this->cache->uid2msguid($uid);
if ($msguid && ($object = $this->cache->get($msguid, $type))) {
return $object;
}
return false;
}
/**
* Fetch a Kolab object attachment which is stored in a separate part
* of the mail MIME message that represents the Kolab record.
*
* @param string Object's UID
* @param string The attachment's mime number
* @param string IMAP folder where message is stored;
* If set, that also implies that the given UID is an IMAP UID
* @param bool True to print the part content
* @param resource File pointer to save the message part
* @param boolean Disables charset conversion
*
* @return mixed The attachment content as binary string
*/
public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false)
{
if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
$this->imap->set_folder($mailbox ? $mailbox : $this->name);
- return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
+
+ if (substr($part, 0, 2) == 'i:') {
+ // attachment data is stored in XML
+ if ($object = $this->cache->get($msguid)) {
+ // load data from XML (attachment content is not stored in cache)
+ if ($object['_formatobj'] && isset($object['_size'])) {
+ $object['_attachments'] = array();
+ $object['_formatobj']->get_attachments($object);
+ }
+
+ foreach ($object['_attachments'] as $k => $attach) {
+ if ($attach['id'] == $part) {
+ if ($print) echo $attach['content'];
+ else if ($fp) fwrite($fp, $attach['content']);
+ else return $attach['content'];
+ return true;
+ }
+ }
+ }
+ }
+ else {
+ // return message part from IMAP directly
+ return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
+ }
}
return null;
}
/**
* Fetch the mime message from the storage server and extract
* the Kolab groupware object from it
*
* @param string The IMAP message UID to fetch
* @param string The object type expected (use wildcard '*' to accept all types)
* @param string The folder name where the message is stored
*
* @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
*/
public function read_object($msguid, $type = null, $folder = null)
{
if (!$type) $type = $this->type;
if (!$folder) $folder = $this->name;
$this->imap->set_folder($folder);
$this->cache->bypass(true);
$message = new rcube_message($msguid);
$this->cache->bypass(false);
// Message doesn't exist?
if (empty($message->headers)) {
return false;
}
// extract the X-Kolab-Type header from the XML attachment part if missing
if (empty($message->headers->others['x-kolab-type'])) {
foreach ((array)$message->attachments as $part) {
if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
$message->headers->others['x-kolab-type'] = $part->mimetype;
break;
}
}
}
// fix buggy messages stating the X-Kolab-Type header twice
else if (is_array($message->headers->others['x-kolab-type'])) {
$message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']);
}
// no object type header found: abort
if (empty($message->headers->others['x-kolab-type'])) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "No X-Kolab-Type information found in message $msguid ($this->name).",
), true);
return false;
}
$object_type = kolab_format::mime2object_type($message->headers->others['x-kolab-type']);
$content_type = kolab_format::KTYPE_PREFIX . $object_type;
// check object type header and abort on mismatch
if ($type != '*' && $object_type != $type)
return false;
$attachments = array();
// get XML part
foreach ((array)$message->attachments as $part) {
- if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
+ if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!', $part->mimetype))) {
$xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
}
else if ($part->filename || $part->content_id) {
$key = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
$size = null;
// Use Content-Disposition 'size' as for the Kolab Format spec.
if (isset($part->d_parameters['size'])) {
$size = $part->d_parameters['size'];
}
// we can trust part size only if it's not encoded
else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') {
$size = $part->size;
}
$attachments[$key] = array(
'id' => $part->mime_id,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'size' => $size,
);
}
}
if (!$xml) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Could not find Kolab data part in message $msguid ($this->name).",
), true);
return false;
}
// check kolab format version
$format_version = $message->headers->others['x-kolab-mime-version'];
if (empty($format_version)) {
list($xmltype, $subtype) = explode('.', $object_type);
$xmlhead = substr($xml, 0, 512);
// detect old Kolab 2.0 format
if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false)
$format_version = '2.0';
else
$format_version = '3.0'; // assume 3.0
}
// get Kolab format handler for the given type
$format = kolab_format::factory($object_type, $format_version);
if (is_a($format, 'PEAR_Error'))
return false;
// load Kolab object from XML part
$format->load($xml);
if ($format->is_valid()) {
$object = $format->to_array(array('_attachments' => $attachments));
$object['_type'] = $object_type;
$object['_msguid'] = $msguid;
$object['_mailbox'] = $this->name;
$object['_formatobj'] = $format;
return $object;
}
else {
// try to extract object UID from XML block
if (preg_match('!<uid>(.+)</uid>!Uims', $xml, $m))
$msgadd = " UID = " . trim(strip_tags($m[1]));
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd,
), true);
}
return false;
}
/**
* Save an object in this folder.
*
* @param array $object The array that holds the data of the object.
* @param string $type The type of the kolab object.
* @param string $uid The UID of the old object if it existed before
* @return boolean True on success, false on error
*/
public function save(&$object, $type = null, $uid = null)
{
if (!$type)
$type = $this->type;
// copy attachments from old message
if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) {
foreach ((array)$old['_attachments'] as $key => $att) {
if (!isset($object['_attachments'][$key])) {
$object['_attachments'][$key] = $old['_attachments'][$key];
}
// unset deleted attachment entries
if ($object['_attachments'][$key] == false) {
unset($object['_attachments'][$key]);
}
// load photo.attachment from old Kolab2 format to be directly embedded in xcard block
else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
if (!isset($object['photo']))
$object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']);
unset($object['_attachments'][$key]);
}
}
}
// save contact photo to attachment for Kolab2 format
if (kolab_storage::$version == '2.0' && $object['photo']) {
$attkey = 'kolab-picture.png'; // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp
$object['_attachments'][$attkey] = array(
'mimetype'=> rcube_mime::image_content_type($object['photo']),
'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']),
);
}
// process attachments
if (is_array($object['_attachments'])) {
$numatt = count($object['_attachments']);
foreach ($object['_attachments'] as $key => $attachment) {
// FIXME: kolab_storage and Roundcube attachment hooks use different fields!
if (empty($attachment['content']) && !empty($attachment['data'])) {
$attachment['content'] = $attachment['data'];
unset($attachment['data'], $object['_attachments'][$key]['data']);
}
// make sure size is set, so object saved in cache contains this info
if (!isset($attachment['size'])) {
if (!empty($attachment['content'])) {
if (is_resource($attachment['content'])) {
// this need to be a seekable resource, otherwise
// fstat() failes and we're unable to determine size
// here nor in rcube_imap_generic before IMAP APPEND
$stat = fstat($attachment['content']);
$attachment['size'] = $stat ? $stat['size'] : 0;
}
else {
$attachment['size'] = strlen($attachment['content']);
}
}
else if (!empty($attachment['path'])) {
$attachment['size'] = filesize($attachment['path']);
}
$object['_attachments'][$key] = $attachment;
}
// generate unique keys (used as content-id) for attachments
if (is_numeric($key) && $key < $numatt) {
// derrive content-id from attachment file name
$ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null;
$basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext)); // to 7bit ascii
if (!$basename) $basename = 'noname';
$cid = $basename . '.' . microtime(true) . $ext;
$object['_attachments'][$cid] = $attachment;
unset($object['_attachments'][$key]);
}
}
}
// save recurrence exceptions as individual objects due to lack of support in Kolab v2 format
if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) {
$this->save_recurrence_exceptions($object, $type);
}
// check IMAP BINARY extension support for 'file' objects
// allow configuration to workaround bug in Cyrus < 2.4.17
$rcmail = rcube::get_instance();
$binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY');
// generate and save object message
if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) {
// resolve old msguid before saving
if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) {
$object['_msguid'] = $msguid;
$object['_mailbox'] = $this->name;
}
$result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);
- // delete old message
- if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
- $this->cache->bypass(true);
- $this->imap->delete_message($object['_msguid'], $object['_mailbox']);
- $this->cache->bypass(false);
- $this->cache->set($object['_msguid'], false, $object['_mailbox']);
- }
-
// update cache with new UID
if ($result) {
+ $old_uid = $object['_msguid'];
+
$object['_msguid'] = $result;
- $this->cache->insert($result, $object);
+ $object['_mailbox'] = $this->name;
- // remove temp file
- if ($body_file) {
- @unlink($body_file);
+ if ($old_uid) {
+ // delete old message
+ $this->cache->bypass(true);
+ $this->imap->delete_message($old_uid, $object['_mailbox']);
+ $this->cache->bypass(false);
}
+
+ // insert/update message in cache
+ $this->cache->save($result, $object, $old_uid);
+ }
+
+ // remove temp file
+ if ($body_file) {
+ @unlink($body_file);
}
}
return $result;
}
/**
* Save recurrence exceptions as individual objects.
* The Kolab v2 format doesn't allow us to save fully embedded exception objects.
*
* @param array Hash array with event properties
* @param string Object type
*/
private function save_recurrence_exceptions(&$object, $type = null)
{
if ($object['recurrence']['EXCEPTIONS']) {
$exdates = array();
foreach ((array)$object['recurrence']['EXDATE'] as $exdate) {
$key = is_a($exdate, 'DateTime') ? $exdate->format('Y-m-d') : strval($exdate);
$exdates[$key] = 1;
}
// save every exception as individual object
foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
$exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd'));
$exception['sequence'] = $object['sequence'] + 1;
if ($exception['thisandfuture']) {
$exception['recurrence'] = $object['recurrence'];
// adjust the recurrence duration of the exception
if ($object['recurrence']['COUNT']) {
$recurrence = new kolab_date_recurrence($object['_formatobj']);
if ($end = $recurrence->end()) {
unset($exception['recurrence']['COUNT']);
- $exception['recurrence']['UNTIL'] = new DateTime('@'.$end);
+ $exception['recurrence']['UNTIL'] = $end;
}
}
// set UNTIL date if we have a thisandfuture exception
$untildate = clone $exception['start'];
$untildate->sub(new DateInterval('P1D'));
$object['recurrence']['UNTIL'] = $untildate;
unset($object['recurrence']['COUNT']);
}
else {
if (!$exdates[$exception['start']->format('Y-m-d')])
$object['recurrence']['EXDATE'][] = clone $exception['start'];
unset($exception['recurrence']);
}
unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']);
$this->save($exception, $type, $exception['uid']);
}
unset($object['recurrence']['EXCEPTIONS']);
}
}
/**
* Generate an object UID with the given recurrence-ID in a way that it is
* unique (the original UID is not a substring) but still recoverable.
*/
private static function recurrence_exception_uid($uid, $recurrence_id)
{
$offset = -2;
return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset);
}
/**
* Delete the specified object from this folder.
*
* @param mixed $object The Kolab object to delete or object UID
* @param boolean $expunge Should the folder be expunged?
*
* @return boolean True if successful, false on error
*/
public function delete($object, $expunge = true)
{
$msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
$success = false;
$this->cache->bypass(true);
if ($msguid && $expunge) {
$success = $this->imap->delete_message($msguid, $this->name);
}
else if ($msguid) {
$success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
}
$this->cache->bypass(false);
if ($success) {
$this->cache->set($msguid, false);
}
return $success;
}
/**
*
*/
public function delete_all()
{
$this->cache->purge();
$this->cache->bypass(true);
$result = $this->imap->clear_folder($this->name);
$this->cache->bypass(false);
return $result;
}
/**
* Restore a previously deleted object
*
* @param string Object UID
* @return mixed Message UID on success, false on error
*/
public function undelete($uid)
{
if ($msguid = $this->cache->uid2msguid($uid, true)) {
$this->cache->bypass(true);
$result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
$this->cache->bypass(false);
if ($result) {
return $msguid;
}
}
return false;
}
/**
* Move a Kolab object message to another IMAP folder
*
* @param string Object UID
* @param string IMAP folder to move object to
* @return boolean True on success, false on failure
*/
public function move($uid, $target_folder)
{
if ($msguid = $this->cache->uid2msguid($uid)) {
$this->cache->bypass(true);
$result = $this->imap->move_message($msguid, $target_folder, $this->name);
$this->cache->bypass(false);
if ($result) {
$this->cache->move($msguid, $uid, $target_folder);
return true;
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(),
), true);
}
}
return false;
}
/**
* Creates source of the configuration object message
*
* @param array $object The array that holds the data of the object.
* @param string $type The type of the kolab object.
* @param bool $binary Enables use of binary encoding of attachment(s)
* @param string $body_file Reference to filename of message body
*
* @return mixed Message as string or array with two elements
* (one for message file path, second for message headers)
*/
private function build_message(&$object, $type, $binary, &$body_file)
{
// load old object to preserve data we don't understand/process
if (is_object($object['_formatobj']))
$format = $object['_formatobj'];
else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox'])))
$format = $old['_formatobj'];
// create new kolab_format instance
if (!$format)
$format = kolab_format::factory($type, kolab_storage::$version);
if (PEAR::isError($format))
return false;
$format->set($object);
$xml = $format->write(kolab_storage::$version);
$object['uid'] = $format->uid; // read UID from format
$object['_formatobj'] = $format;
if (empty($xml) || !$format->is_valid() || empty($object['uid'])) {
return false;
}
$mime = new Mail_mime("\r\n");
$rcmail = rcube::get_instance();
$headers = array();
$files = array();
$part_id = 1;
$encoding = $binary ? 'binary' : 'base64';
if ($user_email = $rcmail->get_user_email()) {
$headers['From'] = $user_email;
$headers['To'] = $user_email;
}
$headers['Date'] = date('r');
$headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type;
$headers['X-Kolab-Mime-Version'] = kolab_storage::$version;
$headers['Subject'] = $object['uid'];
// $headers['Message-ID'] = $rcmail->gen_message_id();
$headers['User-Agent'] = $rcmail->config->get('useragent');
// Check if we have enough memory to handle the message in it
// It's faster than using files, so we'll do this if we only can
if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) {
$memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
foreach ($object['_attachments'] as $id => $attachment) {
$memory += $attachment['size'];
}
// 1.33 is for base64, we need at least 4x more memory than the message size
if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) {
$marker = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%';
$is_file = true;
$temp_dir = unslashify($rcmail->config->get('temp_dir'));
$mime->setParam('delay_file_io', true);
}
}
$mime->headers($headers);
$mime->setTXTBody("This is a Kolab Groupware object. "
. "To view this object you will need an email client that understands the Kolab Groupware format. "
. "For a list of such email clients please visit http://www.kolab.org/\n\n");
$ctype = kolab_storage::$version == '2.0' ? $format->CTYPEv2 : $format->CTYPE;
// Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines"
// when APPENDing from temp file
$xml = preg_replace('/\r?\n/', "\r\n", $xml);
$mime->addAttachment($xml, // file
$ctype, // content-type
'kolab.xml', // filename
false, // is_file
'8bit', // encoding
'attachment', // disposition
RCUBE_CHARSET // charset
);
$part_id++;
// save object attachments as separate parts
foreach ((array)$object['_attachments'] as $key => $att) {
if (empty($att['content']) && !empty($att['id'])) {
// @TODO: use IMAP CATENATE to skip attachment fetch+push operation
$msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
if ($is_file) {
$att['path'] = tempnam($temp_dir, 'rcmAttmnt');
if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) {
fclose($fp);
}
else {
return false;
}
}
else {
$att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, null, true);
}
}
$headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable'));
$name = !empty($att['name']) ? $att['name'] : $key;
// To store binary files we can use faster method
// without writting full message content to a temporary file but
// directly to IMAP, see rcube_imap_generic::append().
// I.e. use file handles where possible
if (!empty($att['path'])) {
if ($is_file && $binary) {
$files[] = fopen($att['path'], 'r');
$mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
else {
$mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
}
else {
if (is_resource($att['content']) && $is_file && $binary) {
$files[] = $att['content'];
$mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
else {
if (is_resource($att['content'])) {
@rewind($att['content']);
$att['content'] = stream_get_contents($att['content']);
}
$mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
}
$object['_attachments'][$key]['id'] = ++$part_id;
}
if (!$is_file || !empty($files)) {
$message = $mime->getMessage();
}
// parse message and build message array with
// attachment file pointers in place of file markers
if (!empty($files)) {
$message = explode($marker, $message);
$tmp = array();
foreach ($message as $msg_part) {
$tmp[] = $msg_part;
if ($file = array_shift($files)) {
$tmp[] = $file;
}
}
$message = $tmp;
}
// write complete message body into temp file
else if ($is_file) {
// use common temp dir
$body_file = tempnam($temp_dir, 'rcmMsg');
if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) {
self::raise_error(array('code' => 650, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$mime_result->getMessage()),
true, false);
return false;
}
$message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r'));
}
return $message;
}
/**
* Triggers any required updates after changes within the
* folder. This is currently only required for handling free/busy
* information with Kolab.
*
* @return boolean|PEAR_Error True if successfull.
*/
public function trigger()
{
$owner = $this->get_owner();
$result = false;
switch($this->type) {
case 'event':
if ($this->get_namespace() == 'personal') {
$result = $this->trigger_url(
sprintf('%s/trigger/%s/%s.pfb',
kolab_storage::get_freebusy_server(),
urlencode($owner),
urlencode($this->imap->mod_folder($this->name))
),
$this->imap->options['user'],
$this->imap->options['password']
);
}
break;
default:
return true;
}
if ($result && is_object($result) && is_a($result, 'PEAR_Error')) {
return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s",
$this->name, $result->getMessage()));
}
return $result;
}
/**
* Triggers a URL.
*
* @param string $url The URL to be triggered.
* @param string $auth_user Username to authenticate with
* @param string $auth_passwd Password for basic auth
* @return boolean|PEAR_Error True if successfull.
*/
private function trigger_url($url, $auth_user = null, $auth_passwd = null)
{
require_once('HTTP/Request2.php');
try {
$request = libkolab::http_request($url);
// set authentication credentials
if ($auth_user && $auth_passwd)
$request->setAuth($auth_user, $auth_passwd);
$result = $request->send();
// rcube::write_log('trigger', $result->getBody());
}
catch (Exception $e) {
return PEAR::raiseError($e->getMessage());
}
return true;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php
new file mode 100644
index 0000000..ea603b1
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -0,0 +1,345 @@
+<?php
+
+/**
+ * Abstract interface class for Kolab storage IMAP folder objects
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, 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/>.
+ */
+abstract class kolab_storage_folder_api
+{
+ /**
+ * Folder identifier
+ * @var string
+ */
+ public $id;
+
+ /**
+ * The folder name.
+ * @var string
+ */
+ public $name;
+
+ /**
+ * The type of this folder.
+ * @var string
+ */
+ public $type;
+
+ /**
+ * The subtype of this folder.
+ * @var string
+ */
+ public $subtype;
+
+ /**
+ * Is this folder set to be the default for its type
+ * @var boolean
+ */
+ public $default = false;
+
+ /**
+ * List of direct child folders
+ * @var array
+ */
+ public $children = array();
+
+ /**
+ * Name of the parent folder
+ * @var string
+ */
+ public $parent = '';
+
+ protected $imap;
+ protected $owner;
+ protected $info;
+ protected $idata;
+ protected $namespace;
+
+
+ /**
+ * Private constructor
+ */
+ protected function __construct($name)
+ {
+ $this->name = $name;
+ $this->id = kolab_storage::folder_id($name);
+ $this->imap = rcube::get_instance()->get_storage();
+ }
+
+
+ /**
+ * Returns the owner of the folder.
+ *
+ * @return string The owner of this folder.
+ */
+ public function get_owner()
+ {
+ // return cached value
+ if (isset($this->owner))
+ return $this->owner;
+
+ $info = $this->get_folder_info();
+ $rcmail = rcube::get_instance();
+
+ switch ($info['namespace']) {
+ case 'personal':
+ $this->owner = $rcmail->get_user_name();
+ break;
+
+ case 'shared':
+ $this->owner = 'anonymous';
+ break;
+
+ default:
+ list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+ if (strpos($user, '@') === false) {
+ $domain = strstr($rcmail->get_user_name(), '@');
+ if (!empty($domain))
+ $user .= $domain;
+ }
+ $this->owner = $user;
+ break;
+ }
+
+ return $this->owner;
+ }
+
+
+ /**
+ * Getter for the name of the namespace to which the IMAP folder belongs
+ *
+ * @return string Name of the namespace (personal, other, shared)
+ */
+ public function get_namespace()
+ {
+ if (!isset($this->namespace))
+ $this->namespace = $this->imap->folder_namespace($this->name);
+ return $this->namespace;
+ }
+
+
+ /**
+ * Get the display name value of this folder
+ *
+ * @return string Folder name
+ */
+ public function get_name()
+ {
+ return kolab_storage::object_name($this->name, $this->get_namespace());
+ }
+
+
+ /**
+ * Getter for the top-end folder name (not the entire path)
+ *
+ * @return string Name of this folder
+ */
+ public function get_foldername()
+ {
+ $parts = explode('/', $this->name);
+ return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+ }
+
+ /**
+ * Getter for parent folder path
+ *
+ * @return string Full path to parent folder
+ */
+ public function get_parent()
+ {
+ $path = explode('/', $this->name);
+ array_pop($path);
+
+ // don't list top-level namespace folder
+ if (count($path) == 1 && in_array($this->get_namespace(), array('other', 'shared'))) {
+ $path = array();
+ }
+
+ return join('/', $path);
+ }
+
+ /**
+ * Getter for the Cyrus mailbox identifier corresponding to this folder
+ * (e.g. user/john.doe/Calendar/Personal@example.org)
+ *
+ * @return string Mailbox ID
+ */
+ public function get_mailbox_id()
+ {
+ $info = $this->get_folder_info();
+ $owner = $this->get_owner();
+ list($user, $domain) = explode('@', $owner);
+
+ switch ($info['namespace']) {
+ case 'personal':
+ return sprintf('user/%s/%s@%s', $user, $this->name, $domain);
+
+ case 'shared':
+ $ns = $this->imap->get_namespace('shared');
+ $prefix = is_array($ns) ? $ns[0][0] : '';
+ list(, $domain) = explode('@', rcube::get_instance()->get_user_name());
+ return substr($this->name, strlen($prefix)) . '@' . $domain;
+
+ default:
+ $ns = $this->imap->get_namespace('other');
+ $prefix = is_array($ns) ? $ns[0][0] : '';
+ list($user, $folder) = explode($this->imap->get_hierarchy_delimiter(), substr($info['name'], strlen($prefix)), 2);
+ if (strpos($user, '@')) {
+ list($user, $domain) = explode('@', $user);
+ }
+ return sprintf('user/%s/%s@%s', $user, $folder, $domain);
+ }
+ }
+
+ /**
+ * Get the color value stored in metadata
+ *
+ * @param string Default color value to return if not set
+ * @return mixed Color value from IMAP metadata or $default is not set
+ */
+ public function get_color($default = null)
+ {
+ // color is defined in folder METADATA
+ $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
+ if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
+ return $color;
+ }
+
+ return $default;
+ }
+
+
+ /**
+ * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
+ *
+ * @param array List of metadata keys to read
+ * @return array Metadata entry-value hash array on success, NULL on error
+ */
+ public function get_metadata($keys)
+ {
+ $metadata = rcube::get_instance()->get_storage()->get_metadata($this->name, (array)$keys);
+ return $metadata[$this->name];
+ }
+
+
+ /**
+ * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
+ *
+ * @param array $entries Entry-value array (use NULL value as NIL)
+ * @return boolean True on success, False on failure
+ */
+ public function set_metadata($entries)
+ {
+ return $this->imap->set_metadata($this->name, $entries);
+ }
+
+
+ /**
+ *
+ */
+ public function get_folder_info()
+ {
+ if (!isset($this->info))
+ $this->info = $this->imap->folder_info($this->name);
+
+ return $this->info;
+ }
+
+ /**
+ * Make IMAP folder data available for this folder
+ */
+ public function get_imap_data()
+ {
+ if (!isset($this->idata))
+ $this->idata = $this->imap->folder_data($this->name);
+
+ return $this->idata;
+ }
+
+
+ /**
+ * Get IMAP ACL information for this folder
+ *
+ * @return string Permissions as string
+ */
+ public function get_myrights()
+ {
+ $rights = $this->info['rights'];
+
+ if (!is_array($rights))
+ $rights = $this->imap->my_rights($this->name);
+
+ return join('', (array)$rights);
+ }
+
+
+ /**
+ * Check activation status of this folder
+ *
+ * @return boolean True if enabled, false if not
+ */
+ public function is_active()
+ {
+ return kolab_storage::folder_is_active($this->name);
+ }
+
+ /**
+ * Change activation status of this folder
+ *
+ * @param boolean The desired subscription status: true = active, false = not active
+ *
+ * @return True on success, false on error
+ */
+ public function activate($active)
+ {
+ return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
+ }
+
+ /**
+ * Check subscription status of this folder
+ *
+ * @return boolean True if subscribed, false if not
+ */
+ public function is_subscribed()
+ {
+ return kolab_storage::folder_is_subscribed($this->name);
+ }
+
+ /**
+ * Change subscription status of this folder
+ *
+ * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+ *
+ * @return True on success, false on error
+ */
+ public function subscribe($subscribed)
+ {
+ return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
+ }
+
+ /**
+ * Return folder name as string representation of this object
+ *
+ * @return string Full IMAP folder name
+ */
+ public function __toString()
+ {
+ return $this->name;
+ }
+}
+
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_user.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_user.php
new file mode 100644
index 0000000..7c141c5
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_user.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * Class that represents a (virtual) folder in the 'other' namespace
+ * implementing a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, 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/>.
+ */
+class kolab_storage_folder_user extends kolab_storage_folder_virtual
+{
+ protected static $ldapcache = array();
+
+ public $ldaprec;
+ public $type;
+
+ /**
+ * Default constructor
+ */
+ public function __construct($name, $parent = '', $ldaprec = null)
+ {
+ parent::__construct($name, $name, 'other', $parent);
+
+ if (!empty($ldaprec)) {
+ self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
+ }
+ // use value cached in memory for repeated lookups
+ else if (array_key_exists($name, self::$ldapcache)) {
+ $this->ldaprec = self::$ldapcache[$name];
+ }
+ // lookup user in LDAP and set $this->ldaprec
+ else if ($ldap = kolab_storage::ldap()) {
+ // get domain from current user
+ list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
+ $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
+ if (!empty($this->ldaprec)) {
+ $this->ldaprec['kolabtargetfolder'] = $name;
+ }
+ self::$ldapcache[$name] = $this->ldaprec;
+ }
+ }
+
+ /**
+ * Getter for the top-end folder name to be displayed
+ *
+ * @return string Name of this folder
+ */
+ public function get_foldername()
+ {
+ return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
+ parent::get_foldername();
+ }
+
+ /**
+ * Getter for a more informative title of this user folder
+ *
+ * @return string Title for the given user record
+ */
+ public function get_title()
+ {
+ return trim($this->ldaprec['displayname'] . '; ' . $this->ldaprec['mail'], '; ');
+ }
+
+ /**
+ * Returns the owner of the folder.
+ *
+ * @return string The owner of this folder.
+ */
+ public function get_owner()
+ {
+ return $this->ldaprec['mail'];
+ }
+
+ /**
+ * Check subscription status of this folder.
+ * Subscription of a virtual user folder depends on the subscriptions of subfolders.
+ *
+ * @return boolean True if subscribed, false if not
+ */
+ public function is_subscribed()
+ {
+ if (!empty($this->type)) {
+ $children = $subscribed = 0;
+ $delimiter = $this->imap->get_hierarchy_delimiter();
+ foreach ((array)kolab_storage::list_folders($this->name . $delimiter, '*', $this->type, false) as $subfolder) {
+ if (kolab_storage::folder_is_subscribed($subfolder)) {
+ $subscribed++;
+ }
+ $children++;
+ }
+ if ($subscribed > 0) {
+ return $subscribed == $children ? true : 2;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Change subscription status of this folder
+ *
+ * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+ *
+ * @return True on success, false on error
+ */
+ public function subscribe($subscribed)
+ {
+ $success = false;
+
+ // (un)subscribe all subfolders of a given type
+ if (!empty($this->type)) {
+ $delimiter = $this->imap->get_hierarchy_delimiter();
+ foreach ((array)kolab_storage::list_folders($this->name . $delimiter, '*', $this->type, false) as $subfolder) {
+ $success |= ($subscribed ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
+ }
+ }
+
+ return $success;
+ }
+
+}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_virtual.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_virtual.php
new file mode 100644
index 0000000..e419ced
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_virtual.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * Helper class that represents a virtual IMAP folder
+ * with a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, 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/>.
+ */
+class kolab_storage_folder_virtual extends kolab_storage_folder_api
+{
+ public $virtual = true;
+
+ protected $displayname;
+
+ public function __construct($name, $dispname, $ns, $parent = '')
+ {
+ parent::__construct($name);
+
+ $this->namespace = $ns;
+ $this->parent = $parent;
+ $this->displayname = $dispname;
+ }
+
+ /**
+ * Get the display name value of this folder
+ *
+ * @return string Folder name
+ */
+ public function get_name()
+ {
+ return $this->displayname ?: parent::get_name();
+ }
+
+ /**
+ * Get the color value stored in metadata
+ *
+ * @param string Default color value to return if not set
+ * @return mixed Color value from IMAP metadata or $default is not set
+ */
+ public function get_color($default = null)
+ {
+ return $default;
+ }
+}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/libkolab.php b/lib/drivers/kolab/plugins/libkolab/libkolab.php
index 48a5033..052724c 100644
--- a/lib/drivers/kolab/plugins/libkolab/libkolab.php
+++ b/lib/drivers/kolab/plugins/libkolab/libkolab.php
@@ -1,126 +1,138 @@
<?php
/**
* Kolab core library
*
* Plugin to setup a basic environment for the interaction with a Kolab server.
* Other Kolab-related plugins will depend on it and can use the library classes
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, 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/>.
*/
class libkolab extends rcube_plugin
{
static $http_requests = array();
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
// load local config
$this->load_config();
- $this->add_hook('storage_init', array($this, 'storage_init'));
-
// extend include path to load bundled lib classes
$include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
set_include_path($include_path);
+ $this->add_hook('storage_init', array($this, 'storage_init'));
+ $this->add_hook('user_delete', array('kolab_storage', 'delete_user_folders'));
+
$rcmail = rcube::get_instance();
try {
kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
rcube::raise_error($e, true);
kolab_format::$timezone = new DateTimeZone('GMT');
}
}
/**
* Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers
*/
function storage_init($p)
{
$p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION');
return $p;
}
/**
* Wrapper function to load and initalize the HTTP_Request2 Object
*
* @param string|Net_Url2 Request URL
* @param string Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
* @param array Configuration for this Request instance, that will be merged
* with default configuration
*
* @return HTTP_Request2 Request object
*/
public static function http_request($url = '', $method = 'GET', $config = array())
{
$rcube = rcube::get_instance();
$http_config = (array) $rcube->config->get('kolab_http_request');
// deprecated configuration options
if (empty($http_config)) {
foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
$value = $rcube->config->get('kolab_' . $option, true);
if (is_bool($value)) {
$http_config[$option] = $value;
}
}
}
if (!empty($config)) {
$http_config = array_merge($http_config, $config);
}
$key = md5(serialize($http_config));
if (!($request = self::$http_requests[$key])) {
// load HTTP_Request2
require_once 'HTTP/Request2.php';
try {
$request = new HTTP_Request2();
$request->setConfig($http_config);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
// proxy User-Agent string
$request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
self::$http_requests[$key] = $request;
}
// cleanup
try {
$request->setBody('');
$request->setUrl($url);
$request->setMethod($method);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
return $request;
}
+
+ /**
+ * Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
+ */
+ public static function html_diff($from, $to)
+ {
+ include_once __dir__ . '/vendor/finediff.php';
+
+ $diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
+ return $diff->renderDiffToHTML();
+ }
}
diff --git a/lib/drivers/kolab/plugins/libkolab/vendor/finediff.php b/lib/drivers/kolab/plugins/libkolab/vendor/finediff.php
new file mode 100644
index 0000000..b3c416c
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/vendor/finediff.php
@@ -0,0 +1,688 @@
+<?php
+/**
+* FINE granularity DIFF
+*
+* Computes a set of instructions to convert the content of
+* one string into another.
+*
+* Copyright (c) 2011 Raymond Hill (http://raymondhill.net/blog/?p=441)
+*
+* Licensed under The MIT License
+*
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in
+* all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+* THE SOFTWARE.
+*
+* @copyright Copyright 2011 (c) Raymond Hill (http://raymondhill.net/blog/?p=441)
+* @link http://www.raymondhill.net/finediff/
+* @version 0.6
+* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+*/
+
+/**
+* Usage (simplest):
+*
+* include 'finediff.php';
+*
+* // for the stock stack, granularity values are:
+* // FineDiff::$paragraphGranularity = paragraph/line level
+* // FineDiff::$sentenceGranularity = sentence level
+* // FineDiff::$wordGranularity = word level
+* // FineDiff::$characterGranularity = character level [default]
+*
+* $opcodes = FineDiff::getDiffOpcodes($from_text, $to_text [, $granularityStack = null] );
+* // store opcodes for later use...
+*
+* ...
+*
+* // restore $to_text from $from_text + $opcodes
+* include 'finediff.php';
+* $to_text = FineDiff::renderToTextFromOpcodes($from_text, $opcodes);
+*
+* ...
+*/
+
+/**
+* Persisted opcodes (string) are a sequence of atomic opcode.
+* A single opcode can be one of the following:
+* c | c{n} | d | d{n} | i:{c} | i{length}:{s}
+* 'c' = copy one character from source
+* 'c{n}' = copy n characters from source
+* 'd' = skip one character from source
+* 'd{n}' = skip n characters from source
+* 'i:{c} = insert character 'c'
+* 'i{n}:{s}' = insert string s, which is of length n
+*
+* Do not exist as of now, under consideration:
+* 'm{n}:{o} = move n characters from source o characters ahead.
+* It would be essentially a shortcut for a delete->copy->insert
+* command (swap) for when the inserted segment is exactly the same
+* as the deleted one, and with only a copy operation in between.
+* TODO: How often this case occurs? Is it worth it? Can only
+* be done as a postprocessing method (->optimize()?)
+*/
+abstract class FineDiffOp {
+ abstract public function getFromLen();
+ abstract public function getToLen();
+ abstract public function getOpcode();
+ }
+
+class FineDiffDeleteOp extends FineDiffOp {
+ public function __construct($len) {
+ $this->fromLen = $len;
+ }
+ public function getFromLen() {
+ return $this->fromLen;
+ }
+ public function getToLen() {
+ return 0;
+ }
+ public function getOpcode() {
+ if ( $this->fromLen === 1 ) {
+ return 'd';
+ }
+ return "d{$this->fromLen}";
+ }
+ }
+
+class FineDiffInsertOp extends FineDiffOp {
+ public function __construct($text) {
+ $this->text = $text;
+ }
+ public function getFromLen() {
+ return 0;
+ }
+ public function getToLen() {
+ return strlen($this->text);
+ }
+ public function getText() {
+ return $this->text;
+ }
+ public function getOpcode() {
+ $to_len = strlen($this->text);
+ if ( $to_len === 1 ) {
+ return "i:{$this->text}";
+ }
+ return "i{$to_len}:{$this->text}";
+ }
+ }
+
+class FineDiffReplaceOp extends FineDiffOp {
+ public function __construct($fromLen, $text) {
+ $this->fromLen = $fromLen;
+ $this->text = $text;
+ }
+ public function getFromLen() {
+ return $this->fromLen;
+ }
+ public function getToLen() {
+ return strlen($this->text);
+ }
+ public function getText() {
+ return $this->text;
+ }
+ public function getOpcode() {
+ if ( $this->fromLen === 1 ) {
+ $del_opcode = 'd';
+ }
+ else {
+ $del_opcode = "d{$this->fromLen}";
+ }
+ $to_len = strlen($this->text);
+ if ( $to_len === 1 ) {
+ return "{$del_opcode}i:{$this->text}";
+ }
+ return "{$del_opcode}i{$to_len}:{$this->text}";
+ }
+ }
+
+class FineDiffCopyOp extends FineDiffOp {
+ public function __construct($len) {
+ $this->len = $len;
+ }
+ public function getFromLen() {
+ return $this->len;
+ }
+ public function getToLen() {
+ return $this->len;
+ }
+ public function getOpcode() {
+ if ( $this->len === 1 ) {
+ return 'c';
+ }
+ return "c{$this->len}";
+ }
+ public function increase($size) {
+ return $this->len += $size;
+ }
+ }
+
+/**
+* FineDiff ops
+*
+* Collection of ops
+*/
+class FineDiffOps {
+ public function appendOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' ) {
+ $edits[] = new FineDiffCopyOp($from_len);
+ }
+ else if ( $opcode === 'd' ) {
+ $edits[] = new FineDiffDeleteOp($from_len);
+ }
+ else /* if ( $opcode === 'i' ) */ {
+ $edits[] = new FineDiffInsertOp(substr($from, $from_offset, $from_len));
+ }
+ }
+ public $edits = array();
+ }
+
+/**
+* FineDiff class
+*
+* TODO: Document
+*
+*/
+class FineDiff {
+
+ /**------------------------------------------------------------------------
+ *
+ * Public section
+ *
+ */
+
+ /**
+ * Constructor
+ * ...
+ * The $granularityStack allows FineDiff to be configurable so that
+ * a particular stack tailored to the specific content of a document can
+ * be passed.
+ */
+ public function __construct($from_text = '', $to_text = '', $granularityStack = null) {
+ // setup stack for generic text documents by default
+ $this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity;
+ $this->edits = array();
+ $this->from_text = $from_text;
+ $this->doDiff($from_text, $to_text);
+ }
+
+ public function getOps() {
+ return $this->edits;
+ }
+
+ public function getOpcodes() {
+ $opcodes = array();
+ foreach ( $this->edits as $edit ) {
+ $opcodes[] = $edit->getOpcode();
+ }
+ return implode('', $opcodes);
+ }
+
+ public function renderDiffToHTML() {
+ $in_offset = 0;
+ $html = '';
+ foreach ( $this->edits as $edit ) {
+ $n = $edit->getFromLen();
+ if ( $edit instanceof FineDiffCopyOp ) {
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
+ }
+ else if ( $edit instanceof FineDiffDeleteOp ) {
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+ }
+ else if ( $edit instanceof FineDiffInsertOp ) {
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ }
+ else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+ $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ }
+ $in_offset += $n;
+ }
+ return $html;
+ }
+
+ /**------------------------------------------------------------------------
+ * Return an opcodes string describing the diff between a "From" and a
+ * "To" string
+ */
+ public static function getDiffOpcodes($from, $to, $granularities = null) {
+ $diff = new FineDiff($from, $to, $granularities);
+ return $diff->getOpcodes();
+ }
+
+ /**------------------------------------------------------------------------
+ * Return an iterable collection of diff ops from an opcodes string
+ */
+ public static function getDiffOpsFromOpcodes($opcodes) {
+ $diffops = new FineDiffOps();
+ FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode'));
+ return $diffops->edits;
+ }
+
+ /**------------------------------------------------------------------------
+ * Re-create the "To" string from the "From" string and an "Opcodes" string
+ */
+ public static function renderToTextFromOpcodes($from, $opcodes) {
+ return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+ }
+
+ /**------------------------------------------------------------------------
+ * Render the diff to an HTML string
+ */
+ public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
+ return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+ }
+
+ /**------------------------------------------------------------------------
+ * Generic opcodes parser, user must supply callback for handling
+ * single opcode
+ */
+ public static function renderFromOpcodes($from, $opcodes, $callback) {
+ if ( !is_callable($callback) ) {
+ return '';
+ }
+ $out = '';
+ $opcodes_len = strlen($opcodes);
+ $from_offset = $opcodes_offset = 0;
+ while ( $opcodes_offset < $opcodes_len ) {
+ $opcode = substr($opcodes, $opcodes_offset, 1);
+ $opcodes_offset++;
+ $n = intval(substr($opcodes, $opcodes_offset));
+ if ( $n ) {
+ $opcodes_offset += strlen(strval($n));
+ }
+ else {
+ $n = 1;
+ }
+ if ( $opcode === 'c' ) { // copy n characters from source
+ $out .= call_user_func($callback, 'c', $from, $from_offset, $n, '');
+ $from_offset += $n;
+ }
+ else if ( $opcode === 'd' ) { // delete n characters from source
+ $out .= call_user_func($callback, 'd', $from, $from_offset, $n, '');
+ $from_offset += $n;
+ }
+ else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
+ $out .= call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
+ $opcodes_offset += 1 + $n;
+ }
+ }
+ return $out;
+ }
+
+ /**
+ * Stock granularity stacks and delimiters
+ */
+
+ const paragraphDelimiters = "\n\r";
+ public static $paragraphGranularity = array(
+ FineDiff::paragraphDelimiters
+ );
+ const sentenceDelimiters = ".\n\r";
+ public static $sentenceGranularity = array(
+ FineDiff::paragraphDelimiters,
+ FineDiff::sentenceDelimiters
+ );
+ const wordDelimiters = " \t.\n\r";
+ public static $wordGranularity = array(
+ FineDiff::paragraphDelimiters,
+ FineDiff::sentenceDelimiters,
+ FineDiff::wordDelimiters
+ );
+ const characterDelimiters = "";
+ public static $characterGranularity = array(
+ FineDiff::paragraphDelimiters,
+ FineDiff::sentenceDelimiters,
+ FineDiff::wordDelimiters,
+ FineDiff::characterDelimiters
+ );
+
+ public static $textStack = array(
+ ".",
+ " \t.\n\r",
+ ""
+ );
+
+ /**------------------------------------------------------------------------
+ *
+ * Private section
+ *
+ */
+
+ /**
+ * Entry point to compute the diff.
+ */
+ private function doDiff($from_text, $to_text) {
+ $this->last_edit = false;
+ $this->stackpointer = 0;
+ $this->from_text = $from_text;
+ $this->from_offset = 0;
+ // can't diff without at least one granularity specifier
+ if ( empty($this->granularityStack) ) {
+ return;
+ }
+ $this->_processGranularity($from_text, $to_text);
+ }
+
+ /**
+ * This is the recursive function which is responsible for
+ * handling/increasing granularity.
+ *
+ * Incrementally increasing the granularity is key to compute the
+ * overall diff in a very efficient way.
+ */
+ private function _processGranularity($from_segment, $to_segment) {
+ $delimiters = $this->granularityStack[$this->stackpointer++];
+ $has_next_stage = $this->stackpointer < count($this->granularityStack);
+ foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) {
+ // increase granularity
+ if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) {
+ $this->_processGranularity(
+ substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()),
+ $fragment_edit->getText()
+ );
+ }
+ // fuse copy ops whenever possible
+ else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) {
+ $this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen());
+ $this->from_offset += $fragment_edit->getFromLen();
+ }
+ else {
+ /* $fragment_edit instanceof FineDiffCopyOp */
+ /* $fragment_edit instanceof FineDiffDeleteOp */
+ /* $fragment_edit instanceof FineDiffInsertOp */
+ $this->edits[] = $this->last_edit = $fragment_edit;
+ $this->from_offset += $fragment_edit->getFromLen();
+ }
+ }
+ $this->stackpointer--;
+ }
+
+ /**
+ * This is the core algorithm which actually perform the diff itself,
+ * fragmenting the strings as per specified delimiters.
+ *
+ * This function is naturally recursive, however for performance purpose
+ * a local job queue is used instead of outright recursivity.
+ */
+ private static function doFragmentDiff($from_text, $to_text, $delimiters) {
+ // Empty delimiter means character-level diffing.
+ // In such case, use code path optimized for character-level
+ // diffing.
+ if ( empty($delimiters) ) {
+ return FineDiff::doCharDiff($from_text, $to_text);
+ }
+
+ $result = array();
+
+ // fragment-level diffing
+ $from_text_len = strlen($from_text);
+ $to_text_len = strlen($to_text);
+ $from_fragments = FineDiff::extractFragments($from_text, $delimiters);
+ $to_fragments = FineDiff::extractFragments($to_text, $delimiters);
+
+ $jobs = array(array(0, $from_text_len, 0, $to_text_len));
+
+ $cached_array_keys = array();
+
+ while ( $job = array_pop($jobs) ) {
+
+ // get the segments which must be diff'ed
+ list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
+
+ // catch easy cases first
+ $from_segment_length = $from_segment_end - $from_segment_start;
+ $to_segment_length = $to_segment_end - $to_segment_start;
+ if ( !$from_segment_length || !$to_segment_length ) {
+ if ( $from_segment_length ) {
+ $result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length);
+ }
+ else if ( $to_segment_length ) {
+ $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length));
+ }
+ continue;
+ }
+
+ // find longest copy operation for the current segments
+ $best_copy_length = 0;
+
+ $from_base_fragment_index = $from_segment_start;
+
+ $cached_array_keys_for_current_segment = array();
+
+ while ( $from_base_fragment_index < $from_segment_end ) {
+ $from_base_fragment = $from_fragments[$from_base_fragment_index];
+ $from_base_fragment_length = strlen($from_base_fragment);
+ // performance boost: cache array keys
+ if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) {
+ if ( !isset($cached_array_keys[$from_base_fragment]) ) {
+ $to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true);
+ }
+ else {
+ $to_all_fragment_indices = $cached_array_keys[$from_base_fragment];
+ }
+ // get only indices which falls within current segment
+ if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) {
+ $to_fragment_indices = array();
+ foreach ( $to_all_fragment_indices as $to_fragment_index ) {
+ if ( $to_fragment_index < $to_segment_start ) { continue; }
+ if ( $to_fragment_index >= $to_segment_end ) { break; }
+ $to_fragment_indices[] = $to_fragment_index;
+ }
+ $cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices;
+ }
+ else {
+ $to_fragment_indices = $to_all_fragment_indices;
+ }
+ }
+ else {
+ $to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment];
+ }
+ // iterate through collected indices
+ foreach ( $to_fragment_indices as $to_base_fragment_index ) {
+ $fragment_index_offset = $from_base_fragment_length;
+ // iterate until no more match
+ for (;;) {
+ $fragment_from_index = $from_base_fragment_index + $fragment_index_offset;
+ if ( $fragment_from_index >= $from_segment_end ) {
+ break;
+ }
+ $fragment_to_index = $to_base_fragment_index + $fragment_index_offset;
+ if ( $fragment_to_index >= $to_segment_end ) {
+ break;
+ }
+ if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) {
+ break;
+ }
+ $fragment_length = strlen($from_fragments[$fragment_from_index]);
+ $fragment_index_offset += $fragment_length;
+ }
+ if ( $fragment_index_offset > $best_copy_length ) {
+ $best_copy_length = $fragment_index_offset;
+ $best_from_start = $from_base_fragment_index;
+ $best_to_start = $to_base_fragment_index;
+ }
+ }
+ $from_base_fragment_index += strlen($from_base_fragment);
+ // If match is larger than half segment size, no point trying to find better
+ // TODO: Really?
+ if ( $best_copy_length >= $from_segment_length / 2) {
+ break;
+ }
+ // no point to keep looking if what is left is less than
+ // current best match
+ if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) {
+ break;
+ }
+ }
+
+ if ( $best_copy_length ) {
+ $jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start);
+ $result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length);
+ $jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end);
+ }
+ else {
+ $result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length));
+ }
+ }
+
+ ksort($result, SORT_NUMERIC);
+ return array_values($result);
+ }
+
+ /**
+ * Perform a character-level diff.
+ *
+ * The algorithm is quite similar to doFragmentDiff(), except that
+ * the code path is optimized for character-level diff -- strpos() is
+ * used to find out the longest common subequence of characters.
+ *
+ * We try to find a match using the longest possible subsequence, which
+ * is at most the length of the shortest of the two strings, then incrementally
+ * reduce the size until a match is found.
+ *
+ * I still need to study more the performance of this function. It
+ * appears that for long strings, the generic doFragmentDiff() is more
+ * performant. For word-sized strings, doCharDiff() is somewhat more
+ * performant.
+ */
+ private static function doCharDiff($from_text, $to_text) {
+ $result = array();
+ $jobs = array(array(0, strlen($from_text), 0, strlen($to_text)));
+ while ( $job = array_pop($jobs) ) {
+ // get the segments which must be diff'ed
+ list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
+ $from_segment_len = $from_segment_end - $from_segment_start;
+ $to_segment_len = $to_segment_end - $to_segment_start;
+
+ // catch easy cases first
+ if ( !$from_segment_len || !$to_segment_len ) {
+ if ( $from_segment_len ) {
+ $result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len);
+ }
+ else if ( $to_segment_len ) {
+ $result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len));
+ }
+ continue;
+ }
+ if ( $from_segment_len >= $to_segment_len ) {
+ $copy_len = $to_segment_len;
+ while ( $copy_len ) {
+ $to_copy_start = $to_segment_start;
+ $to_copy_start_max = $to_segment_end - $copy_len;
+ while ( $to_copy_start <= $to_copy_start_max ) {
+ $from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len));
+ if ( $from_copy_start !== false ) {
+ $from_copy_start += $from_segment_start;
+ break 2;
+ }
+ $to_copy_start++;
+ }
+ $copy_len--;
+ }
+ }
+ else {
+ $copy_len = $from_segment_len;
+ while ( $copy_len ) {
+ $from_copy_start = $from_segment_start;
+ $from_copy_start_max = $from_segment_end - $copy_len;
+ while ( $from_copy_start <= $from_copy_start_max ) {
+ $to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len));
+ if ( $to_copy_start !== false ) {
+ $to_copy_start += $to_segment_start;
+ break 2;
+ }
+ $from_copy_start++;
+ }
+ $copy_len--;
+ }
+ }
+ // match found
+ if ( $copy_len ) {
+ $jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start);
+ $result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len);
+ $jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end);
+ }
+ // no match, so delete all, insert all
+ else {
+ $result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len));
+ }
+ }
+ ksort($result, SORT_NUMERIC);
+ return array_values($result);
+ }
+
+ /**
+ * Efficiently fragment the text into an array according to
+ * specified delimiters.
+ * No delimiters means fragment into single character.
+ * The array indices are the offset of the fragments into
+ * the input string.
+ * A sentinel empty fragment is always added at the end.
+ * Careful: No check is performed as to the validity of the
+ * delimiters.
+ */
+ private static function extractFragments($text, $delimiters) {
+ // special case: split into characters
+ if ( empty($delimiters) ) {
+ $chars = str_split($text, 1);
+ $chars[strlen($text)] = '';
+ return $chars;
+ }
+ $fragments = array();
+ $start = $end = 0;
+ for (;;) {
+ $end += strcspn($text, $delimiters, $end);
+ $end += strspn($text, $delimiters, $end);
+ if ( $end === $start ) {
+ break;
+ }
+ $fragments[$start] = substr($text, $start, $end - $start);
+ $start = $end;
+ }
+ $fragments[$start] = '';
+ return $fragments;
+ }
+
+ /**
+ * Stock opcode renderers
+ */
+ private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' || $opcode === 'i' ) {
+ return substr($from, $from_offset, $from_len);
+ }
+ return '';
+ }
+
+ private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' ) {
+ return htmlentities(substr($from, $from_offset, $from_len));
+ }
+ else if ( $opcode === 'd' ) {
+ $deletion = substr($from, $from_offset, $from_len);
+ if ( strcspn($deletion, " \n\r") === 0 ) {
+ $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
+ }
+ return '<del>' . htmlentities($deletion) . '</del>';
+ }
+ else /* if ( $opcode === 'i' ) */ {
+ return '<ins>' . htmlentities(substr($from, $from_offset, $from_len)) . '</ins>';
+ }
+ return '';
+ }
+ }
+
diff --git a/lib/drivers/kolab/plugins/libkolab/vendor/finediff_modifications.diff b/lib/drivers/kolab/plugins/libkolab/vendor/finediff_modifications.diff
new file mode 100644
index 0000000..3a9ad5c
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/vendor/finediff_modifications.diff
@@ -0,0 +1,121 @@
+--- finediff.php.orig 2014-07-29 14:24:10.000000000 +0200
++++ finediff.php 2014-07-29 14:30:38.000000000 +0200
+@@ -234,25 +234,25 @@
+
+ public function renderDiffToHTML() {
+ $in_offset = 0;
+- ob_start();
++ $html = '';
+ foreach ( $this->edits as $edit ) {
+ $n = $edit->getFromLen();
+ if ( $edit instanceof FineDiffCopyOp ) {
+- FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
++ $html .= FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
+ }
+ else if ( $edit instanceof FineDiffDeleteOp ) {
+- FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
++ $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+ }
+ else if ( $edit instanceof FineDiffInsertOp ) {
+- FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
++ $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ }
+ else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
+- FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+- FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
++ $html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
++ $html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ }
+ $in_offset += $n;
+ }
+- return ob_get_clean();
++ return $html;
+ }
+
+ /**------------------------------------------------------------------------
+@@ -277,18 +277,14 @@
+ * Re-create the "To" string from the "From" string and an "Opcodes" string
+ */
+ public static function renderToTextFromOpcodes($from, $opcodes) {
+- ob_start();
+- FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+- return ob_get_clean();
++ return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+ }
+
+ /**------------------------------------------------------------------------
+ * Render the diff to an HTML string
+ */
+ public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
+- ob_start();
+- FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+- return ob_get_clean();
++ return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+ }
+
+ /**------------------------------------------------------------------------
+@@ -297,8 +293,9 @@
+ */
+ public static function renderFromOpcodes($from, $opcodes, $callback) {
+ if ( !is_callable($callback) ) {
+- return;
++ return '';
+ }
++ $out = '';
+ $opcodes_len = strlen($opcodes);
+ $from_offset = $opcodes_offset = 0;
+ while ( $opcodes_offset < $opcodes_len ) {
+@@ -312,18 +309,19 @@
+ $n = 1;
+ }
+ if ( $opcode === 'c' ) { // copy n characters from source
+- call_user_func($callback, 'c', $from, $from_offset, $n, '');
++ $out .= call_user_func($callback, 'c', $from, $from_offset, $n, '');
+ $from_offset += $n;
+ }
+ else if ( $opcode === 'd' ) { // delete n characters from source
+- call_user_func($callback, 'd', $from, $from_offset, $n, '');
++ $out .= call_user_func($callback, 'd', $from, $from_offset, $n, '');
+ $from_offset += $n;
+ }
+ else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
+- call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
++ $out .= call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
+ $opcodes_offset += 1 + $n;
+ }
+ }
++ return $out;
+ }
+
+ /**
+@@ -665,24 +663,26 @@
+ */
+ private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' || $opcode === 'i' ) {
+- echo substr($from, $from_offset, $from_len);
++ return substr($from, $from_offset, $from_len);
+ }
++ return '';
+ }
+
+ private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
+ if ( $opcode === 'c' ) {
+- echo htmlentities(substr($from, $from_offset, $from_len));
++ return htmlentities(substr($from, $from_offset, $from_len));
+ }
+ else if ( $opcode === 'd' ) {
+ $deletion = substr($from, $from_offset, $from_len);
+ if ( strcspn($deletion, " \n\r") === 0 ) {
+ $deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
+ }
+- echo '<del>', htmlentities($deletion), '</del>';
++ return '<del>' . htmlentities($deletion) . '</del>';
+ }
+ else /* if ( $opcode === 'i' ) */ {
+- echo '<ins>', htmlentities(substr($from, $from_offset, $from_len)), '</ins>';
++ return '<ins>' . htmlentities(substr($from, $from_offset, $from_len)) . '</ins>';
+ }
++ return '';
+ }
+ }
+
diff --git a/lib/drivers/seafile/seafile_api.php b/lib/drivers/seafile/seafile_api.php
index dcc5fe1..6fba80f 100644
--- a/lib/drivers/seafile/seafile_api.php
+++ b/lib/drivers/seafile/seafile_api.php
@@ -1,841 +1,843 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2014, Kolab Systems AG |
| |
| 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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Class implementing access via SeaFile Web API v2
*/
class seafile_api
{
const STATUS_OK = 200;
const CREATED = 201;
const ACCEPTED = 202;
const MOVED_PERMANENTLY = 301;
const BAD_REQUEST = 400;
const FORBIDDEN = 403;
const NOT_FOUND = 404;
const CONFLICT = 409;
const TOO_MANY_REQUESTS = 429;
const REPO_PASSWD_REQUIRED = 440;
const REPO_PASSWD_MAGIC_REQUIRED = 441;
const INTERNAL_SERVER_ERROR = 500;
const OPERATION_FAILED = 520;
const CONNECTION_ERROR = 550;
/**
* Specifies how long max. we'll wait and renew throttled request (in seconds)
*/
const WAIT_LIMIT = 30;
/**
* Configuration
*
* @var array
*/
protected $config = array();
/**
* HTTP request handle
*
* @var HTTP_Request
*/
protected $request;
/**
* Web API URI prefix
*
* @var string
*/
protected $url;
/**
* Session token
*
* @var string
*/
protected $token;
public function __construct($config = array())
{
$this->config = $config;
// set Web API URI
$this->url = rtrim('https://' . ($config['host'] ?: 'localhost'), '/');
if (!preg_match('|/api2$|', $this->url)) {
$this->url .= '/api2/';
}
}
/**
*
* @param array Configuration for this Request instance, that will be merged
* with default configuration
*
* @return HTTP_Request2 Request object
*/
public static function http_request($config = array())
{
// load HTTP_Request2
require_once 'HTTP/Request2.php';
// remove unknown config, otherwise HTTP_Request will throw an error
$config = array_intersect_key($config, array_flip(array(
'connect_timeout', 'timeout', 'use_brackets', 'protocol_version',
'buffer_size', 'store_body', 'follow_redirects', 'max_redirects',
'strict_redirects', 'ssl_verify_peer', 'ssl_verify_host',
'ssl_cafile', 'ssl_capath', 'ssl_local_cert', 'ssl_passphrase'
)));
try {
$request = new HTTP_Request2();
$request->setConfig($config);
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
return;
}
return $request;
}
/**
* Send HTTP request
*
* @param string $method Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
* @param string $url Request API URL
* @param array $get GET parameters
* @param array $post POST parameters
* @param array $upload Uploaded files data
*
* @return string|array Server response
*/
protected function request($method, $url, $get = null, $post = null, $upload = null)
{
if (!preg_match('/^https?:\/\//', $url)) {
$url = $this->url . $url;
// Note: It didn't work for me without the last backslash
$url = rtrim($url, '/') . '/';
}
if (!$this->request) {
$this->config['store_body'] = true;
// some methods respond with 301 redirect, we'll not follow them
// also because of https://github.com/haiwen/seahub/issues/288
$this->config['follow_redirects'] = false;
$this->request = self::http_request($this->config);
if (!$this->request) {
$this->status = self::CONNECTION_ERROR;
return;
}
}
// cleanup
try {
$this->request->setBody('');
$this->request->setUrl($url);
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
$this->status = self::CONNECTION_ERROR;
return;
}
if ($this->config['debug']) {
$log_line = "SeaFile $method: $url";
$json_opt = PHP_VERSION_ID >= 50400 ? JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE : 0;
if (!empty($get)) {
$log_line .= ", GET: " . @json_encode($get, $json_opt);
}
if (!empty($post)) {
$log_line .= ", POST: " . preg_replace('/("password":)[^\},]+/', '\\1"*"', @json_encode($post, $json_opt));
}
if (!empty($upload)) {
$log_line .= ", Files: " . @json_encode(array_keys($upload), $json_opt);
}
rcube::write_log('console', $log_line);
}
$this->request->setMethod($method ?: HTTP_Request2::METHOD_GET);
if (!empty($get)) {
$url = $this->request->getUrl();
$url->setQueryVariables($get);
$this->request->setUrl($url);
}
if (!empty($post)) {
$this->request->addPostParameter($post);
}
if (!empty($upload)) {
foreach ($upload as $field_name => $file) {
$this->request->addUpload($field_name, $file['data'], $file['name'], $file['type']);
}
}
if ($this->token) {
$this->request->setHeader('Authorization', "Token " . $this->token);
}
// some HTTP server configurations require this header
$this->request->setHeader('Accept', "application/json,text/javascript,*/*");
// proxy User-Agent string
$this->request->setHeader('User-Agent', $_SERVER['HTTP_USER_AGENT']);
// send request to the SeaFile API server
try {
$response = $this->request->send();
$this->status = $response->getStatus();
$body = $response->getBody();
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
$this->status = self::CONNECTION_ERROR;
}
if ($this->config['debug']) {
rcube::write_log('console', "SeaFile Response [$this->status]: " . trim($body));
}
// request throttled, try again?
if ($this->status == self::TOO_MANY_REQUESTS) {
if (preg_match('/([0-9]+) second/', $body['detail'], $m) && ($seconds = $m[1]) < self::WAIT_LIMIT) {
sleep($seconds/2); // try to be smart and wait only a half of it
return $this->request($url, $method, $get, $post, $upload);
}
}
// decode response
return $this->status >= 400 ? false : @json_decode($body, true);
}
/**
* Return error code of last operation
*/
public function is_error()
{
return $this->status >= 400 ? $this->status : false;
}
/**
* Authenticate to SeaFile API and get auth token
*
* @param string $username User name (email)
* @param string $password User password
*
* @return string Authentication token
*/
public function authenticate($username, $password)
{
$result = $this->request('POST', 'auth-token', null, array(
'username' => $username,
'password' => $password,
));
if ($result['token']) {
return $this->token = $result['token'];
}
}
/**
* Get account information
*
* @return array Account info (usage, total, email)
*/
public function account_info()
{
return $this->request('GET', "account/info");
}
/**
* Delete a directory
*
* @param string $repo_id Library identifier
* @param string $dir Directory name (with path)
*
* @return bool True on success, False on failure
*/
public function directory_delete($repo_id, $dir)
{
// sanity checks
if ($dir === '' || $dir === '/' || !is_string($dir)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
$this->request('DELETE', "repos/$repo_id/dir", array('p' => $dir));
return $this->is_error() === false;
}
/**
* Rename a directory
*
* @param string $repo_id Library identifier
* @param string $src_dir Directory name (with path)
* @param string $dest_dir New directory name (with path)
*
* @return bool True on success, False on failure
*/
public function directory_rename($repo_id, $src_dir, $dest_dir)
{
// sanity checks
if ($src_dir === '' || $src_dir === '/' || !is_string($src_dir)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($dest_dir === '' || $dest_dir === '/' || !is_string($dest_dir) || $dest_dir === $src_dir) {
$this->status = self::BAD_REQUEST;
return false;
}
$result = $this->request('POST', "repos/$repo_id/dir", array('p' => $src_dir), array(
'operation' => 'rename',
'newname' => $dest_dir,
));
return $this->is_error() === false;
}
/**
* Rename a directory
*
* @param string $repo_id Library identifier
* @param string $dir Directory name (with path)
*
* @return bool True on success, False on failure
*/
public function directory_create($repo_id, $dir)
{
// sanity checks
if ($dir === '' || $dir === '/' || !is_string($dir)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
$result = $this->request('POST', "repos/$repo_id/dir", array('p' => $dir), array(
'operation' => 'mkdir',
));
return $this->is_error() === false;
}
/**
* List directory entries (files and directories)
*
* @param string $repo_id Library identifier
* @param string $dir Directory name (with path)
*
* @return bool|array List of directories/files on success, False on failure
*/
public function directory_entries($repo_id, $dir)
{
// sanity checks
if (!is_string($dir)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($dir === '') {
$dir = '/';
}
// args: p=<$name> ('/' is a root, default), oid=?
// sample result
// [{
// "id": "0000000000000000000000000000000000000000",
// "type": "file",
// "name": "test1.c",
// "size": 0
// },{
// "id": "e4fe14c8cda2206bb9606907cf4fca6b30221cf9",
// "type": "dir",
// "name": "test_dir"
// }]
return $this->request('GET', "repos/$repo_id/dir", array('p' => $dir));
}
/**
* Update a file
*
* @param string $repo_id Library identifier
* @param string $filename File name (with path)
* @param array $file File data (data, type, name)
*
* @return bool True on success, False on failure
*/
public function file_update($repo_id, $filename, $file)
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
// first get the update link
$result = $this->request('GET', "repos/$repo_id/update-link");
if ($this->is_error() || empty($result)) {
return false;
}
$path = explode('/', $filename);
$fn = array_pop($path);
// then update file
$result = $this->request('POST', $result, null, array(
'filename' => $fn,
'target_file' => $filename,
),
array('file' => $file)
);
return $this->is_error() === false;
}
/**
* Upload a file
*
* @param string $repo_id Library identifier
* @param string $filename File name (with path)
* @param array $file File data (data, type, name)
*
* @return bool True on success, False on failure
*/
public function file_upload($repo_id, $filename, $file)
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
// first get upload link
$result = $this->request('GET', "repos/$repo_id/upload-link");
if ($this->is_error() || empty($result)) {
return false;
}
$path = explode('/', $filename);
$filename = array_pop($path);
$dir = '/' . ltrim(implode('/', $path), '/');
+ $file['name'] = $filename;
+
// then update file
$result = $this->request('POST', $result, null, array(
'parent_dir' => $dir
),
array('file' => $file)
);
return $this->is_error() === false;
}
/**
* Delete a file
*
* @param string $repo_id Library identifier
* @param string $filename File name (with path)
*
* @return bool True on success, False on failure
*/
public function file_delete($repo_id, $filename)
{
// sanity check
if ($filename === '' || $filename === '/' || !is_string($filename)) {
$this->status = self::BAD_REQUEST;
return false;
}
$this->request('DELETE', "repos/$repo_id/file", array('p' => $filename));
return $this->is_error() === false;
}
/**
* Copy file(s) (no rename here)
*
* @param string $repo_id Library identifier
* @param string|array $files List of files (without path)
* @param string $src_dir Source directory
* @param string $dest_dir Destination directory
* @param string $dest_repo Destination library (optional)
*
* @return bool True on success, False on failure
*/
public function file_copy($repo_id, $files, $src_dir, $dest_dir, $dest_repo)
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($src_dir === '' || !is_string($src_dir)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($dest_dir === '' || !is_string($dest_dir)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ((!is_array($files) && !strlen($files)) || (is_array($files) && empty($files))) {
$this->status = self::BAD_REQUEST;
return false;
}
if (empty($dest_repo)) {
$dest_repo = $repo_id;
}
$result = $this->request('POST', "repos/$repo_id/fileops/copy", array('p' => $src_dir), array(
'file_names' => implode(':', (array) $files),
'dst_dir' => $dest_dir,
'dst_repo' => $dest_repo,
));
return $this->is_error() === false;
}
/**
* Move a file (no rename here)
*
* @param string $repo_id Library identifier
* @param string $filename File name (with path)
* @param string $dst_dir Destination directory
* @param string $dst_repo Destination library (optional)
*
* @return bool True on success, False on failure
*/
public function file_move($repo_id, $filename, $dst_dir, $dst_repo = null)
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($filename === '' || !is_string($filename)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($dst_dir === '' || !is_string($dst_dir)) {
$this->status = self::BAD_REQUEST;
return false;
}
if (empty($dst_repo)) {
$dst_repo = $repo_id;
}
$result = $this->request('POST', "repos/$repo_id/file", array('p' => $filename), array(
'operation' => 'move',
'dst_dir' => $dst_dir,
'dst_repo' => $dst_repo,
));
return $this->is_error() === false;
}
/**
* Rename a file
*
* @param string $repo_id Library identifier
* @param string $filename File name (with path)
* @param string $new_name New file name (without path)
*
* @return bool True on success, False on failure
*/
public function file_rename($repo_id, $filename, $new_name)
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($filename === '' || !is_string($filename)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($new_name === '' || !is_string($new_name)) {
$this->status = self::BAD_REQUEST;
return false;
}
$result = $this->request('POST', "repos/$repo_id/file", array('p' => $filename), array(
'operation' => 'rename',
'newname' => $new_name,
));
return $this->is_error() === false;
}
/**
* Create an empty file
*
* @param string $repo_id Library identifier
* @param string $filename File name (with path)
*
* @return bool True on success, False on failure
*/
public function file_create($repo_id, $filename)
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($filename === '' || !is_string($filename)) {
$this->status = self::BAD_REQUEST;
return false;
}
$result = $this->request('POST', "repos/$repo_id/file", array('p' => $filename), array(
'operation' => 'create',
));
return $this->is_error() === false;
}
/**
* Get file info
*
* @param string $repo_id Library identifier
* @param string $filename File name (with path)
*
* @return bool|array File info on success, False on failure
*/
public function file_info($repo_id, $filename)
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($filename === '' || !is_string($filename)) {
$this->status = self::BAD_REQUEST;
return false;
}
// sample result:
// "id": "013d3d38fed38b3e8e26b21bb3463eab6831194f",
// "mtime": 1398148877,
// "type": "file",
// "name": "foo.py",
// "size": 22
return $this->request('GET', "repos/$repo_id/file/detail", array('p' => $filename));
}
/**
* Get file content
*
* @param string $repo_id Library identifier
* @param string $filename File name (with path)
*
* @return bool|string File download URI on success, False on failure
*/
public function file_get($repo_id, $filename)
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($filename === '' || !is_string($filename)) {
$this->status = self::BAD_REQUEST;
return false;
}
return $this->request('GET', "repos/$repo_id/file", array('p' => $filename));
}
/**
* List libraries (repositories)
*
* @return array|bool List of libraries on success, False on failure
*/
public function library_list()
{
$result = $this->request('GET', "repos");
// sample result
// [{
// "permission": "rw",
// "encrypted": false,
// "mtime": 1400054900,
// "owner": "user@mail.com",
// "id": "f158d1dd-cc19-412c-b143-2ac83f352290",
// "size": 0,
// "name": "foo",
// "type": "repo",
// "virtual": false,
// "desc": "new library",
// "root": "0000000000000000000000000000000000000000"
// }]
return $result;
}
/**
* Get library info
*
* @param string $repo_id Library identifier
*
* @return array|bool Library info on success, False on failure
*/
public function library_info($repo_id)
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
return $this->request('GET', "repos/$repo_id");
}
/**
* Create library
*
* @param string $name Library name
* @param string $description Library description
*
* @return bool|array Library info on success, False on failure
*/
public function library_create($name, $description = '')
{
if ($name === '' || !is_string($name)) {
$this->status = self::BAD_REQUEST;
return false;
}
return $this->request('POST', "repos", null, array(
'name' => $name,
'desc' => $description,
));
}
/**
* Rename library
*
* @param string $repo_id Library identifier
* @param string $new_name Library description
*
* @return bool True on success, False on failure
*/
public function library_rename($repo_id, $name, $description = '')
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
if ($name === '' || !is_string($name)) {
$this->status = self::BAD_REQUEST;
return false;
}
// Note: probably by mistake the 'op' is a GET parameter
// maybe changed in future to be consistent with other methods
$this->request('POST', "repos/$repo_id", array('op' => 'rename'), array(
'repo_name' => $name,
'repo_desc' => $description,
));
return $this->is_error() === false;
}
/**
* Delete library
*
* @param string $repo_id Library identifier
*
* @return bool True on success, False on failure
*/
public function library_delete($repo_id)
{
if ($repo_id === '' || !is_string($repo_id)) {
$this->status = self::BAD_REQUEST;
return false;
}
$this->request('DELETE', "repos/$repo_id");
return $this->is_error() === false;
}
/**
* Ping the API server
*
* @param string $token If set, auth token will be used
*
* @param bool True on success, False on failure
*/
public function ping($token = null)
{
// can be used to check if token is still valid
if ($token) {
$this->token = $token;
$result = $this->request('GET', 'auth/ping', null, null);
}
// or if api works
else {
$result = $this->request('GET', 'ping', null, null);
}
return $this->is_error() === false;
}
}
diff --git a/lib/drivers/seafile/seafile_file_storage.php b/lib/drivers/seafile/seafile_file_storage.php
index 524cf78..b6feb37 100644
--- a/lib/drivers/seafile/seafile_file_storage.php
+++ b/lib/drivers/seafile/seafile_file_storage.php
@@ -1,994 +1,1098 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2014, Kolab Systems AG |
| |
| 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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class seafile_file_storage implements file_storage
{
/**
* @var rcube
*/
protected $rc;
/**
* @var array
*/
protected $config = array();
/**
* @var seafile_api
*/
protected $api;
/**
* List of SeaFile libraries
*
* @var array
*/
protected $libraries;
+ /**
+ * Instance title (mount point)
+ *
+ * @var string
+ */
+ protected $title;
+
/**
* Class constructor
*/
public function __construct()
{
$this->rc = rcube::get_instance();
}
/**
* Authenticates a user
*
* @param string $username User name
* @param string $password User password
*
* @param bool True on success, False on failure
*/
public function authenticate($username, $password)
{
$this->init(true);
$token = $this->api->authenticate($username, $password);
if ($token) {
- $_SESSION['seafile_user'] = $username;
- $_SESSION['seafile_token'] = $this->rc->encrypt($token);
- $_SESSION['seafile_pass'] = $this->rc->encrypt($password);
+ $_SESSION[$this->title . 'seafile_user'] = $username;
+ $_SESSION[$this->title . 'seafile_token'] = $this->rc->encrypt($token);
+ $_SESSION[$this->title . 'seafile_pass'] = $this->rc->encrypt($password);
return true;
}
$this->api = false;
return false;
}
/**
* Initialize SeaFile Web API connection
*/
protected function init($skip_auth = false)
{
if ($this->api !== null) {
return $this->api !== false;
}
// read configuration
$config = array(
- 'host' => $this->rc->config->get('seafile_host', 'localhost'),
- 'ssl_verify_peer' => $this->rc->config->get('seafile_ssl_verify_peer', true),
- 'ssl_verify_host' => $this->rc->config->get('seafile_ssl_verify_host', true),
- 'debug' => $this->rc->config->get('seafile_debug', false),
+ 'host' => $this->rc->config->get('fileapi_seafile_host', 'localhost'),
+ 'ssl_verify_peer' => $this->rc->config->get('fileapi_seafile_ssl_verify_peer', true),
+ 'ssl_verify_host' => $this->rc->config->get('fileapi_seafile_ssl_verify_host', true),
+ 'debug' => $this->rc->config->get('fileapi_seafile_debug', false),
);
- $this->config = array_merge($this->config, $config);
+ $this->config = array_merge($config, $this->config);
// initialize Web API
$this->api = new seafile_api($this->config);
if ($skip_auth) {
return true;
}
// try session token
- if ($_SESSION['seafile_token'] && ($token = $this->rc->decrypt($_SESSION['seafile_token']))) {
+ if ($_SESSION[$this->title . 'seafile_token']
+ && ($token = $this->rc->decrypt($_SESSION[$this->title . 'seafile_token']))
+ ) {
$valid = $this->api->ping($token);
}
- if (!$valid && $_SESSION['seafile_password'] && $_SESSION['seafile_user']) {
- $pass = $this->rc->decrypt($_SESSION['seafile_pass']);
- $valid = $this->authenticate($_SESSION['seafile_user'], $pass);
+ if (!$valid) {
+ // already authenticated in session
+ if ($_SESSION[$this->title . 'seafile_user']) {
+ $user = $_SESSION[$this->title . 'seafile_user'];
+ $pass = $this->rc->decrypt($_SESSION[$this->title . 'seafile_pass']);
+ }
+ // try user/pass of the main driver
+ else {
+ $user = $this->config['username'];
+ $pass = $this->config['password'];
+ }
+
+ if ($user) {
+ $valid = $this->authenticate($user, $pass);
+ }
+ }
+
+ // throw special exception, so we can ask user for the credentials
+ if (!$valid && empty($_SESSION[$this->title . 'seafile_user'])) {
+ throw new Exception("User credentials not provided", file_storage::ERROR_NOAUTH);
}
return $valid;
}
/**
* Configures environment
*
- * @param array $config Configuration
+ * @param array $config Configuration
+ * @param string $title Source identifier
*/
- public function configure($config)
+ public function configure($config, $title = null)
{
$this->config = array_merge($this->config, $config);
+ $this->title = $title;
+ }
+
+ /**
+ * Returns current instance title
+ *
+ * @return string Instance title (mount point)
+ */
+ public function title()
+ {
+ return $this->title;
}
/**
* Storage driver capabilities
*
* @return array List of capabilities
*/
public function capabilities()
{
// find max filesize value
$max_filesize = parse_bytes(ini_get('upload_max_filesize'));
$max_postsize = parse_bytes(ini_get('post_max_size'));
if ($max_postsize && $max_postsize < $max_filesize) {
$max_filesize = $max_postsize;
}
return array(
file_storage::CAPS_MAX_UPLOAD => $max_filesize,
file_storage::CAPS_QUOTA => true,
file_storage::CAPS_LOCKS => true,
);
}
+ /**
+ * Save configuration of external driver (mount point)
+ *
+ * @param array $driver Driver data
+ *
+ * @throws Exception
+ */
+ public function driver_create($driver)
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Delete configuration of external driver (mount point)
+ *
+ * @param string $name Driver instance name
+ *
+ * @throws Exception
+ */
+ public function driver_delete($name)
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Return list of registered drivers (mount points)
+ *
+ * @return array List of drivers data
+ * @throws Exception
+ */
+ public function driver_list()
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Update configuration of external driver (mount point)
+ *
+ * @param string $name Driver instance name
+ * @param array $driver Driver data
+ *
+ * @throws Exception
+ */
+ public function driver_update($name, $driver)
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
/**
* Create a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_create($file_name, $file)
{
list($fn, $repo_id) = $this->find_library($file_name);
if (empty($repo_id)) {
throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
}
- $file['data'] = $file['path'];
+ if ($file['path']) {
+ $file['data'] = $file['path'];
+ }
+ else if (is_resource($file['content'])) {
+ $file['data'] = $file['content'];
+ }
+ else {
+ $fp = fopen('php://temp', 'wb');
+ fwrite($fp, $file['content'], strlen($file['content']));
+ $file['data'] = $fp;
+ unset($file['content']);
+ }
$created = $this->api->file_upload($repo_id, $fn, $file);
+ if ($fp) {
+ fclose($fp);
+ }
+
if (!$created) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving file to SeaFile server"),
true, false);
throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
}
}
/**
* Update a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_update($file_name, $file)
{
list($fn, $repo_id) = $this->find_library($file_name);
if (empty($repo_id)) {
throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
}
if ($file['path']) {
$file['data'] = $file['path'];
}
+ else if (is_resource($file['content'])) {
+ $file['data'] = $file['content'];
+ }
else {
$fp = fopen('php://temp', 'wb');
fwrite($fp, $file['content'], strlen($file['content']));
$file['data'] = $fp;
unset($file['content']);
}
$saved = $this->api->file_update($repo_id, $fn, $file);
if ($fp) {
fclose($fp);
}
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving file to SeaFile server"),
true, false);
throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
}
}
/**
* Delete a file.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_delete($file_name)
{
list($file_name, $repo_id) = $this->find_library($file_name);
if ($repo_id && $file_name != '/') {
$deleted = $this->api->file_delete($repo_id, $file_name);
}
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting object from SeaFile server"),
true, false);
throw new Exception("Storage error. Deleting file failed.", file_storage::ERROR);
}
}
/**
* Return file body.
*
* @param string $file_name Name of a file (with folder path)
* @param array $params Parameters (force-download)
* @param resource $fp Print to file pointer instead (send no headers)
*
* @throws Exception
*/
public function file_get($file_name, $params = array(), $fp = null)
{
list($fn, $repo_id) = $this->find_library($file_name);
$file = $this->api->file_info($repo_id, $fn);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$file = $this->from_file_object($file);
// get file location on SeaFile server for download
if ($file['size']) {
$link = $this->api->file_get($repo_id, $fn);
}
// write to file pointer, send no headers
if ($fp) {
if ($file['size']) {
$this->save_file_content($link, $fp);
}
return;
}
if (!empty($params['force-download'])) {
$disposition = 'attachment';
header("Content-Type: application/octet-stream");
// @TODO
// if ($browser->ie)
// header("Content-Type: application/force-download");
}
else {
$mimetype = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $file['type']);
$disposition = 'inline';
header("Content-Transfer-Encoding: binary");
header("Content-Type: $mimetype");
}
$filename = addcslashes($file['name'], '"');
// Workaround for nasty IE bug (#1488844)
// If Content-Disposition header contains string "attachment" e.g. in filename
// IE handles data as attachment not inline
/*
@TODO
if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) {
$filename = str_ireplace('attachment', 'attach', $filename);
}
*/
header("Content-Length: " . $file['size']);
header("Content-Disposition: $disposition; filename=\"$filename\"");
// just send redirect to SeaFile server
if ($file['size']) {
header("Location: $link");
}
die;
}
/**
* Returns file metadata.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_info($file_name)
{
list($file, $repo_id) = $this->find_library($file_name);
$file = $this->api->file_info($repo_id, $file);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$file = $this->from_file_object($file);
return array(
'name' => $file['name'],
'size' => (int) $file['size'],
'type' => (string) $file['type'],
'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
'created' => $file['created'] ? $file['created']->format('U') : 0,
);
}
/**
* List files in a folder.
*
* @param string $folder_name Name of a folder with full path
- * @param array $params List parameters ('sort', 'reverse', 'search')
+ * @param array $params List parameters ('sort', 'reverse', 'search', 'prefix')
*
* @return array List of files (file properties array indexed by filename)
* @throws Exception
*/
public function file_list($folder_name, $params = array())
{
list($folder, $repo_id) = $this->find_library($folder_name);
// prepare search filter
if (!empty($params['search'])) {
foreach ($params['search'] as $idx => $value) {
if ($idx == 'name') {
$params['search'][$idx] = mb_strtoupper($value);
}
else if ($idx == 'class') {
$params['search'][$idx] = file_utils::class2mimetypes($value);
}
}
}
// get directory entries
$entries = $this->api->directory_entries($repo_id, $folder);
$result = array();
foreach ((array) $entries as $idx => $file) {
if ($file['type'] != 'file') {
continue;
}
$file = $this->from_file_object($file);
// search filter
if (!empty($params['search'])) {
foreach ($params['search'] as $idx => $value) {
if ($idx == 'name') {
if (strpos(mb_strtoupper($file['name']), $value) === false) {
continue 2;
}
}
else if ($idx == 'class') {
foreach ($value as $v) {
if (stripos($file['type'], $v) === 0) {
break 2;
}
}
continue 2;
}
}
}
- $filename = $folder_name . file_storage::SEPARATOR . $file['name'];
+ $filename = $params['prefix'] . $folder_name . file_storage::SEPARATOR . $file['name'];
$result[$filename] = array(
'name' => $file['name'],
'size' => (int) $file['size'],
'type' => (string) $file['type'],
'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
'created' => $file['created'] ? $file['created']->format('U') : 0,
);
unset($files[$idx]);
}
// @TODO: pagination, search (by filename, mimetype)
// Sorting
$sort = !empty($params['sort']) ? $params['sort'] : 'name';
$index = array();
if ($sort == 'mtime') {
$sort = 'modified';
}
if (in_array($sort, array('name', 'size', 'modified'))) {
foreach ($result as $key => $val) {
$index[$key] = $val[$sort];
}
array_multisort($index, SORT_ASC, SORT_NUMERIC, $result);
}
if ($params['reverse']) {
$result = array_reverse($result, true);
}
return $result;
}
/**
* Copy a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_copy($file_name, $new_name)
{
list($src_name, $repo_id) = $this->find_library($file_name);
list($dst_name, $dst_repo_id) = $this->find_library($new_name);
if ($repo_id && $dst_repo_id) {
$path_src = explode('/', $src_name);
$path_dst = explode('/', $dst_name);
$f_src = array_pop($path_src);
$f_dst = array_pop($path_dst);
$src_dir = '/' . ltrim(implode('/', $path_src), '/');
$dst_dir = '/' . ltrim(implode('/', $path_dst), '/');
$success = $this->api->file_copy($repo_id, $f_old, $src_dir, $dst_dir, $dst_repo_id);
// now rename the file if needed
if ($success && $f_src != $f_dst) {
$success = $this->api->file_rename($dst_repo_id, rtrim($dst_dir, '/') . '/' . $f_src, $f_dst);
}
}
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error copying file on SeaFile server"),
true, false);
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
}
/**
* Move (or rename) a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_move($file_name, $new_name)
{
list($src_name, $repo_id) = $this->find_library($file_name);
list($dst_name, $dst_repo_id) = $this->find_library($new_name);
if ($repo_id && $dst_repo_id) {
$path_src = explode('/', $src_name);
$path_dst = explode('/', $dst_name);
$f_src = array_pop($path_src);
$f_dst = array_pop($path_dst);
$src_dir = '/' . ltrim(implode('/', $path_src), '/');
$dst_dir = '/' . ltrim(implode('/', $path_dst), '/');
if ($src_dir == $dst_dir && $repo_id == $dst_repo_id) {
$success = true;
}
else {
$success = $this->api->file_move($repo_id, $src_name, $dst_dir, $dst_repo_id);
}
// now rename the file if needed
if ($success && $f_src != $f_dst) {
$success = $this->api->file_rename($dst_repo_id, rtrim($dst_dir, '/') . '/' . $f_src, $f_dst);
}
}
if (!$success) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error moving file on SeaFile server"),
true, false);
throw new Exception("Storage error. File rename failed.", file_storage::ERROR);
}
}
/**
* Create a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_create($folder_name)
{
list($folder, $repo_id) = $this->find_library($folder_name, true);
if (empty($repo_id)) {
$success = $this->api->library_create($folder_name);
}
else if ($folder != '/') {
$success = $this->api->directory_create($repo_id, $folder);
}
if (!$success) {
throw new Exception("Storage error. Unable to create folder", file_storage::ERROR);
}
}
/**
* Delete a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_delete($folder_name)
{
list($folder, $repo_id) = $this->find_library($folder_name, true);
if ($repo_id && $folder == '/') {
$success = $this->api->library_delete($repo_id);
}
else if ($repo_id) {
$success = $this->api->directory_delete($repo_id, $folder);
}
if (!$success) {
throw new Exception("Storage error. Unable to delete folder.", file_storage::ERROR);
}
}
/**
* Move/Rename a folder.
*
* @param string $folder_name Name of a folder with full path
* @param string $new_name New name of a folder with full path
*
* @throws Exception on error
*/
public function folder_move($folder_name, $new_name)
{
list($folder, $repo_id, $library) = $this->find_library($folder_name, true);
list($dest_folder, $dest_repo_id) = $this->find_library($new_name, true);
// folders rename/move is possible only in the same library and folder
// @TODO: support folder move between libraries and folders
// @TODO: support converting library into a folder and vice-versa
// library rename
if ($repo_id && !$dest_repo_id && $folder == '/' && strpos($new_name, '/') === false) {
$success = $this->api->library_rename($repo_id, $new_name, $library['desc']);
}
// folder rename
else if ($folder != '/' && $dest_folder != '/' && $repo_id && $repo_id == $dest_repo_id) {
$path_src = explode('/', $folder);
$path_dst = explode('/', $dest_folder);
$f_src = array_pop($path_src);
$f_dst = array_pop($path_dst);
$src_dir = implode('/', $path_src);
$dst_dir = implode('/', $path_dst);
if ($src_dir == $dst_dir) {
$success = $this->api->directory_rename($repo_id, $folder, $f_dst);
}
}
if (!$success) {
throw new Exception("Storage error. Unable to rename/move folder", file_storage::ERROR);
}
}
/**
* Returns list of folders.
*
* @return array List of folders
* @throws Exception
*/
public function folder_list()
{
$libraries = $this->libraries();
$folders = array();
foreach ($this->libraries as $library) {
if ($library['virtual'] || $library['encrypted']) {
continue;
}
$folders[] = $library['name'];
if ($folder_tree = $this->folders_tree($library, '')) {
$folders = array_merge($folders, $folder_tree);
}
}
if (empty($folders)) {
throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR);
}
// sort folders
usort($folders, array($this, 'sort_folder_comparator'));
return $folders;
}
/**
* Recursively builds folders list
*/
protected function folders_tree($library, $folder)
{
$folders = array();
$length = strlen($folder);
if ($content = $this->api->directory_entries($library['id'], '/' . $folder)) {
foreach ($content as $item) {
if ($item['type'] == 'dir' && strlen($item['name'])) {
$f = ($length ? $folder . '/' : '') . $item['name'];
$folders[] = $library['name'] . '/' . $f;
$folders_tree = $this->folders_tree($library, $f);
if (!empty($folders_tree)) {
$folders = array_merge($folders, $folders_tree);
}
}
}
}
return $folders;
}
/**
* Callback for uasort() that implements correct
* locale-aware case-sensitive sorting
*/
protected function sort_folder_comparator($str1, $str2)
{
$path1 = explode('/', $str1);
$path2 = explode('/', $str2);
foreach ($path1 as $idx => $folder1) {
$folder2 = $path2[$idx];
if ($folder1 === $folder2) {
continue;
}
return strcoll($folder1, $folder2);
}
}
/**
* Get list of SeaFile libraries
*/
protected function libraries()
{
// get from memory, @TODO: cache in rcube_cache?
if ($this->libraries !== null) {
return $this->libraries;
}
if (!$this->init()) {
throw new Exception("Storage error. Unable to get list of SeaFile libraries.", file_storage::ERROR);
}
if ($list = $this->api->library_list()) {
$this->libraries = $list;
}
else {
$this->libraries = array();
}
return $this->libraries;
}
/**
* Find library ID from folder name
*/
protected function find_library($folder_name, $no_exception = false)
{
$libraries = $this->libraries();
foreach ($libraries as $lib) {
$path = $lib['name'] . '/';
if ($folder_name == $lib['name'] || strpos($folder_name, $path) === 0) {
if (empty($library) || strlen($library['name']) < strlen($lib['name'])) {
$library = $lib;
}
}
}
if (empty($library)) {
if (!$no_exception) {
throw new Exception("Storage error. Library not found.", file_storage::ERROR);
}
}
else {
$folder = substr($folder_name, strlen($library['name']) + 1);
}
return array(
'/' . ($folder ? $folder : ''),
$library['id'],
$library
);
}
/**
* Returns a list of locks
*
* This method should return all the locks for a particular URI, including
* locks that might be set on a parent URI.
*
* If child_locks is set to true, this method should also look for
* any locks in the subtree of the URI for locks.
*
* @param string $uri URI
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
* @throws Exception
*/
public function lock_list($uri, $child_locks = false)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->uri2resource($uri);
// get locks list
$list = $this->lock_db->lock_list($uri, $child_locks);
// convert back resource string into URIs
foreach ($list as $idx => $lock) {
$list[$idx]['uri'] = $this->resource2uri($lock['uri']);
}
return $list;
}
/**
* Locks a URI
*
* @param string $uri URI
* @param array $lock Lock data
* - depth: 0/'infinite'
* - scope: 'shared'/'exclusive'
* - owner: string
* - token: string
* - timeout: int
*
* @throws Exception
*/
public function lock($uri, $lock)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->uri2resource($uri);
if (!$this->lock_db->lock($uri, $lock)) {
throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
}
}
/**
* Removes a lock from a URI
*
* @param string $path URI
* @param array $lock Lock data
*
* @throws Exception
*/
public function unlock($uri, $lock)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->uri2resource($uri);
if (!$this->lock_db->unlock($uri, $lock)) {
throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR);
}
}
/**
* Return disk quota information for specified folder.
*
* @param string $folder_name Name of a folder with full path
*
* @return array Quota
* @throws Exception
*/
public function quota($folder)
{
if (!$this->init()) {
throw new Exception("Storage error. Unable to get SeaFile account info.", file_storage::ERROR);
}
$account_info = $this->api->account_info();
if (empty($account_info)) {
throw new Exception("Storage error. Unable to get SeaFile account info.", file_storage::ERROR);
}
$quota = array(
'total' => $account_info['total'],
'usage' => $account_info['usage'],
);
return $quota;
}
/**
* Get file object.
*
* @param string $file_name Name of a file (with folder path)
* @param kolab_storage_folder $folder Reference to folder object
*
* @return array File data
* @throws Exception
*/
protected function get_file_object(&$file_name, &$folder = null)
{
// extract file path and file name
$path = explode(file_storage::SEPARATOR, $file_name);
$file_name = array_pop($path);
$folder_name = implode(file_storage::SEPARATOR, $path);
if ($folder_name === '') {
throw new Exception("Missing folder name", file_storage::ERROR);
}
// get folder object
$folder = $this->get_folder_object($folder_name);
$files = $folder->select(array(
array('type', '=', 'file'),
array('filename', '=', $file_name)
));
return $files[0];
}
/**
* Simplify internal structure of the file object
*/
protected function from_file_object($file)
{
if ($file['type'] != 'file') {
return null;
}
// file modification time
if ($file['mtime']) {
try {
$file['changed'] = new DateTime('@' . $file['mtime']);
}
catch (Exception $e) { }
}
// find file mimetype from extension
$file['type'] = file_utils::ext_to_type($file['name']);
unset($file['id']);
unset($file['mtime']);
return $file;
}
/**
* Save remote file into file pointer
*/
protected function save_file_content($location, $fp)
{
if (!$fp || !$location) {
return false;
}
$config = array_merge($this->config, array('store_bodies' => true));
$request = seafile_api::http_request($config);
if (!$request) {
return false;
}
$observer = new seafile_request_observer();
$observer->set_fp($fp);
try {
$request->setUrl($location);
$request->attach($observer);
$response = $request->send();
$status = $response->getStatus();
$response->getBody(); // returns nothing
$request->detach($observer);
if ($status != 200) {
throw new Exception("Unable to save file. Status $status.");
}
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
return false;
}
return true;
}
protected function uri2resource($uri)
{
list($file, $repo_id, $library) = $this->find_library($uri);
// convert to imap charset (to be safe to store in DB)
$uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP');
return 'seafile://' . urlencode($library['owner']) . '@' . $this->config['host'] . '/' . $uri;
}
protected function resource2uri($resource)
{
if (!preg_match('|^seafile://([^@]+)@([^/]+)/(.*)$|', $resource, $matches)) {
throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR);
}
$user = urldecode($matches[1]);
$uri = $matches[3];
// convert from imap charset (to be safe to store in DB)
$uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET);
return $uri;
}
/**
* Initializes file_locks object
*/
protected function init_lock_db()
{
if (!$this->lock_db) {
$this->lock_db = new file_locks;
}
}
}
diff --git a/lib/drivers/seafile/seafile_request_observer.php b/lib/drivers/seafile/seafile_request_observer.php
index ec09c1f..0ab06b5 100644
--- a/lib/drivers/seafile/seafile_request_observer.php
+++ b/lib/drivers/seafile/seafile_request_observer.php
@@ -1,49 +1,51 @@
<?php
/**
* Observer for HTTP_Request2 implementing saving response body into a file
*/
class seafile_request_observer implements SplObserver
{
protected $file;
protected $fp;
public function set_file($file)
{
$this->file = $file;
}
public function set_fp($fp)
{
$this->fp = $fp;
}
public function update(SplSubject $subject)
{
$event = $subject->getLastEvent();
switch ($event['name']) {
case 'receivedHeaders':
if ($this->file) {
$target = $this->dir . DIRECTORY_SEPARATOR . $this->file;
if (!($this->fp = @fopen($target, 'wb'))) {
throw new Exception("Cannot open target file '{$target}'");
}
}
else if (!$this->fp) {
- throw new Exception("Cannot open target file '{$target}'");
+ throw new Exception("File destination not specified");
}
break;
case 'receivedBodyPart':
case 'receivedEncodedBodyPart':
fwrite($this->fp, $event['data']);
break;
case 'receivedBody':
- fclose($this->fp);
+ if ($this->file) {
+ fclose($this->fp);
+ }
break;
}
}
}
diff --git a/lib/file_api.php b/lib/file_api.php
index 3464f1c..88302bb 100644
--- a/lib/file_api.php
+++ b/lib/file_api.php
@@ -1,850 +1,626 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| 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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class file_api
{
- const ERROR_CODE = 500;
+ const ERROR_CODE = 500;
const OUTPUT_JSON = 'application/json';
const OUTPUT_HTML = 'text/html';
public $session;
- public $api;
+ public $output_type = self::OUTPUT_JSON;
+ public $config = array(
+ 'date_format' => 'Y-m-d H:i',
+ 'language' => 'en_US',
+ );
private $app_name = 'Kolab File API';
+ private $drivers = array();
private $conf;
private $browser;
- private $output_type = self::OUTPUT_JSON;
- private $config = array(
- 'date_format' => 'Y-m-d H:i',
- 'language' => 'en_US',
- );
+ private $backend;
public function __construct()
{
$rcube = rcube::get_instance();
$rcube->add_shutdown_function(array($this, 'shutdown'));
$this->conf = $rcube->config;
$this->session_init();
- }
- /**
- * Initialise backend class
- */
- protected function api_init()
- {
- if ($this->api) {
- return;
+ if ($_SESSION['config']) {
+ $this->config = $_SESSION['config'];
}
-
- $driver = $this->conf->get('fileapi_backend', 'kolab');
- $class = $driver . '_file_storage';
-
- $include_path = RCUBE_INSTALL_PATH . "/lib/drivers/$driver" . PATH_SEPARATOR;
- $include_path .= ini_get('include_path');
- set_include_path($include_path);
-
- $this->api = new $class;
-
- // configure api
- $this->api->configure(!empty($_SESSION['config']) ? $_SESSION['config'] : $this->config);
}
/**
* Process the request and dispatch it to the requested service
*/
public function run()
{
$this->request = strtolower($_GET['method']);
// Check the session, authenticate the user
if (!$this->session_validate()) {
$this->session->destroy(session_id());
if ($this->request == 'authenticate') {
$this->session->regenerate_id(false);
if ($username = $this->authenticate()) {
$_SESSION['user'] = $username;
$_SESSION['time'] = time();
$_SESSION['config'] = $this->config;
$this->output_success(array(
'token' => session_id(),
'capabilities' => $this->capabilities(),
));
}
}
throw new Exception("Invalid session", 403);
}
// Call service method
$result = $this->request_handler($this->request);
// Send success response, errors should be handled by driver class
// by throwing exceptions or sending output by itself
$this->output_success($result);
}
/**
* Session validation check and session start
*/
private function session_validate()
{
$sess_id = rcube_utils::request_header('X-Session-Token') ?: $_REQUEST['token'];
if (empty($sess_id)) {
session_start();
return false;
}
session_id($sess_id);
session_start();
if (empty($_SESSION['user'])) {
return false;
}
$timeout = $this->conf->get('session_lifetime', 0) * 60;
if ($timeout && $_SESSION['time'] && $_SESSION['time'] < time() - $timeout) {
return false;
}
// update session time
$_SESSION['time'] = time();
return true;
}
/**
* Initializes session
*/
private function session_init()
{
$rcube = rcube::get_instance();
$sess_name = $this->conf->get('session_name');
$lifetime = $this->conf->get('session_lifetime', 0) * 60;
if ($lifetime) {
ini_set('session.gc_maxlifetime', $lifetime * 2);
}
ini_set('session.name', $sess_name ? $sess_name : 'file_api_sessid');
ini_set('session.use_cookies', 0);
ini_set('session.serialize_handler', 'php');
// use database for storing session data
$this->session = new rcube_session($rcube->get_dbh(), $this->conf);
$this->session->register_gc_handler(array($rcube, 'gc'));
$this->session->set_secret($this->conf->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
$this->session->set_ip_check($this->conf->get('ip_check'));
// this is needed to correctly close session in shutdown function
$rcube->session = $this->session;
}
/**
* Script shutdown handler
*/
public function shutdown()
{
// write performance stats to logs/console
if ($this->conf->get('devel_mode')) {
if (function_exists('memory_get_peak_usage'))
$mem = memory_get_peak_usage();
else if (function_exists('memory_get_usage'))
$mem = memory_get_usage();
$log = trim($this->request . ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : ''));
if (defined('FILE_API_START')) {
rcube::print_timer(FILE_API_START, $log);
}
else {
rcube::console($log);
}
}
}
/**
* Authentication request handler (HTTP Auth)
*/
private function authenticate()
{
if (isset($_POST['username'])) {
$username = $_POST['username'];
$password = $_POST['password'];
}
else if (!empty($_SERVER['PHP_AUTH_USER'])) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
}
// when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule
else if (!isset($_SERVER['PHP_AUTH_USER'])) {
// "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..."
if (isset($_SERVER["REMOTE_USER"])) {
$basicAuthData = base64_decode(substr($_SERVER["REMOTE_USER"], 6));
}
else if (isset($_SERVER["REDIRECT_REMOTE_USER"])) {
$basicAuthData = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6));
}
else if (isset($_SERVER["Authorization"])) {
$basicAuthData = base64_decode(substr($_SERVER["Authorization"], 6));
}
else if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
$basicAuthData = base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6));
}
if (isset($basicAuthData) && !empty($basicAuthData)) {
list($username, $password) = explode(":", $basicAuthData);
}
}
if (!empty($username)) {
- $this->api_init();
- $result = $this->api->authenticate($username, $password);
+ $backend = $this->get_backend();
+ $result = $backend->authenticate($username, $password);
}
if (empty($result)) {
/*
header('WWW-Authenticate: Basic realm="' . $this->app_name .'"');
header('HTTP/1.1 401 Unauthorized');
exit;
*/
throw new Exception("Invalid password or username", file_api::ERROR_CODE);
}
return $username;
}
/**
* Storage/System method handler
*/
private function request_handler($request)
{
// handle "global" requests that don't require api driver
switch ($request) {
case 'ping':
return array();
case 'quit':
$this->session->destroy(session_id());
return array();
case 'configure':
foreach (array_keys($this->config) as $name) {
if (isset($_GET[$name])) {
$this->config[$name] = $_GET[$name];
}
}
$_SESSION['config'] = $this->config;
return $this->config;
case 'upload_progress':
return $this->upload_progress();
case 'mimetypes':
return $this->supported_mimetypes();
case 'capabilities':
- // this one actually uses api driver, but we put it here
- // because we'd need session for the api driver
return $this->capabilities();
}
- // init API driver
- $this->api_init();
-
- // GET arguments
- $args = &$_GET;
-
- // POST arguments (JSON)
- if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- $post = file_get_contents('php://input');
- $args += (array) json_decode($post, true);
- unset($post);
- }
-
- // disable script execution time limit, so we can handle big files
- @set_time_limit(0);
-
// handle request
- switch ($request) {
- case 'file_list':
- $params = array('reverse' => !empty($args['reverse']) && rcube_utils::get_boolean($args['reverse']));
- if (!empty($args['sort'])) {
- $params['sort'] = strtolower($args['sort']);
- }
-
- if (!empty($args['search'])) {
- $params['search'] = $args['search'];
- if (!is_array($params['search'])) {
- $params['search'] = array('name' => $params['search']);
- }
- }
-
- return $this->api->file_list($args['folder'], $params);
-
- case 'file_upload':
- // for Opera upload frame response cannot be application/json
- $this->output_type = self::OUTPUT_HTML;
-
- if (!isset($args['folder']) || $args['folder'] === '') {
- throw new Exception("Missing folder name", file_api::ERROR_CODE);
- }
-
- $uploads = $this->upload();
- $result = array();
-
- foreach ($uploads as $file) {
- $this->api->file_create($args['folder'] . file_storage::SEPARATOR . $file['name'], $file);
- unset($file['path']);
- $result[$file['name']] = array(
- 'type' => $file['type'],
- 'size' => $file['size'],
- );
- }
-
- return $result;
-
- case 'file_create':
- case 'file_update':
- if (!isset($args['file']) || $args['file'] === '') {
- throw new Exception("Missing file name", file_api::ERROR_CODE);
- }
- if (!isset($args['content'])) {
- throw new Exception("Missing file content", file_api::ERROR_CODE);
- }
-
- $file = array(
- 'content' => $args['content'],
- 'type' => rcube_mime::file_content_type($args['content'], $args['file'], $args['content-type'], true),
- );
-
- $this->api->$request($args['file'], $file);
-
- if (!empty($args['info']) && rcube_utils::get_boolean($args['info'])) {
- return $this->api->file_info($args['file']);
- }
-
- return;
-
- case 'file_delete':
- $files = (array) $args['file'];
-
- if (empty($files)) {
- throw new Exception("Missing file name", file_api::ERROR_CODE);
- }
-
- foreach ($files as $file) {
- $this->api->file_delete($file);
- }
- return;
-
- case 'file_info':
- if (!isset($args['file']) || $args['file'] === '') {
- throw new Exception("Missing file name", file_api::ERROR_CODE);
- }
-
- $info = $this->api->file_info($args['file']);
-
- if (!empty($args['viewer']) && rcube_utils::get_boolean($args['viewer'])) {
- $this->file_viewer_info($args['file'], $info);
- }
+ if ($request && preg_match('/^[a-z0-9_-]+$/', $request)) {
+ // request name aliases for backward compatibility
+ $aliases = array(
+ 'lock' => 'lock_create',
+ 'unlock' => 'lock_delete',
+ 'folder_rename' => 'folder_move',
+ );
+
+ $request = $aliases[$request] ?: $request;
+
+ include_once __DIR__ . "/api/$request.php";
+
+ $class_name = "file_api_$request";
+ if (class_exists($class_name, false)) {
+ $handler = new $class_name($this);
+ return $handler->handle();
+ }
+ }
- return $info;
+ throw new Exception("Unknown method", 501);
+ }
- case 'file_get':
- $this->output_type = self::OUTPUT_HTML;
+ /**
+ * Initialise authentication/configuration backend class
+ *
+ * @return file_storage Main storage driver
+ */
+ public function get_backend()
+ {
+ if ($this->backend) {
+ return $this->backend;
+ }
- if (!isset($args['file']) || $args['file'] === '') {
- header("HTTP/1.0 ".file_api::ERROR_CODE." Missing file name");
- }
+ $driver = $this->conf->get('fileapi_backend', 'kolab');
+ $class = $driver . '_file_storage';
- $params = array(
- 'force-download' => !empty($args['force-download']) && rcube_utils::get_boolean($args['force-download']),
- 'force-type' => $args['force-type'],
- );
+ $include_path = RCUBE_INSTALL_PATH . "/lib/drivers/$driver" . PATH_SEPARATOR;
+ $include_path .= ini_get('include_path');
+ set_include_path($include_path);
- if (!empty($args['viewer'])) {
- $this->file_view($args['file'], $args['viewer'], $args, $params);
- }
+ $this->backend = new $class;
- try {
- $this->api->file_get($args['file'], $params);
- }
- catch (Exception $e) {
- header("HTTP/1.0 " . file_api::ERROR_CODE . " " . $e->getMessage());
- }
- exit;
+ // configure api
+ $this->backend->configure($this->config);
- case 'file_move':
- case 'file_copy':
- if (!isset($args['file']) || $args['file'] === '') {
- throw new Exception("Missing file name", file_api::ERROR_CODE);
- }
+ return $this->backend;
+ }
- if (is_array($args['file'])) {
- if (empty($args['file'])) {
- throw new Exception("Missing file name", file_api::ERROR_CODE);
- }
- }
- else {
- if (!isset($args['new']) || $args['new'] === '') {
- throw new Exception("Missing new file name", file_api::ERROR_CODE);
- }
- $args['file'] = array($args['file'] => $args['new']);
- }
+ /**
+ * Return supported/enabled external storage instances
+ *
+ * @param bool $as_objects Return drivers as objects not config data
+ *
+ * @return array List of storage drivers
+ */
+ public function get_drivers($as_objects = false)
+ {
+ $enabled = $this->conf->get('fileapi_drivers');
+ $preconf = $this->conf->get('fileapi_sources');
+ $result = array();
+ $all = array();
- $overwrite = !empty($args['overwrite']) && rcube_utils::get_boolean($args['overwrite']);
- $files = (array) $args['file'];
- $errors = array();
+ if (!empty($enabled)) {
+ $backend = $this->get_backend();
+ $drivers = $backend->driver_list();
- foreach ($files as $file => $new_file) {
- if ($new_file === '') {
- throw new Exception("Missing new file name", file_api::ERROR_CODE);
- }
- if ($new_file === $file) {
- throw new Exception("Old and new file name is the same", file_api::ERROR_CODE);
- }
+ foreach ($drivers as $item) {
+ $all[] = $item['title'];
- try {
- $this->api->{$request}($file, $new_file);
- }
- catch (Exception $e) {
- if ($e->getCode() == file_storage::ERROR_FILE_EXISTS) {
- // delete existing file and do copy/move again
- if ($overwrite) {
- $this->api->file_delete($new_file);
- $this->api->{$request}($file, $new_file);
- }
- // collect file-exists errors, so the client can ask a user
- // what to do and skip or replace file(s)
- else {
- $errors[] = array(
- 'src' => $file,
- 'dst' => $new_file,
- );
- }
- }
- else {
- throw $e;
- }
- }
+ if ($item['enabled'] && !in_array($item['driver'], (array) $enabled)) {
+ $result[] = $as_objects ? $this->get_driver_object($item) : $item;
}
+ }
+ }
- if (!empty($errors)) {
- return array('already_exist' => $errors);
+ if (empty($result) && !empty($preconf)) {
+ foreach ((array) $preconf as $title => $item) {
+ if (!in_array($title, $all)) {
+ $item['title'] = $title;
+ $result[] = $as_objects ? $this->get_driver_object($item) : $item;
}
+ }
+ }
- return;
+ return $result;
+ }
- case 'folder_create':
- if (!isset($args['folder']) || $args['folder'] === '') {
- throw new Exception("Missing folder name", file_api::ERROR_CODE);
- }
- return $this->api->folder_create($args['folder']);
+ /**
+ * Return driver for specified file/folder path
+ *
+ * @param string $path Folder/file path
+ *
+ * @return array Storage driver object and modified path
+ */
+ public function get_driver($path)
+ {
+ $drivers = $this->get_drivers();
- case 'folder_delete':
- if (!isset($args['folder']) || $args['folder'] === '') {
- throw new Exception("Missing folder name", file_api::ERROR_CODE);
- }
- return $this->api->folder_delete($args['folder']);
+ foreach ($drivers as $item) {
+ $prefix = $item['title'] . file_storage::SEPARATOR;
- case 'folder_rename':
- case 'folder_move':
- if (!isset($args['folder']) || $args['folder'] === '') {
- throw new Exception("Missing source folder name", file_api::ERROR_CODE);
- }
- if (!isset($args['new']) || $args['new'] === '') {
- throw new Exception("Missing destination folder name", file_api::ERROR_CODE);
- }
- if ($args['new'] === $args['folder']) {
- return;
- }
- return $this->api->folder_move($args['folder'], $args['new']);
-
- case 'folder_list':
- return $this->api->folder_list();
+ if ($path == $item['title'] || strpos($path, $prefix) === 0) {
+ $selected = $item;
+ break;
+ }
+ }
- case 'quota':
- $quota = $this->api->quota($args['folder']);
+ if (empty($selected)) {
+ return array($this->get_backend(), $path);
+ }
- if (!$quota['total']) {
- $quota_result['percent'] = 0;
- }
- else if ($quota['total']) {
- if (!isset($quota['percent'])) {
- $quota_result['percent'] = min(100, round(($quota['used']/max(1,$quota['total']))*100));
- }
- }
+ $path = substr($path, strlen($selected['title']) + 1);
- return $quota;
+ return array($this->get_driver_object($selected), $path);
+ }
- case 'lock':
- // arguments: uri, owner, timeout, scope, depth, token
- foreach (array('uri', 'token') as $arg) {
- if (!isset($args[$arg]) || $args[$arg] === '') {
- throw new Exception("Missing lock $arg", file_api::ERROR_CODE);
- }
- }
+ /**
+ * Initialize driver instance
+ *
+ * @param array $config Driver config
+ *
+ * @return file_storage Storage driver instance
+ */
+ public function get_driver_object($config)
+ {
+ $key = $config['title'];
- $this->api->lock($args['uri'], $args);
- return;
+ if (empty($this->drivers[$key])) {
+ $class = $config['driver'] . '_file_storage';
- case 'unlock':
- foreach (array('uri', 'token') as $arg) {
- if (!isset($args[$arg]) || $args[$arg] === '') {
- throw new Exception("Missing lock $arg", file_api::ERROR_CODE);
- }
- }
+ if (!class_exists($class, false)) {
+ $include_path = RCUBE_INSTALL_PATH . "/lib/drivers/" . $config['driver'] . PATH_SEPARATOR;
+ $include_path .= ini_get('include_path');
+ set_include_path($include_path);
+ }
- $this->api->unlock($args['uri'], $args);
- return;
+ $this->drivers[$key] = $driver = new $class;
- case 'lock_list':
- $child_locks = !empty($args['child_locks']) && rcube_utils::get_boolean($args['child_locks']);
+ if ($config['username'] == '%u') {
+ $rcube = rcube::get_instance();
+ $config['username'] = $_SESSION['user'];
+ $config['password'] = $rcube->decrypt($_SESSION['password']);
+ }
- return $this->api->lock_list($args['uri'], $child_locks);
+ // configure api
+ $driver->configure(array_merge($config, $this->config), $key);
}
- if ($request) {
- throw new Exception("Unknown method", 501);
- }
+ return $this->drivers[$key];
}
/**
- * File uploads handler
+ * Returns storage(s) capabilities
+ *
+ * @return array Capabilities
*/
- protected function upload()
+ public function capabilities()
{
- $files = array();
-
- if (is_array($_FILES['file']['tmp_name'])) {
- foreach ($_FILES['file']['tmp_name'] as $i => $filepath) {
- if ($err = $_FILES['file']['error'][$i]) {
- if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
- $maxsize = ini_get('upload_max_filesize');
- $maxsize = $this->show_bytes(parse_bytes($maxsize));
- throw new Exception("Maximum file size ($maxsize) exceeded", file_api::ERROR_CODE);
- }
- throw new Exception("File upload failed", file_api::ERROR_CODE);
- }
+ $caps = array();
+ $backend = $this->get_backend();
+
+ // check support for upload progress
+ if (($progress_sec = $this->conf->get('upload_progress'))
+ && ini_get('apc.rfc1867') && function_exists('apc_fetch')
+ ) {
+ $caps[file_storage::CAPS_PROGRESS_NAME] = ini_get('apc.rfc1867_name');
+ $caps[file_storage::CAPS_PROGRESS_TIME] = $progress_sec;
+ }
- $files[] = array(
- 'path' => $filepath,
- 'name' => $_FILES['file']['name'][$i],
- 'size' => filesize($filepath),
- 'type' => rcube_mime::file_content_type($filepath, $_FILES['file']['name'][$i], $_FILES['file']['type']),
- );
+ // get capabilities of main storage module
+ foreach ($backend->capabilities() as $name => $value) {
+ // skip disabled capabilities
+ if ($value !== false) {
+ $caps[$name] = $value;
}
}
- else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- // if filesize exceeds post_max_size then $_FILES array is empty,
- if ($maxsize = ini_get('post_max_size')) {
- $maxsize = $this->show_bytes(parse_bytes($maxsize));
- throw new Exception("Maximum file size ($maxsize) exceeded", file_api::ERROR_CODE);
+
+ // get capabilities of other drivers
+ $drivers = $this->get_drivers(true);
+
+ foreach ($drivers as $driver) {
+ if ($driver != $backend) {
+ $title = $driver->title();
+ foreach ($driver->capabilities() as $name => $value) {
+ // skip disabled capabilities
+ if ($value !== false) {
+ $caps['roots'][$title][$name] = $value;
+ }
+ }
}
- throw new Exception("File upload failed", file_api::ERROR_CODE);
}
- return $files;
+ return $caps;
}
/**
* File upload progress handler
*/
protected function upload_progress()
{
if (function_exists('apc_fetch')) {
$prefix = ini_get('apc.rfc1867_prefix');
$uploadid = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET);
$status = apc_fetch($prefix . $uploadid);
if (!empty($status)) {
$status['percent'] = round($status['current']/$status['total']*100);
if ($status['percent'] < 100) {
$diff = time() - intval($status['start_time']);
// calculate time to end of uploading (in seconds)
$status['eta'] = intval($diff * (100 - $status['percent']) / $status['percent']);
// average speed (bytes per second)
$status['rate'] = intval($status['current'] / $diff);
}
}
$status['id'] = $uploadid;
return $status; // id, done, total, current, percent, start_time, eta, rate
}
throw new Exception("Not supported", file_api::ERROR_CODE);
}
- /*
- * Returns API capabilities
- */
- protected function capabilities()
- {
- $this->api_init();
-
- $caps = array();
-
- // check support for upload progress
- if (($progress_sec = $this->conf->get('upload_progress'))
- && ini_get('apc.rfc1867') && function_exists('apc_fetch')
- ) {
- $caps[file_storage::CAPS_PROGRESS_NAME] = ini_get('apc.rfc1867_name');
- $caps[file_storage::CAPS_PROGRESS_TIME] = $progress_sec;
- }
-
- foreach ($this->api->capabilities() as $name => $value) {
- // skip disabled capabilities
- if ($value !== false) {
- $caps[$name] = $value;
- }
- }
-
- return $caps;
- }
-
/**
* Return mimetypes list supported by built-in viewers
*
* @return array List of mimetypes
*/
protected function supported_mimetypes()
{
$mimetypes = array();
$dir = RCUBE_INSTALL_PATH . 'lib/viewers';
if ($handle = opendir($dir)) {
while (false !== ($file = readdir($handle))) {
if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) {
include_once $dir . '/' . $file;
$class = 'file_viewer_' . $matches[1];
$viewer = new $class($this);
$mimetypes = array_merge($mimetypes, $viewer->supported_mimetypes());
}
}
closedir($handle);
}
return $mimetypes;
}
- /**
- * Merge file viewer data into file info
- */
- protected function file_viewer_info($file, &$info)
- {
- if ($viewer = $this->find_viewer($info['type'])) {
- $info['viewer'] = array();
- if ($frame = $viewer->frame($file, $info['type'])) {
- $info['viewer']['frame'] = $frame;
- }
- else if ($href = $viewer->href($file, $info['type'])) {
- $info['viewer']['href'] = $href;
- }
- }
- }
-
- /**
- * File vieweing request handler
- */
- protected function file_view($file, $viewer, &$args, &$params)
- {
- $path = RCUBE_INSTALL_PATH . "lib/viewers/$viewer.php";
- $class = "file_viewer_$viewer";
-
- if (!file_exists($path)) {
- return;
- }
-
- // get file info
- try {
- $info = $this->api->file_info($file);
- }
- catch (Exception $e) {
- header("HTTP/1.0 " . file_api::ERROR_CODE . " " . $e->getMessage());
- exit;
- }
-
- include_once $path;
- $viewer = new $class($this);
-
- // check if specified viewer supports file type
- // otherwise return (fallback to file_get action)
- if (!$viewer->supports($info['type'])) {
- return;
- }
-
- $viewer->output($file, $info['type']);
- exit;
- }
-
- /**
- * Return built-in viewer opbject for specified mimetype
- *
- * @return object Viewer object
- */
- protected function find_viewer($mimetype)
- {
- $dir = RCUBE_INSTALL_PATH . 'lib/viewers';
-
- if ($handle = opendir($dir)) {
- while (false !== ($file = readdir($handle))) {
- if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) {
- include_once $dir . '/' . $file;
- $class = 'file_viewer_' . $matches[1];
- $viewer = new $class($this);
-
- if ($viewer->supports($mimetype)) {
- return $viewer;
- }
- }
- }
- closedir($handle);
- }
- }
-
/**
* Returns complete File URL
*
* @param string $file File name (with path)
*
* @return string File URL
*/
public function file_url($file)
{
return file_utils::script_uri(). '?method=file_get'
. '&file=' . urlencode($file)
. '&token=' . urlencode(session_id());
}
/**
* Returns web browser object
*
* @return rcube_browser Web browser object
*/
public function get_browser()
{
if ($this->browser === null) {
$this->browser = new rcube_browser;
}
return $this->browser;
}
/**
* Send success response
*
* @param mixed $data Data
*/
public function output_success($data)
{
if (!is_array($data)) {
$data = array();
}
$response = array('status' => 'OK', 'result' => $data);
if (!empty($_REQUEST['req_id'])) {
$response['req_id'] = $_REQUEST['req_id'];
}
$this->output_send($response);
}
/**
* Send error response
*
* @param mixed $response Response data
* @param int $code Error code
*/
public function output_error($response, $code = null)
{
if (is_string($response)) {
$response = array('reason' => $response);
}
$response['status'] = 'ERROR';
if ($code) {
$response['code'] = $code;
}
if (!empty($_REQUEST['req_id'])) {
$response['req_id'] = $_REQUEST['req_id'];
}
if (empty($response['code'])) {
$response['code'] = file_api::ERROR_CODE;
}
$this->output_send($response);
}
/**
* Send response
*
* @param mixed $data Data
*/
protected function output_send($data)
{
// Send response
header("Content-Type: {$this->output_type}; charset=utf-8");
echo json_encode($data);
exit;
}
/**
* Create a human readable string for a number of bytes
*
* @param int Number of bytes
*
* @return string Byte string
*/
public function show_bytes($bytes)
{
if ($bytes >= 1073741824) {
$gb = $bytes/1073741824;
$str = sprintf($gb >= 10 ? "%d " : "%.1f ", $gb) . 'GB';
}
else if ($bytes >= 1048576) {
$mb = $bytes/1048576;
$str = sprintf($mb >= 10 ? "%d " : "%.1f ", $mb) . 'MB';
}
else if ($bytes >= 1024) {
$str = sprintf("%d ", round($bytes/1024)) . 'KB';
}
else {
$str = sprintf('%d ', $bytes) . 'B';
}
return $str;
}
}
diff --git a/lib/file_storage.php b/lib/file_storage.php
index 8c5c526..809e8a4 100644
--- a/lib/file_storage.php
+++ b/lib/file_storage.php
@@ -1,240 +1,287 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| 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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
interface file_storage
{
// capabilities
const CAPS_ACL = 'ACL';
const CAPS_MAX_UPLOAD = 'MAX_UPLOAD';
const CAPS_PROGRESS_NAME = 'PROGRESS_NAME';
const CAPS_PROGRESS_TIME = 'PROGRESS_TIME';
const CAPS_QUOTA = 'QUOTA';
const CAPS_LOCKS = 'LOCKS';
// config
const SEPARATOR = '/';
// error codes
const ERROR = 500;
const ERROR_LOCKED = 423;
const ERROR_FILE_EXISTS = 550;
const ERROR_UNSUPPORTED = 570;
+ const ERROR_NOAUTH = 580;
// locks
const LOCK_SHARED = 'shared';
const LOCK_EXCLUSIVE = 'exclusive';
const LOCK_INFINITE = 'infinite';
+
/**
* Authenticates a user
*
* @param string $username User name
* @param string $password User password
*
* @return bool True on success, False on failure
*/
public function authenticate($username, $password);
/**
* Configures environment
*
- * @param array $config COnfiguration
+ * @param array $config Configuration
+ * @param string $title Driver instance identifier
+ */
+ public function configure($config, $title = null);
+
+ /**
+ * Returns current instance title
+ *
+ * @return string Instance title (mount point)
*/
- public function configure($config);
+ public function title();
/**
* Storage driver capabilities
*
* @return array List of capabilities
*/
public function capabilities();
+ /**
+ * Save configuration of external driver (mount point)
+ *
+ * @param array $driver Driver data
+ *
+ * @throws Exception
+ */
+ public function driver_create($driver);
+
+ /**
+ * Delete configuration of external driver (mount point)
+ *
+ * @param string $name Driver instance name
+ *
+ * @throws Exception
+ */
+ public function driver_delete($name);
+
+ /**
+ * Return list of registered drivers (mount points)
+ *
+ * @return array List of drivers data
+ * @throws Exception
+ */
+ public function driver_list();
+
+ /**
+ * Update configuration of external driver (mount point)
+ *
+ * @param string $name Driver instance name
+ * @param array $driver Driver data
+ *
+ * @throws Exception
+ */
+ public function driver_update($name, $driver);
+
/**
* Create a file.
*
* @param string $file_name Name of a file (with folder path)
- * @param array $file File data (path/content, type)
+ * @param array $file File data (path/content, type), where
+ * content might be a string or resource
*
* @throws Exception
*/
public function file_create($file_name, $file);
/**
* Update a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path/content, type)
*
* @throws Exception
*/
public function file_update($file_name, $file);
/**
* Delete a file.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_delete($file_name);
/**
* Returns file body.
*
* @param string $file_name Name of a file (with folder path)
* @param array $params Parameters (force-download)
* @param resource $fp Print to file pointer instead (send no headers)
*
* @throws Exception
*/
public function file_get($file_name, $params = array(), $fp = null);
/**
* Move (or rename) a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_move($file_name, $new_name);
/**
* Copy a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_copy($file_name, $new_name);
/**
* Returns file metadata.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_info($file_name);
/**
* List files in a folder.
*
* @param string $folder_name Name of a folder with full path
- * @param array $params List parameters ('sort', 'reverse', 'search')
+ * @param array $params List parameters ('sort', 'reverse', 'search', 'prefix')
*
* @return array List of files (file properties array indexed by filename)
* @throws Exception
*/
public function file_list($folder_name, $params = array());
/**
* Create a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_create($folder_name);
/**
* Delete a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_delete($folder_name);
/**
* Move/Rename a folder.
*
* @param string $folder_name Name of a folder with full path
* @param string $new_name New name of a folder with full path
*
* @throws Exception
*/
public function folder_move($folder_name, $new_name);
/**
* Returns list of folders.
*
* @return array List of folders
* @throws Exception
*/
public function folder_list();
/**
* Returns a list of locks
*
* This method should return all the locks for a particular URI, including
* locks that might be set on a parent URI.
*
* If child_locks is set to true, this method should also look for
* any locks in the subtree of the URI for locks.
*
* @param string $uri URI
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
* @throws Exception
*/
public function lock_list($uri, $child_locks = false);
/**
* Locks a URI
*
* @param string $uri URI
* @param array $lock Lock data
* - depth: 0/'infinite'
* - scope: 'shared'/'exclusive'
* - owner: string
* - token: string
* - timeout: int
*
* @throws Exception
*/
public function lock($uri, $lock);
/**
* Removes a lock from a URI
*
* @param string $path URI
* @param array $lock Lock data
*
* @throws Exception
*/
public function unlock($uri, $lock);
/**
* Return disk quota information for specified folder.
*
* @param string $folder_name Name of a folder with full path
*
* @return array Quota
* @throws Exception
*/
public function quota($folder);
}
diff --git a/lib/viewers/image.php b/lib/viewers/image.php
index fee9b3e..8b288a4 100644
--- a/lib/viewers/image.php
+++ b/lib/viewers/image.php
@@ -1,109 +1,111 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-2013, Kolab Systems AG |
| |
| 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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Class implementing image viewer (with format converter)
*
* NOTE: some formats are supported by browser, don't use viewer when not needed.
*/
class file_viewer_image extends file_viewer
{
protected $mimetypes = array(
'image/bmp',
'image/png',
'image/jpeg',
'image/jpg',
'image/pjpeg',
'image/gif',
'image/tiff',
'image/x-tiff',
);
/**
* Class constructor
*
* @param file_api File API object
*/
public function __construct($api)
{
// @TODO: disable types not supported by some browsers
$this->api = $api;
}
/**
* Return file viewer URL
*
* @param string $file File name
* @param string $mimetype File type
*/
public function href($file, $mimetype = null)
{
$href = file_utils::script_uri() . '?method=file_get'
. '&file=' . urlencode($file)
. '&token=' . urlencode(session_id());
// we redirect to self only images with types unsupported
// by browser
if (in_array($mimetype, $this->mimetypes)) {
$href .= '&viewer=image';
}
return $href;
}
/**
* Print output and exit
*
* @param string $file File name
* @param string $mimetype File type
*/
public function output($file, $mimetype = null)
{
/*
// conversion not needed
if (preg_match('/^image/p?jpe?g$/i', $mimetype)) {
$this->api->api->file_get($file);
return;
}
*/
$rcube = rcube::get_instance();
$temp_dir = unslashify($rcube->config->get('temp_dir'));
$file_path = tempnam($temp_dir, 'rcmImage');
+ list($driver, $file) = $this->api->get_driver($file);
+
// write content to temp file
$fd = fopen($file_path, 'w');
- $this->api->api->file_get($file, array(), $fd);
+ $driver->file_get($file, array(), $fd);
fclose($fd);
// convert image to jpeg and send it to the browser
$image = new rcube_image($file_path);
if ($image->convert(rcube_image::TYPE_JPG, $file_path)) {
header("Content-Type: image/jpeg");
header("Content-Length: " . filesize($file_path));
readfile($file_path);
}
unlink($file_path);
}
}
diff --git a/lib/viewers/text.php b/lib/viewers/text.php
index 718692c..8708f77 100644
--- a/lib/viewers/text.php
+++ b/lib/viewers/text.php
@@ -1,214 +1,216 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-2013, Kolab Systems AG |
| |
| 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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Class integrating text editor http://ajaxorg.github.io/ace
*/
class file_viewer_text extends file_viewer
{
/**
* Mimetype to tokenizer map
*
* @var array
*/
protected $mimetypes = array(
'text/plain' => 'text',
'text/html' => 'html',
'text/javascript' => 'javascript',
'text/ecmascript' => 'javascript',
'text/x-c' => 'c_cpp',
'text/css' => 'css',
'text/x-java-source' => 'java',
'text/x-php' => 'php',
'text/x-sh' => 'sh',
'text/xml' => 'xml',
'application/xml' => 'xml',
'application/x-vbscript' => 'vbscript',
'message/rfc822' => 'text',
'application/x-empty' => 'text',
);
/**
* File extension to highligter mode mapping
*
* @var array
*/
protected $extensions = array(
'php' => '/^(php|phpt|inc)$/',
'html' => '/^html?$/',
'css' => '/^css$/',
'xml' => '/^xml$/',
'javascript' => '/^js$/',
'sh' => '/^sh$/',
);
/**
* Returns list of supported mimetype
*
* @return array List of mimetypes
*/
public function supported_mimetypes()
{
// we return only mimetypes not starting with text/
$mimetypes = array();
foreach (array_keys($this->mimetypes) as $type) {
if (strpos($type, 'text/') !== 0) {
$mimetypes[] = $type;
}
}
return $mimetypes;
}
/**
* Check if mimetype is supported by the viewer
*
* @param string $mimetype File type
*
* @return bool
*/
public function supports($mimetype)
{
return $this->mimetypes[$mimetype] || preg_match('/^text\/(?!(pdf|x-pdf))/', $mimetype);
}
/**
* Print file content
*/
protected function print_file($file)
{
$stdout = fopen('php://output', 'w');
stream_filter_register('file_viewer_text', 'file_viewer_content_filter');
stream_filter_append($stdout, 'file_viewer_text');
- $this->api->api->file_get($file, array(), $stdout);
+ list($driver, $file) = $this->api->get_driver($file);
+
+ $driver->file_get($file, array(), $stdout);
}
/**
* Return file viewer URL
*
* @param string $file File name
* @param string $mimetype File type
*/
public function href($file, $mimetype = null)
{
return $this->api->file_url($file) . '&viewer=text';
}
/**
* Print output and exit
*
* @param string $file File name
* @param string $mimetype File type
*/
public function output($file, $mimetype = null)
{
$mode = $this->get_mode($mimetype, $file);
$href = addcslashes($this->api->file_url($file), "'");
echo '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Editor</title>
<script src="viewers/text/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="viewers/text/file_editor.js" type="text/javascript" charset="utf-8"></script>
<style>
#editor { top: 0; right: 0; bottom: 0; left: 0; position: absolute; font-size: 14px; padding: 0; margin: 0; }
.ace_search_options { float: right; }
</style>
</head>
<body>
<pre id="editor">';
$this->print_file($file);
echo "</pre>
<script>
var file_editor = new file_editor;
file_editor.init('editor', '$mode', '$href');
</script>
</body>
</html>";
}
protected function get_mode($mimetype, $filename)
{
$mimetype = strtolower($mimetype);
if ($this->mimetypes[$mimetype]) {
return $this->mimetypes[$mimetype];
}
$filename = explode('.', $filename);
$extension = count($filename) > 1 ? array_pop($filename) : null;
if ($extension) {
foreach ($this->extensions as $mode => $regexp) {
if (preg_match($regexp, $extension)) {
return $mode;
}
}
}
return 'text';
}
}
/**
* PHP stream filter to detect escape html special chars in a file
*/
class file_viewer_content_filter extends php_user_filter
{
private $buffer = '';
private $cutoff = 2048;
function onCreate()
{
$this->cutoff = rand(2048, 3027);
return true;
}
function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$bucket->data = htmlspecialchars($bucket->data, ENT_COMPAT | ENT_HTML401 | ENT_IGNORE);
$this->buffer .= $bucket->data;
// keep buffer small enough
if (strlen($this->buffer) > 4096) {
$this->buffer = substr($this->buffer, $this->cutoff);
}
$consumed += $bucket->datalen; // or strlen($bucket->data)?
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 2:51 PM (1 d, 8 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
197145
Default Alt Text
(614 KB)

Event Timeline