summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksander Machniak <machniak@kolabsys.com>2014-10-14 18:11:28 (GMT)
committerAleksander Machniak <machniak@kolabsys.com>2014-10-14 18:11:28 (GMT)
commit23d233b79e317ceae543aa86f6d606a42299f7fd (patch)
tree4ab2f7b4f660123f4e1b80143ebf2b8f30437c82
parent70e8fc24bce3596f990af9df521019584bc7216b (diff)
parentddbf10493c71515133aedbc861f3b0a2aea615fd (diff)
downloadkolab-chwala-23d233b79e317ceae543aa86f6d606a42299f7fd.tar.gz
Merge branch 'multi-driver'
-rw-r--r--config/config.inc.php.dist51
-rw-r--r--lib/api/common.php142
-rw-r--r--lib/api/file_copy.php29
-rw-r--r--lib/api/file_create.php59
-rw-r--r--lib/api/file_delete.php45
-rw-r--r--lib/api/file_get.php97
-rw-r--r--lib/api/file_info.php66
-rw-r--r--lib/api/file_list.php69
-rw-r--r--lib/api/file_move.php160
-rw-r--r--lib/api/file_update.php29
-rw-r--r--lib/api/file_upload.php64
-rw-r--r--lib/api/folder_auth.php81
-rw-r--r--lib/api/folder_create.php92
-rw-r--r--lib/api/folder_delete.php52
-rw-r--r--lib/api/folder_list.php105
-rw-r--r--lib/api/folder_move.php76
-rw-r--r--lib/api/folder_types.php60
-rw-r--r--lib/api/lock_create.php47
-rw-r--r--lib/api/lock_delete.php47
-rw-r--r--lib/api/lock_list.php42
-rw-r--r--lib/api/quota.php51
-rw-r--r--lib/client/file_ui_client_main.php25
-rw-r--r--lib/drivers/kolab/kolab.pngbin0 -> 841 bytes
-rw-r--r--lib/drivers/kolab/kolab_file_plugin_api.php (renamed from lib/kolab/kolab_file_plugin_api.php)0
-rw-r--r--lib/drivers/kolab/kolab_file_storage.php (renamed from lib/kolab/kolab_file_storage.php)160
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/LICENSE (renamed from lib/kolab/plugins/libkolab/LICENSE)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/config.inc.php.dist (renamed from lib/kolab/plugins/kolab_auth/config.inc.php.dist)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/kolab_auth.php (renamed from lib/kolab/plugins/kolab_auth/kolab_auth.php)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/kolab_auth_ldap.php (renamed from lib/kolab/plugins/kolab_auth/kolab_auth_ldap.php)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/bg_BG.inc (renamed from lib/kolab/plugins/kolab_auth/localization/bg_BG.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/de_CH.inc (renamed from lib/kolab/plugins/kolab_auth/localization/de_CH.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/de_DE.inc (renamed from lib/kolab/plugins/kolab_auth/localization/de_DE.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/en_US.inc (renamed from lib/kolab/plugins/kolab_auth/localization/en_US.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/es_ES.inc (renamed from lib/kolab/plugins/kolab_auth/localization/es_ES.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/et_EE.inc (renamed from lib/kolab/plugins/kolab_auth/localization/et_EE.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/fr_FR.inc (renamed from lib/kolab/plugins/kolab_auth/localization/fr_FR.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/ja_JP.inc (renamed from lib/kolab/plugins/kolab_auth/localization/ja_JP.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/nl_NL.inc (renamed from lib/kolab/plugins/kolab_auth/localization/nl_NL.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/pl_PL.inc (renamed from lib/kolab/plugins/kolab_auth/localization/pl_PL.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/pt_BR.inc (renamed from lib/kolab/plugins/kolab_auth/localization/pt_BR.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/localization/ru_RU.inc (renamed from lib/kolab/plugins/kolab_auth/localization/ru_RU.inc)0
-rw-r--r--lib/drivers/kolab/plugins/kolab_auth/package.xml (renamed from lib/kolab/plugins/kolab_auth/package.xml)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/LICENSE (renamed from lib/kolab/plugins/kolab_auth/LICENSE)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/README (renamed from lib/kolab/plugins/libkolab/README)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql (renamed from lib/kolab/plugins/libkolab/SQL/mysql.initial.sql)64
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013011000.sql (renamed from lib/kolab/plugins/libkolab/SQL/mysql/2013011000.sql)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013041900.sql (renamed from lib/kolab/plugins/libkolab/SQL/mysql/2013041900.sql)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013100400.sql (renamed from lib/kolab/plugins/libkolab/SQL/mysql/2013100400.sql)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013110400.sql1
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013121100.sql13
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014021000.sql9
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014032700.sql8
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/mysql/2014040900.sql16
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/oracle.initial.sql184
-rw-r--r--lib/drivers/kolab/plugins/libkolab/SQL/postgres.initial.sql (renamed from lib/kolab/plugins/libkolab/SQL/postgres.initial.sql)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/UPGRADING (renamed from lib/kolab/plugins/libkolab/UPGRADING)0
-rwxr-xr-xlib/drivers/kolab/plugins/libkolab/bin/modcache.sh (renamed from lib/kolab/plugins/libkolab/bin/modcache.sh)35
-rwxr-xr-xlib/drivers/kolab/plugins/libkolab/bin/randomcontacts.sh181
-rw-r--r--lib/drivers/kolab/plugins/libkolab/composer.json30
-rw-r--r--lib/drivers/kolab/plugins/libkolab/config.inc.php.dist61
-rw-r--r--lib/drivers/kolab/plugins/libkolab/js/folderlist.js350
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api.php82
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_bonnie_api_client.php239
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_date_recurrence.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_date_recurrence.php)12
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_format.php)228
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php282
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_format_contact.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_format_contact.php)76
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php)26
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_format_event.php)26
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_format_file.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_format_file.php)2
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_format_journal.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_format_journal.php)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php153
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_format_task.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_format_task.php)18
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_format_xcal.php)287
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage.php)620
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage_cache.php)525
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php88
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_contact.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_contact.php)16
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php)8
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_file.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_file.php)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_freebusy.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_freebusy.php)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_journal.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_journal.php)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_mongodb.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_mongodb.php)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_note.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_note.php)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_task.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_task.php)4
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php840
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_dataset.php154
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php (renamed from lib/kolab/plugins/libkolab/lib/kolab_storage_folder.php)262
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php345
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_user.php135
-rw-r--r--lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_virtual.php59
-rw-r--r--lib/drivers/kolab/plugins/libkolab/libkolab.php (renamed from lib/kolab/plugins/libkolab/libkolab.php)16
-rw-r--r--lib/drivers/kolab/plugins/libkolab/package.xml (renamed from lib/kolab/plugins/libkolab/package.xml)0
-rw-r--r--lib/drivers/kolab/plugins/libkolab/vendor/finediff.php688
-rw-r--r--lib/drivers/kolab/plugins/libkolab/vendor/finediff_modifications.diff121
-rw-r--r--lib/drivers/seafile/seafile.pngbin0 -> 865 bytes
-rw-r--r--lib/drivers/seafile/seafile_api.php849
-rw-r--r--lib/drivers/seafile/seafile_file_storage.php1200
-rw-r--r--lib/drivers/seafile/seafile_request_observer.php51
-rw-r--r--lib/file_api.php563
-rw-r--r--lib/file_locale.php122
-rw-r--r--lib/file_storage.php72
-rw-r--r--lib/file_ui.php96
-rw-r--r--lib/file_ui_output.php3
-rw-r--r--lib/file_utils.php48
-rw-r--r--lib/init.php4
-rw-r--r--lib/kolab/plugins/libkolab/config.inc.php.dist32
-rw-r--r--lib/kolab/plugins/libkolab/lib/kolab_format_configuration.php139
-rw-r--r--lib/kolab/plugins/libkolab/lib/kolab_format_note.php82
-rw-r--r--lib/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php40
-rw-r--r--lib/locale/en_US.php10
-rw-r--r--lib/viewers/image.php4
-rw-r--r--lib/viewers/text.php4
-rw-r--r--public_html/js/files_api.js14
-rw-r--r--public_html/js/files_ui.js254
-rw-r--r--public_html/js/wModal.js2
-rw-r--r--public_html/skins/default/style.css69
117 files changed, 10182 insertions, 1389 deletions
diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist
new file mode 100644
index 0000000..a26f4c4
--- /dev/null
+++ b/config/config.inc.php.dist
@@ -0,0 +1,51 @@
+<?php
+
+// This file contains Chwala configuration options.
+// Real config file must contain or include Roundcube Framework config.
+
+// ------------------------------------------------
+// Global settings
+// ------------------------------------------------
+
+// Main files source, backend driver which handles
+// authentication and configuration of Chwala
+// Note: Currently only 'kolab' is supported
+$config['fileapi_backend'] = 'kolab';
+
+// Enabled external storage drivers
+// Note: Currenty only 'seafile' is available
+$config['fileapi_drivers'] = array('seafile');
+
+// Pre-defined list of external storage sources.
+// Here admins can define sources which will be "mounted" into users folder tree
+/*
+$config['fileapi_sources'] = array(
+ 'Seafile' => array(
+ 'driver' => 'seafile',
+ 'host' => 'seacloud.cc',
+ // when username is set to '%u' current user name and password
+ // will be used to authenticate to this storage source
+ 'username' => '%u',
+ ),
+);
+*/
+
+// ------------------------------------------------
+// SeaFile driver settings
+// ------------------------------------------------
+
+// Enables SeaFile Web API conversation log
+$config['fileapi_seafile_debug'] = true;
+
+// Enables caching of some SeaFile information e.g. folders list
+// Note: 'db', 'apc' and 'memcache' are supported
+$config['fileapi_seafile_cache'] = 'db';
+
+// Expiration time of SeaFile cache entries
+$config['fileapi_seafile_cache_ttl'] = '7d';
+
+// Enables SSL certificates validation when connecting
+// with any SeaFile server
+$config['fileapi_seafile_ssl_verify_host'] = false;
+$config['fileapi_seafile_ssl_verify_peer'] = false;
+
diff --git a/lib/api/common.php b/lib/api/common.php
new file mode 100644
index 0000000..6cdb6cf
--- /dev/null
+++ b/lib/api/common.php
@@ -0,0 +1,142 @@
+<?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);
+ }
+ }
+
+ /**
+ * Parse driver metadata information
+ */
+ protected function parse_metadata($metadata, $default = false)
+ {
+ if ($default) {
+ unset($metadata['form']);
+ $metadata['name'] .= ' (' . $this->api->translate('localstorage') . ')';
+ }
+
+ // localize form labels
+ foreach ($metadata['form'] as $key => $val) {
+ $label = $this->api->translate('form.' . $val);
+ if (strpos($label, 'form.') !== 0) {
+ $metadata['form'][$key] = $label;
+ }
+ }
+
+ return $metadata;
+ }
+}
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_auth.php b/lib/api/folder_auth.php
new file mode 100644
index 0000000..3304cbb
--- /dev/null
+++ b/lib/api/folder_auth.php
@@ -0,0 +1,81 @@
+<?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_auth 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);
+ }
+
+ $drivers = $this->api->get_drivers();
+
+ foreach ($drivers as $driver_config) {
+ if ($driver_config['title'] === $this->args['folder']) {
+ $driver = $this->api->get_driver_object($driver_config);
+ $meta = $driver->driver_metadata();
+ }
+ }
+
+ if (empty($driver)) {
+ throw new Exception("Unknown folder", file_api::ERROR_CODE);
+ }
+
+ // check if authentication works
+ $data = array_fill_keys(array_keys($meta['form']), '');
+ $data = array_merge($data, $this->args);
+ $data = $driver->driver_validate($data);
+
+ // save changed data (except password)
+ unset($data['password']);
+ foreach (array_keys($meta['form']) as $key) {
+ if ($meta['form_values'][$key] != $data[$key]) {
+ // @TODO: save current driver config
+ break;
+ }
+ }
+
+ $result = array('folder' => $this->args['folder']);
+
+ // get list if folders if requested
+ if (rcube_utils::get_boolean((string) $this->args['list'])) {
+ $prefix = $this->args['folder'] . file_storage::SEPARATOR;
+ $result['list'] = array();
+
+ foreach ($driver->folder_list() as $folder) {
+ $result['list'][] = $prefix . $folder;
+ }
+ }
+
+ return $result;
+ }
+}
diff --git a/lib/api/folder_create.php b/lib/api/folder_create.php
new file mode 100644
index 0000000..55b3b3a
--- /dev/null
+++ b/lib/api/folder_create.php
@@ -0,0 +1,92 @@
+<?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);
+ }
+
+ // normal folder
+ if (empty($this->args['driver']) || $this->args['driver'] == 'default') {
+ list($driver, $path) = $this->api->get_driver($this->args['folder']);
+
+ return $driver->folder_create($path);
+ }
+
+ // external storage (mount point)
+ if (strpos($this->args['folder'], file_storage::SEPARATOR) !== false) {
+ throw new Exception("Unable to mount external storage into a sub-folder", file_api::ERROR_CODE);
+ }
+
+ // check if driver is enabled
+ $enabled = $this->rc->config->get('fileapi_drivers');
+
+ if (!in_array($this->args['driver'], $enabled)) {
+ throw new Exception("Unsupported storage driver", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ // check if folder/mount point already exists
+ $drivers = $this->api->get_drivers();
+ foreach ($drivers as $driver) {
+ if ($driver['title'] === $this->args['folder']) {
+ throw new Exception("Specified folder already exists", file_storage::ERROR_FILE_EXISTS);
+ }
+ }
+
+ $backend = $this->api->get_backend();
+ $folders = $backend->folder_list();
+
+ if (in_array($this->args['folder'], $folders)) {
+ throw new Exception("Specified folder already exists", file_storage::ERROR_FILE_EXISTS);
+ }
+
+ // load driver
+ $driver = $this->api->load_driver_object($this->args['driver']);
+ $driver->configure($this->api->config, $this->args['folder']);
+
+ // check if authentication works
+ $data = $driver->driver_validate($this->args);
+
+ $data['title'] = $this->args['folder'];
+ $data['driver'] = $this->args['driver'];
+ $data['enabled'] = 1;
+
+ // don't store password
+ // @TODO: store passwords encrypted?
+ unset($data['password']);
+
+ // save the mount point info in config
+ $backend->driver_create($data);
+ }
+}
diff --git a/lib/api/folder_delete.php b/lib/api/folder_delete.php
new file mode 100644
index 0000000..694c0d5
--- /dev/null
+++ b/lib/api/folder_delete.php
@@ -0,0 +1,52 @@
+<?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']);
+
+ // delete mount point...
+ if ($driver->title() === $this->args['folder']) {
+ $backend = $this->api->get_backend();
+ $backend->driver_delete($this->args['folder']);
+ return;
+ }
+
+ // delete folder...
+ $driver->folder_delete($path);
+ }
+}
diff --git a/lib/api/folder_list.php b/lib/api/folder_list.php
new file mode 100644
index 0000000..5659804
--- /dev/null
+++ b/lib/api/folder_list.php
@@ -0,0 +1,105 @@
+<?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;
+ $errors = array();
+
+ // 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 to ask user for credentials
+ $errors[$title] = $this->parse_metadata($driver->driver_metadata());
+ }
+ }
+ }
+ }
+
+ // re-sort the list
+ if ($has_more) {
+ usort($folders, array($this, 'sort_folder_comparator'));
+ }
+
+ return array(
+ 'list' => $folders,
+ 'auth_errors' => $errors,
+ );
+ }
+
+ /**
+ * 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..41828c6
--- /dev/null
+++ b/lib/api/folder_move.php
@@ -0,0 +1,76 @@
+<?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']);
+
+ // mount point (driver title) rename
+ if ($driver->title() === $this->args['folder'] && strpos($this->args['new'], file_storage::SEPARATOR) === false) {
+ // @TODO
+ throw new Exception("Unsupported operation", file_api::ERROR_CODE);
+ }
+
+ // cross-driver move
+ if ($driver != $new_driver) {
+ // @TODO
+ throw new Exception("Unsupported operation", file_api::ERROR_CODE);
+ }
+
+ // make sure destination folder is not an existing mount point
+ if (strpos($this->args['new'], file_storage::SEPARATOR) === false) {
+ $drivers = $this->api->get_drivers();
+
+ foreach ($drivers as $driver) {
+ if ($driver['title'] === $this->args['new']) {
+ throw new Exception("Destination folder exists", file_api::ERROR_CODE);
+ }
+ }
+ }
+
+ return $driver->folder_move($path, $new_path);
+ }
+}
diff --git a/lib/api/folder_types.php b/lib/api/folder_types.php
new file mode 100644
index 0000000..fd10ea7
--- /dev/null
+++ b/lib/api/folder_types.php
@@ -0,0 +1,60 @@
+<?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_types extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
+
+ $drivers = $this->rc->config->get('fileapi_drivers');
+ $result = array();
+
+ if (!empty($drivers)) {
+ foreach ((array) $drivers as $driver_name) {
+ if ($driver_name != 'kolab' && !isset($result[$driver_name])) {
+ $driver = $this->api->load_driver_object($driver_name);
+ $meta = $driver->driver_metadata();
+
+ $result[$driver_name] = $this->parse_metadata($meta);
+ }
+ }
+ }
+/*
+ // add local storage to the list
+ if (!empty($result)) {
+ $backend = $this->api->get_backend();
+ $meta = $backend->driver_metadata();
+
+ $result = array_merge(array('default' => $this->parse_metadata($meta, true)), $result);
+ }
+*/
+ return $result;
+ }
+}
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/client/file_ui_client_main.php b/lib/client/file_ui_client_main.php
index 82112e4..bc78220 100644
--- a/lib/client/file_ui_client_main.php
+++ b/lib/client/file_ui_client_main.php
@@ -31,7 +31,7 @@ class file_ui_client_main extends file_ui
'collection.audio', 'collection.video', 'collection.image', 'collection.document',
'moving', 'copying', 'file.skip', 'file.skipall', 'file.overwrite', 'file.overwriteall',
'file.moveconfirm', 'file.progress', 'upload.size', 'upload.size.error', 'upload.progress',
- 'upload.eta', 'upload.rate'
+ 'upload.eta', 'upload.rate', 'folder.authenticate', 'form.submit', 'form.cancel'
);
$result = $this->api_get('mimetypes');
@@ -48,6 +48,7 @@ class file_ui_client_main extends file_ui
'type' => 'text',
'name' => 'name',
'value' => '',
+ 'id' => 'folder-name-input',
));
$input_parent = new html_checkbox(array(
'name' => 'parent',
@@ -65,15 +66,29 @@ class file_ui_client_main extends file_ui
'value' => $this->translate('form.cancel'),
));
+ $drivers_input = new html_checkbox(array(
+ 'name' => 'external',
+ 'value' => '1',
+ 'id' => 'folder-driver-checkbox',
+ ));
+ $drivers = html::div('drivers',
+ html::span('drivers-header', $drivers_input->show() . '&nbsp;'
+ . html::label('folder-driver-checkbox', $this->translate('folder.driverselect')))
+ . html::div('drivers-list', '')
+ );
+
$table = new html_table;
- $table->add(null, $input_name->show() . $input_parent->show()
- . html::label('folder-parent-checkbox', $this->translate('folder.under')));
+ $table->add(null, html::label('folder-name-input', $this->translate('folder.name')) . $input_name->show());
$table->add('buttons', $submit->show() . $cancel->show());
$content = html::tag('fieldset', null,
- html::tag('legend', null,
- $this->translate('folder.createtitle')) . $table->show());
+ html::tag('legend', null, $this->translate('folder.createtitle'))
+ . $table->show()
+ . $input_parent->show() . '&nbsp;'
+ . html::label('folder-parent-checkbox', $this->translate('folder.under'))
+ . $drivers
+ );
$form = html::tag('form', array(
'id' => 'folder-create-form',
diff --git a/lib/drivers/kolab/kolab.png b/lib/drivers/kolab/kolab.png
new file mode 100644
index 0000000..0e87eec
--- /dev/null
+++ b/lib/drivers/kolab/kolab.png
Binary files differ
diff --git a/lib/kolab/kolab_file_plugin_api.php b/lib/drivers/kolab/kolab_file_plugin_api.php
index eda15c7..eda15c7 100644
--- a/lib/kolab/kolab_file_plugin_api.php
+++ b/lib/drivers/kolab/kolab_file_plugin_api.php
diff --git a/lib/kolab/kolab_file_storage.php b/lib/drivers/kolab/kolab_file_storage.php
index 6acfa3e..70a7f43 100644
--- a/lib/kolab/kolab_file_storage.php
+++ b/lib/drivers/kolab/kolab_file_storage.php
@@ -39,6 +39,11 @@ class kolab_file_storage implements file_storage
*/
protected $config;
+ /**
+ * @var string
+ */
+ protected $title;
+
/**
* Class constructor
@@ -210,6 +215,7 @@ class kolab_file_storage implements file_storage
return false;
}
}
+
// set session vars
$_SESSION['user_id'] = $user->ID;
$_SESSION['username'] = $user->data['username'];
@@ -243,11 +249,23 @@ class kolab_file_storage implements file_storage
/**
* 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 '';
}
/**
@@ -275,6 +293,140 @@ class kolab_file_storage implements file_storage
}
/**
+ * 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']] = $driver;
+ }
+
+ 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);
+ }
+ }
+
+ /**
+ * Returns metadata of the driver
+ *
+ * @return array Driver meta data (image, name, form)
+ */
+ public function driver_metadata()
+ {
+ $image_content = file_get_contents(__DIR__ . '/kolab.png');
+
+ $metadata = array(
+ 'image' => 'data:image/png;base64,' . base64_encode($image_content),
+ 'name' => 'Kolab Groupware',
+ 'ref' => 'http://kolab.org',
+ 'description' => 'Kolab Groupware server',
+ 'form' => array(
+ 'host' => 'hostname',
+ 'username' => 'username',
+ 'password' => 'password',
+ ),
+ );
+
+ return $metadata;
+ }
+
+ /**
+ * Validate metadata (config) of the driver
+ *
+ * @param array $metadata Driver metadata
+ *
+ * @return array Driver meta data to be stored in configuration
+ * @throws Exception
+ */
+ public function driver_validate($metadata)
+ {
+ throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+ }
+
+ /**
* Create a file.
*
* @param string $file_name Name of a file (with folder path)
@@ -465,7 +617,7 @@ class kolab_file_storage implements file_storage
* 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
@@ -503,7 +655,7 @@ class kolab_file_storage implements file_storage
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'],
diff --git a/lib/kolab/plugins/libkolab/LICENSE b/lib/drivers/kolab/plugins/kolab_auth/LICENSE
index dba13ed..dba13ed 100644
--- a/lib/kolab/plugins/libkolab/LICENSE
+++ b/lib/drivers/kolab/plugins/kolab_auth/LICENSE
diff --git a/lib/kolab/plugins/kolab_auth/config.inc.php.dist b/lib/drivers/kolab/plugins/kolab_auth/config.inc.php.dist
index e7b9d15..e7b9d15 100644
--- a/lib/kolab/plugins/kolab_auth/config.inc.php.dist
+++ b/lib/drivers/kolab/plugins/kolab_auth/config.inc.php.dist
diff --git a/lib/kolab/plugins/kolab_auth/kolab_auth.php b/lib/drivers/kolab/plugins/kolab_auth/kolab_auth.php
index 7ff5761..7ff5761 100644
--- a/lib/kolab/plugins/kolab_auth/kolab_auth.php
+++ b/lib/drivers/kolab/plugins/kolab_auth/kolab_auth.php
diff --git a/lib/kolab/plugins/kolab_auth/kolab_auth_ldap.php b/lib/drivers/kolab/plugins/kolab_auth/kolab_auth_ldap.php
index b9b3e4a..b9b3e4a 100644
--- a/lib/kolab/plugins/kolab_auth/kolab_auth_ldap.php
+++ b/lib/drivers/kolab/plugins/kolab_auth/kolab_auth_ldap.php
diff --git a/lib/kolab/plugins/kolab_auth/localization/bg_BG.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/bg_BG.inc
index 1f7a573..1f7a573 100644
--- a/lib/kolab/plugins/kolab_auth/localization/bg_BG.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/bg_BG.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/de_CH.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/de_CH.inc
index 5e85a01..5e85a01 100644
--- a/lib/kolab/plugins/kolab_auth/localization/de_CH.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/de_CH.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/de_DE.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/de_DE.inc
index 5e85a01..5e85a01 100644
--- a/lib/kolab/plugins/kolab_auth/localization/de_DE.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/de_DE.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/en_US.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/en_US.inc
index e1adb3f..e1adb3f 100644
--- a/lib/kolab/plugins/kolab_auth/localization/en_US.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/en_US.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/es_ES.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/es_ES.inc
index acb6c35..acb6c35 100644
--- a/lib/kolab/plugins/kolab_auth/localization/es_ES.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/es_ES.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/et_EE.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/et_EE.inc
index acb6c35..acb6c35 100644
--- a/lib/kolab/plugins/kolab_auth/localization/et_EE.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/et_EE.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/fr_FR.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/fr_FR.inc
index 6f72695..6f72695 100644
--- a/lib/kolab/plugins/kolab_auth/localization/fr_FR.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/fr_FR.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/ja_JP.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/ja_JP.inc
index ed0358a..ed0358a 100644
--- a/lib/kolab/plugins/kolab_auth/localization/ja_JP.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/ja_JP.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/nl_NL.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/nl_NL.inc
index a98283f..a98283f 100644
--- a/lib/kolab/plugins/kolab_auth/localization/nl_NL.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/nl_NL.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/pl_PL.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/pl_PL.inc
index 124c373..124c373 100644
--- a/lib/kolab/plugins/kolab_auth/localization/pl_PL.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/pl_PL.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/pt_BR.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/pt_BR.inc
index 26e9541..26e9541 100644
--- a/lib/kolab/plugins/kolab_auth/localization/pt_BR.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/pt_BR.inc
diff --git a/lib/kolab/plugins/kolab_auth/localization/ru_RU.inc b/lib/drivers/kolab/plugins/kolab_auth/localization/ru_RU.inc
index 9e28c12..9e28c12 100644
--- a/lib/kolab/plugins/kolab_auth/localization/ru_RU.inc
+++ b/lib/drivers/kolab/plugins/kolab_auth/localization/ru_RU.inc
diff --git a/lib/kolab/plugins/kolab_auth/package.xml b/lib/drivers/kolab/plugins/kolab_auth/package.xml
index 5a2093b..5a2093b 100644
--- a/lib/kolab/plugins/kolab_auth/package.xml
+++ b/lib/drivers/kolab/plugins/kolab_auth/package.xml
diff --git a/lib/kolab/plugins/kolab_auth/LICENSE b/lib/drivers/kolab/plugins/libkolab/LICENSE
index dba13ed..dba13ed 100644
--- a/lib/kolab/plugins/kolab_auth/LICENSE
+++ b/lib/drivers/kolab/plugins/libkolab/LICENSE
diff --git a/lib/kolab/plugins/libkolab/README b/lib/drivers/kolab/plugins/libkolab/README
index 2f94839..2f94839 100644
--- a/lib/kolab/plugins/libkolab/README
+++ b/lib/drivers/kolab/plugins/libkolab/README
diff --git a/lib/kolab/plugins/libkolab/SQL/mysql.initial.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql
index 4f23a52..2aa046d 100644
--- a/lib/kolab/plugins/libkolab/SQL/mysql.initial.sql
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql.initial.sql
@@ -1,7 +1,7 @@
/**
* libkolab database schema
*
- * @version 1.0
+ * @version 1.1
* @author Thomas Bruederli
* @licence GNU AGPL
**/
@@ -29,15 +29,20 @@ CREATE TABLE `kolab_cache_contact` (
`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`;
@@ -48,15 +53,16 @@ CREATE TABLE `kolab_cache_event` (
`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`;
@@ -67,15 +73,16 @@ CREATE TABLE `kolab_cache_task` (
`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`;
@@ -86,15 +93,16 @@ CREATE TABLE `kolab_cache_journal` (
`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`;
@@ -105,13 +113,14 @@ CREATE TABLE `kolab_cache_note` (
`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`;
@@ -122,15 +131,16 @@ CREATE TABLE `kolab_cache_file` (
`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`;
@@ -141,15 +151,16 @@ CREATE TABLE `kolab_cache_configuration` (
`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`;
@@ -160,16 +171,17 @@ CREATE TABLE `kolab_cache_freebusy` (
`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/kolab/plugins/libkolab/SQL/mysql/2013011000.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013011000.sql
index fe6741a..fe6741a 100644
--- a/lib/kolab/plugins/libkolab/SQL/mysql/2013011000.sql
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013011000.sql
diff --git a/lib/kolab/plugins/libkolab/SQL/mysql/2013041900.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013041900.sql
index 76577e6..76577e6 100644
--- a/lib/kolab/plugins/libkolab/SQL/mysql/2013041900.sql
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013041900.sql
diff --git a/lib/kolab/plugins/libkolab/SQL/mysql/2013100400.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013100400.sql
index d41d0e1..d41d0e1 100644
--- a/lib/kolab/plugins/libkolab/SQL/mysql/2013100400.sql
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013100400.sql
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/kolab/plugins/libkolab/SQL/postgres.initial.sql b/lib/drivers/kolab/plugins/libkolab/SQL/postgres.initial.sql
index e06346c..e06346c 100644
--- a/lib/kolab/plugins/libkolab/SQL/postgres.initial.sql
+++ b/lib/drivers/kolab/plugins/libkolab/SQL/postgres.initial.sql
diff --git a/lib/kolab/plugins/libkolab/UPGRADING b/lib/drivers/kolab/plugins/libkolab/UPGRADING
index e7f04d8..e7f04d8 100644
--- a/lib/kolab/plugins/libkolab/UPGRADING
+++ b/lib/drivers/kolab/plugins/libkolab/UPGRADING
diff --git a/lib/kolab/plugins/libkolab/bin/modcache.sh b/lib/drivers/kolab/plugins/libkolab/bin/modcache.sh
index da6e4f8..533fefd 100755
--- a/lib/kolab/plugins/libkolab/bin/modcache.sh
+++ b/lib/drivers/kolab/plugins/libkolab/bin/modcache.sh
@@ -1,4 +1,4 @@
-#!/usr/bin/env php -d enable_dl=On
+#!/usr/bin/env php
<?php
/**
@@ -7,7 +7,7 @@
* @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
@@ -65,6 +65,7 @@ $db->db_connect('w');
if (!$db->is_connected() || $db->is_error())
die("No DB connection\n");
+ini_set('display_errors', 1);
/*
* Script controller
@@ -109,7 +110,7 @@ case 'clear':
}
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;
@@ -142,6 +143,32 @@ case 'prewarm':
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
@@ -194,7 +221,7 @@ function authenticate(&$opts)
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");
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
new file mode 100644
index 0000000..79d2aa8
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist
@@ -0,0 +1,61 @@
+<?php
+
+/* Configuration for libkolab */
+
+// Enable caching of Kolab objects in local database
+$config['kolab_cache'] = true;
+
+// Specify format version to write Kolab objects (must be a string value!)
+$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
+$config['kolab_freebusy_server'] = null;
+
+// Enables listing of only subscribed folders. This e.g. will limit
+// folders in calendar view or available addressbooks
+$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!)
+$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)
+$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
+$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/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/kolab/plugins/libkolab/lib/kolab_date_recurrence.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_date_recurrence.php
@@ -101,7 +101,7 @@ class kolab_date_recurrence
/**
* 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()
{
@@ -109,25 +109,25 @@ class kolab_date_recurrence
// 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/kolab/plugins/libkolab/lib/kolab_format.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php
index aa88f69..8c6b1d4 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php
@@ -45,7 +45,118 @@ abstract class kolab_format
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';
+
+ // mapping table for valid PHP timezones not supported by libkolabxml
+ // basically the entire list of ftp://ftp.iana.org/tz/data/backward
+ protected static $timezone_map = array(
+ 'Africa/Asmera' => 'Africa/Asmara',
+ 'Africa/Timbuktu' => 'Africa/Abidjan',
+ 'America/Argentina/ComodRivadavia' => 'America/Argentina/Catamarca',
+ 'America/Atka' => 'America/Adak',
+ 'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
+ 'America/Catamarca' => 'America/Argentina/Catamarca',
+ 'America/Coral_Harbour' => 'America/Atikokan',
+ 'America/Cordoba' => 'America/Argentina/Cordoba',
+ 'America/Ensenada' => 'America/Tijuana',
+ 'America/Fort_Wayne' => 'America/Indiana/Indianapolis',
+ 'America/Indianapolis' => 'America/Indiana/Indianapolis',
+ 'America/Jujuy' => 'America/Argentina/Jujuy',
+ 'America/Knox_IN' => 'America/Indiana/Knox',
+ 'America/Louisville' => 'America/Kentucky/Louisville',
+ 'America/Mendoza' => 'America/Argentina/Mendoza',
+ 'America/Porto_Acre' => 'America/Rio_Branco',
+ 'America/Rosario' => 'America/Argentina/Cordoba',
+ 'America/Virgin' => 'America/Port_of_Spain',
+ 'Asia/Ashkhabad' => 'Asia/Ashgabat',
+ 'Asia/Calcutta' => 'Asia/Kolkata',
+ 'Asia/Chungking' => 'Asia/Shanghai',
+ 'Asia/Dacca' => 'Asia/Dhaka',
+ 'Asia/Katmandu' => 'Asia/Kathmandu',
+ 'Asia/Macao' => 'Asia/Macau',
+ 'Asia/Saigon' => 'Asia/Ho_Chi_Minh',
+ 'Asia/Tel_Aviv' => 'Asia/Jerusalem',
+ 'Asia/Thimbu' => 'Asia/Thimphu',
+ 'Asia/Ujung_Pandang' => 'Asia/Makassar',
+ 'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar',
+ 'Atlantic/Faeroe' => 'Atlantic/Faroe',
+ 'Atlantic/Jan_Mayen' => 'Europe/Oslo',
+ 'Australia/ACT' => 'Australia/Sydney',
+ 'Australia/Canberra' => 'Australia/Sydney',
+ 'Australia/LHI' => 'Australia/Lord_Howe',
+ 'Australia/NSW' => 'Australia/Sydney',
+ 'Australia/North' => 'Australia/Darwin',
+ 'Australia/Queensland' => 'Australia/Brisbane',
+ 'Australia/South' => 'Australia/Adelaide',
+ 'Australia/Tasmania' => 'Australia/Hobart',
+ 'Australia/Victoria' => 'Australia/Melbourne',
+ 'Australia/West' => 'Australia/Perth',
+ 'Australia/Yancowinna' => 'Australia/Broken_Hill',
+ 'Brazil/Acre' => 'America/Rio_Branco',
+ 'Brazil/DeNoronha' => 'America/Noronha',
+ 'Brazil/East' => 'America/Sao_Paulo',
+ 'Brazil/West' => 'America/Manaus',
+ 'Canada/Atlantic' => 'America/Halifax',
+ 'Canada/Central' => 'America/Winnipeg',
+ 'Canada/East-Saskatchewan' => 'America/Regina',
+ 'Canada/Eastern' => 'America/Toronto',
+ 'Canada/Mountain' => 'America/Edmonton',
+ 'Canada/Newfoundland' => 'America/St_Johns',
+ 'Canada/Pacific' => 'America/Vancouver',
+ 'Canada/Saskatchewan' => 'America/Regina',
+ 'Canada/Yukon' => 'America/Whitehorse',
+ 'Chile/Continental' => 'America/Santiago',
+ 'Chile/EasterIsland' => 'Pacific/Easter',
+ 'Cuba' => 'America/Havana',
+ 'Egypt' => 'Africa/Cairo',
+ 'Eire' => 'Europe/Dublin',
+ 'Europe/Belfast' => 'Europe/London',
+ 'Europe/Tiraspol' => 'Europe/Chisinau',
+ 'GB' => 'Europe/London',
+ 'GB-Eire' => 'Europe/London',
+ 'Greenwich' => 'Etc/GMT',
+ 'Hongkong' => 'Asia/Hong_Kong',
+ 'Iceland' => 'Atlantic/Reykjavik',
+ 'Iran' => 'Asia/Tehran',
+ 'Israel' => 'Asia/Jerusalem',
+ 'Jamaica' => 'America/Jamaica',
+ 'Japan' => 'Asia/Tokyo',
+ 'Kwajalein' => 'Pacific/Kwajalein',
+ 'Libya' => 'Africa/Tripoli',
+ 'Mexico/BajaNorte' => 'America/Tijuana',
+ 'Mexico/BajaSur' => 'America/Mazatlan',
+ 'Mexico/General' => 'America/Mexico_City',
+ 'NZ' => 'Pacific/Auckland',
+ 'NZ-CHAT' => 'Pacific/Chatham',
+ 'Navajo' => 'America/Denver',
+ 'PRC' => 'Asia/Shanghai',
+ 'Pacific/Ponape' => 'Pacific/Pohnpei',
+ 'Pacific/Samoa' => 'Pacific/Pago_Pago',
+ 'Pacific/Truk' => 'Pacific/Chuuk',
+ 'Pacific/Yap' => 'Pacific/Chuuk',
+ 'Poland' => 'Europe/Warsaw',
+ 'Portugal' => 'Europe/Lisbon',
+ 'ROC' => 'Asia/Taipei',
+ 'ROK' => 'Asia/Seoul',
+ 'Singapore' => 'Asia/Singapore',
+ 'Turkey' => 'Europe/Istanbul',
+ 'UCT' => 'Etc/UCT',
+ 'US/Alaska' => 'America/Anchorage',
+ 'US/Aleutian' => 'America/Adak',
+ 'US/Arizona' => 'America/Phoenix',
+ 'US/Central' => 'America/Chicago',
+ 'US/East-Indiana' => 'America/Indiana/Indianapolis',
+ 'US/Eastern' => 'America/New_York',
+ 'US/Hawaii' => 'Pacific/Honolulu',
+ 'US/Indiana-Starke' => 'America/Indiana/Knox',
+ 'US/Michigan' => 'America/Detroit',
+ 'US/Mountain' => 'America/Denver',
+ 'US/Pacific' => 'America/Los_Angeles',
+ 'US/Samoa' => 'Pacific/Pago_Pago',
+ 'Universal' => 'Etc/UTC',
+ 'W-SU' => 'Europe/Moscow',
+ 'Zulu' => 'Etc/UTC',
+ );
/**
* Factory method to instantiate a kolab_format object of the given type and version
@@ -63,7 +174,7 @@ abstract class kolab_format
if (!self::supports($version))
return PEAR::raiseError("No support for Kolab format version " . $version);
- $type = preg_replace('/configuration\.[a-z.]+$/', 'configuration', $type);
+ $type = preg_replace('/configuration\.[a-z._]+$/', 'configuration', $type);
$suffix = preg_replace('/[^a-z]+/', '', $type);
$classname = 'kolab_format_' . $suffix;
if (class_exists($classname))
@@ -123,10 +234,15 @@ abstract class kolab_format
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());
+ }
+ else if ($tz !== false) {
+ $tzid = $tz->getName();
+ if (array_key_exists($tzid, self::$timezone_map))
+ $tzid = self::$timezone_map[$tzid];
+ $result->setTimezone($tzid);
+ }
}
return $result;
@@ -174,7 +290,7 @@ abstract class kolab_format
* 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)
{
@@ -208,7 +324,11 @@ abstract class kolab_format
*/
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))
+ );
}
@@ -242,7 +362,8 @@ abstract class kolab_format
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;
@@ -410,7 +531,7 @@ abstract class kolab_format
$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))
@@ -418,7 +539,8 @@ abstract class kolab_format
}
$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++) {
@@ -451,10 +573,12 @@ abstract class kolab_format
}
// 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
@@ -496,4 +620,80 @@ abstract class kolab_format
{
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
new file mode 100644
index 0000000..24bc8de
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php
@@ -0,0 +1,282 @@
+<?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 $CTYPEv2 = 'application/x-vnd.kolab.configuration';
+
+ protected $objclass = 'Configuration';
+ protected $read_func = 'readConfiguration';
+ protected $write_func = 'writeConfiguration';
+
+ private $type_map = array(
+ 'category' => Configuration::TypeCategoryColor,
+ 'dictionary' => Configuration::TypeDictionary,
+ 'file_driver' => Configuration::TypeFileDriver,
+ 'relation' => Configuration::TypeRelation,
+ 'snippet' => Configuration::TypeSnippet,
+ );
+
+ private $driver_settings_fields = array('host', 'port', 'username', 'password');
+
+ /**
+ * 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 'file_driver':
+ $driver = new FileDriver($object['driver'], $object['title']);
+
+ $driver->setEnabled((bool) $object['enabled']);
+
+ foreach ($this->driver_settings_fields as $field) {
+ $value = $object[$field];
+ if ($value !== null) {
+ $driver->{'set' . ucfirst($field)}($value);
+ }
+ }
+
+ $this->obj = new Configuration($driver);
+ 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)) {
+ 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 'file_driver':
+ $driver = $this->obj->file_driver();
+
+ $object['driver'] = $driver->driver();
+ $object['title'] = $driver->title();
+ $object['enabled'] = $driver->enabled();
+
+ foreach ($this->driver_settings_fields as $field) {
+ $object[$field] = $driver->{$field}();
+ }
+
+ 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']) {
+ $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();
+
+ 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'];
+ }
+ else {
+ // derive message identifier from URI
+ $words[] = md5($url);
+ }
+ }
+
+ return $words;
+ }
+}
diff --git a/lib/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/kolab/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_contact.php
@@ -107,8 +107,8 @@ class kolab_format_contact extends kolab_format
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;
@@ -117,17 +117,17 @@ class kolab_format_contact extends kolab_format
$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);
@@ -203,6 +203,8 @@ class kolab_format_contact extends kolab_format
$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']))
@@ -219,12 +221,19 @@ class kolab_format_contact extends kolab_format
// 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);
@@ -296,7 +305,7 @@ class kolab_format_contact extends kolab_format
$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)
@@ -304,7 +313,7 @@ class kolab_format_contact extends kolab_format
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);
}
@@ -345,12 +354,13 @@ class kolab_format_contact extends kolab_format
$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])
@@ -362,7 +372,7 @@ class kolab_format_contact extends kolab_format
$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();
@@ -407,6 +417,22 @@ class kolab_format_contact extends kolab_format
}
/**
+ * 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)
@@ -429,7 +455,7 @@ class kolab_format_contact extends kolab_format
/**
* 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);
@@ -438,13 +464,19 @@ class kolab_format_contact extends kolab_format
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/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/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -44,17 +44,29 @@ class kolab_format_distributionlist extends kolab_format
$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);
diff --git a/lib/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/kolab/plugins/libkolab/lib/kolab_format_event.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php
@@ -26,6 +26,8 @@ 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';
@@ -87,10 +89,12 @@ class kolab_format_event extends kolab_format_xcal
$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;
@@ -163,20 +167,22 @@ class kolab_format_event extends kolab_format_xcal
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;
@@ -189,16 +195,12 @@ class kolab_format_event extends kolab_format_xcal
*/
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;
}
diff --git a/lib/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/kolab/plugins/libkolab/lib/kolab_format_file.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_file.php
@@ -25,7 +25,7 @@
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';
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_journal.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_journal.php
index f7ccd31..f7ccd31 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_journal.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_journal.php
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php
new file mode 100644
index 0000000..bca5156
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php
@@ -0,0 +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/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);
+
+ $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);
+
+ $sensitivity_map = array_flip($this->sensitivity_map);
+
+ // 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/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/kolab/plugins/libkolab/lib/kolab_format_task.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_task.php
@@ -26,6 +26,8 @@ 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';
@@ -43,9 +45,14 @@ class kolab_format_task extends kolab_format_xcal
$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;
@@ -106,17 +113,14 @@ class kolab_format_task extends kolab_format_xcal
*/
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'];
diff --git a/lib/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/kolab/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php
@@ -30,6 +30,8 @@ abstract class kolab_format_xcal extends kolab_format
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,
@@ -76,11 +78,15 @@ abstract class kolab_format_xcal extends kolab_format
'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(
@@ -121,6 +127,10 @@ abstract class kolab_format_xcal extends kolab_format
'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(
@@ -137,6 +147,16 @@ abstract class kolab_format_xcal extends kolab_format
$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()],
@@ -144,6 +164,8 @@ abstract class kolab_format_xcal extends kolab_format
'rsvp' => $attendee->rsvp(),
'email' => $cr->email(),
'name' => $cr->name(),
+ 'delegated-from' => $delegators,
+ 'delegated-to' => $delegatees,
);
}
}
@@ -191,54 +213,83 @@ abstract class kolab_format_xcal extends kolab_format
}
}
+ 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;
}
@@ -253,14 +304,43 @@ abstract class kolab_format_xcal extends kolab_format
$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']);
@@ -270,9 +350,13 @@ abstract class kolab_format_xcal extends kolab_format
$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;
}
@@ -285,7 +369,24 @@ abstract class kolab_format_xcal extends kolab_format
$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);
@@ -311,7 +412,7 @@ abstract class kolab_format_xcal extends kolab_format
$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'])
@@ -368,9 +469,77 @@ abstract class kolab_format_xcal extends kolab_format
$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
@@ -401,24 +570,7 @@ abstract class kolab_format_xcal extends kolab_format
}
$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);
}
/**
@@ -449,4 +601,27 @@ abstract class kolab_format_xcal extends kolab_format
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/kolab/plugins/libkolab/lib/kolab_storage.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php
index 5f8b9c6..dfd1887 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php
@@ -7,7 +7,7 @@
* @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
@@ -37,12 +37,16 @@ class kolab_storage
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(
@@ -83,7 +87,6 @@ class kolab_storage
'skip_deleted' => true,
'threading' => false,
));
- self::$imap->set_pagesize(9999);
}
else if (!class_exists('kolabformat')) {
rcube::raise_error(array(
@@ -101,6 +104,41 @@ class kolab_storage
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
@@ -178,15 +216,45 @@ class kolab_storage
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
@@ -196,17 +264,58 @@ class kolab_storage
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 '';
}
@@ -223,6 +332,9 @@ class kolab_storage
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();
@@ -243,6 +355,12 @@ class kolab_storage
{
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) {
@@ -280,6 +398,10 @@ class kolab_storage
{
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);
@@ -433,13 +555,14 @@ class kolab_storage
// 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;
@@ -467,7 +590,7 @@ class kolab_storage
$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';
@@ -492,7 +615,8 @@ class kolab_storage
}
/**
- * 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)
{
@@ -504,10 +628,29 @@ class kolab_storage
$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;
@@ -525,8 +668,8 @@ class kolab_storage
*/
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();
@@ -540,13 +683,24 @@ class kolab_storage
// 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();
@@ -558,14 +712,6 @@ class kolab_storage
$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);
@@ -619,18 +765,29 @@ class kolab_storage
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();
@@ -643,15 +800,21 @@ class kolab_storage
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 (?)
@@ -661,6 +824,11 @@ class kolab_storage
// 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)) {
@@ -674,6 +842,72 @@ class kolab_storage
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
@@ -683,26 +917,96 @@ class kolab_storage
*/
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;
}
@@ -719,13 +1023,57 @@ class kolab_storage
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];
}
@@ -756,6 +1104,13 @@ class kolab_storage
{
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)) {
@@ -797,17 +1152,21 @@ class kolab_storage
* 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']));
}
@@ -815,14 +1174,25 @@ class kolab_storage
* 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;
}
@@ -835,14 +1205,22 @@ class kolab_storage
* 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;
}
@@ -875,6 +1253,8 @@ class kolab_storage
*/
public static function folder_activate($folder)
{
+ // activation implies temporary subscription
+ self::folder_subscribe($folder, true);
return self::set_state($folder, true);
}
@@ -888,6 +1268,9 @@ class kolab_storage
*/
public static function folder_deactivate($folder)
{
+ // remove from temp subscriptions, really?
+ self::folder_unsubscribe($folder, true);
+
return self::set_state($folder, false);
}
@@ -911,7 +1294,9 @@ class kolab_storage
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, '**');
@@ -968,7 +1353,7 @@ class kolab_storage
// 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;
@@ -1046,4 +1431,137 @@ class kolab_storage
}
}
+
+ /**
+ *
+ * @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/kolab/plugins/libkolab/lib/kolab_storage_cache.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php
index 651dc18..bced3b3 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php
@@ -24,12 +24,13 @@
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;
@@ -43,6 +44,8 @@ class kolab_storage_cache
protected $max_sync_lock_time = 600;
protected $binary_items = array();
protected $extra_cols = array();
+ protected $order_by = null;
+ protected $limit = null;
/**
@@ -58,8 +61,10 @@ class kolab_storage_cache
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);
}
}
@@ -85,6 +90,24 @@ class kolab_storage_cache
$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
@@ -104,7 +127,7 @@ class kolab_storage_cache
$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;
}
@@ -117,6 +140,15 @@ class kolab_storage_cache
}
/**
+ * 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()
@@ -128,65 +160,71 @@ class kolab_storage_cache
// 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();
@@ -214,21 +252,23 @@ class kolab_storage_cache
$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);
+ }
}
}
@@ -258,13 +298,13 @@ class kolab_storage_cache
// 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
@@ -274,41 +314,48 @@ class kolab_storage_cache
/**
- * 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(
@@ -329,26 +376,32 @@ class kolab_storage_cache
*
* @param string Entry's IMAP message UID
* @param string Entry's Object UID
- * @param string Target IMAP folder to move it to
+ * @param object kolab_storage_folder Target storage folder instance
*/
- public function move($msguid, $uid, $target_folder)
+ public function move($msguid, $uid, $target)
{
- $target = kolab_storage::get_folder($target_folder);
+ if ($this->ready) {
+ // clear cached uid mapping and force new lookup
+ unset($target->cache->uid2msg[$uid]);
- // 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);
}
@@ -362,12 +415,17 @@ class kolab_storage_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);
}
@@ -378,12 +436,16 @@ class kolab_storage_cache
*/
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
);
@@ -399,43 +461,67 @@ class kolab_storage_cache
*/
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
}
@@ -445,7 +531,7 @@ class kolab_storage_cache
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]);
}
}
@@ -461,32 +547,67 @@ class kolab_storage_cache
*/
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
@@ -515,7 +636,7 @@ class kolab_storage_cache
$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 {
@@ -557,7 +678,7 @@ class kolab_storage_cache
*/
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;
@@ -568,43 +689,6 @@ class kolab_storage_cache
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
*/
@@ -645,7 +729,9 @@ class kolab_storage_cache
}
}
- $sql_data['data'] = serialize($data);
+ // use base64 encoding (Bug #1912, #2662)
+ $sql_data['data'] = base64_encode(serialize($data));
+
return $sql_data;
}
@@ -654,8 +740,23 @@ class kolab_storage_cache
*/
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)) {
@@ -663,12 +764,15 @@ class kolab_storage_cache
}
}
+ $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;
}
@@ -686,6 +790,40 @@ class kolab_storage_cache
$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),
@@ -704,12 +842,18 @@ class kolab_storage_cache
}
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',
@@ -743,16 +887,23 @@ class kolab_storage_cache
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();
}
@@ -767,7 +918,7 @@ class kolab_storage_cache
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()) {
@@ -784,7 +935,7 @@ class kolab_storage_cache
}
// 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);
}
/**
@@ -796,7 +947,7 @@ class kolab_storage_cache
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
);
@@ -813,12 +964,28 @@ class kolab_storage_cache
*/
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];
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
new file mode 100644
index 0000000..c3c7ac4
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php
@@ -0,0 +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;
+ }
+
+ /**
+ * 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/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/kolab/plugins/libkolab/lib/kolab_storage_cache_contact.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_contact.php
@@ -23,7 +23,7 @@
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',
@@ -40,6 +40,20 @@ class kolab_storage_cache_contact extends kolab_storage_cache
$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/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/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php
@@ -34,14 +34,14 @@ class kolab_storage_cache_event extends kolab_storage_cache
{
$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;
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_file.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_file.php
index ea1823d..ea1823d 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_file.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_file.php
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_freebusy.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_freebusy.php
index d8ab554..d8ab554 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_freebusy.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_freebusy.php
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_journal.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_journal.php
index a63577b..a63577b 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_journal.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_journal.php
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_mongodb.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_mongodb.php
index 8ae95e4..8ae95e4 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_mongodb.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_mongodb.php
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_note.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_note.php
index 8546927..8546927 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_note.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_note.php
diff --git a/lib/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/kolab/plugins/libkolab/lib/kolab_storage_cache_task.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_task.php
@@ -35,9 +35,9 @@ class kolab_storage_cache_task extends kolab_storage_cache
$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;
}
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..d58e3c0
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php
@@ -0,0 +1,840 @@
+<?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,
+ );
+ }
+
+ return false;
+ }
+
+ /**
+ * 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 objects linked with the given groupware object through a relation
+ *
+ * @param string Object UUID
+ * @param array List of related URIs
+ */
+ public function get_object_links($uid)
+ {
+ $links = array();
+ $object_uri = self::build_member_url($uid);
+
+ foreach ($this->get_relations_for_member($uid) as $relation) {
+ if (in_array($object_uri, (array) $relation['members'])) {
+ // make relation members up-to-date
+ kolab_storage_config::resolve_members($relation);
+
+ foreach ($relation['members'] as $member) {
+ if ($member != $object_uri) {
+ $links[] = $member;
+ }
+ }
+ }
+ }
+
+ return array_unique($links);
+ }
+
+ /**
+ *
+ */
+ public function save_object_links($uid, $links, $remove = array())
+ {
+ $object_uri = self::build_member_url($uid);
+ $relations = $this->get_relations_for_member($uid);
+ $done = false;
+
+ foreach ($relations as $relation) {
+ // make relation members up-to-date
+ kolab_storage_config::resolve_members($relation);
+
+ // remove and add links
+ $members = array_diff($relation['members'], (array)$remove);
+ $members = array_unique(array_merge($members, $links));
+
+ // make sure the object_uri is still a member
+ if (!in_array($object_uri, $members)) {
+ $members[$object_uri];
+ }
+
+ // remove relation if no other members remain
+ if (count($members) <= 1) {
+ $done = $this->delete($relation['uid']);
+ }
+ // update relation object if members changed
+ else if (count(array_diff($members, $relation['members'])) || count(array_diff($relation['members'], $members))) {
+ $relation['members'] = $members;
+ $done = $this->save($relation, 'relation');
+ $links = array();
+ }
+ // no changes, we're happy
+ else {
+ $done = true;
+ $links = array();
+ }
+ }
+
+ // create a new relation
+ if (!$done && !empty($links)) {
+ $relation = array(
+ 'members' => array_merge($links, array($object_uri)),
+ 'category' => 'generic',
+ );
+
+ $ret = $this->save($relation, 'relation');
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Find relation objects referring to specified note
+ */
+ public function get_relations_for_member($uid, $reltype = 'generic')
+ {
+ $default = true;
+ $filter = array(
+ array('type', '=', 'relation'),
+ array('category', '=', $reltype),
+ array('member', '=', $uid),
+ );
+
+ return $this->get_objects($filter, $default, 100);
+ }
+
+ /**
+ * 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)
+ {
+ static $_cache = array();
+
+ $result = array();
+ $uids = array();
+ $default = true;
+ $uri = self::get_message_uri($message, $folder);
+ $filter = array(
+ array('type', '=', 'relation'),
+ array('category', '=', 'generic'),
+ );
+
+ // query by message-id
+ $member_id = $message->get('message-id', false);
+ if (empty($member_id)) {
+ // derive message identifier from URI
+ $member_id = md5($uri);
+ }
+ $filter[] = array('member', '=', $member_id);
+
+ if (!isset($_cache[$uri])) {
+ // get UIDs of related groupware objects
+ 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 groupware object UID(s)
+ foreach ($relation['members'] as $member) {
+ if (strpos($member, 'urn:uuid:') === 0) {
+ $uids[] = substr($member, 9);
+ }
+ }
+ }
+
+ // remember this lookup
+ $_cache[$uri] = $uids;
+ }
+ else {
+ $uids = $_cache[$uri];
+ }
+
+ // 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/kolab/plugins/libkolab/lib/kolab_storage_folder.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php
index aabc130..2435fa3 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder.php
@@ -22,38 +22,15 @@
* 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;
@@ -62,7 +39,7 @@ class kolab_storage_folder
*/
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);
}
@@ -81,8 +58,12 @@ class kolab_storage_folder
$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)
@@ -94,149 +75,6 @@ class kolab_storage_folder
/**
- *
- */
- 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()
@@ -280,10 +118,13 @@ class kolab_storage_folder
}
// 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());
}
/**
@@ -424,6 +265,21 @@ class kolab_storage_folder
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
@@ -494,7 +350,30 @@ class kolab_storage_folder
{
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;
@@ -564,7 +443,7 @@ class kolab_storage_folder
// 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) {
@@ -753,23 +632,27 @@ class kolab_storage_folder
$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);
}
}
@@ -805,7 +688,7 @@ class kolab_storage_folder
$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;
}
}
@@ -916,9 +799,12 @@ class kolab_storage_folder
*/
public function move($uid, $target_folder)
{
+ if (is_string($target_folder))
+ $target_folder = kolab_storage::get_folder($target_folder);
+
if ($msguid = $this->cache->uid2msguid($uid)) {
$this->cache->bypass(true);
- $result = $this->imap->move_message($msguid, $target_folder, $this->name);
+ $result = $this->imap->move_message($msguid, $target_folder->name, $this->name);
$this->cache->bypass(false);
if ($result) {
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/kolab/plugins/libkolab/libkolab.php b/lib/drivers/kolab/plugins/libkolab/libkolab.php
index 48a5033..052724c 100644
--- a/lib/kolab/plugins/libkolab/libkolab.php
+++ b/lib/drivers/kolab/plugins/libkolab/libkolab.php
@@ -37,12 +37,13 @@ class libkolab extends rcube_plugin
// 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'));
@@ -123,4 +124,15 @@ class libkolab extends rcube_plugin
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/kolab/plugins/libkolab/package.xml b/lib/drivers/kolab/plugins/libkolab/package.xml
index cd3e3a0..cd3e3a0 100644
--- a/lib/kolab/plugins/libkolab/package.xml
+++ b/lib/drivers/kolab/plugins/libkolab/package.xml
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.png b/lib/drivers/seafile/seafile.png
new file mode 100644
index 0000000..505ba4c
--- /dev/null
+++ b/lib/drivers/seafile/seafile.png
Binary files differ
diff --git a/lib/drivers/seafile/seafile_api.php b/lib/drivers/seafile/seafile_api.php
new file mode 100644
index 0000000..f3ec251
--- /dev/null
+++ b/lib/drivers/seafile/seafile_api.php
@@ -0,0 +1,849 @@
+<?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, $m) && ($seconds = $m[1]) < self::WAIT_LIMIT) {
+ sleep($seconds);
+ return $this->request($method, $url, $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)
+ {
+ // sanity checks
+ if ($username === '' || !is_string($username) || $password === '' || !is_string($password)) {
+ $this->status = self::BAD_REQUEST;
+ return false;
+ }
+
+ $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
new file mode 100644
index 0000000..1531bcc
--- /dev/null
+++ b/lib/drivers/seafile/seafile_file_storage.php
@@ -0,0 +1,1200 @@
+<?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[$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('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),
+ 'cache' => $this->rc->config->get('fileapi_seafile_cache'),
+ 'cache_ttl' => $this->rc->config->get('fileapi_seafile_cache', '14d'),
+ 'debug' => $this->rc->config->get('fileapi_seafile_debug', false),
+ );
+
+ $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[$this->title . 'seafile_token']
+ && ($token = $this->rc->decrypt($_SESSION[$this->title . 'seafile_token']))
+ ) {
+ $valid = $this->api->ping($token);
+ }
+
+ 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);
+ }
+ else if (!$valid && $this->api->is_error() == seafile_api::TOO_MANY_REQUESTS) {
+ throw new Exception("SeaFile storage temporarily unavailable (too many requests)", file_storage::ERROR);
+ }
+
+ return $valid;
+ }
+
+ /**
+ * Configures environment
+ *
+ * @param array $config Configuration
+ * @param string $title Source identifier
+ */
+ 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);
+ }
+
+ /**
+ * Returns metadata of the driver
+ *
+ * @return array Driver meta data (image, name, form)
+ */
+ public function driver_metadata()
+ {
+ $image_content = file_get_contents(__DIR__ . '/seafile.png');
+
+ $metadata = array(
+ 'image' => 'data:image/png;base64,' . base64_encode($image_content),
+ 'name' => 'SeaFile',
+ 'ref' => 'http://seafile.com',
+ 'description' => 'Storage implementing SeaFile API access',
+ 'form' => array(
+ 'host' => 'hostname',
+ 'username' => 'username',
+ 'password' => 'password',
+ ),
+ );
+
+ // these are returned when authentication on folders list fails
+ if ($this->config['username']) {
+ $metadata['form_values'] = array(
+ 'host' => $this->config['host'],
+ 'username' => $this->config['username'],
+ );
+ }
+
+ return $metadata;
+ }
+
+ /**
+ * Validate metadata (config) of the driver
+ *
+ * @param array $metadata Driver metadata
+ *
+ * @return array Driver meta data to be stored in configuration
+ * @throws Exception
+ */
+ public function driver_validate($metadata)
+ {
+ if (!is_string($metadata['username']) || !strlen($metadata['username'])) {
+ throw new Exception("Missing user name.", file_storage::ERROR);
+ }
+
+ if (!is_string($metadata['password']) || !strlen($metadata['password'])) {
+ throw new Exception("Missing user password.", file_storage::ERROR);
+ }
+
+ if (!is_string($metadata['host']) || !strlen($metadata['host'])) {
+ throw new Exception("Missing host name.", file_storage::ERROR);
+ }
+
+ $this->config['host'] = $metadata['host'];
+
+ if (!$this->authenticate($metadata['username'], $metadata['password'])) {
+ throw new Exception("Unable to authenticate user", file_storage::ERROR_NOAUTH);
+ }
+
+ return array(
+ 'host' => $metadata['host'],
+ 'username' => $metadata['username'],
+ 'password' => $metadata['password'],
+ );
+ }
+
+ /**
+ * 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);
+ }
+
+ 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', '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 = $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();
+
+ if ($this->config['cache']) {
+ $cache = $this->rc->get_cache('seafile_' . $this->title,
+ $this->config['cache'], $this->config['cache_ttl'], true);
+
+ if ($cache) {
+ $cached = $cache->get('folders');
+ }
+ }
+
+ foreach ($this->libraries as $library) {
+ if ($library['virtual'] || $library['encrypted']) {
+ continue;
+ }
+
+ $folders[$library['name']] = $library['mtime'];
+
+ if ($folder_tree = $this->folders_tree($library, '', $library, $cached)) {
+ $folders = array_merge($folders, $folder_tree);
+ }
+ }
+
+ if (empty($folders)) {
+ throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR);
+ }
+
+ if ($cache) {
+ $cache->set('folders', $folders);
+ }
+
+ // sort folders
+ $folders = array_keys($folders);
+ usort($folders, array($this, 'sort_folder_comparator'));
+
+ 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)
+ {
+ 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(
+ // expected values in kB
+ 'total' => intval($account_info['total'] / 1024),
+ 'used' => intval($account_info['usage'] / 1024),
+ );
+
+ return $quota;
+ }
+
+ /**
+ * Recursively builds folders list
+ */
+ protected function folders_tree($library, $path, $folder, $cached)
+ {
+ $folders = array();
+ $fname = strlen($path) ? $path . $folder['name'] : '/';
+ $root = $library['name'] . ($fname != '/' ? $fname : '');
+
+ // nothing changed, use cached folders tree of this folder
+ if ($cached && $cached[$root] && $cached[$root] == $folder['mtime']) {
+ foreach ($cached as $folder_name => $mtime) {
+ if (strpos($folder_name, $root . '/') === 0) {
+ $folders[$folder_name] = $mtime;
+ }
+ }
+ }
+ // get folder content (files and sub-folders)
+ // there's no API method to get only folders
+ else if ($content = $this->api->directory_entries($library['id'], $fname)) {
+ if ($fname != '/') {
+ $fname .= '/';
+ }
+
+ foreach ($content as $item) {
+ if ($item['type'] == 'dir' && strlen($item['name'])) {
+ $folders[$root . '/' . $item['name']] = $item['mtime'];
+
+ // get subfolders recursively
+ $folders_tree = $this->folders_tree($library, $fname, $item, $cached);
+ 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
+ );
+ }
+
+ /**
+ * 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
new file mode 100644
index 0000000..0ab06b5
--- /dev/null
+++ b/lib/drivers/seafile/seafile_request_observer.php
@@ -0,0 +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("File destination not specified");
+ }
+
+ break;
+
+ case 'receivedBodyPart':
+ case 'receivedEncodedBodyPart':
+ fwrite($this->fp, $event['data']);
+ break;
+
+ case 'receivedBody':
+ if ($this->file) {
+ fclose($this->fp);
+ }
+ break;
+ }
+ }
+}
diff --git a/lib/file_api.php b/lib/file_api.php
index c9631c6..4871e65 100644
--- a/lib/file_api.php
+++ b/lib/file_api.php
@@ -22,53 +22,39 @@
+--------------------------------------------------------------------------+
*/
-class file_api
+class file_api extends file_locale
{
- 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/' . $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);
+ $this->locale_init();
}
/**
@@ -223,8 +209,8 @@ class file_api
}
if (!empty($username)) {
- $this->api_init();
- $result = $this->api->authenticate($username, $password);
+ $backend = $this->get_backend();
+ $result = $backend->authenticate($username, $password);
}
if (empty($result)) {
@@ -270,308 +256,211 @@ class file_api
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']);
+ if ($path == $item['title'] || strpos($path, $prefix) === 0) {
+ $selected = $item;
+ break;
+ }
+ }
- case 'folder_list':
- return $this->api->folder_list();
+ if (empty($selected)) {
+ return array($this->get_backend(), $path);
+ }
- case 'quota':
- $quota = $this->api->quota($args['folder']);
+ $path = substr($path, strlen($selected['title']) + 1);
- 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));
- }
- }
+ return array($this->get_driver_object($selected), $path);
+ }
- return $quota;
+ /**
+ * Initialize driver instance
+ *
+ * @param array $config Driver config
+ *
+ * @return file_storage Storage driver instance
+ */
+ public function get_driver_object($config)
+ {
+ $key = $config['title'];
- 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);
- }
- }
+ if (empty($this->drivers[$key])) {
+ $this->drivers[$key] = $driver = $this->load_driver_object($config['driver']);
- $this->api->lock($args['uri'], $args);
- return;
+ if ($config['username'] == '%u') {
+ $rcube = rcube::get_instance();
+ $config['username'] = $_SESSION['user'];
+ $config['password'] = $rcube->decrypt($_SESSION['password']);
+ }
- case 'unlock':
- foreach (array('uri', 'token') as $arg) {
- if (!isset($args[$arg]) || $args[$arg] === '') {
- throw new Exception("Missing lock $arg", file_api::ERROR_CODE);
- }
- }
+ // configure api
+ $driver->configure(array_merge($config, $this->config), $key);
+ }
- $this->api->unlock($args['uri'], $args);
- return;
+ return $this->drivers[$key];
+ }
- case 'lock_list':
- $child_locks = !empty($args['child_locks']) && rcube_utils::get_boolean($args['child_locks']);
+ /**
+ * Loads a driver
+ */
+ public function load_driver_object($name)
+ {
+ $class = $name . '_file_storage';
- return $this->api->lock_list($args['uri'], $child_locks);
+ if (!class_exists($class, false)) {
+ $include_path = RCUBE_INSTALL_PATH . "/lib/drivers/$name" . PATH_SEPARATOR;
+ $include_path .= ini_get('include_path');
+ set_include_path($include_path);
}
- if ($request) {
- throw new Exception("Unknown method", 501);
- }
+ return new $class;
}
/**
- * 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;
}
/**
@@ -603,33 +492,6 @@ class file_api
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
*
@@ -657,81 +519,6 @@ class file_api
}
/**
- * 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)
diff --git a/lib/file_locale.php b/lib/file_locale.php
new file mode 100644
index 0000000..d924a54
--- /dev/null
+++ b/lib/file_locale.php
@@ -0,0 +1,122 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2011-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> |
+ | Author: Jeroen van Meeuwen <vanmeeuwen@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+class file_locale
+{
+ protected static $translation = array();
+
+
+ /**
+ * Localization initialization.
+ */
+ protected function locale_init()
+ {
+ $language = $this->get_language();
+ $LANG = array();
+
+ if (!$language) {
+ $language = 'en_US';
+ }
+
+ @include RCUBE_INSTALL_PATH . "/lib/locale/en_US.php";
+
+ if ($language != 'en_US') {
+ @include RCUBE_INSTALL_PATH . "/lib/locale/$language.php";
+ }
+
+ setlocale(LC_ALL, $language . '.utf8', $language . 'UTF-8', 'en_US.utf8', 'en_US.UTF-8');
+
+ self::$translation = $LANG;
+ }
+
+ /**
+ * Returns system language (locale) setting.
+ *
+ * @return string Language code
+ */
+ protected function get_language()
+ {
+ $aliases = array(
+ 'de' => 'de_DE',
+ 'en' => 'en_US',
+ 'pl' => 'pl_PL',
+ );
+
+ // UI language
+ $langs = !empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
+ $langs = explode(',', $langs);
+
+ if (!empty($_SESSION['user']) && !empty($_SESSION['user']['language'])) {
+ array_unshift($langs, $_SESSION['user']['language']);
+ }
+
+ while ($lang = array_shift($langs)) {
+ $lang = explode(';', $lang);
+ $lang = $lang[0];
+ $lang = str_replace('-', '_', $lang);
+
+ if (file_exists(RCUBE_INSTALL_PATH . "/lib/locale/$lang.php")) {
+ return $lang;
+ }
+
+ if (isset($aliases[$lang]) && ($alias = $aliases[$lang])
+ && file_exists(RCUBE_INSTALL_PATH . "/lib/locale/$alias.php")
+ ) {
+ return $alias;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns translation of defined label/message.
+ *
+ * @return string Translated string.
+ */
+ public static function translate()
+ {
+ $args = func_get_args();
+
+ if (is_array($args[0])) {
+ $args = $args[0];
+ }
+
+ $label = $args[0];
+
+ if (isset(self::$translation[$label])) {
+ $content = trim(self::$translation[$label]);
+ }
+ else {
+ $content = $label;
+ }
+
+ for ($i = 1, $len = count($args); $i < $len; $i++) {
+ $content = str_replace('$'.$i, $args[$i], $content);
+ }
+
+ return $content;
+ }
+}
diff --git a/lib/file_storage.php b/lib/file_storage.php
index 8c5c526..7abb20f 100644
--- a/lib/file_storage.php
+++ b/lib/file_storage.php
@@ -40,12 +40,14 @@ interface file_storage
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
*
@@ -59,9 +61,17 @@ interface file_storage
/**
* 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
@@ -71,10 +81,64 @@ interface file_storage
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();
+
+ /**
+ * Returns metadata of the driver
+ *
+ * @return array Driver meta data (image, name, form)
+ */
+ public function driver_metadata();
+
+ /**
+ * Validate metadata (config) of the driver
+ *
+ * @param array $metadata Driver metadata
+ *
+ * @return array Driver meta data to be stored in configuration
+ * @throws Exception
+ */
+ public function driver_validate($metadata);
+
+ /**
+ * 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
*/
@@ -143,7 +207,7 @@ interface file_storage
* 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
diff --git a/lib/file_ui.php b/lib/file_ui.php
index 071301b..858030a 100644
--- a/lib/file_ui.php
+++ b/lib/file_ui.php
@@ -23,7 +23,7 @@
+--------------------------------------------------------------------------+
*/
-class file_ui
+class file_ui extends file_locale
{
/**
* @var kolab_client_output
@@ -47,8 +47,6 @@ class file_ui
protected $devel_mode = false;
protected $object_types = array();
- protected static $translation = array();
-
/**
* Class constructor.
@@ -77,29 +75,6 @@ class file_ui
}
/**
- * Localization initialization.
- */
- protected function locale_init()
- {
- $language = $this->get_language();
- $LANG = array();
-
- if (!$language) {
- $language = 'en_US';
- }
-
- @include RCUBE_INSTALL_PATH . '/lib/locale/en_US.php';
-
- if ($language != 'en_US') {
- @include RCUBE_INSTALL_PATH . "/lib/locale/$language.php";
- }
-
- setlocale(LC_ALL, $language . '.utf8', $language . 'UTF-8', 'en_US.utf8', 'en_US.UTF-8');
-
- self::$translation = $LANG;
- }
-
- /**
* Configuration initialization.
*/
private function config_init()
@@ -169,46 +144,6 @@ class file_ui
}
/**
- * Returns system language (locale) setting.
- *
- * @return string Language code
- */
- private function get_language()
- {
- $aliases = array(
- 'de' => 'de_DE',
- 'en' => 'en_US',
- 'pl' => 'pl_PL',
- );
-
- // UI language
- $langs = !empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : '';
- $langs = explode(',', $langs);
-
- if (!empty($_SESSION['user']) && !empty($_SESSION['user']['language'])) {
- array_unshift($langs, $_SESSION['user']['language']);
- }
-
- while ($lang = array_shift($langs)) {
- $lang = explode(';', $lang);
- $lang = $lang[0];
- $lang = str_replace('-', '_', $lang);
-
- if (file_exists(RCUBE_INSTALL_PATH . "/lib/locale/$lang.php")) {
- return $lang;
- }
-
- if (isset($aliases[$lang]) && ($alias = $aliases[$lang])
- && file_exists(RCUBE_INSTALL_PATH . "/lib/locale/$alias.php")
- ) {
- return $alias;
- }
- }
-
- return null;
- }
-
- /**
* User authentication (and authorization).
*/
private function auth()
@@ -465,35 +400,6 @@ class file_ui
}
/**
- * Returns translation of defined label/message.
- *
- * @return string Translated string.
- */
- public static function translate()
- {
- $args = func_get_args();
-
- if (is_array($args[0])) {
- $args = $args[0];
- }
-
- $label = $args[0];
-
- if (isset(self::$translation[$label])) {
- $content = trim(self::$translation[$label]);
- }
- else {
- $content = $label;
- }
-
- for ($i = 1, $len = count($args); $i < $len; $i++) {
- $content = str_replace('$'.$i, $args[$i], $content);
- }
-
- return $content;
- }
-
- /**
* Returns input parameter value.
*
* @param string $name Parameter name
diff --git a/lib/file_ui_output.php b/lib/file_ui_output.php
index 1dad12a..efb91a4 100644
--- a/lib/file_ui_output.php
+++ b/lib/file_ui_output.php
@@ -145,6 +145,7 @@ class file_ui_output
$this->tpl->assign($name, $value);
}
+ $this->env['skin_path'] = 'skins/' . $this->skin . '/';
$script = '';
if (!empty($this->env)) {
@@ -170,7 +171,7 @@ class file_ui_output
$script[] = sprintf('ui.%s(%s);', $cname, implode(',', $args));
}
- $this->tpl->assign('skin_path', 'skins/' . $this->skin . '/');
+ $this->tpl->assign('skin_path', $this->env['skin_path']);
if ($script) {
$script = "<script type=\"text/javascript\">\n" . implode("\n", $script) . "\n</script>";
$this->tpl->assign('script', $script);
diff --git a/lib/file_utils.php b/lib/file_utils.php
index 8ad36a9..416d151 100644
--- a/lib/file_utils.php
+++ b/lib/file_utils.php
@@ -68,6 +68,25 @@ class file_utils
),
);
+ // list of known file extensions, more in Roundcube config
+ static $ext_map = array(
+ 'doc' => 'application/msword',
+ 'gz' => 'application/gzip',
+ 'htm' => 'text/html',
+ 'html' => 'text/html',
+ 'mp3' => 'audio/mpeg',
+ 'odp' => 'application/vnd.oasis.opendocument.presentation',
+ 'ods' => 'application/vnd.oasis.opendocument.spreadsheet',
+ 'odt' => 'application/vnd.oasis.opendocument.text',
+ 'ogg' => 'application/ogg',
+ 'pdf' => 'application/pdf',
+ 'ppt' => 'application/vnd.ms-powerpoint',
+ 'rar' => 'application/x-rar-compressed',
+ 'tgz' => 'application/gzip',
+ 'txt' => 'text/plain',
+ 'zip' => 'application/zip',
+ );
+
/**
* Return list of mimetype prefixes for specified file class
@@ -121,6 +140,35 @@ class file_utils
}
/**
+ * Find mimetype from file name (extension)
+ *
+ * @param string $filename File name
+ * @param string $fallback Follback mimetype
+ *
+ * @return string File mimetype
+ */
+ static function ext_to_type($filename, $fallback = 'application/octet-stream')
+ {
+ static $mime_ext = array();
+
+ $config = rcube::get_instance()->config;
+ $ext = substr($filename, strrpos($filename, '.') + 1);
+
+ if (empty($mime_ext)) {
+ $mime_ext = self::$ext_map;
+ foreach ($config->resolve_paths('mimetypes.php') as $fpath) {
+ $mime_ext = array_merge($mime_ext, (array) @include($fpath));
+ }
+ }
+
+ if (is_array($mime_ext) && $ext) {
+ $mimetype = $mime_ext[strtolower($ext)];
+ }
+
+ return $mimetype ?: $fallback;
+ }
+
+ /**
* Returns script URI
*
* @return string Script URI
diff --git a/lib/init.php b/lib/init.php
index 6b8e86a..2ff13a1 100644
--- a/lib/init.php
+++ b/lib/init.php
@@ -25,9 +25,9 @@
// Roundcube Framework constants
define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
+define('RCUBE_INSTALL_PATH', realpath(__DIR__) . '/../');
define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
+define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/drivers/kolab/plugins');
// Define include path
$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
diff --git a/lib/kolab/plugins/libkolab/config.inc.php.dist b/lib/kolab/plugins/libkolab/config.inc.php.dist
deleted file mode 100644
index 0c612a3..0000000
--- a/lib/kolab/plugins/libkolab/config.inc.php.dist
+++ /dev/null
@@ -1,32 +0,0 @@
-<?php
-
-/* Configuration for libkolab */
-
-// Enable caching of Kolab objects in local database
-$rcmail_config['kolab_cache'] = true;
-
-// Specify format version to write Kolab objects (must be a string value!)
-$rcmail_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>';
-
-// Enables listing of only subscribed folders. This e.g. will limit
-// folders in calendar view or available addressbooks
-$rcmail_config['kolab_use_subscriptions'] = false;
-
-// Enables the use of displayname folder annotations as introduced in KEP:?
-// for displaying resource folder names (experimental!)
-$rcmail_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();
-
-// 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;
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_configuration.php b/lib/kolab/plugins/libkolab/lib/kolab_format_configuration.php
deleted file mode 100644
index 5a8d3ff..0000000
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_configuration.php
+++ /dev/null
@@ -1,139 +0,0 @@
-<?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 $CTYPEv2 = 'application/x-vnd.kolab.configuration';
-
- protected $objclass = 'Configuration';
- protected $read_func = 'readConfiguration';
- protected $write_func = 'writeConfiguration';
-
- private $type_map = array(
- 'dictionary' => Configuration::TypeDictionary,
- 'category' => Configuration::TypeCategoryColor,
- );
-
-
- /**
- * 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;
- 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))
- 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;
- }
-
- // adjust content-type string
- 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')
- $tags = array($this->data['language']);
-
- return $tags;
- }
-
-}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_note.php b/lib/kolab/plugins/libkolab/lib/kolab_format_note.php
deleted file mode 100644
index 04a8421..0000000
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_note.php
+++ /dev/null
@@ -1,82 +0,0 @@
-<?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 $CTYPEv2 = 'application/x-vnd.kolab.note';
-
- protected $objclass = 'Note';
- protected $read_func = 'readNote';
- protected $write_func = 'writeNote';
-
-
- /**
- * 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
-
- // 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
-
- $this->data = $object;
- return $this->data;
- }
-
-}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php b/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php
deleted file mode 100644
index 8380aa8..0000000
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php
+++ /dev/null
@@ -1,40 +0,0 @@
-<?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
diff --git a/lib/locale/en_US.php b/lib/locale/en_US.php
index f002c59..192d3b0 100644
--- a/lib/locale/en_US.php
+++ b/lib/locale/en_US.php
@@ -35,10 +35,16 @@ $LANG['folder.createtitle'] = 'Create Folder';
$LANG['folder.delete'] = 'Delete';
$LANG['folder.edit'] = 'Edit';
$LANG['folder.edittitle'] = 'Edit Folder';
-$LANG['folder.under'] = 'under current folder';
+$LANG['folder.under'] = 'inside the current folder';
+$LANG['folder.driverselect'] = 'bind with the external storage';
+$LANG['folder.name'] = 'Name:';
+$LANG['folder.authenticate'] = 'Logon to $title';
$LANG['form.submit'] = 'Submit';
$LANG['form.cancel'] = 'Cancel';
+$LANG['form.hostname'] = 'Hostname:';
+$LANG['form.username'] = 'Username:';
+$LANG['form.password'] = 'Password:';
$LANG['login.username'] = 'Username';
$LANG['login.password'] = 'Password';
@@ -48,6 +54,7 @@ $LANG['reqtime'] = 'Request time: $1 sec.';
$LANG['maxupload'] = 'Maximum file size: $1';
$LANG['internalerror'] = 'Internal system error!';
$LANG['loginerror'] = 'Incorrect username or password!';
+$LANG['authenticating'] = 'Authenticating...';
$LANG['loading'] = 'Loading...';
$LANG['saving'] = 'Saving...';
$LANG['deleting'] = 'Deleting...';
@@ -57,6 +64,7 @@ $LANG['logout'] = 'Logout';
$LANG['close'] = 'Close';
$LANG['servererror'] = 'Server Error!';
$LANG['session.expired'] = 'Session has expired. Login again, please';
+$LANG['localstorage'] = 'local storage';
$LANG['search'] = 'Search';
$LANG['search.loading'] = 'Searching...';
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
@@ -92,9 +92,11 @@ class file_viewer_image extends file_viewer
$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
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
@@ -104,7 +104,9 @@ class file_viewer_text extends file_viewer
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);
}
/**
diff --git a/public_html/js/files_api.js b/public_html/js/files_api.js
index 7a7f133..061c81f 100644
--- a/public_html/js/files_api.js
+++ b/public_html/js/files_api.js
@@ -211,11 +211,13 @@ function files_api()
};
// Folder list parser, converts it into structure
- this.folder_list_parse = function(list)
+ this.folder_list_parse = function(list, num)
{
- var i, n, items, items_len, f, tmp, folder, num = 1,
+ var i, n, items, items_len, f, tmp, folder,
len = list.length, folders = {};
+ if (!num) num = 1;
+
for (i=0; i<len; i++) {
folder = list[i];
items = folder.split(this.env.directory_separator);
@@ -510,6 +512,14 @@ RegExp.escape = function(str)
return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};
+// define String's startsWith() method for old browsers
+if (!String.prototype.startsWith) {
+ String.prototype.startsWith = function(search, position) {
+ position = position || 0;
+ return this.slice(position, search.length) === search;
+ };
+};
+
// make a string URL safe (and compatible with PHP's rawurlencode())
function urlencode(str)
{
diff --git a/public_html/js/files_ui.js b/public_html/js/files_ui.js
index 3c96ba4..c39ed85 100644
--- a/public_html/js/files_ui.js
+++ b/public_html/js/files_ui.js
@@ -483,7 +483,7 @@ function files_ui()
var elem = $('#folderlist'), table = $('table', elem);
- this.env.folders = this.folder_list_parse(response.result);
+ this.env.folders = this.folder_list_parse(response.result ? response.result.list : []);
table.empty();
@@ -509,6 +509,9 @@ function files_ui()
// add tree icons
this.folder_list_tree(this.env.folders);
+
+ // handle authentication errors on external sources
+ this.folder_list_auth_errors(response.result);
};
this.folder_select = function(folder, is_collection)
@@ -557,8 +560,12 @@ function files_ui()
return;
}
+ if (typeof folder != 'object') {
+ folder = {folder: folder};
+ }
+
this.set_busy(true, 'saving');
- this.request('folder_create', {folder: folder}, 'folder_create_response');
+ this.request('folder_create', folder, 'folder_create_response');
};
// folder create response handler
@@ -567,6 +574,7 @@ function files_ui()
if (!this.response(response))
return;
+ this.folder_create_stop();
this.folder_list();
};
@@ -995,6 +1003,118 @@ function files_ui()
/********* Command helpers *********/
/*********************************************************/
+ // handle auth errors on folder list
+ this.folder_list_auth_errors = function(result)
+ {
+ if (result && result.auth_errors) {
+ if (!this.auth_errors)
+ this.auth_errors = {};
+
+ $.extend(this.auth_errors, result.auth_errors);
+ }
+
+ // ask for password to the first storage on the list
+ $.each(this.auth_errors || [], function(i, v) {
+ ui.folder_list_auth_dialog(i, v);
+ return false;
+ });
+ };
+
+ // create dialog for user credentials of external storage
+ this.folder_list_auth_dialog = function(label, driver)
+ {
+ var buttons = {},
+ content = this.folder_list_auth_form(driver),
+ title = this.t('folder.authenticate').replace('$title', label);
+
+ buttons['form.submit'] = function() {
+ var data = {folder: label, list: 1};
+
+ $('input', this.modal).each(function() {
+ data[this.name] = this.value;
+ });
+
+ ui.open_dialog = this;
+ ui.set_busy(true, 'authenticating');
+ ui.request('folder_auth', data, 'folder_auth_response');
+ };
+
+ buttons['form.cancel'] = function() {
+ delete ui.auth_errors[label];
+ this.hide();
+ // go to the next one
+ ui.folder_list_auth_errors();
+ };
+
+ this.modal_dialog(content, buttons, {
+ title: title,
+ fxOpen: function(win) {
+ // focus first empty input
+ $('input', win.modal).each(function() {
+ if (!this.value) {
+ this.focus();
+ return false;
+ }
+ });
+ }
+ });
+ };
+
+ // folder_auth handler
+ this.folder_auth_response = function(response)
+ {
+ if (!this.response(response))
+ return;
+
+ var cnt = 0, folders,
+ folder = response.result.folder,
+ parent = $('#' + this.env.folders[folder].id);
+
+ delete this.auth_errors[folder];
+ this.open_dialog.hide();
+
+ // go to the next one
+ this.folder_list_auth_errors();
+
+ // count folders on the list
+ $.each(this.env.folders, function() { cnt++; });
+
+ // parse result
+ folders = this.folder_list_parse(response.result.list, cnt);
+ delete folders[folder]; // remove root added in folder_list_parse()
+
+ // add folders from the external source to the list
+ $.each(folders, function(i, f) {
+ var row = ui.folder_list_row(i, f);
+ parent.after(row);
+ parent = row;
+ });
+
+ // add tree icons
+ this.folder_list_tree(folders);
+
+ $.extend(this.env.folders, folders);
+ };
+
+ // returns content of the external storage authentication form
+ this.folder_list_auth_form = function(driver)
+ {
+ var elements = [];
+
+ $.each(driver.form, function(fi, fv) {
+ var id = 'authinput' + fi,
+ attrs = {type: fi.match(/pass/) ? 'password' : 'text', size: 25, name: fi, id: id},
+ input = $('<input>').attr(attrs);
+
+ if (driver.form_values && driver.form_values[fi])
+ input.attr({value: driver.form_values[fi]});
+
+ elements.push($('<span class="formrow">').append($('<label>').attr('for', id).text(fv)).append(input));
+ });
+
+ return $('<div class="form">').append(elements);
+ };
+
// create folders table row
this.folder_list_row = function(folder, data)
{
@@ -1422,10 +1542,20 @@ function files_ui()
// Display folder creation form
this.folder_create_start = function()
{
- var form = this.form_show('folder-create');
+ var form = $('#folder-create-form');
+
+ $('.drivers', form).hide();
$('input[name="name"]', form).val('').focus();
$('input[name="parent"]', form).prop('checked', this.env.folder)
.prop('disabled', !this.env.folder);
+ $('#folder-driver-checkbox').prop('checked', false);
+
+ this.form_show('folder-create');
+
+ if (!this.folder_types)
+ this.request('folder_types', {}, 'folder_types_response');
+ else
+ this.folder_types_init();
};
// Hide folder creation form
@@ -1437,18 +1567,117 @@ function files_ui()
// Submit folder creation form
this.folder_create_submit = function()
{
- var folder = '', data = this.serialize_form('#folder-create-form');
+ var args = {}, folder = '', data = this.serialize_form('#folder-create-form');
if (!data.name)
return;
- if (data.parent && this.env.folder)
+ if (data.parent && this.env.folder) {
folder = this.env.folder + this.env.directory_separator;
+ }
+ else if (data.external && data.driver) {
+ args.driver = data.driver;
+ $.each(data, function(i, v) {
+ if (i.startsWith(data.driver + '[')) {
+ args[i.substring(data.driver.length + 1, i.length - 1)] = v;
+ }
+ });
+ }
folder += data.name;
+ args.folder = folder;
- this.folder_create_stop();
- this.command('folder.create', folder);
+ this.command('folder.create', args);
+ };
+
+ // folder_types response handler
+ this.folder_types_response = function(response)
+ {
+ if (!this.response(response))
+ return;
+
+ if (response.result) {
+ this.folder_types = response.result;
+
+ var list = [];
+
+ $.each(this.folder_types, function(i, v) {
+ var form = [], item = $('<div>').data('id', i),
+ content = $('<div class="content">')
+ label = $('<span class="name">').text(v.name),
+ desc = $('<span class="description">').text(v.description),
+ img = $('<img>').attr({alt: i, title: i, src: v.image}),
+ input = $('<input>').attr({type: 'radio', name: 'driver'}).val(i);
+
+ item.append(input)
+ .append(img)
+ .append(content);
+
+ content.append(label).append($('<br>')).append(desc);
+
+ $.each(v.form || [], function(fi, fv) {
+ var id = 'input' +i + fi,
+ attrs = {type: fi.match(/pass/) ? 'password' : 'text', size: 25, name: i + '[' + fi + ']', id: id};
+
+ form.push($('<span class="formrow">')
+ .append($('<label>').attr('for', id).text(fv))
+ .append($('<input>').attr(attrs))
+ );
+ });
+
+ if (form.length) {
+ $('<div class="form">').append(form).appendTo(content);
+ }
+
+ list.push(item);
+ });
+
+ if (list.length) {
+ var drivers_list = $('.drivers-list');
+
+ drivers_list.append(list);
+ this.form_show('folder-create');
+
+ $.each(list, function() {
+ this.click(function() {
+ $('.selected', drivers_list).removeClass('selected');
+ drivers_list.find('.form').hide();
+ $(this).addClass('selected').find('.form').show();
+ $('input[type="radio"]', this).prop('checked', true);
+ ref.form_show('folder-create');
+ });
+ });
+
+ $('#folder-parent-checkbox').change(function() {
+ if (this.checked)
+ $('#folder-create-form div.drivers').hide();
+ ref.folder_types_init();
+ });
+
+ $('#folder-driver-checkbox').change(function() {
+ drivers_list[this.checked ? 'show' : 'hide']();
+ ref.folder_types_init();
+ });
+
+ this.folder_types_init();
+ }
+ }
+ };
+
+ // initialize folder types list on folder create form display
+ this.folder_types_init = function()
+ {
+ var form = $('#folder-create-form'),
+ list = $('.drivers-list > div', form);
+
+ if (list.length && !$('input[name="parent"]', form).is(':checked')) {
+ $('#folder-create-form div.drivers').show();
+ list[0].click();
+ }
+
+ $('.drivers-list')[list.length && $('#folder-driver-checkbox:checked').length ? 'show' : 'hide']();
+
+ ref.form_show('folder-create');
};
// Display folder edit form
@@ -1621,6 +1850,9 @@ function files_ui()
footer.push({name: n, label: ui.t(i)});
});
+ // open function
+ settings.fxOpen = opts.fxOpen;
+
// if (!settings.btns.cancel && (!opts || !opts.no_cancel))
// settings.btns.cancel = function() { this.hide(); };
@@ -1641,8 +1873,12 @@ function files_ui()
this.form_show = function(name)
{
var form = $('#' + name + '-form');
- $('#forms > form').hide();
- form.show();
+
+ if (form.is(':hidden')) {
+ $('#forms > form').hide();
+ form.show();
+ }
+
$('#taskcontent').css('top', form.height() + 20);
return form;
diff --git a/public_html/js/wModal.js b/public_html/js/wModal.js
index 4992717..dd53440 100644
--- a/public_html/js/wModal.js
+++ b/public_html/js/wModal.js
@@ -228,7 +228,7 @@
fxShowFade: function()
{
var _this = this;
- this.bg.fadeIn(100, function(){ _this.modal.fadeIn(300); });
+ this.bg.fadeIn(100, function(){ _this.modal.fadeIn(300, function() { if (_this.settings.fxOpen) _this.settings.fxOpen(_this); }); });
},
fxHideFade: function()
diff --git a/public_html/skins/default/style.css b/public_html/skins/default/style.css
index f92a2be..e6a6173 100644
--- a/public_html/skins/default/style.css
+++ b/public_html/skins/default/style.css
@@ -1356,6 +1356,75 @@ td span.branch span.l3
background: url(images/document.png) 0 0 no-repeat;
}
+#folder-create-form input {
+ vertical-align: middle;
+}
+
+#folder-create-form table td.buttons {
+ vertical-align: top;
+}
+
+.drivers-list {
+ max-height: 160px;
+ overflow: auto;
+}
+
+.drivers-list > div {
+ border: 1px solid white;
+ border-radius: 3px;
+ margin-top: 3px;
+ padding: 2px 0;
+}
+
+.drivers-list > div.selected {
+ background-color: #e8e8e8;
+}
+
+.drivers-list div.content {
+ display: inline-block;
+ vertical-align: middle;
+ margin-left: 5px;
+}
+
+.drivers-list .name {
+ font-weight: bold;
+}
+
+.drivers-list .description {
+ font-size: 9px;
+ color: #666;
+}
+
+.drivers-list img {
+ vertical-align: middle;
+ background-color: #e0e0e0;
+ border-radius: 3px;
+ margin: 3px;
+ background-image: -moz-linear-gradient(center top, #888, #333);
+ background-image: -webkit-linear-gradient(top, #888, #333);
+ background-image: -ms-linear-gradient(top, #888, #333);
+}
+
+.drivers-list input {
+ vertical-align: middle;
+}
+
+.drivers-list div.content div.form {
+ padding-top: 4px;
+ width: 400px;
+}
+
+div.form .formrow {
+ display: block;
+ padding: 1px;
+}
+
+div.form label {
+ width: 80px;
+ display: inline-block;
+}
+
+
/****** File open interface elements ******/
#actionbar #file-edit-button {