Page MenuHomePhorge

No OneTemporary

This file is larger than 256 KB, so syntax highlighting was skipped.
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/init.php b/lib/api/file_copy.php
similarity index 63%
copy from lib/init.php
copy to lib/api/file_copy.php
index 6b8e86a..7509af1 100644
--- a/lib/init.php
+++ b/lib/api/file_copy.php
@@ -1,40 +1,29 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
-
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
+require_once __DIR__ . "/file_move.php";
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+class file_api_file_copy extends file_api_file_move
+{
+}
diff --git a/lib/init.php b/lib/api/file_create.php
similarity index 54%
copy from lib/init.php
copy to lib/api/file_create.php
index 6b8e86a..210d7ec 100644
--- a/lib/init.php
+++ b/lib/api/file_create.php
@@ -1,40 +1,59 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
-
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
-
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+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/init.php b/lib/api/file_delete.php
similarity index 63%
copy from lib/init.php
copy to lib/api/file_delete.php
index 6b8e86a..2458f42 100644
--- a/lib/init.php
+++ b/lib/api/file_delete.php
@@ -1,40 +1,45 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
+require_once __DIR__ . "/common.php";
+
+class file_api_file_delete extends file_api_common
+{
+ /**
+ * Request handler
+ */
+ public function handle()
+ {
+ parent::handle();
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
+ if (empty($this->args['file'])) {
+ throw new Exception("Missing file name", file_api::ERROR_CODE);
+ }
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+ 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/init.php b/lib/api/file_info.php
similarity index 52%
copy from lib/init.php
copy to lib/api/file_info.php
index 6b8e86a..41d2639 100644
--- a/lib/init.php
+++ b/lib/api/file_info.php
@@ -1,40 +1,66 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
-
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
-
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+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/init.php b/lib/api/file_list.php
similarity index 50%
copy from lib/init.php
copy to lib/api/file_list.php
index 6b8e86a..994804e 100644
--- a/lib/init.php
+++ b/lib/api/file_list.php
@@ -1,40 +1,69 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
-
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
-
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+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/init.php b/lib/api/file_update.php
similarity index 63%
copy from lib/init.php
copy to lib/api/file_update.php
index 6b8e86a..6386064 100644
--- a/lib/init.php
+++ b/lib/api/file_update.php
@@ -1,40 +1,29 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
-
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
+require_once __DIR__ . "/file_create.php";
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+class file_api_file_update extends file_api_file_create
+{
+}
diff --git a/lib/init.php b/lib/api/file_upload.php
similarity index 54%
copy from lib/init.php
copy to lib/api/file_upload.php
index 6b8e86a..42ff127 100644
--- a/lib/init.php
+++ b/lib/api/file_upload.php
@@ -1,40 +1,64 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
-
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
-
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+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/init.php b/lib/api/folder_delete.php
similarity index 61%
copy from lib/init.php
copy to lib/api/folder_delete.php
index 6b8e86a..694c0d5 100644
--- a/lib/init.php
+++ b/lib/api/folder_delete.php
@@ -1,40 +1,52 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
+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']);
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
+ // delete mount point...
+ if ($driver->title() === $this->args['folder']) {
+ $backend = $this->api->get_backend();
+ $backend->driver_delete($this->args['folder']);
+ return;
+ }
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+ // 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/init.php b/lib/api/folder_types.php
similarity index 55%
copy from lib/init.php
copy to lib/api/folder_types.php
index 6b8e86a..fd10ea7 100644
--- a/lib/init.php
+++ b/lib/api/folder_types.php
@@ -1,40 +1,60 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
+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();
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
+ 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();
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+ $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/init.php b/lib/api/lock_create.php
similarity index 63%
copy from lib/init.php
copy to lib/api/lock_create.php
index 6b8e86a..01c9f79 100644
--- a/lib/init.php
+++ b/lib/api/lock_create.php
@@ -1,40 +1,47 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
+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);
+ }
+ }
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
+ list($driver, $uri) = $this->api->get_driver($this->args['uri']);
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+ $driver->lock($uri, $this->args);
+ }
+}
diff --git a/lib/init.php b/lib/api/lock_delete.php
similarity index 63%
copy from lib/init.php
copy to lib/api/lock_delete.php
index 6b8e86a..ff90835 100644
--- a/lib/init.php
+++ b/lib/api/lock_delete.php
@@ -1,40 +1,47 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
+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);
+ }
+ }
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
+ list($driver, $uri) = $this->api->get_driver($this->args['uri']);
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+ $driver->unlock($uri, $this->args);
+ }
+}
diff --git a/lib/init.php b/lib/api/lock_list.php
similarity index 63%
copy from lib/init.php
copy to lib/api/lock_list.php
index 6b8e86a..db0cfa5 100644
--- a/lib/init.php
+++ b/lib/api/lock_list.php
@@ -1,40 +1,42 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
+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']);
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_path);
+ list($driver, $uri) = $this->api->get_driver($this->args['uri']);
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+ return $driver->lock_list($uri, $child_locks);
+ }
+}
diff --git a/lib/init.php b/lib/api/quota.php
similarity index 63%
copy from lib/init.php
copy to lib/api/quota.php
index 6b8e86a..2bda6f2 100644
--- a/lib/init.php
+++ b/lib/api/quota.php
@@ -1,40 +1,51 @@
<?php
-
-/**
+/*
+--------------------------------------------------------------------------+
- | Kolab File API |
+ | This file is part of the Kolab File API |
| |
- | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com> |
+ | 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> |
+--------------------------------------------------------------------------+
*/
-// Roundcube Framework constants
-define('FILE_API_START', microtime(true));
-define('RCUBE_INSTALL_PATH', realpath(dirname(__FILE__)) . '/../');
-define('RCUBE_CONFIG_DIR', RCUBE_INSTALL_PATH . 'config/');
-define('RCUBE_PLUGINS_DIR', RCUBE_INSTALL_PATH . 'lib/kolab/plugins');
+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);
-// Define include path
-$include_path = RCUBE_INSTALL_PATH . '/lib' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
-$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
-$include_path .= ini_get('include_path');
-set_include_path($include_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));
+ }
+ }
-// include global functions from Roundcube Framework
-require_once 'Roundcube/bootstrap.php';
+ 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
@@ -1,170 +1,185 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class file_ui_client_main extends file_ui
{
public function action_default()
{
// assign default set of translations
$this->output->add_translation('saving', 'deleting', 'search', 'search.loading',
'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');
$this->output->set_env('search_threads', $this->config->get('files_search_threads'));
$this->output->set_env('supported_mimetypes', $result->get());
$this->ui_init();
}
public function folder_create_form()
{
$input_name = new html_inputfield(array(
'type' => 'text',
'name' => 'name',
'value' => '',
+ 'id' => 'folder-name-input',
));
$input_parent = new html_checkbox(array(
'name' => 'parent',
'value' => '1',
'id' => 'folder-parent-checkbox',
));
$submit = new html_inputfield(array(
'type' => 'button',
'onclick' => 'ui.folder_create_submit()',
'value' => $this->translate('form.submit'),
));
$cancel = new html_inputfield(array(
'type' => 'button',
'onclick' => 'ui.folder_create_stop()',
'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',
'onsubmit' => 'ui.folder_create_submit(); return false'),
$content);
return $form;
}
public function folder_edit_form()
{
$input_name = new html_inputfield(array(
'type' => 'text',
'name' => 'name',
'value' => '',
));
$submit = new html_inputfield(array(
'type' => 'button',
'onclick' => 'ui.folder_edit_submit()',
'value' => $this->translate('form.submit'),
));
$cancel = new html_inputfield(array(
'type' => 'button',
'onclick' => 'ui.folder_edit_stop()',
'value' => $this->translate('form.cancel'),
));
$table = new html_table;
$table->add(null, $input_name->show());
$table->add('buttons', $submit->show() . $cancel->show());
$content = html::tag('fieldset', null,
html::tag('legend', null,
$this->translate('folder.edittitle')) . $table->show());
$form = html::tag('form', array(
'id' => 'folder-edit-form',
'onsubmit' => 'ui.folder_edit_submit(); return false'),
$content);
return $form;
}
public function file_search_form()
{
$input_name = new html_inputfield(array(
'type' => 'text',
'name' => 'name',
'value' => '',
));
$input_in1 = new html_inputfield(array(
'type' => 'radio',
'name' => 'all_folders',
'value' => '0',
'id' => 'all-folders-radio1',
));
$input_in2 = new html_inputfield(array(
'type' => 'radio',
'name' => 'all_folders',
'value' => '1',
'id' => 'all-folders-radio2',
));
$submit = new html_inputfield(array(
'type' => 'button',
'onclick' => 'ui.file_search_submit()',
'value' => $this->translate('form.submit'),
));
$cancel = new html_inputfield(array(
'type' => 'button',
'onclick' => 'ui.file_search_stop()',
'value' => $this->translate('form.cancel'),
));
$table = new html_table;
$table->add(null, $input_name->show()
. $input_in1->show() . html::label('all-folders-radio1', $this->translate('search.in_current_folder'))
. $input_in2->show() . html::label('all-folders-radio2', $this->translate('search.in_all_folders'))
);
$table->add('buttons', $submit->show() . $cancel->show());
$content = html::tag('fieldset', null,
html::tag('legend', null,
$this->translate('file.search')) . $table->show());
$form = html::tag('form', array(
'id' => 'file-search-form',
'onsubmit' => 'ui.file_search_submit(); return false'),
$content);
return $form;
}
}
diff --git a/lib/drivers/kolab/kolab.png b/lib/drivers/kolab/kolab.png
new file mode 100644
index 0000000..0e87eec
Binary files /dev/null and b/lib/drivers/kolab/kolab.png differ
diff --git a/lib/kolab/kolab_file_plugin_api.php b/lib/drivers/kolab/kolab_file_plugin_api.php
similarity index 100%
rename from lib/kolab/kolab_file_plugin_api.php
rename to 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
similarity index 89%
rename from lib/kolab/kolab_file_storage.php
rename to 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
@@ -1,1051 +1,1203 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class kolab_file_storage implements file_storage
{
/**
* @var rcube
*/
protected $rc;
/**
* @var array
*/
protected $folders;
/**
* @var array
*/
protected $config;
+ /**
+ * @var string
+ */
+ protected $title;
+
/**
* Class constructor
*/
public function __construct()
{
$this->rc = rcube::get_instance();
// Get list of plugins
// WARNING: We can use only plugins that are prepared for this
// e.g. are not using output or rcmail objects or
// doesn't throw errors when using them
$plugins = (array)$this->rc->config->get('fileapi_plugins', array('kolab_auth'));
$required = array('libkolab');
// Kolab WebDAV server supports plugins, no need to overwrite object
if (!is_a($this->rc->plugins, 'rcube_plugin_api')) {
// Initialize/load plugins
$this->rc->plugins = kolab_file_plugin_api::get_instance();
$this->rc->plugins->init($this, '');
}
$this->rc->plugins->load_plugins($plugins, $required);
$this->init();
}
/**
* Authenticates a user
*
* @param string $username User name
* @param string $password User password
*
* @param bool True on success, False on failure
*/
public function authenticate($username, $password)
{
$auth = $this->rc->plugins->exec_hook('authenticate', array(
'host' => $this->select_host($username),
'user' => $username,
'pass' => $password,
'valid' => true,
));
// Authenticate - get Roundcube user ID
if ($auth['valid'] && !$auth['abort']
&& ($this->login($auth['user'], $auth['pass'], $auth['host']))) {
return true;
}
$this->rc->plugins->exec_hook('login_failed', array(
'host' => $auth['host'],
'user' => $auth['user'],
));
}
/**
* Storage host selection
*/
private function select_host($username)
{
// Get IMAP host
$host = $this->rc->config->get('default_host');
if (is_array($host)) {
list($user, $domain) = explode('@', $username);
// try to select host by mail domain
if (!empty($domain)) {
foreach ($host as $storage_host => $mail_domains) {
if (is_array($mail_domains) && in_array_nocase($domain, $mail_domains)) {
$host = $storage_host;
break;
}
else if (stripos($storage_host, $domain) !== false || stripos(strval($mail_domains), $domain) !== false) {
$host = is_numeric($storage_host) ? $mail_domains : $storage_host;
break;
}
}
}
// take the first entry if $host is not found
if (is_array($host)) {
list($key, $val) = each($default_host);
$host = is_numeric($key) ? $val : $key;
}
}
return rcube_utils::parse_host($host);
}
/**
* Authenticates a user in IMAP
*/
private function login($username, $password, $host)
{
if (empty($username)) {
return false;
}
$login_lc = $this->rc->config->get('login_lc');
$default_port = $this->rc->config->get('default_port', 143);
// parse $host
$a_host = parse_url($host);
if ($a_host['host']) {
$host = $a_host['host'];
$ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? $a_host['scheme'] : null;
if (!empty($a_host['port'])) {
$port = $a_host['port'];
}
else if ($ssl && $ssl != 'tls' && (!$default_port || $default_port == 143)) {
$port = 993;
}
}
if (!$port) {
$port = $default_port;
}
// Convert username to lowercase. If storage backend
// is case-insensitive we need to store always the same username
if ($login_lc) {
if ($login_lc == 2 || $login_lc === true) {
$username = mb_strtolower($username);
}
else if (strpos($username, '@')) {
// lowercase domain name
list($local, $domain) = explode('@', $username);
$username = $local . '@' . mb_strtolower($domain);
}
}
// Here we need IDNA ASCII
// Only rcube_contacts class is using domain names in Unicode
$host = rcube_utils::idn_to_ascii($host);
$username = rcube_utils::idn_to_ascii($username);
// user already registered?
if ($user = rcube_user::query($username, $host)) {
$username = $user->data['username'];
}
// authenticate user in IMAP
$storage = $this->rc->get_storage();
if (!$storage->connect($host, $username, $password, $port, $ssl)) {
return false;
}
// No user in database, but IMAP auth works
if (!is_object($user)) {
if ($this->rc->config->get('auto_create_user')) {
// create a new user record
$user = rcube_user::create($username, $host);
if (!$user) {
rcube::raise_error(array(
'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to create a user record",
), true, false);
return false;
}
}
else {
rcube::raise_error(array(
'code' => 620, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__,
'message' => "Access denied for new user $username. 'auto_create_user' is disabled",
), true, false);
return false;
}
}
+
// set session vars
$_SESSION['user_id'] = $user->ID;
$_SESSION['username'] = $user->data['username'];
$_SESSION['storage_host'] = $host;
$_SESSION['storage_port'] = $port;
$_SESSION['storage_ssl'] = $ssl;
$_SESSION['password'] = $this->rc->encrypt($password);
$this->init($user);
// force reloading of mailboxes list/data
$storage->clear_cache('mailboxes', true);
return true;
}
protected function init($user = null)
{
if ($_SESSION['user_id'] || $user) {
// overwrite config with user preferences
$this->rc->user = $user ? $user : new rcube_user($_SESSION['user_id']);
$this->rc->config->set_user_prefs((array)$this->rc->user->get_prefs());
$storage = $this->rc->get_storage();
$storage->set_charset($this->rc->config->get('default_charset', RCUBE_CHARSET));
setlocale(LC_ALL, 'en_US.utf8', 'en_US.UTF-8');
}
}
/**
* Configures environment
*
- * @param array $config COnfiguration
+ * @param array $config Configuration
+ * @param string $title Source identifier
*/
- public function configure($config)
+ public function configure($config, $title = null)
{
$this->config = $config;
+ // @TODO: this is currently not possible to have multiple sessions in Roundcube
+ }
+
+ /**
+ * Returns current instance title
+ *
+ * @return string Instance title (mount point)
+ */
+ public function title()
+ {
+ return '';
}
/**
* Storage driver capabilities
*
* @return array List of capabilities
*/
public function capabilities()
{
// find max filesize value
$max_filesize = parse_bytes(ini_get('upload_max_filesize'));
$max_postsize = parse_bytes(ini_get('post_max_size'));
if ($max_postsize && $max_postsize < $max_filesize) {
$max_filesize = $max_postsize;
}
$storage = $this->rc->get_storage();
$quota = $storage->get_capability('QUOTA');
return array(
file_storage::CAPS_MAX_UPLOAD => $max_filesize,
file_storage::CAPS_QUOTA => $quota,
file_storage::CAPS_LOCKS => true,
);
}
+ /**
+ * Save configuration of external driver (mount point)
+ *
+ * @param array $driver Driver data
+ *
+ * @throws Exception
+ */
+ public function driver_create($driver)
+ {
+ $drivers = $this->driver_list();
+
+ if ($drivers[$driver['title']]) {
+ throw new Exception("Driver exists", file_storage::ERROR);
+ }
+
+ $config = kolab_storage_config::get_instance();
+ $status = $config->save($driver, 'file_driver');
+
+ if (!$status) {
+ throw new Exception("Driver create failed", file_storage::ERROR);
+ }
+ }
+
+ /**
+ * Delete configuration of external driver (mount point)
+ *
+ * @param string $name Driver instance name
+ *
+ * @throws Exception
+ */
+ public function driver_delete($name)
+ {
+ $drivers = $this->driver_list();
+
+ if ($driver = $drivers[$name]) {
+ $config = kolab_storage_config::get_instance();
+ $status = $config->delete($driver['uid']);
+
+ if (!$status) {
+ throw new Exception("Driver delete failed", file_storage::ERROR);
+ }
+
+ return;
+ }
+
+ throw new Exception("Driver not found", file_storage::ERROR);
+ }
+
+ /**
+ * Return list of registered drivers (mount points)
+ *
+ * @return array List of drivers data
+ * @throws Exception
+ */
+ public function driver_list()
+ {
+ // get current relations state
+ $config = kolab_storage_config::get_instance();
+ $default = true;
+ $filter = array(
+ array('type', '=', 'file_driver'),
+ );
+
+ $drivers = $config->get_objects($filter, $default, 100);
+ $result = array();
+
+ foreach ($drivers as $driver) {
+ $result[$driver['title']] = $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)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_create($file_name, $file)
{
$exists = $this->get_file_object($file_name, $folder);
if (!empty($exists)) {
throw new Exception("Storage error. File exists.", file_storage::ERROR);
}
$object = $this->to_file_object(array(
'name' => $file_name,
'type' => $file['type'],
'path' => $file['path'],
'content' => $file['content'],
));
// save the file object in IMAP
$saved = $folder->save($object, 'file');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving object to Kolab server"),
true, false);
throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
}
}
/**
* Update a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path, type)
*
* @throws Exception
*/
public function file_update($file_name, $file)
{
$file_object = $this->get_file_object($file_name, $folder);
if (empty($file_object)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$key = key($file_object['_attachments']);
$file_object['_attachments'] = array(
0 => array(
'name' => $file_name,
'path' => $file['path'],
'content' => $file['content'],
'mimetype' => $file['type'],
),
$key => false,
);
// save the file object in IMAP
$saved = $folder->save($file_object, 'file', $file_object['_msguid']);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving object to Kolab server"),
true, false);
throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
}
}
/**
* Delete a file.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_delete($file_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$deleted = $folder->delete($file);
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting object from Kolab server"),
true, false);
throw new Exception("Storage error. Deleting file failed.", file_storage::ERROR);
}
}
/**
* Return file body.
*
* @param string $file_name Name of a file (with folder path)
* @param array $params Parameters (force-download)
* @param resource $fp Print to file pointer instead (send no headers)
*
* @throws Exception
*/
public function file_get($file_name, $params = array(), $fp = null)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$file = $this->from_file_object($file);
// write to file pointer, send no headers
if ($fp) {
if ($file['size']) {
$folder->get_attachment($file['_msguid'], $file['fileid'], $file['_mailbox'], false, $fp);
}
return;
}
if (!empty($params['force-download'])) {
$disposition = 'attachment';
header("Content-Type: application/octet-stream");
// @TODO
// if ($browser->ie)
// header("Content-Type: application/force-download");
}
else {
$mimetype = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $file['type']);
$disposition = 'inline';
header("Content-Transfer-Encoding: binary");
header("Content-Type: $mimetype");
}
$filename = addcslashes($file['name'], '"');
// Workaround for nasty IE bug (#1488844)
// If Content-Disposition header contains string "attachment" e.g. in filename
// IE handles data as attachment not inline
/*
@TODO
if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) {
$filename = str_ireplace('attachment', 'attach', $filename);
}
*/
header("Content-Length: " . $file['size']);
header("Content-Disposition: $disposition; filename=\"$filename\"");
if ($file['size']) {
$folder->get_attachment($file['_msguid'], $file['fileid'], $file['_mailbox'], true);
}
}
/**
* Returns file metadata.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_info($file_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$file = $this->from_file_object($file);
return array(
'name' => $file['name'],
'size' => (int) $file['size'],
'type' => (string) $file['type'],
'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
'created' => $file['created'] ? $file['created']->format('U') : 0,
);
}
/**
* List files in a folder.
*
* @param string $folder_name Name of a folder with full path
- * @param array $params List parameters ('sort', 'reverse', 'search')
+ * @param array $params List parameters ('sort', 'reverse', 'search', 'prefix')
*
* @return array List of files (file properties array indexed by filename)
* @throws Exception
*/
public function file_list($folder_name, $params = array())
{
$filter = array(array('type', '=', 'file'));
if (!empty($params['search'])) {
foreach ($params['search'] as $idx => $value) {
switch ($idx) {
case 'name':
$filter[] = array('filename', '~', $value);
break;
case 'class':
foreach (file_utils::class2mimetypes($value) as $tag) {
$for[] = array('tags', '~', ' ' . $tag);
}
$filter[] = array($for, 'OR');
break;
}
}
}
// get files list
$folder = $this->get_folder_object($folder_name);
$files = $folder->select($filter);
$result = array();
// convert to kolab_storage files list data format
foreach ($files as $idx => $file) {
$file = $this->from_file_object($file);
if (!isset($file['name'])) {
continue;
}
- $filename = $folder_name . file_storage::SEPARATOR . $file['name'];
+ $filename = $params['prefix'] . $folder_name . file_storage::SEPARATOR . $file['name'];
$result[$filename] = array(
'name' => $file['name'],
'size' => (int) $file['size'],
'type' => (string) $file['type'],
'mtime' => $file['changed'] ? $file['changed']->format($this->config['date_format']) : '',
'ctime' => $file['created'] ? $file['created']->format($this->config['date_format']) : '',
'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
'created' => $file['created'] ? $file['created']->format('U') : 0,
);
unset($files[$idx]);
}
// @TODO: pagination, search (by filename, mimetype)
// Sorting
$sort = !empty($params['sort']) ? $params['sort'] : 'name';
$index = array();
if ($sort == 'mtime') {
$sort = 'modified';
}
if (in_array($sort, array('name', 'size', 'modified'))) {
foreach ($result as $key => $val) {
$index[$key] = $val[$sort];
}
array_multisort($index, SORT_ASC, SORT_NUMERIC, $result);
}
if ($params['reverse']) {
$result = array_reverse($result, true);
}
return $result;
}
/**
* Copy a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_copy($file_name, $new_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$new = $this->get_file_object($new_name, $new_folder);
if (!empty($new)) {
throw new Exception("Storage error. File exists.", file_storage::ERROR_FILE_EXISTS);
}
$file = $this->from_file_object($file);
// Save to temp file
// @TODO: use IMAP CATENATE extension
$temp_dir = unslashify($this->rc->config->get('temp_dir'));
$file_path = tempnam($temp_dir, 'rcmAttmnt');
$fh = fopen($file_path, 'w');
if (!$fh) {
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
if ($file['size']) {
$folder->get_attachment($file['uid'], $file['fileid'], null, false, $fh, true);
}
fclose($fh);
if (!file_exists($file_path)) {
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
// Update object
$file['_attachments'] = array(
0 => array(
'name' => $file['name'],
'path' => $file_path,
'mimetype' => $file['type'],
'size' => $file['size'],
));
$fields = array('created', 'changed', '_attachments', 'notes', 'sensitivity', 'categories', 'x-custom');
$file = array_intersect_key($file, array_combine($fields, $fields));
$saved = $new_folder->save($file, 'file');
@unlink($file_path);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error updating object on Kolab server"),
true, false);
throw new Exception("Storage error. File copying failed.", file_storage::ERROR);
}
}
/**
* Move (or rename) a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_move($file_name, $new_name)
{
$file = $this->get_file_object($file_name, $folder);
if (empty($file)) {
throw new Exception("Storage error. File not found.", file_storage::ERROR);
}
$new = $this->get_file_object($new_name, $new_folder);
if (!empty($new)) {
throw new Exception("Storage error. File exists.", file_storage::ERROR_FILE_EXISTS);
}
// Move the file
if ($folder->name != $new_folder->name) {
$saved = $folder->move($file['uid'], $new_folder->name);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error moving object on Kolab server"),
true, false);
throw new Exception("Storage error. File move failed.", file_storage::ERROR);
}
$folder = $new_folder;
}
if ($file_name === $new_name) {
return;
}
// Update object (changing the name)
$cid = key($file['_attachments']);
$file['_attachments'][$cid]['name'] = $new_name;
$file['_attachments'][0] = $file['_attachments'][$cid];
$file['_attachments'][$cid] = false;
$saved = $folder->save($file, 'file');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error updating object on Kolab server"),
true, false);
throw new Exception("Storage error. File rename failed.", file_storage::ERROR);
}
}
/**
* Create a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_create($folder_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$success = kolab_storage::folder_create($folder_name, 'file');
if (!$success) {
throw new Exception("Storage error. Unable to create folder", file_storage::ERROR);
}
}
/**
* Delete a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception on error
*/
public function folder_delete($folder_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$success = kolab_storage::folder_delete($folder_name);
if (!$success) {
throw new Exception("Storage error. Unable to delete folder.", file_storage::ERROR);
}
}
/**
* Move/Rename a folder.
*
* @param string $folder_name Name of a folder with full path
* @param string $new_name New name of a folder with full path
*
* @throws Exception on error
*/
public function folder_move($folder_name, $new_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$new_name = rcube_charset::convert($new_name, RCUBE_CHARSET, 'UTF7-IMAP');
$success = kolab_storage::folder_rename($folder_name, $new_name);
if (!$success) {
throw new Exception("Storage error. Unable to rename folder", file_storage::ERROR);
}
}
/**
* Returns list of folders.
*
* @return array List of folders
* @throws Exception
*/
public function folder_list()
{
$folders = kolab_storage::list_folders('', '*', 'file', false);
if (!is_array($folders)) {
throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR);
}
// create 'Files' folder in case there's no folder of type 'file'
if (empty($folders)) {
if (kolab_storage::folder_create('Files', 'file')) {
$folders[] = 'Files';
}
}
else {
$callback = function($folder) { return rcube_charset::convert($folder, 'UTF7-IMAP', RCUBE_CHARSET); };
$folders = array_map($callback, $folders);
}
return $folders;
}
/**
* Returns a list of locks
*
* This method should return all the locks for a particular URI, including
* locks that might be set on a parent URI.
*
* If child_locks is set to true, this method should also look for
* any locks in the subtree of the URI for locks.
*
* @param string $uri URI
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
* @throws Exception
*/
public function lock_list($uri, $child_locks = false)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->uri2resource($uri);
// get locks list
$list = $this->lock_db->lock_list($uri, $child_locks);
// convert back resource string into URIs
foreach ($list as $idx => $lock) {
$list[$idx]['uri'] = $this->resource2uri($lock['uri']);
}
return $list;
}
/**
* Locks a URI
*
* @param string $uri URI
* @param array $lock Lock data
* - depth: 0/'infinite'
* - scope: 'shared'/'exclusive'
* - owner: string
* - token: string
* - timeout: int
*
* @throws Exception
*/
public function lock($uri, $lock)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->uri2resource($uri);
if (!$this->lock_db->lock($uri, $lock)) {
throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
}
}
/**
* Removes a lock from a URI
*
* @param string $path URI
* @param array $lock Lock data
*
* @throws Exception
*/
public function unlock($uri, $lock)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->uri2resource($uri);
if (!$this->lock_db->unlock($uri, $lock)) {
throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR);
}
}
/**
* Return disk quota information for specified folder.
*
* @param string $folder_name Name of a folder with full path
*
* @return array Quota
* @throws Exception
*/
public function quota($folder)
{
$storage = $this->rc->get_storage();
$quota = $storage->get_quota();
$quota = $this->rc->plugins->exec_hook('quota', $quota);
unset($quota['abort']);
return $quota;
}
/**
* Get file object.
*
* @param string $file_name Name of a file (with folder path)
* @param kolab_storage_folder $folder Reference to folder object
*
* @return array File data
* @throws Exception
*/
protected function get_file_object(&$file_name, &$folder = null)
{
// extract file path and file name
$path = explode(file_storage::SEPARATOR, $file_name);
$file_name = array_pop($path);
$folder_name = implode(file_storage::SEPARATOR, $path);
if ($folder_name === '') {
throw new Exception("Missing folder name", file_storage::ERROR);
}
// get folder object
$folder = $this->get_folder_object($folder_name);
$files = $folder->select(array(
array('type', '=', 'file'),
array('filename', '=', $file_name)
));
return $files[0];
}
/**
* Get folder object.
*
* @param string $folder_name Name of a folder with full path
*
* @return kolab_storage_folder Folder object
* @throws Exception
*/
protected function get_folder_object($folder_name)
{
if ($folder_name === null || $folder_name === '') {
throw new Exception("Missing folder name", file_storage::ERROR);
}
if (empty($this->folders[$folder_name])) {
$storage = $this->rc->get_storage();
$separator = $storage->get_hierarchy_delimiter();
$folder_name = str_replace(file_storage::SEPARATOR, $separator, $folder_name);
$imap_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$folder = kolab_storage::get_folder($imap_name);
if (!$folder) {
throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
}
$this->folders[$folder_name] = $folder;
}
return $this->folders[$folder_name];
}
/**
* Simplify internal structure of the file object
*/
protected function from_file_object($file)
{
if (empty($file['_attachments'])) {
return $file;
}
$attachment = array_shift($file['_attachments']);
$file['name'] = $attachment['name'];
$file['size'] = $attachment['size'];
$file['type'] = $attachment['mimetype'];
$file['fileid'] = $attachment['id'];
unset($file['_attachments']);
return $file;
}
/**
* Convert to kolab_format internal structure of the file object
*/
protected function to_file_object($file)
{
// @TODO if path is empty and fileid exists it is an update
// get attachment body and save it in path
$file['_attachments'] = array(
0 => array(
'name' => $file['name'],
'path' => $file['path'],
'content' => $file['content'],
'mimetype' => $file['type'],
'size' => $file['size'],
));
unset($file['name']);
unset($file['size']);
unset($file['type']);
unset($file['path']);
unset($file['fileid']);
return $file;
}
protected function uri2resource($uri)
{
$storage = $this->rc->get_storage();
$namespace = $storage->get_namespace();
$separator = $storage->get_hierarchy_delimiter();
$uri = str_replace(file_storage::SEPARATOR, $separator, $uri);
$owner = $this->rc->get_user_name();
// find the owner and remove namespace prefix
foreach ($namespace as $type => $ns) {
foreach ($ns as $root) {
if (is_array($root) && $root[0] && strpos($uri, $root[0]) === 0) {
$uri = substr($uri, strlen($root[0]));
switch ($type) {
case 'shared':
// in theory there can be more than one shared root
// we add it to dummy user name, so we can revert conversion
$owner = "shared({$root[0]})";
break;
case 'other':
list($user, $uri) = explode($separator, $uri, 2);
if (strpos($user, '@') === false) {
$domain = strstr($owner, '@');
if (!empty($domain)) {
$user .= $domain;
}
}
$owner = $user;
break;
}
break 2;
}
}
}
// convert to imap charset (to be safe to store in DB)
$uri = rcube_charset::convert($uri, RCUBE_CHARSET, 'UTF7-IMAP');
return 'imap://' . urlencode($owner) . '@' . $storage->options['host'] . '/' . $uri;
}
protected function resource2uri($resource)
{
if (!preg_match('|^imap://([^@]+)@([^/]+)/(.*)$|', $resource, $matches)) {
throw new Exception("Internal storage error. Unexpected data format.", file_storage::ERROR);
}
$storage = $this->rc->get_storage();
$separator = $storage->get_hierarchy_delimiter();
$owner = $this->rc->get_user_name();
$user = urldecode($matches[1]);
$uri = $matches[3];
// convert from imap charset (to be safe to store in DB)
$uri = rcube_charset::convert($uri, 'UTF7-IMAP', RCUBE_CHARSET);
// personal namespace
if ($user == $owner) {
// do nothing
// Note: that might not work if personal namespace uses e.g. INBOX/ prefix.
}
// shared namespace
else if (preg_match('/^shared\((.*)\)$/', $user, $matches)) {
$uri = $matches[1] . $uri;
}
// other users namespace
else {
$namespace = $storage->get_namespace('other');
list($local, $domain) = explode('@', $user);
// here we assume there's only one other users namespace root
$uri = $namespace[0][0] . $local . $separator . $uri;
}
$uri = str_replace($separator, file_storage::SEPARATOR, $uri);
return $uri;
}
/**
* Initializes file_locks object
*/
protected function init_lock_db()
{
if (!$this->lock_db) {
$this->lock_db = new file_locks;
}
}
}
diff --git a/lib/kolab/plugins/kolab_auth/LICENSE b/lib/drivers/kolab/plugins/kolab_auth/LICENSE
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/LICENSE
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/config.inc.php.dist
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/kolab_auth.php
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/kolab_auth_ldap.php
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/bg_BG.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/de_CH.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/de_DE.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/en_US.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/es_ES.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/et_EE.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/fr_FR.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/ja_JP.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/nl_NL.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/pl_PL.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/pt_BR.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/localization/ru_RU.inc
rename to 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
similarity index 100%
rename from lib/kolab/plugins/kolab_auth/package.xml
rename to lib/drivers/kolab/plugins/kolab_auth/package.xml
diff --git a/lib/kolab/plugins/libkolab/LICENSE b/lib/drivers/kolab/plugins/libkolab/LICENSE
similarity index 100%
rename from lib/kolab/plugins/libkolab/LICENSE
rename to lib/drivers/kolab/plugins/libkolab/LICENSE
diff --git a/lib/kolab/plugins/libkolab/README b/lib/drivers/kolab/plugins/libkolab/README
similarity index 100%
rename from lib/kolab/plugins/libkolab/README
rename to 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
similarity index 80%
rename from lib/kolab/plugins/libkolab/SQL/mysql.initial.sql
rename to 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,175 +1,187 @@
/**
* libkolab database schema
*
- * @version 1.0
+ * @version 1.1
* @author Thomas Bruederli
* @licence GNU AGPL
**/
DROP TABLE IF EXISTS `kolab_folders`;
CREATE TABLE `kolab_folders` (
`folder_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`resource` VARCHAR(255) NOT NULL,
`type` VARCHAR(32) NOT NULL,
`synclock` INT(10) NOT NULL DEFAULT '0',
`ctag` VARCHAR(40) DEFAULT NULL,
PRIMARY KEY(`folder_id`),
INDEX `resource_type` (`resource`, `type`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache`;
DROP TABLE IF EXISTS `kolab_cache_contact`;
CREATE TABLE `kolab_cache_contact` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
+ `firstname` VARCHAR(255) NOT NULL,
+ `surname` VARCHAR(255) NOT NULL,
+ `email` VARCHAR(255) NOT NULL,
CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
- INDEX `contact_type` (`folder_id`,`type`)
+ INDEX `contact_type` (`folder_id`,`type`),
+ INDEX `contact_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_event`;
CREATE TABLE `kolab_cache_event` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_event_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- PRIMARY KEY(`folder_id`,`msguid`)
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `event_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_task`;
CREATE TABLE `kolab_cache_task` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_task_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- PRIMARY KEY(`folder_id`,`msguid`)
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `task_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_journal`;
CREATE TABLE `kolab_cache_journal` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_journal_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- PRIMARY KEY(`folder_id`,`msguid`)
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `journal_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_note`;
CREATE TABLE `kolab_cache_note` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- PRIMARY KEY(`folder_id`,`msguid`)
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `note_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_file`;
CREATE TABLE `kolab_cache_file` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`filename` varchar(255) DEFAULT NULL,
CONSTRAINT `fk_kolab_cache_file_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
- INDEX `folder_filename` (`folder_id`, `filename`)
+ INDEX `folder_filename` (`folder_id`, `filename`),
+ INDEX `file_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_configuration`;
CREATE TABLE `kolab_cache_configuration` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
CONSTRAINT `fk_kolab_cache_configuration_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
- INDEX `configuration_type` (`folder_id`,`type`)
+ INDEX `configuration_type` (`folder_id`,`type`),
+ INDEX `configuration_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
DROP TABLE IF EXISTS `kolab_cache_freebusy`;
CREATE TABLE `kolab_cache_freebusy` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
- `data` TEXT NOT NULL,
- `xml` TEXT NOT NULL,
+ `data` LONGTEXT NOT NULL,
+ `xml` LONGBLOB NOT NULL,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_freebusy_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
- PRIMARY KEY(`folder_id`,`msguid`)
+ PRIMARY KEY(`folder_id`,`msguid`),
+ INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013100400');
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2014021000');
diff --git a/lib/kolab/plugins/libkolab/SQL/mysql/2013011000.sql b/lib/drivers/kolab/plugins/libkolab/SQL/mysql/2013011000.sql
similarity index 100%
rename from lib/kolab/plugins/libkolab/SQL/mysql/2013011000.sql
rename to 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
similarity index 100%
rename from lib/kolab/plugins/libkolab/SQL/mysql/2013041900.sql
rename to 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
similarity index 100%
rename from lib/kolab/plugins/libkolab/SQL/mysql/2013100400.sql
rename to 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
similarity index 100%
rename from lib/kolab/plugins/libkolab/SQL/postgres.initial.sql
rename to lib/drivers/kolab/plugins/libkolab/SQL/postgres.initial.sql
diff --git a/lib/kolab/plugins/libkolab/UPGRADING b/lib/drivers/kolab/plugins/libkolab/UPGRADING
similarity index 100%
rename from lib/kolab/plugins/libkolab/UPGRADING
rename to 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
similarity index 83%
rename from lib/kolab/plugins/libkolab/bin/modcache.sh
rename to 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,208 +1,235 @@
-#!/usr/bin/env php -d enable_dl=On
+#!/usr/bin/env php
<?php
/**
* Kolab storage cache modification script
*
* @version 3.1
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
- * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
define('INSTALL_PATH', realpath('.') . '/' );
ini_set('display_errors', 1);
if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
die("Execute this from the Roundcube installation dir!\n\n");
require_once INSTALL_PATH . 'program/include/clisetup.php';
function print_usage()
{
print "Usage: modcache.sh [OPTIONS] ACTION [USERNAME ARGS ...]\n";
print "Possible actions are: expunge, clear, prewarm\n";
print "-a, --all Clear/expunge all caches\n";
print "-h, --host IMAP host name\n";
print "-u, --user IMAP user name to authenticate\n";
print "-t, --type Object types to clear/expunge cache\n";
print "-l, --limit Limit the number of records to be expunged\n";
}
// read arguments
$opts = get_opt(array(
'a' => 'all',
'h' => 'host',
'u' => 'user',
'p' => 'password',
't' => 'type',
'l' => 'limit',
'v' => 'verbose',
));
$opts['username'] = !empty($opts[1]) ? $opts[1] : $opts['user'];
$action = $opts[0];
$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
// connect to database
$db = $rcmail->get_dbh();
$db->db_connect('w');
if (!$db->is_connected() || $db->is_error())
die("No DB connection\n");
+ini_set('display_errors', 1);
/*
* Script controller
*/
switch (strtolower($action)) {
/*
* Clear/expunge all cache records
*/
case 'expunge':
$folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','journal','note','task');
$folder_types_db = array_map(array($db, 'quote'), $folder_types);
$expire = strtotime(!empty($opts[2]) ? $opts[2] : 'now - 10 days');
$sql_where = "type IN (" . join(',', $folder_types_db) . ")";
if ($opts['username']) {
$sql_where .= ' AND resource LIKE ?';
}
$sql_query = "DELETE FROM %s WHERE folder_id IN (SELECT folder_id FROM kolab_folders WHERE $sql_where) AND created <= " . $db->quote(date('Y-m-d 00:00:00', $expire));
if ($opts['limit']) {
$sql_query = ' LIMIT ' . intval($opts['limit']);
}
foreach ($folder_types as $type) {
$table_name = 'kolab_cache_' . $type;
$db->query(sprintf($sql_query, $table_name), resource_prefix($opts).'%');
echo $db->affected_rows() . " records deleted from '$table_name'\n";
}
$db->query("UPDATE kolab_folders SET ctag='' WHERE $sql_where", resource_prefix($opts).'%');
break;
case 'clear':
$folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','journal','note','task');
$folder_types_db = array_map(array($db, 'quote'), $folder_types);
if ($opts['all']) {
$sql_query = "DELETE FROM kolab_folders WHERE 1";
}
else if ($opts['username']) {
$sql_query = "DELETE FROM kolab_folders WHERE type IN (" . join(',', $folder_types_db) . ") AND resource LIKE ?";
}
if ($sql_query) {
- $db->query($sql_query . $sql_add, resource_prefix($opts).'%');
+ $db->query($sql_query, resource_prefix($opts).'%');
echo $db->affected_rows() . " records deleted from 'kolab_folders'\n";
}
break;
/*
* Prewarm cache by synchronizing objects for the given user
*/
case 'prewarm':
// make sure libkolab classes are loaded
$rcmail->plugins->load_plugin('libkolab');
if (authenticate($opts)) {
$folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
foreach ($folder_types as $type) {
// sync every folder of the given type
foreach (kolab_storage::get_folders($type) as $folder) {
echo "Synching " . $folder->name . " ($type) ... ";
echo $folder->count($type) . "\n";
// also sync distribution lists in contact folders
if ($type == 'contact') {
echo "Synching " . $folder->name . " (distribution-list) ... ";
echo $folder->count('distribution-list') . "\n";
}
}
}
}
else
die("Authentication failed for " . $opts['user']);
break;
+/**
+ * Update the cache meta columns from the serialized/xml data
+ * (might be run after a schema update)
+ */
+case 'update':
+ // make sure libkolab classes are loaded
+ $rcmail->plugins->load_plugin('libkolab');
+
+ $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
+ foreach ($folder_types as $type) {
+ $class = 'kolab_storage_cache_' . $type;
+ $sql_result = $db->query("SELECT folder_id FROM kolab_folders WHERE type=? AND synclock = 0", $type);
+ while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
+ $folder = new $class;
+ $folder->select_by_id($sql_arr['folder_id']);
+ echo "Updating " . $sql_arr['folder_id'] . " ($type) ";
+ foreach ($folder->select() as $object) {
+ $object['_formatobj']->to_array(); // load data
+ $folder->save($object['_msguid'], $object, $object['_msguid']);
+ echo ".";
+ }
+ echo "done.\n";
+ }
+ }
+ break;
+
/*
* Unknown action => show usage
*/
default:
print_usage();
exit;
}
/**
* Compose cache resource URI prefix for the given user credentials
*/
function resource_prefix($opts)
{
return 'imap://' . str_replace('%', '\\%', urlencode($opts['username'])) . '@' . $opts['host'] . '/';
}
/**
* Authenticate to the IMAP server with the given user credentials
*/
function authenticate(&$opts)
{
global $rcmail;
// prompt for password
if (empty($opts['password']) && ($opts['username'] || $opts['user'])) {
$opts['password'] = prompt_silent("Password: ");
}
// simulate "login as" feature
if ($opts['user'] && $opts['user'] != $opts['username'])
$_POST['_loginas'] = $opts['username'];
else if (empty($opts['user']))
$opts['user'] = $opts['username'];
// let the kolab_auth plugin do its magic
$auth = $rcmail->plugins->exec_hook('authenticate', array(
'host' => trim($opts['host']),
'user' => trim($opts['user']),
'pass' => $opts['password'],
'cookiecheck' => false,
'valid' => !empty($opts['user']) && !empty($opts['host']),
));
if ($auth['valid']) {
$storage = $rcmail->get_storage();
if ($storage->connect($auth['host'], $auth['user'], $auth['pass'], 143, false)) {
if ($opts['verbose'])
echo "IMAP login succeeded.\n";
if (($user = rcube_user::query($opts['username'], $auth['host'])) && $user->ID)
- $rcmail->set_user($user);
+ $rcmail->user = $user;
}
else
die("Login to IMAP server failed!\n");
}
else {
die("Invalid login credentials!\n");
}
return $auth['valid'];
}
diff --git a/lib/drivers/kolab/plugins/libkolab/bin/randomcontacts.sh b/lib/drivers/kolab/plugins/libkolab/bin/randomcontacts.sh
new file mode 100755
index 0000000..e4a820c
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/bin/randomcontacts.sh
@@ -0,0 +1,181 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * Generate a number contacts with random data
+ *
+ * @version 3.1
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALL_PATH', realpath('.') . '/' );
+ini_set('display_errors', 1);
+
+if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
+ die("Execute this from the Roundcube installation dir!\n\n");
+
+require_once INSTALL_PATH . 'program/include/clisetup.php';
+
+function print_usage()
+{
+ print "Usage: randomcontacts.sh [OPTIONS] USERNAME FOLDER\n";
+ print "Create random contact that for then given user in the specified folder.\n";
+ print "-n, --num Number of contacts to be created, defaults to 50\n";
+ print "-h, --host IMAP host name\n";
+ print "-p, --password IMAP user password\n";
+}
+
+// read arguments
+$opts = get_opt(array(
+ 'n' => 'num',
+ 'h' => 'host',
+ 'u' => 'user',
+ 'p' => 'pass',
+ 'v' => 'verbose',
+));
+
+$opts['username'] = !empty($opts[0]) ? $opts[0] : $opts['user'];
+$opts['folder'] = $opts[1];
+
+$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
+$rcmail->plugins->load_plugins(array('libkolab'));
+ini_set('display_errors', 1);
+
+
+if (empty($opts['host'])) {
+ $opts['host'] = $rcmail->config->get('default_host');
+ if (is_array($opts['host'])) // not unique
+ $opts['host'] = null;
+}
+
+if (empty($opts['username']) || empty($opts['folder']) || empty($opts['host'])) {
+ print_usage();
+ exit;
+}
+
+// prompt for password
+if (empty($opts['pass'])) {
+ $opts['pass'] = rcube_utils::prompt_silent("Password: ");
+}
+
+// parse $host URL
+$a_host = parse_url($opts['host']);
+if ($a_host['host']) {
+ $host = $a_host['host'];
+ $imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? TRUE : FALSE;
+ $imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
+}
+else {
+ $host = $opts['host'];
+ $imap_port = 143;
+}
+
+// instantiate IMAP class
+$IMAP = $rcmail->get_storage();
+
+// try to connect to IMAP server
+if ($IMAP->connect($host, $opts['username'], $opts['pass'], $imap_port, $imap_ssl)) {
+ print "IMAP login successful.\n";
+ $user = rcube_user::query($opts['username'], $host);
+ $rcmail->user = $user ?: new rcube_user(null, array('username' => $opts['username'], 'host' => $host));
+}
+else {
+ die("IMAP login failed for user " . $opts['username'] . " @ $host\n");
+}
+
+// get contacts folder
+$folder = kolab_storage::get_folder($opts['folder']);
+if (!$folder || empty($folder->type)) {
+ die("Invalid Address Book " . $opts['folder'] . "\n");
+}
+
+$format = new kolab_format_contact;
+
+$num = $opts['num'] ? intval($opts['num']) : 50;
+echo "Creating $num contacts in " . $folder->get_resource_uri() . "\n";
+
+for ($i=0; $i < $num; $i++) {
+ // generate random names
+ $contact = array(
+ 'surname' => random_string(rand(1,2)),
+ 'firstname' => random_string(rand(1,2)),
+ 'organization' => random_string(rand(0,2)),
+ 'profession' => random_string(rand(1,2)),
+ 'email' => array(),
+ 'phone' => array(),
+ 'address' => array(),
+ 'notes' => random_string(rand(10,200)),
+ );
+
+ // randomly add email addresses
+ $em = rand(1,3);
+ for ($e=0; $e < $em; $e++) {
+ $type = array_rand($format->emailtypes);
+ $contact['email'][] = array(
+ 'address' => strtolower(random_string(1) . '@' . random_string(1) . '.tld'),
+ 'type' => $type,
+ );
+ }
+
+ // randomly add phone numbers
+ $ph = rand(1,4);
+ for ($p=0; $p < $ph; $p++) {
+ $type = array_rand($format->phonetypes);
+ $contact['phone'][] = array(
+ 'number' => '+'.rand(2,8).rand(1,9).rand(1,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9),
+ 'type' => $type,
+ );
+ }
+
+ // randomly add addresses
+ $ad = rand(0,2);
+ for ($a=0; $a < $ad; $a++) {
+ $type = array_rand($format->addresstypes);
+ $contact['address'][] = array(
+ 'street' => random_string(rand(1,3)),
+ 'locality' => random_string(rand(1,2)),
+ 'code' => rand(1000, 89999),
+ 'country' => random_string(1),
+ 'type' => $type,
+ );
+ }
+
+ $contact['name'] = $contact['firstname'] . ' ' . $contact['surname'];
+
+ if ($folder->save($contact, 'contact')) {
+ echo ".";
+ }
+ else {
+ echo "x";
+ break; // abort on error
+ }
+}
+
+echo " done.\n";
+
+
+
+function random_string($len)
+{
+ $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a features is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform hereafter referred to without the classical prefix retains many applications, as most manufac- tured parts and many anatomical parts investigated in medical imagery contain feature boundaries which can be described by regular curves. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise.");
+ for ($i = 0; $i < $len; $i++) {
+ $str .= $words[rand(0,count($words)-1)] . " ";
+ }
+
+ return rtrim($str);
+}
diff --git a/lib/drivers/kolab/plugins/libkolab/composer.json b/lib/drivers/kolab/plugins/libkolab/composer.json
new file mode 100644
index 0000000..8926037
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/composer.json
@@ -0,0 +1,30 @@
+{
+ "name": "kolab/libkolab",
+ "type": "roundcube-plugin",
+ "description": "Plugin to setup a basic environment for the interaction with a Kolab server.",
+ "homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
+ "license": "AGPLv3",
+ "version": "1.1.0",
+ "authors": [
+ {
+ "name": "Thomas Bruederli",
+ "email": "bruederli@kolabsys.com",
+ "role": "Lead"
+ },
+ {
+ "name": "Alensader Machniak",
+ "email": "machniak@kolabsys.com",
+ "role": "Developer"
+ }
+ ],
+ "repositories": [
+ {
+ "type": "composer",
+ "url": "http://plugins.roundcube.net"
+ }
+ ],
+ "require": {
+ "php": ">=5.3.0",
+ "roundcube/plugin-installer": ">=0.1.3"
+ }
+}
diff --git a/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist b/lib/drivers/kolab/plugins/libkolab/config.inc.php.dist
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
similarity index 92%
rename from lib/kolab/plugins/libkolab/lib/kolab_date_recurrence.php
rename to 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
@@ -1,135 +1,135 @@
<?php
/**
* Recurrence computation class for xcal-based Kolab format objects
*
* Utility class to compute instances of recurring events.
* It requires the libcalendaring PHP module to be installed and loaded.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_date_recurrence
{
private /* EventCal */ $engine;
private /* kolab_format_xcal */ $object;
private /* DateTime */ $start;
private /* DateTime */ $next;
private /* cDateTime */ $cnext;
private /* DateInterval */ $duration;
/**
* Default constructor
*
* @param array The Kolab object to operate on
*/
function __construct($object)
{
$data = $object->to_array();
$this->object = $object;
$this->engine = $object->to_libcal();
$this->start = $this->next = $data['start'];
$this->cnext = kolab_format::get_datetime($this->next);
if (is_object($data['start']) && is_object($data['end']))
$this->duration = $data['start']->diff($data['end']);
else
$this->duration = new DateInterval('PT' . ($data['end'] - $data['start']) . 'S');
}
/**
* Get date/time of the next occurence of this event
*
* @param boolean Return a Unix timestamp instead of a DateTime object
* @return mixed DateTime object/unix timestamp or False if recurrence ended
*/
public function next_start($timestamp = false)
{
$time = false;
if ($this->engine && $this->next) {
if (($cnext = new cDateTime($this->engine->getNextOccurence($this->cnext))) && $cnext->isValid()) {
$next = kolab_format::php_datetime($cnext);
$time = $timestamp ? $next->format('U') : $next;
$this->cnext = $cnext;
$this->next = $next;
}
}
return $time;
}
/**
* Get the next recurring instance of this event
*
* @return mixed Array with event properties or False if recurrence ended
*/
public function next_instance()
{
if ($next_start = $this->next_start()) {
$next_end = clone $next_start;
$next_end->add($this->duration);
$next = $this->object->to_array();
$next['recurrence_id'] = $next_start->format('Y-m-d');
$next['start'] = $next_start;
$next['end'] = $next_end;
unset($next['_formatobj']);
return $next;
}
return false;
}
/**
* Get the end date of the occurence of this recurrence cycle
*
- * @return mixed Timestamp with end date of the last event or False if recurrence exceeds limit
+ * @return DateTime|bool End datetime of the last event or False if recurrence exceeds limit
*/
public function end()
{
$event = $this->object->to_array();
// recurrence end date is given
if ($event['recurrence']['UNTIL'] instanceof DateTime) {
- return $event['recurrence']['UNTIL']->format('U');
+ return $event['recurrence']['UNTIL'];
}
// let libkolab do the work
if ($this->engine && ($cend = $this->engine->getLastOccurrence()) && ($end_dt = kolab_format::php_datetime(new cDateTime($cend)))) {
- return $end_dt->format('U');
+ return $end_dt;
}
// determine a reasonable end date if none given
- if (!$event['recurrence']['COUNT']) {
+ if (!$event['recurrence']['COUNT'] && $event['end'] instanceof DateTime) {
switch ($event['recurrence']['FREQ']) {
case 'YEARLY': $intvl = 'P100Y'; break;
case 'MONTHLY': $intvl = 'P20Y'; break;
default: $intvl = 'P10Y'; break;
}
- $end_dt = clone $event['start'];
+ $end_dt = clone $event['end'];
$end_dt->add(new DateInterval($intvl));
- return $end_dt->format('U');
+ return $end_dt;
}
return false;
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format.php
similarity index 61%
rename from lib/kolab/plugins/libkolab/lib/kolab_format.php
rename to 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
@@ -1,499 +1,699 @@
<?php
/**
* Kolab format model class wrapping libkolabxml bindings
*
* Abstract base class for different Kolab groupware objects read from/written
* to the new Kolab 3 format using the PHP bindings of libkolabxml.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
abstract class kolab_format
{
public static $timezone;
public /*abstract*/ $CTYPE;
public /*abstract*/ $CTYPEv2;
protected /*abstract*/ $objclass;
protected /*abstract*/ $read_func;
protected /*abstract*/ $write_func;
protected $obj;
protected $data;
protected $xmldata;
protected $xmlobject;
protected $formaterror;
protected $loaded = false;
protected $version = '3.0';
const KTYPE_PREFIX = 'application/x-vnd.kolab.';
- const PRODUCT_ID = 'Roundcube-libkolab-0.9';
+ const PRODUCT_ID = 'Roundcube-libkolab-1.1';
+
+ // 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
*
* @param string Object type to instantiate
* @param float Format version
* @param string Cached xml data to initialize with
* @return object kolab_format
*/
public static function factory($type, $version = '3.0', $xmldata = null)
{
if (!isset(self::$timezone))
self::$timezone = new DateTimeZone('UTC');
if (!self::supports($version))
return PEAR::raiseError("No support for Kolab format version " . $version);
- $type = preg_replace('/configuration\.[a-z.]+$/', 'configuration', $type);
+ $type = preg_replace('/configuration\.[a-z._]+$/', 'configuration', $type);
$suffix = preg_replace('/[^a-z]+/', '', $type);
$classname = 'kolab_format_' . $suffix;
if (class_exists($classname))
return new $classname($xmldata, $version);
return PEAR::raiseError("Failed to load Kolab Format wrapper for type " . $type);
}
/**
* Determine support for the given format version
*
* @param float Format version to check
* @return boolean True if supported, False otherwise
*/
public static function supports($version)
{
if ($version == '2.0')
return class_exists('kolabobject');
// default is version 3
return class_exists('kolabformat');
}
/**
* Convert the given date/time value into a cDateTime object
*
* @param mixed Date/Time value either as unix timestamp, date string or PHP DateTime object
* @param DateTimeZone The timezone the date/time is in. Use global default if Null, local time if False
* @param boolean True of the given date has no time component
* @return object The libkolabxml date/time object
*/
public static function get_datetime($datetime, $tz = null, $dateonly = false)
{
// use timezone information from datetime of global setting
if (!$tz && $tz !== false) {
if ($datetime instanceof DateTime)
$tz = $datetime->getTimezone();
if (!$tz)
$tz = self::$timezone;
}
$result = new cDateTime();
try {
// got a unix timestamp (in UTC)
if (is_numeric($datetime)) {
$datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
if ($tz) $datetime->setTimezone($tz);
}
else if (is_string($datetime) && strlen($datetime)) {
$datetime = new DateTime($datetime, $tz ?: null);
}
}
catch (Exception $e) {}
if ($datetime instanceof DateTime) {
$result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
if (!$dateonly)
$result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
- if ($tz && $tz->getName() == 'UTC')
+ if ($tz && in_array($tz->getName(), array('UTC', 'GMT', '+00:00', 'Z'))) {
$result->setUTC(true);
- else if ($tz !== false)
- $result->setTimezone($tz->getName());
+ }
+ 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;
}
/**
* Convert the given cDateTime into a PHP DateTime object
*
* @param object cDateTime The libkolabxml datetime object
* @return object DateTime PHP datetime instance
*/
public static function php_datetime($cdt)
{
if (!is_object($cdt) || !$cdt->isValid())
return null;
$d = new DateTime;
$d->setTimezone(self::$timezone);
try {
if ($tzs = $cdt->timezone()) {
$tz = new DateTimeZone($tzs);
$d->setTimezone($tz);
}
else if ($cdt->isUTC()) {
$d->setTimezone(new DateTimeZone('UTC'));
}
}
catch (Exception $e) { }
$d->setDate($cdt->year(), $cdt->month(), $cdt->day());
if ($cdt->isDateOnly()) {
$d->_dateonly = true;
$d->setTime(12, 0, 0); // set time to noon to avoid timezone troubles
}
else {
$d->setTime($cdt->hour(), $cdt->minute(), $cdt->second());
}
return $d;
}
/**
* Convert a libkolabxml vector to a PHP array
*
* @param object vector Object
- * @return array Indexed array contaning vector elements
+ * @return array Indexed array containing vector elements
*/
public static function vector2array($vec, $max = PHP_INT_MAX)
{
$arr = array();
for ($i=0; $i < $vec->size() && $i < $max; $i++)
$arr[] = $vec->get($i);
return $arr;
}
/**
* Build a libkolabxml vector (string) from a PHP array
*
* @param array Array with vector elements
* @return object vectors
*/
public static function array2vector($arr)
{
$vec = new vectors;
foreach ((array)$arr as $val) {
if (strlen($val))
$vec->push($val);
}
return $vec;
}
/**
* Parse the X-Kolab-Type header from MIME messages and return the object type in short form
*
* @param string X-Kolab-Type header value
* @return string Kolab object type (contact,event,task,note,etc.)
*/
public static function mime2object_type($x_kolab_type)
{
- return preg_replace('/dictionary.[a-z.]+$/', 'dictionary', substr($x_kolab_type, strlen(self::KTYPE_PREFIX)));
+ return preg_replace(
+ array('/dictionary.[a-z.]+$/', '/contact.distlist$/'),
+ array( 'dictionary', 'distribution-list'),
+ substr($x_kolab_type, strlen(self::KTYPE_PREFIX))
+ );
}
/**
* Default constructor of all kolab_format_* objects
*/
public function __construct($xmldata = null, $version = null)
{
$this->obj = new $this->objclass;
$this->xmldata = $xmldata;
if ($version)
$this->version = $version;
// use libkolab module if available
if (class_exists('kolabobject'))
$this->xmlobject = new XMLObject();
}
/**
* Check for format errors after calling kolabformat::write*()
*
* @return boolean True if there were errors, False if OK
*/
protected function format_errors()
{
$ret = $log = false;
switch (kolabformat::error()) {
case kolabformat::NoError:
$ret = false;
break;
case kolabformat::Warning:
$ret = false;
- $log = "Warning";
+ $uid = is_object($this->obj) ? $this->obj->uid() : $this->data['uid'];
+ $log = "Warning @ $uid";
break;
default:
$ret = true;
$log = "Error";
}
if ($log && !isset($this->formaterror)) {
rcube::raise_error(array(
'code' => 660,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "kolabformat $log: " . kolabformat::errorMessage(),
), true);
$this->formaterror = $ret;
}
return $ret;
}
/**
* Save the last generated UID to the object properties.
* Should be called after kolabformat::writeXXXX();
*/
protected function update_uid()
{
// get generated UID
if (!$this->data['uid']) {
if ($this->xmlobject) {
$this->data['uid'] = $this->xmlobject->getSerializedUID();
}
if (empty($this->data['uid'])) {
$this->data['uid'] = kolabformat::getSerializedUID();
}
$this->obj->setUid($this->data['uid']);
}
}
/**
* Initialize libkolabxml object with cached xml data
*/
protected function init()
{
if (!$this->loaded) {
if ($this->xmldata) {
$this->load($this->xmldata);
$this->xmldata = null;
}
$this->loaded = true;
}
}
/**
* Get constant value for libkolab's version parameter
*
* @param float Version value to convert
* @return int Constant value of either kolabobject::KolabV2 or kolabobject::KolabV3 or false if kolabobject module isn't available
*/
protected function libversion($v = null)
{
if (class_exists('kolabobject')) {
$version = $v ?: $this->version;
if ($version <= '2.0')
return kolabobject::KolabV2;
else
return kolabobject::KolabV3;
}
return false;
}
/**
* Determine the correct libkolab(xml) wrapper function for the given call
* depending on the available PHP modules
*/
protected function libfunc($func)
{
if (is_array($func) || strpos($func, '::'))
return $func;
else if (class_exists('kolabobject'))
return array($this->xmlobject, $func);
else
return 'kolabformat::' . $func;
}
/**
* Direct getter for object properties
*/
public function __get($var)
{
return $this->data[$var];
}
/**
* Load Kolab object data from the given XML block
*
* @param string XML data
* @return boolean True on success, False on failure
*/
public function load($xml)
{
$this->formaterror = null;
$read_func = $this->libfunc($this->read_func);
if (is_array($read_func))
$r = call_user_func($read_func, $xml, $this->libversion());
else
$r = call_user_func($read_func, $xml, false);
if (is_resource($r))
$this->obj = new $this->objclass($r);
else if (is_a($r, $this->objclass))
$this->obj = $r;
$this->loaded = !$this->format_errors();
}
/**
* Write object data to XML format
*
* @param float Format version to write
* @return string XML data
*/
public function write($version = null)
{
$this->formaterror = null;
$this->init();
$write_func = $this->libfunc($this->write_func);
if (is_array($write_func))
$this->xmldata = call_user_func($write_func, $this->obj, $this->libversion($version), self::PRODUCT_ID);
else
$this->xmldata = call_user_func($write_func, $this->obj, self::PRODUCT_ID);
if (!$this->format_errors())
$this->update_uid();
else
$this->xmldata = null;
return $this->xmldata;
}
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
$this->init();
if (!empty($object['uid']))
$this->obj->setUid($object['uid']);
// set some automatic values if missing
if (empty($object['created']) && method_exists($this->obj, 'setCreated')) {
$cdt = $this->obj->created();
$object['created'] = $cdt && $cdt->isValid() ? self::php_datetime($cdt) : new DateTime('now', new DateTimeZone('UTC'));
if (!$cdt || !$cdt->isValid())
$this->obj->setCreated(self::get_datetime($object['created']));
}
$object['changed'] = new DateTime('now', new DateTimeZone('UTC'));
$this->obj->setLastModified(self::get_datetime($object['changed']));
// Save custom properties of the given object
- if (isset($object['x-custom'])) {
+ if (isset($object['x-custom']) && method_exists($this->obj, 'setCustomProperties')) {
$vcustom = new vectorcs;
foreach ((array)$object['x-custom'] as $cp) {
if (is_array($cp))
$vcustom->push(new CustomProperty($cp[0], $cp[1]));
}
$this->obj->setCustomProperties($vcustom);
}
- else { // load custom properties from XML for caching (#2238)
+ // load custom properties from XML for caching (#2238) if method exists (#3125)
+ else if (method_exists($this->obj, 'customProperties')) {
$object['x-custom'] = array();
$vcustom = $this->obj->customProperties();
for ($i=0; $i < $vcustom->size(); $i++) {
$cp = $vcustom->get($i);
$object['x-custom'][] = array($cp->identifier, $cp->value);
}
}
}
/**
* Convert the Kolab object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Kolab object data as hash array
*/
public function to_array($data = array())
{
$this->init();
// read object properties into local data object
$object = array(
'uid' => $this->obj->uid(),
'changed' => self::php_datetime($this->obj->lastModified()),
);
// not all container support the created property
if (method_exists($this->obj, 'created')) {
$object['created'] = self::php_datetime($this->obj->created());
}
// read custom properties
- $vcustom = $this->obj->customProperties();
- for ($i=0; $i < $vcustom->size(); $i++) {
- $cp = $vcustom->get($i);
- $object['x-custom'][] = array($cp->identifier, $cp->value);
+ if (method_exists($this->obj, 'customProperties')) {
+ $vcustom = $this->obj->customProperties();
+ for ($i=0; $i < $vcustom->size(); $i++) {
+ $cp = $vcustom->get($i);
+ $object['x-custom'][] = array($cp->identifier, $cp->value);
+ }
}
// merge with additional data, e.g. attachments from the message
if ($data) {
foreach ($data as $idx => $value) {
if (is_array($value)) {
$object[$idx] = array_merge((array)$object[$idx], $value);
}
else {
$object[$idx] = $value;
}
}
}
return $object;
}
/**
* Object validation method to be implemented by derived classes
*/
abstract public function is_valid();
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
return array();
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
return array();
}
+
+ /**
+ * Utility function to extract object attachment data
+ *
+ * @param array Hash array reference to append attachment data into
+ */
+ public function get_attachments(&$object)
+ {
+ $this->init();
+
+ // handle attachments
+ $vattach = $this->obj->attachments();
+ for ($i=0; $i < $vattach->size(); $i++) {
+ $attach = $vattach->get($i);
+
+ // skip cid: attachments which are mime message parts handled by kolab_storage_folder
+ if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
+ $name = $attach->label();
+ $key = $name . (isset($object['_attachments'][$name]) ? '.'.$i : '');
+ $content = $attach->data();
+ $object['_attachments'][$key] = array(
+ 'id' => 'i:'.$i,
+ 'name' => $name,
+ 'mimetype' => $attach->mimetype(),
+ 'size' => strlen($content),
+ 'content' => $content,
+ );
+ }
+ else if (in_array(substr($attach->uri(), 0, 4), array('http','imap'))) {
+ $object['links'][] = $attach->uri();
+ }
+ }
+ }
+
+ /**
+ * Utility function to set attachment properties to the kolabformat object
+ *
+ * @param array Object data as hash array
+ * @param boolean True to always overwrite attachment information
+ */
+ protected function set_attachments($object, $write = true)
+ {
+ // save attachments
+ $vattach = new vectorattachment;
+ foreach ((array) $object['_attachments'] as $cid => $attr) {
+ if (empty($attr))
+ continue;
+ $attach = new Attachment;
+ $attach->setLabel((string)$attr['name']);
+ $attach->setUri('cid:' . $cid, $attr['mimetype'] ?: 'application/octet-stream');
+ if ($attach->isValid()) {
+ $vattach->push($attach);
+ $write = true;
+ }
+ else {
+ rcube::raise_error(array(
+ 'code' => 660,
+ 'type' => 'php',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ 'message' => "Invalid attributes for attachment $cid: " . var_export($attr, true),
+ ), true);
+ }
+ }
+
+ foreach ((array) $object['links'] as $link) {
+ $attach = new Attachment;
+ $attach->setUri($link, 'unknown');
+ $vattach->push($attach);
+ $write = true;
+ }
+
+ if ($write) {
+ $this->obj->setAttachments($vattach);
+ }
+ }
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_configuration.php
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
similarity index 87%
rename from lib/kolab/plugins/libkolab/lib/kolab_format_contact.php
rename to 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
@@ -1,450 +1,482 @@
<?php
/**
* Kolab Contact model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_contact extends kolab_format
{
public $CTYPE = 'application/vcard+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.contact';
protected $objclass = 'Contact';
protected $read_func = 'readContact';
protected $write_func = 'writeContact';
public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email:address');
public $phonetypes = array(
'home' => Telephone::Home,
'work' => Telephone::Work,
'text' => Telephone::Text,
'main' => Telephone::Voice,
'homefax' => Telephone::Fax,
'workfax' => Telephone::Fax,
'mobile' => Telephone::Cell,
'video' => Telephone::Video,
'pager' => Telephone::Pager,
'car' => Telephone::Car,
'other' => Telephone::Textphone,
);
public $emailtypes = array(
'home' => Email::Home,
'work' => Email::Work,
'other' => Email::NoType,
);
public $addresstypes = array(
'home' => Address::Home,
'work' => Address::Work,
'office' => 0,
);
private $gendermap = array(
'female' => Contact::Female,
'male' => Contact::Male,
);
private $relatedmap = array(
'manager' => Related::Manager,
'assistant' => Related::Assistant,
'spouse' => Related::Spouse,
'children' => Related::Child,
);
/**
* Default constructor
*/
function __construct($xmldata = null, $version = 3.0)
{
parent::__construct($xmldata, $version);
// complete phone types
$this->phonetypes['homefax'] |= Telephone::Home;
$this->phonetypes['workfax'] |= Telephone::Work;
}
/**
* Set contact properties to the kolabformat object
*
* @param array Contact data as hash array
*/
public function set(&$object)
{
// set common object properties
parent::set($object);
// do the hard work of setting object values
$nc = new NameComponents;
$nc->setSurnames(self::array2vector($object['surname']));
$nc->setGiven(self::array2vector($object['firstname']));
$nc->setAdditional(self::array2vector($object['middlename']));
$nc->setPrefixes(self::array2vector($object['prefix']));
$nc->setSuffixes(self::array2vector($object['suffix']));
$this->obj->setNameComponents($nc);
$this->obj->setName($object['name']);
$this->obj->setCategories(self::array2vector($object['categories']));
if (isset($object['nickname']))
$this->obj->setNickNames(self::array2vector($object['nickname']));
- if (isset($object['profession']))
- $this->obj->setTitles(self::array2vector($object['profession']));
+ if (isset($object['jobtitle']))
+ $this->obj->setTitles(self::array2vector($object['jobtitle']));
// organisation related properties (affiliation)
$org = new Affiliation;
$offices = new vectoraddress;
if ($object['organization'])
$org->setOrganisation($object['organization']);
if ($object['department'])
$org->setOrganisationalUnits(self::array2vector($object['department']));
- if ($object['jobtitle'])
- $org->setRoles(self::array2vector($object['jobtitle']));
+ if ($object['profession'])
+ $org->setRoles(self::array2vector($object['profession']));
$rels = new vectorrelated;
- if ($object['manager']) {
- foreach ((array)$object['manager'] as $manager)
- $rels->push(new Related(Related::Text, $manager, Related::Manager));
- }
- if ($object['assistant']) {
- foreach ((array)$object['assistant'] as $assistant)
- $rels->push(new Related(Related::Text, $assistant, Related::Assistant));
+ foreach (array('manager','assistant') as $field) {
+ if (!empty($object[$field])) {
+ $reltype = $this->relatedmap[$field];
+ foreach ((array)$object[$field] as $value) {
+ $rels->push(new Related(Related::Text, $value, $reltype));
+ }
+ }
}
$org->setRelateds($rels);
// im, email, url
$this->obj->setIMaddresses(self::array2vector($object['im']));
if (class_exists('vectoremail')) {
$vemails = new vectoremail;
foreach ((array)$object['email'] as $email) {
$type = $this->emailtypes[$email['type']];
$vemails->push(new Email($email['address'], intval($type)));
}
}
else {
$vemails = self::array2vector(array_map(function($v){ return $v['address']; }, $object['email']));
}
$this->obj->setEmailAddresses($vemails);
$vurls = new vectorurl;
foreach ((array)$object['website'] as $url) {
$type = $url['type'] == 'blog' ? Url::Blog : Url::NoType;
$vurls->push(new Url($url['url'], $type));
}
$this->obj->setUrls($vurls);
// addresses
$adrs = new vectoraddress;
foreach ((array)$object['address'] as $address) {
$adr = new Address;
$type = $this->addresstypes[$address['type']];
if (isset($type))
$adr->setTypes($type);
else if ($address['type'])
$adr->setLabel($address['type']);
if ($address['street'])
$adr->setStreet($address['street']);
if ($address['locality'])
$adr->setLocality($address['locality']);
if ($address['code'])
$adr->setCode($address['code']);
if ($address['region'])
$adr->setRegion($address['region']);
if ($address['country'])
$adr->setCountry($address['country']);
if ($address['type'] == 'office')
$offices->push($adr);
else
$adrs->push($adr);
}
$this->obj->setAddresses($adrs);
$org->setAddresses($offices);
// add org affiliation after addresses are set
$orgs = new vectoraffiliation;
$orgs->push($org);
$this->obj->setAffiliations($orgs);
// telephones
$tels = new vectortelephone;
foreach ((array)$object['phone'] as $phone) {
$tel = new Telephone;
if (isset($this->phonetypes[$phone['type']]))
$tel->setTypes($this->phonetypes[$phone['type']]);
$tel->setNumber($phone['number']);
$tels->push($tel);
}
$this->obj->setTelephones($tels);
if (isset($object['gender']))
$this->obj->setGender($this->gendermap[$object['gender']] ? $this->gendermap[$object['gender']] : Contact::NotSet);
if (isset($object['notes']))
$this->obj->setNote($object['notes']);
if (isset($object['freebusyurl']))
$this->obj->setFreeBusyUrl($object['freebusyurl']);
+ if (isset($object['lang']))
+ $this->obj->setLanguages(self::array2vector($object['lang']));
if (isset($object['birthday']))
$this->obj->setBDay(self::get_datetime($object['birthday'], false, true));
if (isset($object['anniversary']))
$this->obj->setAnniversary(self::get_datetime($object['anniversary'], false, true));
if (!empty($object['photo'])) {
if ($type = rcube_mime::image_content_type($object['photo']))
$this->obj->setPhoto($object['photo'], $type);
}
else if (isset($object['photo']))
$this->obj->setPhoto('','');
else if ($this->obj->photoMimetype()) // load saved photo for caching
$object['photo'] = $this->obj->photo();
// spouse and children are relateds
$rels = new vectorrelated;
- if ($object['spouse']) {
- $rels->push(new Related(Related::Text, $object['spouse'], Related::Spouse));
+ foreach (array('spouse','children') as $field) {
+ if (!empty($object[$field])) {
+ $reltype = $this->relatedmap[$field];
+ foreach ((array)$object[$field] as $value) {
+ $rels->push(new Related(Related::Text, $value, $reltype));
+ }
+ }
}
- if ($object['children']) {
- foreach ((array)$object['children'] as $child)
- $rels->push(new Related(Related::Text, $child, Related::Child));
+ // add other relateds
+ if (is_array($object['related'])) {
+ foreach ($object['related'] as $value) {
+ $rels->push(new Related(Related::Text, $value));
+ }
}
$this->obj->setRelateds($rels);
// insert/replace crypto keys
$pgp_index = $pkcs7_index = -1;
$keys = $this->obj->keys();
for ($i=0; $i < $keys->size(); $i++) {
$key = $keys->get($i);
if ($pgp_index < 0 && $key->type() == Key::PGP)
$pgp_index = $i;
else if ($pkcs7_index < 0 && $key->type() == Key::PKCS7_MIME)
$pkcs7_index = $i;
}
$pgpkey = $object['pgppublickey'] ? new Key($object['pgppublickey'], Key::PGP) : new Key();
$pkcs7key = $object['pkcs7publickey'] ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
if ($pgp_index >= 0)
$keys->set($pgp_index, $pgpkey);
else if (!empty($object['pgppublickey']))
$keys->push($pgpkey);
if ($pkcs7_index >= 0)
$keys->set($pkcs7_index, $pkcs7key);
else if (!empty($object['pkcs7publickey']))
$keys->push($pkcs7key);
$this->obj->setKeys($keys);
// TODO: handle language, gpslocation, etc.
// set type property for proper caching
$object['_type'] = 'contact';
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/));
}
/**
* Convert the Contact object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Contact data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common object props into local data object
$object = parent::to_array($data);
$object['name'] = $this->obj->name();
$nc = $this->obj->nameComponents();
$object['surname'] = join(' ', self::vector2array($nc->surnames()));
$object['firstname'] = join(' ', self::vector2array($nc->given()));
$object['middlename'] = join(' ', self::vector2array($nc->additional()));
$object['prefix'] = join(' ', self::vector2array($nc->prefixes()));
$object['suffix'] = join(' ', self::vector2array($nc->suffixes()));
$object['nickname'] = join(' ', self::vector2array($this->obj->nickNames()));
- $object['profession'] = join(' ', self::vector2array($this->obj->titles()));
+ $object['jobtitle'] = join(' ', self::vector2array($this->obj->titles()));
$object['categories'] = self::vector2array($this->obj->categories());
// organisation related properties (affiliation)
$orgs = $this->obj->affiliations();
if ($orgs->size()) {
$org = $orgs->get(0);
$object['organization'] = $org->organisation();
- $object['jobtitle'] = join(' ', self::vector2array($org->roles()));
+ $object['profession'] = join(' ', self::vector2array($org->roles()));
$object['department'] = join(' ', self::vector2array($org->organisationalUnits()));
$this->read_relateds($org->relateds(), $object);
}
$object['im'] = self::vector2array($this->obj->imAddresses());
$emails = $this->obj->emailAddresses();
if ($emails instanceof vectoremail) {
$emailtypes = array_flip($this->emailtypes);
for ($i=0; $i < $emails->size(); $i++) {
$email = $emails->get($i);
$object['email'][] = array('address' => $email->address(), 'type' => $emailtypes[$email->types()]);
}
}
else {
$object['email'] = self::vector2array($emails);
}
$urls = $this->obj->urls();
for ($i=0; $i < $urls->size(); $i++) {
$url = $urls->get($i);
$subtype = $url->type() == Url::Blog ? 'blog' : 'homepage';
$object['website'][] = array('url' => $url->url(), 'type' => $subtype);
}
// addresses
$this->read_addresses($this->obj->addresses(), $object);
if ($org && ($offices = $org->addresses()))
$this->read_addresses($offices, $object, 'office');
// telehones
$tels = $this->obj->telephones();
$teltypes = array_flip($this->phonetypes);
for ($i=0; $i < $tels->size(); $i++) {
$tel = $tels->get($i);
$object['phone'][] = array('number' => $tel->number(), 'type' => $teltypes[$tel->types()]);
}
$object['notes'] = $this->obj->note();
$object['freebusyurl'] = $this->obj->freeBusyUrl();
+ $object['lang'] = self::vector2array($this->obj->languages());
if ($bday = self::php_datetime($this->obj->bDay()))
- $object['birthday'] = $bday->format('c');
+ $object['birthday'] = $bday;
if ($anniversary = self::php_datetime($this->obj->anniversary()))
- $object['anniversary'] = $anniversary->format('c');
+ $object['anniversary'] = $anniversary;
$gendermap = array_flip($this->gendermap);
if (($g = $this->obj->gender()) && $gendermap[$g])
$object['gender'] = $gendermap[$g];
if ($this->obj->photoMimetype())
$object['photo'] = $this->obj->photo();
else if ($this->xmlobject && ($photo_name = $this->xmlobject->pictureAttachmentName()))
$object['photo'] = $photo_name;
// relateds -> spouse, children
- $this->read_relateds($this->obj->relateds(), $object);
+ $this->read_relateds($this->obj->relateds(), $object, 'related');
// crypto settings: currently only key values are supported
$keys = $this->obj->keys();
for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
$key = $keys->get($i);
if ($key->type() == Key::PGP)
$object['pgppublickey'] = $key->key();
else if ($key->type() == Key::PKCS7_MIME)
$object['pkcs7publickey'] = $key->key();
}
$this->data = $object;
return $this->data;
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
$data = '';
foreach (self::$fulltext_cols as $colname) {
list($col, $field) = explode(':', $colname);
if ($field) {
$a = array();
foreach ((array)$this->data[$col] as $attr)
$a[] = $attr[$field];
$val = join(' ', $a);
}
else {
$val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
}
if (strlen($val))
$data .= $val . ' ';
}
return array_unique(rcube_utils::normalize_string($data, true));
}
+ /**
+ * Callback for kolab_storage_cache to get object specific tags to cache
+ *
+ * @return array List of tags to save in cache
+ */
+ public function get_tags()
+ {
+ $tags = array();
+
+ if (!empty($this->data['birthday'])) {
+ $tags[] = 'x-has-birthday';
+ }
+
+ return $tags;
+ }
+
/**
* Helper method to copy contents of an Address vector to the contact data object
*/
private function read_addresses($addresses, &$object, $type = null)
{
$adrtypes = array_flip($this->addresstypes);
for ($i=0; $i < $addresses->size(); $i++) {
$adr = $addresses->get($i);
$object['address'][] = array(
'type' => $type ? $type : ($adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : ''), /*$adr->label()),*/
'street' => $adr->street(),
'code' => $adr->code(),
'locality' => $adr->locality(),
'region' => $adr->region(),
'country' => $adr->country()
);
}
}
/**
* Helper method to map contents of a Related vector to the contact data object
*/
- private function read_relateds($rels, &$object)
+ private function read_relateds($rels, &$object, $catchall = null)
{
$typemap = array_flip($this->relatedmap);
for ($i=0; $i < $rels->size(); $i++) {
$rel = $rels->get($i);
if ($rel->type() != Related::Text) // we can't handle UID relations yet
continue;
+ $known = false;
$types = $rel->relationTypes();
foreach ($typemap as $t => $field) {
if ($types & $t) {
$object[$field][] = $rel->text();
+ $known = true;
break;
}
}
+
+ if (!$known && $catchall) {
+ $object[$catchall][] = $rel->text();
+ }
}
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php
similarity index 85%
rename from lib/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php
rename to 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
@@ -1,113 +1,125 @@
<?php
/**
* Kolab Distribution List model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_distributionlist extends kolab_format
{
public $CTYPE = 'application/vcard+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.distribution-list';
protected $objclass = 'DistList';
protected $read_func = 'readDistlist';
protected $write_func = 'writeDistlist';
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
// set common object properties
parent::set($object);
$this->obj->setName($object['name']);
+ $seen = array();
$members = new vectorcontactref;
- foreach ((array)$object['member'] as $member) {
- if ($member['uid'])
+ foreach ((array)$object['member'] as $i => $member) {
+ if ($member['uid']) {
+ $key = 'uid:' . $member['uid'];
$m = new ContactReference(ContactReference::UidReference, $member['uid']);
- else if ($member['email'])
+ }
+ else if ($member['email']) {
+ $key = 'mailto:' . $member['email'];
$m = new ContactReference(ContactReference::EmailReference, $member['email']);
- else
+ $m->setName($member['name']);
+ }
+ else {
continue;
-
- $m->setName($member['name']);
- $members->push($m);
+ }
+
+ if (!$seen[$key]++) {
+ $members->push($m);
+ }
+ else {
+ // remove dupes for caching
+ unset($object['member'][$i]);
+ }
}
$this->obj->setMembers($members);
// set type property for proper caching
$object['_type'] = 'distribution-list';
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
* Convert the Distlist object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Distribution list data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common object props into local data object
$object = parent::to_array($data);
// add object properties
$object += array(
'name' => $this->obj->name(),
'member' => array(),
'_type' => 'distribution-list',
);
$members = $this->obj->members();
for ($i=0; $i < $members->size(); $i++) {
$member = $members->get($i);
// if ($member->type() == ContactReference::UidReference && ($uid = $member->uid()))
$object['member'][] = array(
'uid' => $member->uid(),
'email' => $member->email(),
'name' => $member->name(),
);
}
$this->data = $object;
return $this->data;
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_event.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_event.php
similarity index 89%
rename from lib/kolab/plugins/libkolab/lib/kolab_format_event.php
rename to 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
@@ -1,234 +1,236 @@
<?php
/**
* Kolab Event model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_event extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.event';
+ public $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+
protected $objclass = 'Event';
protected $read_func = 'readEvent';
protected $write_func = 'writeEvent';
/**
* Default constructor
*/
function __construct($data = null, $version = 3.0)
{
parent::__construct(is_string($data) ? $data : null, $version);
// got an Event object as argument
if (is_object($data) && is_a($data, $this->objclass)) {
$this->obj = $data;
$this->loaded = true;
}
}
/**
* Clones into an instance of libcalendaring's extended EventCal class
*
* @return mixed EventCal object or false on failure
*/
public function to_libcal()
{
static $error_logged = false;
if (class_exists('kolabcalendaring')) {
return new EventCal($this->obj);
}
else if (!$error_logged) {
$error_logged = true;
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabcalendaring module not found"
), true);
}
return false;
}
/**
* Set event properties to the kolabformat object
*
* @param array Event data as hash array
*/
public function set(&$object)
{
// set common xcal properties
parent::set($object);
// do the hard work of setting object values
$this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
$this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
$this->obj->setTransparency($object['free_busy'] == 'free');
$status = kolabformat::StatusUndefined;
if ($object['free_busy'] == 'tentative')
$status = kolabformat::StatusTentative;
if ($object['cancelled'])
$status = kolabformat::StatusCancelled;
+ else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
+ $status = $this->status_map[$object['status']];
$this->obj->setStatus($status);
// save recurrence exceptions
- if ($object['recurrence']['EXCEPTIONS']) {
+ if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
$vexceptions = new vectorevent;
foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
$exevent = new kolab_format_event;
$exevent->set($this->compact_exception($exception, $object)); // only save differing values
$exevent->obj->setRecurrenceID(self::get_datetime($exception['start'], null, true), (bool)$exception['thisandfuture']);
$vexceptions->push($exevent->obj);
}
$this->obj->setExceptions($vexceptions);
}
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return !$this->formaterror && (($this->data && !empty($this->data['start']) && !empty($this->data['end'])) ||
(is_object($this->obj) && $this->obj->isValid() && $this->obj->uid()));
}
/**
* Convert the Event object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Event data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common xcal props
$object = parent::to_array($data);
// read object properties
$object += array(
'end' => self::php_datetime($this->obj->end()),
'allday' => $this->obj->start()->isDateOnly(),
'free_busy' => $this->obj->transparency() ? 'free' : 'busy', // TODO: transparency is only boolean
'attendees' => array(),
);
// derive event end from duration (#1916)
if (!$object['end'] && $object['start'] && ($duration = $this->obj->duration()) && $duration->isValid()) {
$interval = new DateInterval('PT0S');
$interval->d = $duration->weeks() * 7 + $duration->days();
$interval->h = $duration->hours();
$interval->i = $duration->minutes();
$interval->s = $duration->seconds();
$object['end'] = clone $object['start'];
$object['end']->add($interval);
}
// organizer is part of the attendees list in Roundcube
if ($object['organizer']) {
$object['organizer']['role'] = 'ORGANIZER';
array_unshift($object['attendees'], $object['organizer']);
}
// status defines different event properties...
$status = $this->obj->status();
if ($status == kolabformat::StatusTentative)
$object['free_busy'] = 'tentative';
else if ($status == kolabformat::StatusCancelled)
$object['cancelled'] = true;
+ // this is an exception object
+ if ($this->obj->recurrenceID()->isValid()) {
+ $object['thisandfuture'] = $this->obj->thisAndFuture();
+ }
// read exception event objects
- if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
+ else if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
+ $recurrence_exceptions = array();
for ($i=0; $i < $exceptions->size(); $i++) {
if (($exobj = $exceptions->get($i))) {
$exception = new kolab_format_event($exobj);
if ($exception->is_valid()) {
- $object['recurrence']['EXCEPTIONS'][] = $this->expand_exception($exception->to_array(), $object);
+ $recurrence_exceptions[] = $this->expand_exception($exception->to_array(), $object);
}
}
}
- }
- // this is an exception object
- else if ($this->obj->recurrenceID()->isValid()) {
- $object['thisandfuture'] = $this->obj->thisAndFuture();
+ $object['recurrence']['EXCEPTIONS'] = $recurrence_exceptions;
}
return $this->data = $object;
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
- $tags = array();
+ $tags = parent::get_tags();
foreach ((array)$this->data['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
- if (!empty($this->data['alarms'])) {
- $tags[] = 'x-has-alarms';
- }
-
return $tags;
}
/**
* Remove some attributes from the exception container
*/
private function compact_exception($exception, $master)
{
$forbidden = array('recurrence','organizer','attendees','sequence');
foreach ($forbidden as $prop) {
if (array_key_exists($prop, $exception)) {
unset($exception[$prop]);
}
}
return $exception;
}
/**
* Copy attributes not specified by the exception from the master event
*/
private function expand_exception($exception, $master)
{
foreach ($master as $prop => $value) {
if (empty($exception[$prop]) && !empty($value))
$exception[$prop] = $value;
}
return $exception;
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_file.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_file.php
similarity index 98%
copy from lib/kolab/plugins/libkolab/lib/kolab_format_file.php
copy to 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
@@ -1,156 +1,156 @@
<?php
/**
* Kolab File model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_file extends kolab_format
{
- public $CTYPE = 'application/x-vnd.kolab.file';
+ public $CTYPE = 'application/vnd.kolab+xml';
protected $objclass = 'File';
protected $read_func = 'kolabformat::readKolabFile';
protected $write_func = 'kolabformat::writeKolabFile';
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
'private' => kolabformat::ClassPrivate,
'confidential' => kolabformat::ClassConfidential,
);
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
// set common object properties
parent::set($object);
$this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
$this->obj->setCategories(self::array2vector($object['categories']));
if (isset($object['notes'])) {
$this->obj->setNote($object['notes']);
}
// Add file attachment
if (!empty($object['_attachments'])) {
$cid = key($object['_attachments']);
$attach_attr = $object['_attachments'][$cid];
$attach = new Attachment;
$attach->setLabel((string)$attach_attr['name']);
$attach->setUri('cid:' . $cid, $attach_attr['mimetype']);
$this->obj->setFile($attach);
// make sure size is set, so object saved in cache contains this info
if (!isset($attach_attr['size'])) {
$size = 0;
if (!empty($attach_attr['content'])) {
if (is_resource($attach_attr['content'])) {
$stat = fstat($attach_attr['content']);
$size = $stat ? $stat['size'] : 0;
}
else {
$size = strlen($attach_attr['content']);
}
}
else if (isset($attach_attr['path'])) {
$size = @filesize($attach_attr['path']);
}
$object['_attachments'][$cid]['size'] = $size;
}
}
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
* Check if object's data validity
*/
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
* Convert the Configuration object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data)) {
return $this->data;
}
// read common object props into local data object
$object = parent::to_array($data);
$sensitivity_map = array_flip($this->sensitivity_map);
// read object properties
$object += array(
'sensitivity' => $sensitivity_map[$this->obj->classification()],
'categories' => self::vector2array($this->obj->categories()),
'notes' => $this->obj->note(),
);
return $this->data = $object;
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
$tags = array();
foreach ((array)$this->data['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
// Add file mimetype to tags
if (!empty($this->data['_attachments'])) {
reset($this->data['_attachments']);
$key = key($this->data['_attachments']);
$attachment = $this->data['_attachments'][$key];
if ($attachment['mimetype']) {
$tags[] = $attachment['mimetype'];
}
}
return $tags;
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_journal.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_journal.php
similarity index 100%
rename from lib/kolab/plugins/libkolab/lib/kolab_format_journal.php
rename to lib/drivers/kolab/plugins/libkolab/lib/kolab_format_journal.php
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_file.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php
similarity index 58%
rename from lib/kolab/plugins/libkolab/lib/kolab_format_file.php
rename to lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php
index 5f73bf1..bca5156 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_file.php
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_note.php
@@ -1,156 +1,153 @@
<?php
/**
- * Kolab File model class
+ * Kolab Note model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
- * @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-class kolab_format_file extends kolab_format
+class kolab_format_note extends kolab_format
{
- public $CTYPE = 'application/x-vnd.kolab.file';
+ public $CTYPE = 'application/vnd.kolab+xml';
+ public $CTYPEv2 = 'application/x-vnd.kolab.note';
- protected $objclass = 'File';
- protected $read_func = 'kolabformat::readKolabFile';
- protected $write_func = 'kolabformat::writeKolabFile';
+ 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']));
- if (isset($object['notes'])) {
- $this->obj->setNote($object['notes']);
- }
-
- // Add file attachment
- if (!empty($object['_attachments'])) {
- $cid = key($object['_attachments']);
- $attach_attr = $object['_attachments'][$cid];
- $attach = new Attachment;
-
- $attach->setLabel((string)$attach_attr['name']);
- $attach->setUri('cid:' . $cid, $attach_attr['mimetype']);
- $this->obj->setFile($attach);
-
- // make sure size is set, so object saved in cache contains this info
- if (!isset($attach_attr['size'])) {
- $size = 0;
-
- if (!empty($attach_attr['content'])) {
- if (is_resource($attach_attr['content'])) {
- $stat = fstat($attach_attr['content']);
- $size = $stat ? $stat['size'] : 0;
- }
- else {
- $size = strlen($attach_attr['content']);
- }
- }
- else if (isset($attach_attr['path'])) {
- $size = @filesize($attach_attr['path']);
- }
-
- $object['_attachments'][$cid]['size'] = $size;
- }
- }
+ $this->set_attachments($object);
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
- * Check if object's data validity
+ *
*/
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
* Convert the Configuration object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = array())
{
// return cached result
- if (!empty($this->data)) {
+ if (!empty($this->data))
return $this->data;
- }
// read common object props into local data object
$object = parent::to_array($data);
$sensitivity_map = array_flip($this->sensitivity_map);
// read object properties
$object += array(
'sensitivity' => $sensitivity_map[$this->obj->classification()],
'categories' => self::vector2array($this->obj->categories()),
- 'notes' => $this->obj->note(),
+ '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 file mimetype to tags
- if (!empty($this->data['_attachments'])) {
- reset($this->data['_attachments']);
- $key = key($this->data['_attachments']);
- $attachment = $this->data['_attachments'][$key];
-
- if ($attachment['mimetype']) {
- $tags[] = $attachment['mimetype'];
+ // 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
similarity index 82%
rename from lib/kolab/plugins/libkolab/lib/kolab_format_task.php
rename to 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
@@ -1,125 +1,129 @@
<?php
/**
* Kolab Task (ToDo) model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_task extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.task';
+ public $scheduling_properties = array('start', 'due', 'summary', 'status');
+
protected $objclass = 'Todo';
protected $read_func = 'readTodo';
protected $write_func = 'writeTodo';
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
// set common xcal properties
parent::set($object);
$this->obj->setPercentComplete(intval($object['complete']));
- if (isset($object['start']))
- $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
+ $status = kolabformat::StatusUndefined;
+ if ($object['complete'] == 100 && !array_key_exists('status', $object))
+ $status = kolabformat::StatusCompleted;
+ else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
+ $status = $this->status_map[$object['status']];
+ $this->obj->setStatus($status);
+ $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
$this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly));
$related = new vectors;
if (!empty($object['parent_id']))
$related->push($object['parent_id']);
$this->obj->setRelatedTo($related);
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
}
/**
* Convert the Configuration object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common xcal props
$object = parent::to_array($data);
$object['complete'] = intval($this->obj->percentComplete());
// if due date is set
if ($due = $this->obj->due())
$object['due'] = self::php_datetime($due);
// related-to points to parent task; we only support one relation
$related = self::vector2array($this->obj->relatedTo());
if (count($related))
$object['parent_id'] = $related[0];
// TODO: map more properties
$this->data = $object;
return $this->data;
}
/**
* Callback for kolab_storage_cache to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags()
{
- $tags = array();
+ $tags = parent::get_tags();
- if ($this->data['status'] == 'COMPLETED' || $this->data['complete'] == 100)
+ if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status'])))
$tags[] = 'x-complete';
if ($this->data['priority'] == 1)
$tags[] = 'x-flagged';
- if (!empty($this->data['alarms']))
- $tags[] = 'x-has-alarms';
-
if ($this->data['parent_id'])
$tags[] = 'x-parent:' . $this->data['parent_id'];
return $tags;
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_xcal.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_format_xcal.php
similarity index 60%
rename from lib/kolab/plugins/libkolab/lib/kolab_format_xcal.php
rename to 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
@@ -1,452 +1,627 @@
<?php
/**
* Xcal based Kolab format class wrapping libkolabxml bindings
*
* Base class for xcal-based Kolab groupware objects such as event, todo, journal
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
abstract class kolab_format_xcal extends kolab_format
{
public $CTYPE = 'application/calendar+xml';
public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
+ public $scheduling_properties = array('start', 'end', 'location');
+
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
'private' => kolabformat::ClassPrivate,
'confidential' => kolabformat::ClassConfidential,
);
protected $role_map = array(
'REQ-PARTICIPANT' => kolabformat::Required,
'OPT-PARTICIPANT' => kolabformat::Optional,
'NON-PARTICIPANT' => kolabformat::NonParticipant,
'CHAIR' => kolabformat::Chair,
);
protected $cutype_map = array(
'INDIVIDUAL' => kolabformat::CutypeIndividual,
'GROUP' => kolabformat::CutypeGroup,
'ROOM' => kolabformat::CutypeRoom,
'RESOURCE' => kolabformat::CutypeResource,
'UNKNOWN' => kolabformat::CutypeUnknown,
);
protected $rrule_type_map = array(
'MINUTELY' => RecurrenceRule::Minutely,
'HOURLY' => RecurrenceRule::Hourly,
'DAILY' => RecurrenceRule::Daily,
'WEEKLY' => RecurrenceRule::Weekly,
'MONTHLY' => RecurrenceRule::Monthly,
'YEARLY' => RecurrenceRule::Yearly,
);
protected $weekday_map = array(
'MO' => kolabformat::Monday,
'TU' => kolabformat::Tuesday,
'WE' => kolabformat::Wednesday,
'TH' => kolabformat::Thursday,
'FR' => kolabformat::Friday,
'SA' => kolabformat::Saturday,
'SU' => kolabformat::Sunday,
);
protected $alarm_type_map = array(
'DISPLAY' => Alarm::DisplayAlarm,
'EMAIL' => Alarm::EMailAlarm,
'AUDIO' => Alarm::AudioAlarm,
);
- private $status_map = array(
+ protected $status_map = array(
'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
'IN-PROCESS' => kolabformat::StatusInProcess,
'COMPLETED' => kolabformat::StatusCompleted,
'CANCELLED' => kolabformat::StatusCancelled,
+ 'TENTATIVE' => kolabformat::StatusTentative,
+ 'CONFIRMED' => kolabformat::StatusConfirmed,
+ 'DRAFT' => kolabformat::StatusDraft,
+ 'FINAL' => kolabformat::StatusFinal,
);
protected $part_status_map = array(
'UNKNOWN' => kolabformat::PartNeedsAction,
'NEEDS-ACTION' => kolabformat::PartNeedsAction,
'TENTATIVE' => kolabformat::PartTentative,
'ACCEPTED' => kolabformat::PartAccepted,
'DECLINED' => kolabformat::PartDeclined,
'DELEGATED' => kolabformat::PartDelegated,
);
/**
* Convert common xcard properties into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Object data as hash array
*/
public function to_array($data = array())
{
// read common object props
$object = parent::to_array($data);
$status_map = array_flip($this->status_map);
$sensitivity_map = array_flip($this->sensitivity_map);
$object += array(
'sequence' => intval($this->obj->sequence()),
'title' => $this->obj->summary(),
'location' => $this->obj->location(),
'description' => $this->obj->description(),
'url' => $this->obj->url(),
'status' => $status_map[$this->obj->status()],
'sensitivity' => $sensitivity_map[$this->obj->classification()],
'priority' => $this->obj->priority(),
'categories' => self::vector2array($this->obj->categories()),
'start' => self::php_datetime($this->obj->start()),
);
+ if (method_exists($this->obj, 'comment')) {
+ $object['comment'] = $this->obj->comment();
+ }
+
// read organizer and attendees
if (($organizer = $this->obj->organizer()) && ($organizer->email() || $organizer->name())) {
$object['organizer'] = array(
'email' => $organizer->email(),
'name' => $organizer->name(),
);
}
$role_map = array_flip($this->role_map);
$cutype_map = array_flip($this->cutype_map);
$part_status_map = array_flip($this->part_status_map);
$attvec = $this->obj->attendees();
for ($i=0; $i < $attvec->size(); $i++) {
$attendee = $attvec->get($i);
$cr = $attendee->contact();
if ($cr->email() != $object['organizer']['email']) {
+ $delegators = $delegatees = array();
+ $vdelegators = $attendee->delegatedFrom();
+ for ($j=0; $j < $vdelegators->size(); $j++) {
+ $delegators[] = $vdelegators->get($j)->email();
+ }
+ $vdelegatees = $attendee->delegatedTo();
+ for ($j=0; $j < $vdelegatees->size(); $j++) {
+ $delegatees[] = $vdelegatees->get($j)->email();
+ }
+
$object['attendees'][] = array(
'role' => $role_map[$attendee->role()],
'cutype' => $cutype_map[$attendee->cutype()],
'status' => $part_status_map[$attendee->partStat()],
'rsvp' => $attendee->rsvp(),
'email' => $cr->email(),
'name' => $cr->name(),
+ 'delegated-from' => $delegators,
+ 'delegated-to' => $delegatees,
);
}
}
// read recurrence rule
if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
$rrule_type_map = array_flip($this->rrule_type_map);
$object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
if ($intvl = $rr->interval())
$object['recurrence']['INTERVAL'] = $intvl;
if (($count = $rr->count()) && $count > 0) {
$object['recurrence']['COUNT'] = $count;
}
else if ($until = self::php_datetime($rr->end())) {
$until->setTime($object['start']->format('G'), $object['start']->format('i'), 0);
$object['recurrence']['UNTIL'] = $until;
}
if (($byday = $rr->byday()) && $byday->size()) {
$weekday_map = array_flip($this->weekday_map);
$weekdays = array();
for ($i=0; $i < $byday->size(); $i++) {
$daypos = $byday->get($i);
$prefix = $daypos->occurence();
$weekdays[] = ($prefix ? $prefix : '') . $weekday_map[$daypos->weekday()];
}
$object['recurrence']['BYDAY'] = join(',', $weekdays);
}
if (($bymday = $rr->bymonthday()) && $bymday->size()) {
$object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
}
if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
$object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
}
if ($exdates = $this->obj->exceptionDates()) {
for ($i=0; $i < $exdates->size(); $i++) {
if ($exdate = self::php_datetime($exdates->get($i)))
$object['recurrence']['EXDATE'][] = $exdate;
}
}
}
+ if ($rdates = $this->obj->recurrenceDates()) {
+ for ($i=0; $i < $rdates->size(); $i++) {
+ if ($rdate = self::php_datetime($rdates->get($i)))
+ $object['recurrence']['RDATE'][] = $rdate;
+ }
+ }
+
// read alarm
$valarms = $this->obj->alarms();
$alarm_types = array_flip($this->alarm_type_map);
+ $object['valarms'] = array();
for ($i=0; $i < $valarms->size(); $i++) {
$alarm = $valarms->get($i);
$type = $alarm_types[$alarm->type()];
- if ($type == 'DISPLAY' || $type == 'EMAIL') { // only DISPLAY and EMAIL alarms are supported
+ if ($type == 'DISPLAY' || $type == 'EMAIL' || $type == 'AUDIO') { // only some alarms are supported
+ $valarm = array(
+ 'action' => $type,
+ 'summary' => $alarm->summary(),
+ 'description' => $alarm->description(),
+ );
+
+ if ($type == 'EMAIL') {
+ $valarm['attendees'] = array();
+ $attvec = $alarm->attendees();
+ for ($j=0; $j < $attvec->size(); $j++) {
+ $cr = $attvec->get($j);
+ $valarm['attendees'][] = $cr->email();
+ }
+ }
+ else if ($type == 'AUDIO') {
+ $attach = $alarm->audioFile();
+ $valarm['uri'] = $attach->uri();
+ }
+
if ($start = self::php_datetime($alarm->start())) {
$object['alarms'] = '@' . $start->format('U');
+ $valarm['trigger'] = $start;
}
else if ($offset = $alarm->relativeStart()) {
- $value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+ $prefix = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+ $value = $time = '';
if ($w = $offset->weeks()) $value .= $w . 'W';
else if ($d = $offset->days()) $value .= $d . 'D';
- else if ($h = $offset->hours()) $value .= $h . 'H';
- else if ($m = $offset->minutes()) $value .= $m . 'M';
- else if ($s = $offset->seconds()) $value .= $s . 'S';
- else continue;
+ else if ($h = $offset->hours()) $time .= $h . 'H';
+ else if ($m = $offset->minutes()) $time .= $m . 'M';
+ else if ($s = $offset->seconds()) $time .= $s . 'S';
+
+ // assume 'at event time'
+ if (empty($value) && empty($time)) {
+ $prefix = '';
+ $time = '0S';
+ }
- $object['alarms'] = $value;
+ $object['alarms'] = $prefix . $value . $time;
+ $valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : '');
}
- $object['alarms'] .= ':' . $type;
- break;
- }
- }
- // handle attachments
- $vattach = $this->obj->attachments();
- for ($i=0; $i < $vattach->size(); $i++) {
- $attach = $vattach->get($i);
-
- // skip cid: attachments which are mime message parts handled by kolab_storage_folder
- if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
- $name = $attach->label();
- $content = $attach->data();
- $object['_attachments'][$name] = array(
- 'name' => $name,
- 'mimetype' => $attach->mimetype(),
- 'size' => strlen($content),
- 'content' => $content,
- );
- }
- else if (substr($attach->uri(), 0, 4) == 'http') {
- $object['links'][] = $attach->uri();
+ // read alarm duration and repeat properties
+ if (($duration = $alarm->duration()) && $duration->isValid()) {
+ $value = $time = '';
+ if ($w = $duration->weeks()) $value .= $w . 'W';
+ else if ($d = $duration->days()) $value .= $d . 'D';
+ else if ($h = $duration->hours()) $time .= $h . 'H';
+ else if ($m = $duration->minutes()) $time .= $m . 'M';
+ else if ($s = $duration->seconds()) $time .= $s . 'S';
+ $valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : '');
+ $valarm['repeat'] = $alarm->numrepeat();
+ }
+
+ $object['alarms'] .= ':' . $type; // legacy property
+ $object['valarms'][] = array_filter($valarm);
}
}
+ $this->get_attachments($object);
+
return $object;
}
/**
* Set common xcal properties to the kolabformat object
*
* @param array Event data as hash array
*/
public function set(&$object)
{
$this->init();
$is_new = !$this->obj->uid();
+ $old_sequence = $this->obj->sequence();
+ $reschedule = $is_new;
// set common object properties
parent::set($object);
- // increment sequence on updates
- if (empty($object['sequence']))
- $object['sequence'] = !$is_new ? $this->obj->sequence()+1 : 0;
- $this->obj->setSequence($object['sequence']);
+ // set sequence value
+ if (!isset($object['sequence'])) {
+ if ($is_new) {
+ $object['sequence'] = 0;
+ }
+ else {
+ $object['sequence'] = $old_sequence;
+ $old = $this->data['uid'] ? $this->data : $this->to_array();
+
+ // increment sequence when updating properties relevant for scheduling.
+ // RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
+ // TODO: make the list of properties considered 'significant' for scheduling configurable
+ foreach ($this->scheduling_properties as $prop) {
+ $a = $old[$prop];
+ $b = $object[$prop];
+ if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
+ $a = $a->format('Y-m-d');
+ $b = $b->format('Y-m-d');
+ }
+ if ($a != $b) {
+ $object['sequence']++;
+ break;
+ }
+ }
+ }
+ }
+ $this->obj->setSequence(intval($object['sequence']));
+
+ if ($object['sequence'] > $old_sequence) {
+ $reschedule = true;
+ }
$this->obj->setSummary($object['title']);
$this->obj->setLocation($object['location']);
$this->obj->setDescription($object['description']);
$this->obj->setPriority($object['priority']);
$this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
$this->obj->setCategories(self::array2vector($object['categories']));
$this->obj->setUrl(strval($object['url']));
+ if (method_exists($this->obj, 'setComment')) {
+ $this->obj->setComment($object['comment']);
+ }
+
// process event attendees
$attendees = new vectorattendee;
- foreach ((array)$object['attendees'] as $attendee) {
+ foreach ((array)$object['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$object['organizer'] = $attendee;
}
else if ($attendee['email'] != $object['organizer']['email']) {
$cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
$cr->setName($attendee['name']);
$att = new Attendee;
$att->setContact($cr);
$att->setPartStat($this->part_status_map[$attendee['status']]);
$att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
$att->setCutype($this->cutype_map[$attendee['cutype']] ? $this->cutype_map[$attendee['cutype']] : kolabformat::CutypeIndividual);
- $att->setRSVP((bool)$attendee['rsvp']);
+ $att->setRSVP((bool)$attendee['rsvp'] || $reschedule);
+
+ $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] || $reschedule;
+
+ if (!empty($attendee['delegated-from'])) {
+ $vdelegators = new vectorcontactref;
+ foreach ((array)$attendee['delegated-from'] as $delegator) {
+ $vdelegators->push(new ContactReference(ContactReference::EmailReference, $delegator));
+ }
+ $att->setDelegatedFrom($vdelegators);
+ }
+ if (!empty($attendee['delegated-to'])) {
+ $vdelegatees = new vectorcontactref;
+ foreach ((array)$attendee['delegated-to'] as $delegatee) {
+ $vdelegatees->push(new ContactReference(ContactReference::EmailReference, $delegatee));
+ }
+ $att->setDelegatedTo($vdelegatees);
+ }
if ($att->isValid()) {
$attendees->push($att);
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid event attendee: " . json_encode($attendee),
), true);
}
}
}
$this->obj->setAttendees($attendees);
if ($object['organizer']) {
$organizer = new ContactReference(ContactReference::EmailReference, $object['organizer']['email']);
$organizer->setName($object['organizer']['name']);
$this->obj->setOrganizer($organizer);
}
// save recurrence rule
$rr = new RecurrenceRule;
$rr->setFrequency(RecurrenceRule::FreqNone);
- if ($object['recurrence']) {
+ if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) {
$rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
if ($object['recurrence']['INTERVAL'])
$rr->setInterval(intval($object['recurrence']['INTERVAL']));
if ($object['recurrence']['BYDAY']) {
$byday = new vectordaypos;
foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
$occurrence = 0;
if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
$occurrence = intval($m[1]);
$day = $m[2];
}
if (isset($this->weekday_map[$day]))
$byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
}
$rr->setByday($byday);
}
if ($object['recurrence']['BYMONTHDAY']) {
$bymday = new vectori;
foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
$bymday->push(intval($day));
$rr->setBymonthday($bymday);
}
if ($object['recurrence']['BYMONTH']) {
$bymonth = new vectori;
foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
$bymonth->push(intval($month));
$rr->setBymonth($bymonth);
}
if ($object['recurrence']['COUNT'])
$rr->setCount(intval($object['recurrence']['COUNT']));
else if ($object['recurrence']['UNTIL'])
$rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true));
if ($rr->isValid()) {
// add exception dates (only if recurrence rule is valid)
$exdates = new vectordatetime;
foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
$exdates->push(self::get_datetime($exdate, null, true));
$this->obj->setExceptionDates($exdates);
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
), true);
}
}
$this->obj->setRecurrenceRule($rr);
+ // save recurrence dates (aka RDATE)
+ if (!empty($object['recurrence']['RDATE'])) {
+ $rdates = new vectordatetime;
+ foreach ((array)$object['recurrence']['RDATE'] as $rdate)
+ $rdates->push(self::get_datetime($rdate, null, true));
+ $this->obj->setRecurrenceDates($rdates);
+ }
+
// save alarm
$valarms = new vectoralarm;
- if ($object['alarms']) {
+ if ($object['valarms']) {
+ foreach ($object['valarms'] as $valarm) {
+ if (!array_key_exists($valarm['action'], $this->alarm_type_map)) {
+ continue; // skip unknown alarm types
+ }
+
+ if ($valarm['action'] == 'EMAIL') {
+ $recipients = new vectorcontactref;
+ foreach (($valarm['attendees'] ?: array($object['_owner'])) as $email) {
+ $recipients->push(new ContactReference(ContactReference::EmailReference, $email));
+ }
+ $alarm = new Alarm(
+ strval($valarm['summary'] ?: $object['title']),
+ strval($valarm['description'] ?: $object['description']),
+ $recipients
+ );
+ }
+ else if ($valarm['action'] == 'AUDIO') {
+ $attach = new Attachment;
+ $attach->setUri($valarm['uri'] ?: 'null', 'unknown');
+ $alarm = new Alarm($attach);
+ }
+ else {
+ // action == DISPLAY
+ $alarm = new Alarm(strval($valarm['summary'] ?: $object['title']));
+ }
+
+ if (is_object($valarm['trigger']) && $valarm['trigger'] instanceof DateTime) {
+ $alarm->setStart(self::get_datetime($valarm['trigger'], new DateTimeZone('UTC')));
+ }
+ else {
+ try {
+ $prefix = $valarm['trigger'][0];
+ $period = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger']));
+ $duration = new Duration($period->d, $period->h, $period->i, $period->s, $prefix == '-');
+ }
+ catch (Exception $e) {
+ // skip alarm with invalid trigger values
+ rcube::raise_error($e, true);
+ continue;
+ }
+
+ $alarm->setRelativeStart($duration, $prefix == '-' ? kolabformat::Start : kolabformat::End);
+ }
+
+ if ($valarm['duration']) {
+ try {
+ $d = new DateInterval($valarm['duration']);
+ $duration = new Duration($d->d, $d->h, $d->i, $d->s);
+ $alarm->setDuration($duration, intval($valarm['repeat']));
+ }
+ catch (Exception $e) {
+ // ignore
+ }
+ }
+
+ $valarms->push($alarm);
+ }
+ }
+ // legacy support
+ else if ($object['alarms']) {
list($offset, $type) = explode(":", $object['alarms']);
if ($type == 'EMAIL' && !empty($object['_owner'])) { // email alarms implicitly go to event owner
$recipients = new vectorcontactref;
$recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
$alarm = new Alarm($object['title'], strval($object['description']), $recipients);
}
else { // default: display alarm
$alarm = new Alarm($object['title']);
}
if (preg_match('/^@(\d+)/', $offset, $d)) {
$alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
}
else if (preg_match('/^([-+]?)P?T?(\d+)([SMHDW])/', $offset, $d)) {
$days = $hours = $minutes = $seconds = 0;
switch ($d[3]) {
case 'W': $days = 7*intval($d[2]); break;
case 'D': $days = intval($d[2]); break;
case 'H': $hours = intval($d[2]); break;
case 'M': $minutes = intval($d[2]); break;
case 'S': $seconds = intval($d[2]); break;
}
$alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
}
$valarms->push($alarm);
}
$this->obj->setAlarms($valarms);
- // save attachments
- $vattach = new vectorattachment;
- foreach ((array)$object['_attachments'] as $cid => $attr) {
- if (empty($attr))
- continue;
- $attach = new Attachment;
- $attach->setLabel((string)$attr['name']);
- $attach->setUri('cid:' . $cid, $attr['mimetype']);
- $vattach->push($attach);
- }
-
- foreach ((array)$object['links'] as $link) {
- $attach = new Attachment;
- $attach->setUri($link, 'unknown');
- $vattach->push($attach);
- }
-
- $this->obj->setAttachments($vattach);
+ $this->set_attachments($object);
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
$data = '';
foreach (self::$fulltext_cols as $colname) {
list($col, $field) = explode(':', $colname);
if ($field) {
$a = array();
foreach ((array)$this->data[$col] as $attr)
$a[] = $attr[$field];
$val = join(' ', $a);
}
else {
$val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
}
if (strlen($val))
$data .= $val . ' ';
}
return array_unique(rcube_utils::normalize_string($data, true));
}
+ /**
+ * Callback for kolab_storage_cache to get object specific tags to cache
+ *
+ * @return array List of tags to save in cache
+ */
+ public function get_tags()
+ {
+ $tags = array();
+
+ if (!empty($this->data['valarms'])) {
+ $tags[] = 'x-has-alarms';
+ }
+
+ // create tags reflecting participant status
+ if (is_array($this->data['attendees'])) {
+ foreach ($this->data['attendees'] as $attendee) {
+ if (!empty($attendee['email']) && !empty($attendee['status']))
+ $tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
+ }
+ }
+
+ return $tags;
+ }
}
\ No newline at end of file
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage.php
similarity index 57%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage.php
rename to 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
@@ -1,1049 +1,1567 @@
<?php
/**
* Kolab storage class providing static methods to access groupware objects on a Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
- * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage
{
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
const NAME_KEY_SHARED = '/shared/vendor/kolab/displayname';
const NAME_KEY_PRIVATE = '/private/vendor/kolab/displayname';
const UID_KEY_SHARED = '/shared/vendor/kolab/uniqueid';
const UID_KEY_PRIVATE = '/private/vendor/kolab/uniqueid';
const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
public static $version = '3.0';
public static $last_error;
+ public static $encode_ids = false;
private static $ready = false;
+ private static $with_tempsubs = true;
private static $subscriptions;
+ private static $typedata = array();
private static $states;
private static $config;
private static $imap;
+ private static $ldap;
// Default folder names
private static $default_folders = array(
'event' => 'Calendar',
'contact' => 'Contacts',
'task' => 'Tasks',
'note' => 'Notes',
'file' => 'Files',
'configuration' => 'Configuration',
'journal' => 'Journal',
'mail.inbox' => 'INBOX',
'mail.drafts' => 'Drafts',
'mail.sentitems' => 'Sent',
'mail.wastebasket' => 'Trash',
'mail.outbox' => 'Outbox',
'mail.junkemail' => 'Junk',
);
/**
* Setup the environment needed by the libs
*/
public static function setup()
{
if (self::$ready)
return true;
$rcmail = rcube::get_instance();
self::$config = $rcmail->config;
self::$version = strval($rcmail->config->get('kolab_format_version', self::$version));
self::$imap = $rcmail->get_storage();
self::$ready = class_exists('kolabformat') &&
(self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
if (self::$ready) {
// set imap options
self::$imap->set_options(array(
'skip_deleted' => true,
'threading' => false,
));
- self::$imap->set_pagesize(9999);
}
else if (!class_exists('kolabformat')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabformat module not found"
), true);
}
else {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "IMAP server doesn't support METADATA or ANNOTATEMORE"
), true);
}
return self::$ready;
}
+ /**
+ * Initializes LDAP object to resolve Kolab users
+ */
+ public static function ldap()
+ {
+ if (self::$ldap) {
+ return self::$ldap;
+ }
+
+ self::setup();
+
+ $config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook'));
+
+ if (!is_array($config)) {
+ $ldap_config = (array)self::$config->get('ldap_public');
+ $config = $ldap_config[$config];
+ }
+
+ if (empty($config)) {
+ return null;
+ }
+
+ // overwrite filter option
+ if ($filter = self::$config->get('kolab_users_filter')) {
+ self::$config->set('kolab_auth_filter', $filter);
+ }
+
+ // re-use the LDAP wrapper class from kolab_auth plugin
+ require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php';
+
+ self::$ldap = new kolab_auth_ldap($config);
+
+ return self::$ldap;
+ }
+
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type, $subscribed = null)
{
$folders = $folderdata = array();
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
$folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Getter for the storage folder for the given type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @return object kolab_storage_folder The folder object
*/
public static function get_default_folder($type)
{
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
return new kolab_storage_folder($foldername, $folderdata[$foldername]);
}
}
return null;
}
/**
* Getter for a specific storage folder
*
* @param string IMAP folder to access (UTF7-IMAP)
* @return object kolab_storage_folder The folder object
*/
public static function get_folder($folder)
{
return self::setup() ? new kolab_storage_folder($folder) : null;
}
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
*
* @param string Object UID
* @param string Object type (contact,event,task,journal,file,note,configuration)
* @return array The Kolab object represented as hash array or false if not found
*/
public static function get_object($uid, $type)
{
self::setup();
$folder = null;
foreach ((array)self::list_folders('', '*', $type) as $foldername) {
if (!$folder)
$folder = new kolab_storage_folder($foldername);
else
$folder->set_folder($foldername);
if ($object = $folder->get_object($uid, '*'))
return $object;
}
return false;
}
-
/**
+ * Execute cross-folder searches with the given query.
*
+ * @param array Pseudo-SQL query as list of filter parameter triplets
+ * @param string Object type (contact,event,task,journal,file,note,configuration)
+ * @return array List of Kolab data objects (each represented as hash array)
+ * @see kolab_storage_format::select()
*/
- public static function get_freebusy_server()
+ public static function select($query, $type)
{
- return unslashify(self::$config->get('kolab_freebusy_server', 'https://' . $_SESSION['imap_host'] . '/freebusy'));
+ self::setup();
+ $folder = null;
+ $result = array();
+
+ foreach ((array)self::list_folders('', '*', $type) as $foldername) {
+ if (!$folder)
+ $folder = new kolab_storage_folder($foldername);
+ else
+ $folder->set_folder($foldername);
+
+ foreach ($folder->select($query, '*') as $object) {
+ $result[] = $object;
+ }
+ }
+
+ return $result;
}
+ /**
+ * Returns Free-busy server URL
+ */
+ public static function get_freebusy_server()
+ {
+ $url = 'https://' . $_SESSION['imap_host'] . '/freebusy';
+ $url = self::$config->get('kolab_freebusy_server', $url);
+ $url = rcube_utils::resolve_url($url);
+
+ return unslashify($url);
+ }
/**
* Compose an URL to query the free/busy status for the given user
*/
public static function get_freebusy_url($email)
{
return self::get_freebusy_server() . '/' . $email . '.ifb';
}
-
/**
* Creates folder ID from folder name
*
- * @param string $folder Folder name (UTF7-IMAP)
- *
+ * @param string $folder Folder name (UTF7-IMAP)
+ * @param boolean $enc Use lossless encoding
* @return string Folder ID string
*/
- public static function folder_id($folder)
+ public static function folder_id($folder, $enc = null)
+ {
+ return $enc == true || ($enc === null && self::$encode_ids) ?
+ self::id_encode($folder) :
+ asciiwords(strtr($folder, '/.-', '___'));
+ }
+
+ /**
+ * Encode the given ID to a safe ascii representation
+ *
+ * @param string $id Arbitrary identifier string
+ *
+ * @return string Ascii representation
+ */
+ public static function id_encode($id)
{
- return asciiwords(strtr($folder, '/.-', '___'));
+ return rtrim(strtr(base64_encode($id), '+/', '-_'), '=');
+ }
+
+ /**
+ * Convert the given identifier back to it's raw value
+ *
+ * @param string $id Ascii identifier
+ * @return string Raw identifier string
+ */
+ public static function id_decode($id)
+ {
+ return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
+ }
+
+ /**
+ * Return the (first) path of the requested IMAP namespace
+ *
+ * @param string Namespace name (personal, shared, other)
+ * @return string IMAP root path for that namespace
+ */
+ public static function namespace_root($name)
+ {
+ foreach ((array)self::$imap->get_namespace($name) as $paths) {
+ if (strlen($paths[0]) > 1) {
+ return $paths[0];
+ }
+ }
+
+ return '';
}
/**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_delete($name)
{
// clear cached entries first
if ($folder = self::get_folder($name))
$folder->cache->purge();
+ $rcmail = rcube::get_instance();
+ $plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name));
+
$success = self::$imap->delete_folder($name);
self::$last_error = self::$imap->get_error_str();
return $success;
}
/**
* Creates IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $subscribed Sets folder subscription
* @param bool $active Sets folder state (client-side subscription)
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type = null, $subscribed = false, $active = false)
{
self::setup();
+ $rcmail = rcube::get_instance();
+ $plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array(
+ 'name' => $name,
+ 'subscribe' => $subscribed,
+ )));
+
if ($saved = self::$imap->create_folder($name, $subscribed)) {
// set metadata for folder type
if ($type) {
$saved = self::set_folder_type($name, $type);
// revert if metadata could not be set
if (!$saved) {
self::$imap->delete_folder($name);
}
// activate folder
else if ($active) {
self::set_state($name, true);
}
}
}
if ($saved) {
return true;
}
self::$last_error = self::$imap->get_error_str();
return false;
}
/**
* Renames IMAP folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_rename($oldname, $newname)
{
self::setup();
+ $rcmail = rcube::get_instance();
+ $plugin = $rcmail->plugins->exec_hook('folder_rename', array(
+ 'oldname' => $oldname, 'newname' => $newname));
+
$oldfolder = self::get_folder($oldname);
$active = self::folder_is_active($oldname);
$success = self::$imap->rename_folder($oldname, $newname);
self::$last_error = self::$imap->get_error_str();
// pass active state to new folder name
if ($success && $active) {
self::set_state($oldnam, false);
self::set_state($newname, true);
}
// assign existing cache entries to new resource uri
if ($success && $oldfolder) {
$oldfolder->cache->rename($newname);
}
return $success;
}
/**
* Rename or Create a new IMAP folder.
*
* Does additional checks for permissions and folder name restrictions
*
* @param array Hash array with folder properties and metadata
* - name: Folder name
* - oldname: Old folder name when changed
* - parent: Parent folder to create the new one in
* - type: Folder type to create
* - subscribed: Subscribed flag (IMAP subscription)
* - active: Activation flag (client-side subscription)
* @return mixed New folder name or False on failure
*/
public static function folder_update(&$prop)
{
self::setup();
$folder = rcube_charset::convert($prop['name'], RCUBE_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$delimiter = self::$imap->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = self::$imap->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
self::$last_error = 'cannotbeempty';
return false;
}
else if (strlen($folder) > 128) {
self::$last_error = 'nametoolong';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $char) !== false) {
self::$last_error = 'forbiddencharacter';
return false;
}
}
}
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$folder = self::$imap->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$parent_opts = self::$imap->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
self::$last_error = 'No permission to create folder';
return false;
}
}
// update the folder name
if (strlen($oldfolder)) {
if ($oldfolder != $folder) {
$result = self::folder_rename($oldfolder, $folder);
}
else
$result = true;
}
// create new folder
else {
$result = self::folder_create($folder, $prop['type'], $prop['subscribed'], $prop['active']);
}
if ($result) {
self::set_folder_props($folder, $prop);
}
return $result ? $folder : false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns=null)
{
self::setup();
// find custom display name in folder METADATA
if ($name = self::custom_displayname($folder)) {
return $name;
}
$found = false;
$namespace = self::$imap->get_namespace();
if (!empty($namespace['shared'])) {
foreach ($namespace['shared'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
$prefix = '';
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
$found = true;
$folder_ns = 'shared';
break;
}
}
}
if (!$found && !empty($namespace['other'])) {
foreach ($namespace['other'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
// get username
$pos = strpos($folder, $delim);
if ($pos) {
- $prefix = '('.substr($folder, 0, $pos).') ';
+ $prefix = '('.substr($folder, 0, $pos).')';
$folder = substr($folder, $pos+1);
}
else {
$prefix = '('.$folder.')';
$folder = '';
}
+
$found = true;
$folder_ns = 'other';
break;
}
}
}
if (!$found && !empty($namespace['personal'])) {
foreach ($namespace['personal'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$prefix = '';
$delim = $ns[1];
$found = true;
break;
}
}
}
if (empty($delim))
$delim = self::$imap->get_hierarchy_delimiter();
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$folder = html::quote($folder);
$folder = str_replace(html::quote($delim), ' &raquo; ', $folder);
if ($prefix)
- $folder = html::quote($prefix) . ' ' . $folder;
+ $folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
if (!$folder_ns)
$folder_ns = 'personal';
return $folder;
}
/**
* Get custom display name (saved in metadata) for the given folder
*/
public static function custom_displayname($folder)
{
// find custom display name in folder METADATA
if (self::$config->get('kolab_custom_display_names', true)) {
$metadata = self::$imap->get_metadata($folder, array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
if (($name = $metadata[$folder][self::NAME_KEY_PRIVATE]) || ($name = $metadata[$folder][self::NAME_KEY_SHARED])) {
return $name;
}
}
return false;
}
/**
- * Helper method to generate a truncated folder name to display
+ * Helper method to generate a truncated folder name to display.
+ * Note: $origname is a string returned by self::object_name()
*/
public static function folder_displayname($origname, &$names)
{
$name = $origname;
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i] . ' &raquo; ') === 0) {
$length = strlen($names[$i] . ' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
- $name = str_repeat('&nbsp;&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
+ $diff = 1;
+
+ // check if prefix folder is in other users namespace
+ for ($n = count($names)-1; $n >= 0; $n--) {
+ if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
+ $diff = 0;
+ break;
+ }
+ }
+
+ $name = str_repeat('&nbsp;&nbsp;&nbsp;', $count - $diff) . '&raquo; ' . substr($name, $length);
+ break;
+ }
+ // other users namespace and parent folder exists
+ else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
+ $length = strlen('(' . $names[$i] . ') ');
+ $prefix = substr($name, 0, $length);
+ $count = count(explode(' &raquo; ', $prefix));
+ $name = str_repeat('&nbsp;&nbsp;&nbsp;', $count) . '&raquo; ' . substr($name, $length);
break;
}
}
+
$names[] = $origname;
return $name;
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public static function folder_selector($type, $attrs, $current = '')
{
- // get all folders of specified type
- $folders = self::get_folders($type, false);
+ // get all folders of specified type (sorted)
+ $folders = self::get_folders($type, true);
$delim = self::$imap->get_hierarchy_delimiter();
$names = array();
$len = strlen($current);
if ($len && ($rpos = strrpos($current, $delim))) {
$parent = substr($current, 0, $rpos);
$p_len = strlen($parent);
}
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
+
// skip current folder and it's subfolders
- if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
- continue;
+ if ($len) {
+ if ($name == $current) {
+ // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
+ if ($p_len && !isset($names[$parent])) {
+ $names[$parent] = self::object_name($parent);
+ }
+ continue;
+ }
+ if (strpos($name, $current.$delim) === 0) {
+ continue;
+ }
}
// always show the parent of current folder
- if ($p_len && $name == $parent) { }
+ if ($p_len && $name == $parent) {
+ }
// skip folders where user have no rights to create subfolders
else if ($c_folder->get_owner() != $_SESSION['username']) {
$rights = $c_folder->get_myrights();
if (!preg_match('/[ck]/', $rights)) {
continue;
}
}
$names[$name] = self::object_name($name);
}
- // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
- if ($p_len && !isset($names[$parent])) {
- $names[$parent] = self::object_name($parent);
- }
-
- // Sort folders list
- asort($names, SORT_LOCALE_STRING);
-
// Build SELECT field of parent folder
$attrs['is_escaped'] = true;
$select = new html_select($attrs);
$select->add('---', '');
$listnames = array();
foreach (array_keys($names) as $imap_name) {
$name = $origname = $names[$imap_name];
// find folder prefix to truncate
for ($i = count($listnames)-1; $i >= 0; $i--) {
if (strpos($name, $listnames[$i].' &raquo; ') === 0) {
$length = strlen($listnames[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$listnames[] = $origname;
$select->add($name, $imap_name);
}
return $select;
}
/**
* Returns a list of folder names
*
* @param string Optional root folder
* @param string Optional name pattern
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
{
if (!self::setup()) {
return null;
}
// use IMAP subscriptions
if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) {
$subscribed = true;
}
if (!$filter) {
// Get ALL folders list, standard way
if ($subscribed) {
- return self::$imap->list_folders_subscribed($root, $mbox);
+ $folders = self::$imap->list_folders_subscribed($root, $mbox);
+ // add temporarily subscribed folders
+ if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
+ $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
+ }
}
else {
- return self::$imap->list_folders($root, $mbox);
+ $folders = self::_imap_list_folders($root, $mbox);
}
- }
+ return $folders;
+ }
$prefix = $root . $mbox;
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
- // get folders types
- $folderdata = self::folders_typedata($prefix);
+ // get folders types for all folders
+ if (!$subscribed || $prefix == '*' || !self::$config->get('kolab_skip_namespace')) {
+ $folderdata = self::folders_typedata($prefix);
+ }
+ else {
+ // fetch folder types for the effective list of (subscribed) folders when post-filtering
+ $folderdata = array();
+ }
if (!is_array($folderdata)) {
return array();
}
// In some conditions we can skip LIST command (?)
if (!$subscribed && $filter != 'mail' && $prefix == '*') {
foreach ($folderdata as $folder => $type) {
if (!preg_match($regexp, $type)) {
unset($folderdata[$folder]);
}
}
- return array_keys($folderdata);
+
+ return self::$imap->sort_folder_list(array_keys($folderdata), true);
}
// Get folders list
if ($subscribed) {
$folders = self::$imap->list_folders_subscribed($root, $mbox);
+
+ // add temporarily subscribed folders
+ if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
+ $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
+ }
}
else {
- $folders = self::$imap->list_folders($root, $mbox);
+ $folders = self::_imap_list_folders($root, $mbox);
}
// In case of an error, return empty list (?)
if (!is_array($folders)) {
return array();
}
// Filter folders list
foreach ($folders as $idx => $folder) {
+ // lookup folder type
+ if (!array_key_exists($folder, $folderdata)) {
+ $folderdata[$folder] = self::folder_type($folder);
+ }
+
$type = $folderdata[$folder];
if ($filter == 'mail' && empty($type)) {
continue;
}
if (empty($type) || !preg_match($regexp, $type)) {
unset($folders[$idx]);
}
}
return $folders;
}
+ /**
+ * Wrapper for rcube_imap::list_folders() with optional post-filtering
+ */
+ protected static function _imap_list_folders($root, $mbox)
+ {
+ $postfilter = null;
+
+ // compose a post-filter expression for the excluded namespaces
+ if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
+ $excludes = array();
+ foreach ((array)$skip_ns as $ns) {
+ if ($ns_root = self::namespace_root($ns)) {
+ $excludes[] = $ns_root;
+ }
+ }
+
+ if (count($excludes)) {
+ $postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!';
+ }
+ }
+
+ // use normal LIST command to return all folders, it's fast enough
+ $folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter));
+
+ if (!empty($postfilter)) {
+ $folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); });
+ $folders = self::$imap->sort_folder_list($folders);
+ }
+
+ return $folders;
+ }
+
+
+ /**
+ * Search for shared or otherwise not listed groupware folders the user has access
+ *
+ * @param string Folder type of folders to search for
+ * @param string Search string
+ * @param array Namespace(s) to exclude results from
+ *
+ * @return array List of matching kolab_storage_folder objects
+ */
+ public static function search_folders($type, $query, $exclude_ns = array())
+ {
+ if (!self::setup()) {
+ return array();
+ }
+
+ $folders = array();
+ $query = str_replace('*', '', $query);
+
+ // find unsubscribed IMAP folders of the given type
+ foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
+ // FIXME: only consider the last part of the folder path for searching?
+ $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
+ if (($query == '' || strpos($realname, $query) !== false) &&
+ !self::folder_is_subscribed($foldername, true) &&
+ !in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
+ ) {
+ $folders[] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ }
+ }
+
+ return $folders;
+ }
+
/**
* Sort the given list of kolab folders by namespace/name
*
* @param array List of kolab_storage_folder objects
* @return array Sorted list of folders
*/
public static function sort_folders($folders)
{
- $pad = ' ';
+ $pad = ' ';
+ $out = array();
$nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
+
foreach ($folders as $folder) {
$folders[$folder->name] = $folder;
$ns = $folder->get_namespace();
$nsnames[$ns][$folder->name] = strtolower(html_entity_decode(self::object_name($folder->name, $ns), ENT_COMPAT, RCUBE_CHARSET)) . $pad; // decode &raquo;
}
- $names = array();
- foreach ($nsnames as $ns => $dummy) {
+ // $folders is a result of get_folders() we can assume folders were already sorted
+ foreach (array_keys($nsnames) as $ns) {
asort($nsnames[$ns], SORT_LOCALE_STRING);
- $names += $nsnames[$ns];
+ foreach (array_keys($nsnames[$ns]) as $utf7name) {
+ $out[] = $folders[$utf7name];
+ }
}
- $out = array();
- foreach ($names as $utf7name => $name) {
- $out[] = $folders[$utf7name];
+ return $out;
+ }
+
+
+ /**
+ * Check the folder tree and add the missing parents as virtual folders
+ *
+ * @param array $folders Folders list
+ * @param object $tree Reference to the root node of the folder tree
+ *
+ * @return array Flat folders list
+ */
+ public static function folder_hierarchy($folders, &$tree = null)
+ {
+ $_folders = array();
+ $delim = self::$imap->get_hierarchy_delimiter();
+ $other_ns = rtrim(self::namespace_root('other'), $delim);
+ $tree = new kolab_storage_folder_virtual('', '<root>', ''); // create tree root
+ $refs = array('' => $tree);
+
+ foreach ($folders as $idx => $folder) {
+ $path = explode($delim, $folder->name);
+ array_pop($path);
+ $folder->parent = join($delim, $path);
+ $folder->children = array(); // reset list
+
+ // skip top folders or ones with a custom displayname
+ if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
+ $tree->children[] = $folder;
+ }
+ else {
+ $parents = array();
+ $depth = $folder->get_namespace() == 'personal' ? 1 : 2;
+
+ while (count($path) >= $depth && ($parent = join($delim, $path))) {
+ array_pop($path);
+ $parent_parent = join($delim, $path);
+ if (!$refs[$parent]) {
+ if ($folder->type && self::folder_type($parent) == $folder->type) {
+ $refs[$parent] = new kolab_storage_folder($parent, $folder->type);
+ $refs[$parent]->parent = $parent_parent;
+ }
+ else if ($parent_parent == $other_ns) {
+ $refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
+ }
+ else {
+ $name = kolab_storage::object_name($parent, $folder->get_namespace());
+ $refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
+ }
+ $parents[] = $refs[$parent];
+ }
+ }
+
+ if (!empty($parents)) {
+ $parents = array_reverse($parents);
+ foreach ($parents as $parent) {
+ $parent_node = $refs[$parent->parent] ?: $tree;
+ $parent_node->children[] = $parent;
+ $_folders[] = $parent;
+ }
+ }
+
+ $parent_node = $refs[$folder->parent] ?: $tree;
+ $parent_node->children[] = $folder;
+ }
+
+ $refs[$folder->name] = $folder;
+ $_folders[] = $folder;
+ unset($folders[$idx]);
}
- return $out;
+ return $_folders;
}
/**
* Returns folder types indexed by folder name
*
* @param string $prefix Folder prefix (Default '*' for all folders)
*
* @return array|bool List of folders, False on failure
*/
public static function folders_typedata($prefix = '*')
{
if (!self::setup()) {
return false;
}
- $folderdata = self::$imap->get_metadata($prefix, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
+ // return cached result
+ if (is_array(self::$typedata[$prefix])) {
+ return self::$typedata[$prefix];
+ }
+
+ $type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
+
+ // fetch metadata from *some* folders only
+ if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
+ $delimiter = self::$imap->get_hierarchy_delimiter();
+ $folderdata = $blacklist = array();
+ foreach ((array)$skip_ns as $ns) {
+ if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
+ $blacklist[] = $ns_root;
+ }
+ }
+ foreach (array('personal','other','shared') as $ns) {
+ if (!in_array($ns, (array)$skip_ns)) {
+ $ns_root = rtrim(self::namespace_root($ns), $delimiter);
+
+ // list top-level folders and their childs one by one
+ // GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
+ if ($ns_root == '') {
+ foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
+ if (!in_array($folder, $blacklist)) {
+ $folderdata[$folder] = $metadata;
+ $opts = self::$imap->folder_attributes($folder);
+ if (!in_array('\\HasNoChildren', $opts) && ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
+ $folderdata += $data;
+ }
+ }
+ }
+ }
+ else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
+ $folderdata += $data;
+ }
+ }
+ }
+ }
+ else {
+ $folderdata = self::$imap->get_metadata($prefix, $type_keys);
+ }
if (!is_array($folderdata)) {
return false;
}
- return array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+ // keep list in memory
+ self::$typedata[$prefix] = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+
+ return self::$typedata[$prefix];
}
/**
* Callback for array_map to select the correct annotation value
*/
public static function folder_select_metadata($types)
{
if (!empty($types[self::CTYPE_KEY_PRIVATE])) {
return $types[self::CTYPE_KEY_PRIVATE];
}
else if (!empty($types[self::CTYPE_KEY])) {
list($ctype, $suffix) = explode('.', $types[self::CTYPE_KEY]);
return $ctype;
}
return null;
}
/**
* Returns type of IMAP folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
public static function folder_type($folder)
{
self::setup();
+ // return in-memory cached result
+ foreach (self::$typedata as $typedata) {
+ if (array_key_exists($folder, $typedata)) {
+ return $typedata[$folder];
+ }
+ }
+
$metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($metadata)) {
return null;
}
if (!empty($metadata[$folder])) {
return self::folder_select_metadata($metadata[$folder]);
}
return 'mail';
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return boolean True on success
*/
public static function set_folder_type($folder, $type='mail')
{
self::setup();
list($ctype, $subtype) = explode('.', $type);
$success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
if (!$success) // fallback: only set private annotation
$success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
return $success;
}
/**
* Check subscription status of this folder
*
* @param string $folder Folder name
+ * @param boolean $temp Include temporary/session subscriptions
*
* @return boolean True if subscribed, false if not
*/
- public static function folder_is_subscribed($folder)
+ public static function folder_is_subscribed($folder, $temp = false)
{
if (self::$subscriptions === null) {
self::setup();
+ self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
+ self::$with_tempsubs = true;
}
- return in_array($folder, self::$subscriptions);
+ return in_array($folder, self::$subscriptions) ||
+ ($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
+ * @param boolean $temp Only subscribe temporarily for the current session
*
* @return True on success, false on error
*/
- public static function folder_subscribe($folder)
+ public static function folder_subscribe($folder, $temp = false)
{
self::setup();
- if (self::$imap->subscribe($folder)) {
+ // temporary/session subscription
+ if ($temp) {
+ if (self::folder_is_subscribed($folder)) {
+ return true;
+ }
+ else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) {
+ $_SESSION['kolab_subscribed_folders'][] = $folder;
+ return true;
+ }
+ }
+ else if (self::$imap->subscribe($folder)) {
self::$subscriptions === null;
return true;
}
return false;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
+ * @param boolean $temp Only remove temporary subscription
*
* @return True on success, false on error
*/
- public static function folder_unsubscribe($folder)
+ public static function folder_unsubscribe($folder, $temp = false)
{
self::setup();
- if (self::$imap->unsubscribe($folder)) {
+ // temporary/session subscription
+ if ($temp) {
+ if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
+ unset($_SESSION['kolab_subscribed_folders'][$i]);
+ }
+ return true;
+ }
+ else if (self::$imap->unsubscribe($folder)) {
self::$subscriptions === null;
return true;
}
return false;
}
/**
* Check activation status of this folder
*
* @param string $folder Folder name
*
* @return boolean True if active, false if not
*/
public static function folder_is_active($folder)
{
$active_folders = self::get_states();
return in_array($folder, $active_folders);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_activate($folder)
{
+ // activation implies temporary subscription
+ self::folder_subscribe($folder, true);
return self::set_state($folder, true);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_deactivate($folder)
{
+ // remove from temp subscriptions, really?
+ self::folder_unsubscribe($folder, true);
+
return self::set_state($folder, false);
}
/**
* Return list of active folders
*/
private static function get_states()
{
if (self::$states !== null) {
return self::$states;
}
$rcube = rcube::get_instance();
$folders = $rcube->config->get('kolab_active_folders');
if ($folders !== null) {
self::$states = !empty($folders) ? explode('**', $folders) : array();
}
// for backward-compatibility copy server-side subscriptions to activation states
else {
self::setup();
if (self::$subscriptions === null) {
+ self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
+ self::$with_tempsubs = true;
}
self::$states = self::$subscriptions;
$folders = implode(self::$states, '**');
$rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
return self::$states;
}
/**
* Update list of active folders
*/
private static function set_state($folder, $state)
{
self::get_states();
// update in-memory list
$idx = array_search($folder, self::$states);
if ($state && $idx === false) {
self::$states[] = $folder;
}
else if (!$state && $idx !== false) {
unset(self::$states[$idx]);
}
// update user preferences
$folders = implode(self::$states, '**');
$rcube = rcube::get_instance();
return $rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
/**
* Creates default folder of specified type
* To be run when none of subscribed folders (of specified type) is found
*
* @param string $type Folder type
* @param string $props Folder properties (color, etc)
*
* @return string Folder name
*/
public static function create_default_folder($type, $props = array())
{
if (!self::setup()) {
return;
}
$folders = self::$imap->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE));
// from kolab_folders config
$folder_type = strpos($type, '.') ? str_replace('.', '_', $type) : $type . '_default';
$default_name = self::$config->get('kolab_folders_' . $folder_type);
$folder_type = str_replace('_', '.', $folder_type);
// check if we have any folder in personal namespace
// folder(s) may exist but not subscribed
- foreach ($folders as $f => $data) {
+ foreach ((array)$folders as $f => $data) {
if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
$folder = $f;
break;
}
}
if (!$folder) {
if (!$default_name) {
$default_name = self::$default_folders[$type];
}
if (!$default_name) {
return;
}
$folder = rcube_charset::convert($default_name, RCUBE_CHARSET, 'UTF7-IMAP');
$prefix = self::$imap->get_namespace('prefix');
// add personal namespace prefix if needed
if ($prefix && strpos($folder, $prefix) !== 0 && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
if (!self::$imap->folder_exists($folder)) {
if (!self::$imap->create_folder($folder)) {
return;
}
}
self::set_folder_type($folder, $folder_type);
}
self::folder_subscribe($folder);
if ($props['active']) {
self::set_state($folder, true);
}
if (!empty($props)) {
self::set_folder_props($folder, $props);
}
return $folder;
}
/**
* Sets folder metadata properties
*
* @param string $folder Folder name
* @param array $prop Folder properties
*/
public static function set_folder_props($folder, &$prop)
{
if (!self::setup()) {
return;
}
// TODO: also save 'showalarams' and other properties here
$ns = self::$imap->folder_namespace($folder);
$supported = array(
'color' => array(self::COLOR_KEY_SHARED, self::COLOR_KEY_PRIVATE),
'displayname' => array(self::NAME_KEY_SHARED, self::NAME_KEY_PRIVATE),
);
foreach ($supported as $key => $metakeys) {
if (array_key_exists($key, $prop)) {
$meta_saved = false;
if ($ns == 'personal') // save in shared namespace for personal folders
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key]));
if (!$meta_saved) // try in private namespace
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key]));
if ($meta_saved)
unset($prop[$key]); // unsetting will prevent fallback to local user prefs
}
}
}
+
+ /**
+ *
+ * @param mixed $query Search value (or array of field => value pairs)
+ * @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
+ * @param array $required List of fields that shall ot be empty
+ * @param int $limit Maximum number of records
+ * @param int $count Returns the number of records found
+ *
+ * @return array List or false on error
+ */
+ public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
+ {
+ $query = str_replace('*', '', $query);
+
+ // requires a working LDAP setup
+ if (!self::ldap() || strlen($query) == 0) {
+ return array();
+ }
+
+ // search users using the configured attributes
+ $results = self::$ldap->dosearch(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit, $count);
+
+ // exclude myself
+ if ($_SESSION['kolab_dn']) {
+ unset($results[$_SESSION['kolab_dn']]);
+ }
+
+ // resolve to IMAP folder name
+ $root = self::namespace_root('other');
+ $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
+
+ array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
+ list($localpart, $domain) = explode('@', $user[$user_attrib]);
+ $user['kolabtargetfolder'] = $root . $localpart;
+ });
+
+ return $results;
+ }
+
+
+ /**
+ * Returns a list of IMAP folders shared by the given user
+ *
+ * @param array User entry from LDAP
+ * @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
+ * @param boolean Return subscribed folders only (null to use configured subscription mode)
+ * @param array Will be filled with folder-types data
+ *
+ * @return array List of folders
+ */
+ public static function list_user_folders($user, $type, $subscribed = null, &$folderdata = array())
+ {
+ self::setup();
+
+ $folders = array();
+
+ // use localpart of user attribute as root for folder listing
+ $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
+ if (!empty($user[$user_attrib])) {
+ list($mbox) = explode('@', $user[$user_attrib]);
+
+ $delimiter = self::$imap->get_hierarchy_delimiter();
+ $other_ns = self::namespace_root('other');
+ $folders = self::list_folders($other_ns . $mbox . $delimiter, '*', $type, $subscribed, $folderdata);
+ }
+
+ return $folders;
+ }
+
+
+ /**
+ * Get a list of (virtual) top-level folders from the other users namespace
+ *
+ * @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
+ * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
+ *
+ * @return array List of kolab_storage_folder_user objects
+ */
+ public static function get_user_folders($type, $subscribed)
+ {
+ $folders = $folderdata = array();
+
+ if (self::setup()) {
+ $delimiter = self::$imap->get_hierarchy_delimiter();
+ $other_ns = rtrim(self::namespace_root('other'), $delimiter);
+ $path_len = count(explode($delimiter, $other_ns));
+
+ foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
+ if ($foldername == 'INBOX') // skip INBOX which is added by default
+ continue;
+
+ $path = explode($delimiter, $foldername);
+
+ // compare folder type if a subfolder is listed
+ if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) {
+ continue;
+ }
+
+ // truncate folder path to top-level folders of the 'other' namespace
+ $foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
+
+ if (!$folders[$foldername]) {
+ $folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
+ }
+ }
+
+ // for every (subscribed) user folder, list all (unsubscribed) subfolders
+ foreach ($folders as $userfolder) {
+ foreach ((array)self::list_folders($userfolder->name . $delimiter, '*', $type, false, $folderdata) as $foldername) {
+ if (!$folders[$foldername]) {
+ $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ $userfolder->children[] = $folders[$foldername];
+ }
+ }
+ }
+ }
+
+ return $folders;
+ }
+
+
+ /**
+ * Handler for user_delete plugin hooks
+ *
+ * Remove all cache data from the local database related to the given user.
+ */
+ public static function delete_user_folders($args)
+ {
+ $db = rcmail::get_instance()->get_dbh();
+ $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
+ $db->query("DELETE FROM " . $db->table_name('kolab_folders', true) . " WHERE `resource` LIKE ?", $prefix);
+ }
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache.php
similarity index 59%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage_cache.php
rename to 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
@@ -1,895 +1,1062 @@
<?php
/**
* Kolab storage cache class providing a local caching layer for Kolab groupware objects.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_cache
{
+ const DB_DATE_FORMAT = 'Y-m-d H:i:s';
+
protected $db;
protected $imap;
protected $folder;
protected $uid2msg;
protected $objects;
- protected $index = array();
protected $metadata = array();
protected $folder_id;
protected $resource_uri;
protected $enabled = true;
protected $synched = false;
protected $synclock = false;
protected $ready = false;
protected $cache_table;
protected $folders_table;
protected $max_sql_packet;
protected $max_sync_lock_time = 600;
protected $binary_items = array();
protected $extra_cols = array();
+ protected $order_by = null;
+ protected $limit = null;
/**
* Factory constructor
*/
public static function factory(kolab_storage_folder $storage_folder)
{
$subclass = 'kolab_storage_cache_' . $storage_folder->type;
if (class_exists($subclass)) {
return new $subclass($storage_folder);
}
else {
rcube::raise_error(array(
'code' => 900,
'type' => 'php',
- 'message' => "No kolab_storage_cache class found for folder of type " . $storage_folder->type
+ 'message' => "No kolab_storage_cache class found for folder '$storage_folder->name' of type '$storage_folder->type'"
), true);
+
+ return new kolab_storage_cache($storage_folder);
}
}
/**
* Default constructor
*/
public function __construct(kolab_storage_folder $storage_folder = null)
{
$rcmail = rcube::get_instance();
$this->db = $rcmail->get_dbh();
$this->imap = $rcmail->get_storage();
$this->enabled = $rcmail->config->get('kolab_cache', false);
if ($this->enabled) {
// always read folder cache and lock state from DB master
$this->db->set_table_dsn('kolab_folders', 'w');
// remove sync-lock on script termination
$rcmail->add_shutdown_function(array($this, '_sync_unlock'));
}
if ($storage_folder)
$this->set_folder($storage_folder);
}
+ /**
+ * Direct access to cache by folder_id
+ * (only for internal use)
+ */
+ public function select_by_id($folder_id)
+ {
+ $folders_table = $this->db->table_name('kolab_folders', true);
+ $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM $folders_table WHERE `folder_id` = ?", $folder_id));
+ if ($sql_arr) {
+ $this->metadata = $sql_arr;
+ $this->folder_id = $sql_arr['folder_id'];
+ $this->folder = new StdClass;
+ $this->folder->type = $sql_arr['type'];
+ $this->resource_uri = $sql_arr['resource'];
+ $this->cache_table = $this->db->table_name('kolab_cache_' . $sql_arr['type']);
+ $this->ready = true;
+ }
+ }
/**
* Connect cache with a storage folder
*
* @param kolab_storage_folder The storage folder instance to connect with
*/
public function set_folder(kolab_storage_folder $storage_folder)
{
$this->folder = $storage_folder;
if (empty($this->folder->name)) {
$this->ready = false;
return;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = $this->folder->get_resource_uri();
$this->folders_table = $this->db->table_name('kolab_folders');
$this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
- $this->ready = $this->enabled;
+ $this->ready = $this->enabled && !empty($this->folder->type);
$this->folder_id = null;
}
/**
* Returns true if this cache supports query by type
*/
public function has_type_col()
{
return in_array('type', $this->extra_cols);
}
+ /**
+ * Getter for the numeric ID used in cache tables
+ */
+ public function get_folder_id()
+ {
+ $this->_read_folder_data();
+ return $this->folder_id;
+ }
+
/**
* Synchronize local cache data with remote
*/
public function synchronize()
{
// only sync once per request cycle
if ($this->synched)
return;
// increase time limit
@set_time_limit($this->max_sync_lock_time);
- // read cached folder metadata
- $this->_read_folder_data();
+ if (!$this->ready) {
+ // kolab cache is disabled, synchronize IMAP mailbox cache only
+ $this->imap->folder_sync($this->folder->name);
+ }
+ else {
+ // read cached folder metadata
+ $this->_read_folder_data();
- // check cache status hash first ($this->metadata is set in _read_folder_data())
- if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
+ // check cache status hash first ($this->metadata is set in _read_folder_data())
+ if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
+ // lock synchronization for this folder or wait if locked
+ $this->_sync_lock();
- // lock synchronization for this folder or wait if locked
- $this->_sync_lock();
+ // disable messages cache if configured to do so
+ $this->bypass(true);
- // disable messages cache if configured to do so
- $this->bypass(true);
+ // synchronize IMAP mailbox cache
+ $this->imap->folder_sync($this->folder->name);
- // synchronize IMAP mailbox cache
- $this->imap->folder_sync($this->folder->name);
+ // compare IMAP index with object cache index
+ $imap_index = $this->imap->index($this->folder->name, null, null, true, true);
- // compare IMAP index with object cache index
- $imap_index = $this->imap->index($this->folder->name);
- $this->index = $imap_index->get();
+ // determine objects to fetch or to invalidate
+ if (!$imap_index->is_error()) {
+ $imap_index = $imap_index->get();
- // determine objects to fetch or to invalidate
- if ($this->ready) {
- // read cache index
- $sql_result = $this->db->query(
- "SELECT msguid, uid FROM $this->cache_table WHERE folder_id=?",
- $this->folder_id
- );
+ // read cache index
+ $sql_result = $this->db->query(
+ "SELECT `msguid`, `uid` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
+ $this->folder_id
+ );
- $old_index = array();
- while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $old_index[] = $sql_arr['msguid'];
- $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
- }
+ $old_index = array();
+ while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $old_index[] = $sql_arr['msguid'];
+ $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+ }
- // fetch new objects from imap
- foreach (array_diff($this->index, $old_index) as $msguid) {
- if ($object = $this->folder->read_object($msguid, '*')) {
- $this->_extended_insert($msguid, $object);
+ // fetch new objects from imap
+ foreach (array_diff($imap_index, $old_index) as $msguid) {
+ if ($object = $this->folder->read_object($msguid, '*')) {
+ $this->_extended_insert($msguid, $object);
+ }
+ }
+ $this->_extended_insert(0, null);
+
+ // delete invalid entries from local DB
+ $del_index = array_diff($old_index, $imap_index);
+ if (!empty($del_index)) {
+ $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
+ $this->db->query(
+ "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` IN ($quoted_ids)",
+ $this->folder_id
+ );
}
- }
- $this->_extended_insert(0, null);
-
- // delete invalid entries from local DB
- $del_index = array_diff($old_index, $this->index);
- if (!empty($del_index)) {
- $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
- $this->db->query(
- "DELETE FROM $this->cache_table WHERE folder_id=? AND msguid IN ($quoted_ids)",
- $this->folder_id
- );
- }
- // update ctag value (will be written to database in _sync_unlock())
- $this->metadata['ctag'] = $this->folder->get_ctag();
- }
+ // update ctag value (will be written to database in _sync_unlock())
+ $this->metadata['ctag'] = $this->folder->get_ctag();
+ }
- $this->bypass(false);
+ $this->bypass(false);
- // remove lock
- $this->_sync_unlock();
+ // remove lock
+ $this->_sync_unlock();
+ }
}
$this->synched = time();
}
/**
* Read a single entry from cache or from IMAP directly
*
* @param string Related IMAP message UID
* @param string Object type to read
* @param string IMAP folder name the entry relates to
* @param array Hash array with object properties or null if not found
*/
public function get($msguid, $type = null, $foldername = null)
{
// delegate to another cache instance
if ($foldername && $foldername != $this->folder->name) {
return kolab_storage::get_folder($foldername)->cache->get($msguid, $type);
}
// load object if not in memory
if (!isset($this->objects[$msguid])) {
if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
- "SELECT * FROM $this->cache_table ".
- "WHERE folder_id=? AND msguid=?",
+ "SELECT * FROM `{$this->cache_table}` ".
+ "WHERE `folder_id` = ? AND `msguid` = ?",
$this->folder_id,
$msguid
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $this->objects[$msguid] = $this->_unserialize($sql_arr);
+ $this->objects = array($msguid => $this->_unserialize($sql_arr)); // store only this object in memory (#2827)
}
}
// fetch from IMAP if not present in cache
if (empty($this->objects[$msguid])) {
- $result = $this->_fetch(array($msguid), $type, $foldername);
- $this->objects[$msguid] = $result[0];
+ if ($object = $this->folder->read_object($msguid, $type ?: '*', $foldername)) {
+ $this->objects = array($msguid => $object);
+ $this->set($msguid, $object);
+ }
}
}
return $this->objects[$msguid];
}
/**
* Insert/Update a cache entry
*
* @param string Related IMAP message UID
* @param mixed Hash array with object properties to save or false to delete the cache entry
* @param string IMAP folder name the entry relates to
*/
public function set($msguid, $object, $foldername = null)
{
if (!$msguid) {
return;
}
// delegate to another cache instance
if ($foldername && $foldername != $this->folder->name) {
kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
return;
}
// remove old entry
if ($this->ready) {
$this->_read_folder_data();
- $this->db->query("DELETE FROM $this->cache_table WHERE folder_id=? AND msguid=?",
+ $this->db->query("DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` = ?",
$this->folder_id, $msguid);
}
if ($object) {
// insert new object data...
- $this->insert($msguid, $object);
+ $this->save($msguid, $object);
}
else {
// ...or set in-memory cache to false
$this->objects[$msguid] = $object;
}
}
/**
- * Insert a cache entry
+ * Insert (or update) a cache entry
*
- * @param string Related IMAP message UID
+ * @param int Related IMAP message UID
* @param mixed Hash array with object properties to save or false to delete the cache entry
+ * @param int Optional old message UID (for update)
*/
- public function insert($msguid, $object)
+ public function save($msguid, $object, $olduid = null)
{
// write to cache
if ($this->ready) {
$this->_read_folder_data();
$sql_data = $this->_serialize($object);
+ $sql_data['folder_id'] = $this->folder_id;
+ $sql_data['msguid'] = $msguid;
+ $sql_data['uid'] = $object['uid'];
- $extra_cols = $this->extra_cols ? ', ' . join(', ', $this->extra_cols) : '';
- $extra_fields = $this->extra_cols ? str_repeat(', ?', count($this->extra_cols)) : '';
+ $args = array();
+ $cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'xml', 'tags', 'words');
+ $cols = array_merge($cols, $this->extra_cols);
- $args = array(
- "INSERT INTO $this->cache_table ".
- " (folder_id, msguid, uid, created, changed, data, xml, tags, words $extra_cols)".
- " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_fields)",
- $this->folder_id,
- $msguid,
- $object['uid'],
- $sql_data['changed'],
- $sql_data['data'],
- $sql_data['xml'],
- $sql_data['tags'],
- $sql_data['words'],
- );
+ foreach ($cols as $idx => $col) {
+ $cols[$idx] = $this->db->quote_identifier($col);
+ $args[] = $sql_data[$col];
+ }
- foreach ($this->extra_cols as $col) {
- $args[] = $sql_data[$col];
+ if ($olduid) {
+ foreach ($cols as $idx => $col) {
+ $cols[$idx] = "$col = ?";
+ }
+
+ $query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
+ . " WHERE `folder_id` = ? AND `msguid` = ?";
+ $args[] = $this->folder_id;
+ $args[] = $olduid;
+ }
+ else {
+ $query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
+ . ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
}
- $result = call_user_func_array(array($this->db, 'query'), $args);
+ $result = $this->db->query($query, $args);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "Failed to write to kolab cache"
), true);
}
}
// keep a copy in memory for fast access
$this->objects = array($msguid => $object);
$this->uid2msg = array($object['uid'] => $msguid);
}
/**
* Move an existing cache entry to a new resource
*
* @param string Entry's IMAP message UID
* @param string Entry's Object UID
- * @param string Target IMAP folder to move it to
+ * @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);
}
unset($this->uid2msg[$uid]);
}
/**
* Remove all objects from local cache
*/
public function purge($type = null)
{
+ if (!$this->ready) {
+ return true;
+ }
+
$this->_read_folder_data();
$result = $this->db->query(
- "DELETE FROM $this->cache_table WHERE folder_id=?".
+ "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ?",
$this->folder_id
);
+
return $this->db->affected_rows($result);
}
/**
* Update resource URI for existing cache entries
*
* @param string Target IMAP folder to move it to
*/
public function rename($new_folder)
{
+ if (!$this->ready) {
+ return;
+ }
+
$target = kolab_storage::get_folder($new_folder);
// resolve new message UID in target folder
$this->db->query(
- "UPDATE $this->folders_table SET resource=? ".
- "WHERE resource=?",
+ "UPDATE `{$this->folders_table}` SET `resource` = ? ".
+ "WHERE `resource` = ?",
$target->get_resource_uri(),
$this->resource_uri
);
}
/**
* Select Kolab objects filtered by the given query
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* triplet: array('<colname>', '<comparator>', '<value>')
* @param boolean Set true to only return UIDs instead of complete objects
* @return array List of Kolab data objects (each represented as hash array) or UIDs
*/
public function select($query = array(), $uids = false)
{
- $result = array();
+ $result = $uids ? array() : new kolab_storage_dataset($this);
// read from local cache DB (assume it to be synchronized)
if ($this->ready) {
$this->_read_folder_data();
- $sql_result = $this->db->query(
- "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM $this->cache_table ".
- "WHERE folder_id=? " . $this->_sql_where($query),
- $this->folder_id
- );
+ // fetch full object data on one query if a small result set is expected
+ $fetchall = !$uids && ($this->limit ? $this->limit[0] : $this->count($query)) < 500;
+ $sql_query = "SELECT " . ($fetchall ? '*' : '`msguid` AS `_msguid`, `uid`') . " FROM `{$this->cache_table}` ".
+ "WHERE `folder_id` = ? " . $this->_sql_where($query);
+ if (!empty($this->order_by)) {
+ $sql_query .= ' ORDER BY ' . $this->order_by;
+ }
+ $sql_result = $this->limit ?
+ $this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
+ $this->db->query($sql_query, $this->folder_id);
+
+ if ($this->db->is_error($sql_result)) {
+ if ($uids) {
+ return null;
+ }
+ $result->set_error(true);
+ return $result;
+ }
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if ($uids) {
- $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+ $this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid'];
$result[] = $sql_arr['uid'];
}
- else if ($object = $this->_unserialize($sql_arr)) {
+ else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
$result[] = $object;
}
+ else if (!$fetchall) {
+ // only add msguid to dataset index
+ $result[] = $sql_arr;
+ }
}
}
+ // use IMAP
else {
- // extract object type from query parameter
$filter = $this->_query2assoc($query);
- // use 'list' for folder's default objects
- if ($filter['type'] == $this->type) {
- $index = $this->index;
- }
- else { // search by object type
+ if ($filter['type']) {
$search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
- $index = $this->imap->search_once($this->folder->name, $search)->get();
+ $index = $this->imap->search_once($this->folder->name, $search);
+ }
+ else {
+ $index = $this->imap->index($this->folder->name, null, null, true, true);
+ }
+
+ if ($index->is_error()) {
+ if ($uids) {
+ return null;
+ }
+ $result->set_error(true);
+ return $result;
}
- // fetch all messages in $index from IMAP
- $result = $uids ? $this->_fetch_uids($index, $filter['type']) : $this->_fetch($index, $filter['type']);
+ $index = $index->get();
+ $result = $uids ? $index : $this->_fetch($index, $filter['type']);
// TODO: post-filter result according to query
}
// We don't want to cache big results in-memory, however
// if we select only one object here, there's a big chance we will need it later
if (!$uids && count($result) == 1) {
if ($msguid = $result[0]['_msguid']) {
$this->uid2msg[$result[0]['uid']] = $msguid;
- $this->objects[$msguid] = $result[0];
+ $this->objects = array($msguid => $result[0]);
}
}
return $result;
}
/**
* Get number of objects mathing the given query
*
* @param array $query Pseudo-SQL query as list of filter parameter triplets
* @return integer The number of objects of the given type
*/
public function count($query = array())
{
- $count = 0;
-
- // cache is in sync, we can count records in local DB
- if ($this->synched) {
+ // read from local cache DB (assume it to be synchronized)
+ if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
- "SELECT COUNT(*) AS numrows FROM $this->cache_table ".
- "WHERE folder_id=? " . $this->_sql_where($query),
+ "SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
+ "WHERE `folder_id` = ?" . $this->_sql_where($query),
$this->folder_id
);
+ if ($this->db->is_error($sql_result)) {
+ return null;
+ }
+
$sql_arr = $this->db->fetch_assoc($sql_result);
- $count = intval($sql_arr['numrows']);
+ $count = intval($sql_arr['numrows']);
}
+ // use IMAP
else {
- // search IMAP by object type
$filter = $this->_query2assoc($query);
- $ctype = kolab_format::KTYPE_PREFIX . $filter['type'];
- $index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
+
+ if ($filter['type']) {
+ $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
+ $index = $this->imap->search_once($this->folder->name, $search);
+ }
+ else {
+ $index = $this->imap->index($this->folder->name, null, null, true, true);
+ }
+
+ if ($index->is_error()) {
+ return null;
+ }
+
+ // TODO: post-filter result according to query
+
$count = $index->count();
}
return $count;
}
+ /**
+ * Define ORDER BY clause for cache queries
+ */
+ public function set_order_by($sortcols)
+ {
+ if (!empty($sortcols)) {
+ $this->order_by = '`' . join('`, `', (array)$sortcols) . '`';
+ }
+ else {
+ $this->order_by = null;
+ }
+ }
+
+ /**
+ * Define LIMIT clause for cache queries
+ */
+ public function set_limit($length, $offset = 0)
+ {
+ $this->limit = array($length, $offset);
+ }
/**
* Helper method to compose a valid SQL query from pseudo filter triplets
*/
protected function _sql_where($query)
{
$sql_where = '';
foreach ((array) $query as $param) {
if (is_array($param[0])) {
$subq = array();
foreach ($param[0] as $q) {
$subq[] = preg_replace('/^\s*AND\s+/i', '', $this->_sql_where(array($q)));
}
if (!empty($subq)) {
$sql_where .= ' AND (' . implode($param[1] == 'OR' ? ' OR ' : ' AND ', $subq) . ')';
}
continue;
}
else if ($param[1] == '=' && is_array($param[2])) {
$qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')';
$param[1] = 'IN';
}
else if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') {
$not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : '';
$param[1] = $not . 'LIKE';
$qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
}
else if ($param[0] == 'tags') {
- $param[1] = 'LIKE';
+ $param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE';
$qvalue = $this->db->quote('% '.$param[2].' %');
}
else {
$qvalue = $this->db->quote($param[2]);
}
$sql_where .= sprintf(' AND %s %s %s',
$this->db->quote_identifier($param[0]),
$param[1],
$qvalue
);
}
return $sql_where;
}
/**
* Helper method to convert the given pseudo-query triplets into
* an associative filter array with 'equals' values only
*/
protected function _query2assoc($query)
{
// extract object type from query parameter
$filter = array();
foreach ($query as $param) {
if ($param[1] == '=')
$filter[$param[0]] = $param[2];
}
return $filter;
}
/**
* Fetch messages from IMAP
*
* @param array List of message UIDs to fetch
* @param string Requested object type or * for all
* @param string IMAP folder to read from
* @return array List of parsed Kolab objects
*/
protected function _fetch($index, $type = null, $folder = null)
{
- $results = array();
+ $results = new kolab_storage_dataset($this);
foreach ((array)$index as $msguid) {
if ($object = $this->folder->read_object($msguid, $type, $folder)) {
$results[] = $object;
$this->set($msguid, $object);
}
}
return $results;
}
-
- /**
- * Fetch object UIDs (aka message subjects) from IMAP
- *
- * @param array List of message UIDs to fetch
- * @param string Requested object type or * for all
- * @param string IMAP folder to read from
- * @return array List of parsed Kolab objects
- */
- protected function _fetch_uids($index, $type = null)
- {
- if (!$type)
- $type = $this->folder->type;
-
- $this->bypass(true);
-
- $results = array();
- $headers = $this->imap->fetch_headers($this->folder->name, $index, false);
-
- $this->bypass(false);
-
- foreach ((array)$headers as $msguid => $headers) {
- $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
-
- // check object type header and abort on mismatch
- if ($type != '*' && $object_type != $type)
- return false;
-
- $uid = $headers->subject;
- $this->uid2msg[$uid] = $msguid;
- $results[] = $uid;
- }
-
- return $results;
- }
-
-
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*/
protected function _serialize($object)
{
$sql_data = array('changed' => null, 'xml' => '', 'tags' => '', 'words' => '');
if ($object['changed']) {
$sql_data['changed'] = date('Y-m-d H:i:s', is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']);
}
if ($object['_formatobj']) {
$sql_data['xml'] = preg_replace('!(</?[a-z0-9:-]+>)[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write(3.0));
$sql_data['tags'] = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' '; // pad with spaces for strict/prefix search
$sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' ';
}
// extract object data
$data = array();
foreach ($object as $key => $val) {
// skip empty properties
if ($val === "" || $val === null) {
continue;
}
// mark binary data to be extracted from xml on unserialize()
if (isset($this->binary_items[$key])) {
$data[$key] = true;
}
else if ($key[0] != '_') {
$data[$key] = $val;
}
else if ($key == '_attachments') {
foreach ($val as $k => $att) {
unset($att['content'], $att['path']);
if ($att['id'])
$data[$key][$k] = $att;
}
}
}
- $sql_data['data'] = serialize($data);
+ // use base64 encoding (Bug #1912, #2662)
+ $sql_data['data'] = base64_encode(serialize($data));
+
return $sql_data;
}
/**
* Helper method to turn stored cache data into a valid storage object
*/
protected function _unserialize($sql_arr)
{
+ // check if data is a base64-encoded string, for backward compat.
+ if (strpos(substr($sql_arr['data'], 0, 64), ':') === false) {
+ $sql_arr['data'] = base64_decode($sql_arr['data']);
+ }
+
$object = unserialize($sql_arr['data']);
+ // de-serialization failed
+ if ($object === false) {
+ rcube::raise_error(array(
+ 'code' => 900, 'type' => 'php',
+ 'message' => "Malformed data for {$this->resource_uri}/{$sql_arr['msguid']} object."
+ ), true);
+
+ return null;
+ }
+
// decode binary properties
foreach ($this->binary_items as $key => $regexp) {
if (!empty($object[$key]) && preg_match($regexp, $sql_arr['xml'], $m)) {
$object[$key] = base64_decode($m[1]);
}
}
+ $object_type = $sql_arr['type'] ?: $this->folder->type;
+ $format_type = $this->folder->type == 'configuration' ? 'configuration' : $object_type;
+
// add meta data
- $object['_type'] = $sql_arr['type'] ?: $this->folder->type;
- $object['_msguid'] = $sql_arr['msguid'];
- $object['_mailbox'] = $this->folder->name;
- $object['_size'] = strlen($sql_arr['xml']);
- $object['_formatobj'] = kolab_format::factory($object['_type'], 3.0, $sql_arr['xml']);
+ $object['_type'] = $object_type;
+ $object['_msguid'] = $sql_arr['msguid'];
+ $object['_mailbox'] = $this->folder->name;
+ $object['_size'] = strlen($sql_arr['xml']);
+ $object['_formatobj'] = kolab_format::factory($format_type, 3.0, $sql_arr['xml']);
return $object;
}
/**
* Write records into cache using extended inserts to reduce the number of queries to be executed
*
* @param int Message UID. Set 0 to commit buffered inserts
* @param array Kolab object to cache
*/
protected function _extended_insert($msguid, $object)
{
static $buffer = '';
$line = '';
if ($object) {
$sql_data = $this->_serialize($object);
+
+ // Skip multifolder insert for Oracle, we can't put long data inline
+ if ($this->db->db_provider == 'oracle') {
+ $extra_cols = '';
+ if ($this->extra_cols) {
+ $extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols);
+ $extra_cols = ', ' . join(', ', $extra_cols);
+ $extra_args = str_repeat(', ?', count($this->extra_cols));
+ }
+
+ $params = array($this->folder_id, $msguid, $object['uid'], $sql_data['changed'],
+ $sql_data['data'], $sql_data['xml'], $sql_data['tags'], $sql_data['words']);
+
+ foreach ($this->extra_cols as $col) {
+ $params[] = $sql_data[$col];
+ }
+
+ $result = $this->db->query(
+ "INSERT INTO `{$this->cache_table}` "
+ . " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)"
+ . " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_args)",
+ $params
+ );
+
+ if (!$this->db->affected_rows($result)) {
+ rcube::raise_error(array(
+ 'code' => 900, 'type' => 'php',
+ 'message' => "Failed to write to kolab cache"
+ ), true);
+ }
+
+ return;
+ }
+
$values = array(
$this->db->quote($this->folder_id),
$this->db->quote($msguid),
$this->db->quote($object['uid']),
$this->db->now(),
$this->db->quote($sql_data['changed']),
$this->db->quote($sql_data['data']),
$this->db->quote($sql_data['xml']),
$this->db->quote($sql_data['tags']),
$this->db->quote($sql_data['words']),
);
foreach ($this->extra_cols as $col) {
$values[] = $this->db->quote($sql_data[$col]);
}
$line = '(' . join(',', $values) . ')';
}
if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
- $extra_cols = $this->extra_cols ? ', ' . join(', ', $this->extra_cols) : '';
+ $extra_cols = '';
+ if ($this->extra_cols) {
+ $extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols);
+ $extra_cols = ', ' . join(', ', $extra_cols);
+ }
+
$result = $this->db->query(
- "INSERT INTO $this->cache_table ".
- " (folder_id, msguid, uid, created, changed, data, xml, tags, words $extra_cols)".
+ "INSERT INTO `{$this->cache_table}` ".
+ " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)".
" VALUES $buffer"
);
+
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "Failed to write to kolab cache"
), true);
}
$buffer = '';
}
$buffer .= ($buffer ? ',' : '') . $line;
}
/**
* Returns max_allowed_packet from mysql config
*/
protected function max_sql_packet()
{
if (!$this->max_sql_packet) {
// mysql limit or max 4 MB
$value = $this->db->get_variable('max_allowed_packet', 1048500);
$this->max_sql_packet = min($value, 4*1024*1024) - 2000;
}
return $this->max_sql_packet;
}
/**
* Read this folder's ID and cache metadata
*/
protected function _read_folder_data()
{
// already done
- if (!empty($this->folder_id))
+ if (!empty($this->folder_id) || !$this->ready)
return;
- $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT folder_id, synclock, ctag FROM $this->folders_table WHERE resource=?", $this->resource_uri));
+ $sql_arr = $this->db->fetch_assoc($this->db->query(
+ "SELECT `folder_id`, `synclock`, `ctag`"
+ . " FROM `{$this->folders_table}` WHERE `resource` = ?",
+ $this->resource_uri
+ ));
+
if ($sql_arr) {
$this->metadata = $sql_arr;
$this->folder_id = $sql_arr['folder_id'];
}
else {
- $this->db->query("INSERT INTO $this->folders_table (resource, type) VALUES (?, ?)", $this->resource_uri, $this->folder->type);
+ $this->db->query("INSERT INTO `{$this->folders_table}` (`resource`, `type`)"
+ . " VALUES (?, ?)", $this->resource_uri, $this->folder->type);
+
$this->folder_id = $this->db->insert_id('kolab_folders');
$this->metadata = array();
}
}
/**
* Check lock record for this folder and wait if locked or set lock
*/
protected function _sync_lock()
{
if (!$this->ready)
return;
$this->_read_folder_data();
- $sql_query = "SELECT synclock, ctag FROM $this->folders_table WHERE folder_id=?";
+ $sql_query = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?";
// abort if database is not set-up
if ($this->db->is_error()) {
$this->ready = false;
return;
}
$this->synclock = true;
// wait if locked (expire locks after 10 minutes)
while ($this->metadata && intval($this->metadata['synclock']) > 0 && $this->metadata['synclock'] + $this->max_sync_lock_time > time()) {
usleep(500000);
$this->metadata = $this->db->fetch_assoc($this->db->query($sql_query, $this->folder_id));
}
// set lock
- $this->db->query("UPDATE $this->folders_table SET synclock = ? WHERE folder_id = ?", time(), $this->folder_id);
+ $this->db->query("UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ?", time(), $this->folder_id);
}
/**
* Remove lock for this folder
*/
public function _sync_unlock()
{
if (!$this->ready || !$this->synclock)
return;
$this->db->query(
- "UPDATE $this->folders_table SET synclock = 0, ctag = ? WHERE folder_id = ?",
+ "UPDATE `{$this->folders_table}` SET `synclock` = 0, `ctag` = ? WHERE `folder_id` = ?",
$this->metadata['ctag'],
$this->folder_id
);
$this->synclock = false;
}
/**
* Resolve an object UID into an IMAP message UID
*
* @param string Kolab object UID
* @param boolean Include deleted objects
* @return int The resolved IMAP message UID
*/
public function uid2msguid($uid, $deleted = false)
{
+ // query local database if available
+ if (!isset($this->uid2msg[$uid]) && $this->ready) {
+ $this->_read_folder_data();
+
+ $sql_result = $this->db->query(
+ "SELECT `msguid` FROM `{$this->cache_table}` ".
+ "WHERE `folder_id` = ? AND `uid` = ? ORDER BY `msguid` DESC",
+ $this->folder_id,
+ $uid
+ );
+
+ if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $this->uid2msg[$uid] = $sql_arr['msguid'];
+ }
+ }
+
if (!isset($this->uid2msg[$uid])) {
// use IMAP SEARCH to get the right message
$index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') .
'HEADER SUBJECT ' . rcube_imap_generic::escape($uid));
$results = $index->get();
- $this->uid2msg[$uid] = $results[0];
+ $this->uid2msg[$uid] = end($results);
}
return $this->uid2msg[$uid];
}
/**
* Getter for protected member variables
*/
public function __get($name)
{
if ($name == 'folder_id') {
$this->_read_folder_data();
}
return $this->$name;
}
/**
* Bypass Roundcube messages cache.
* Roundcube cache duplicates information already stored in kolab_cache.
*
* @param bool $disable True disables, False enables messages cache
*/
public function bypass($disable = false)
{
// if kolab cache is disabled do nothing
if (!$this->enabled) {
return;
}
static $messages_cache, $cache_bypass;
if ($messages_cache === null) {
$rcmail = rcube::get_instance();
$messages_cache = (bool) $rcmail->config->get('messages_cache');
$cache_bypass = (int) $rcmail->config->get('kolab_messages_cache_bypass');
}
if ($messages_cache) {
// handle recurrent (multilevel) bypass() calls
if ($disable) {
$this->cache_bypassed += 1;
if ($this->cache_bypassed > 1) {
return;
}
}
else {
$this->cache_bypassed -= 1;
if ($this->cache_bypassed > 0) {
return;
}
}
switch ($cache_bypass) {
case 2:
// Disable messages cache completely
$this->imap->set_messages_caching(!$disable);
break;
case 1:
// We'll disable messages cache, but keep index cache.
// Default mode is both (MODE_INDEX | MODE_MESSAGE)
$mode = rcube_imap_cache::MODE_INDEX;
if (!$disable) {
$mode |= rcube_imap_cache::MODE_MESSAGE;
}
$this->imap->set_messages_caching(true, $mode);
}
}
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_configuration.php
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
similarity index 65%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_contact.php
rename to 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
@@ -1,45 +1,59 @@
<?php
/**
* Kolab storage cache class for contact objects
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_cache_contact extends kolab_storage_cache
{
- protected $extra_cols = array('type');
+ protected $extra_cols = array('type','name','firstname','surname','email');
protected $binary_items = array(
'photo' => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
'pgppublickey' => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></key>|i',
'pkcs7publickey' => '|<key><uri>date:application/pkcs7-mime;base64,([^<]+)</uri></key>|i',
);
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*
* @override
*/
protected function _serialize($object)
{
$sql_data = parent::_serialize($object);
$sql_data['type'] = $object['_type'];
+ // columns for sorting
+ $sql_data['name'] = rcube_charset::clean($object['name'] . $object['prefix']);
+ $sql_data['firstname'] = rcube_charset::clean($object['firstname'] . $object['middlename'] . $object['surname']);
+ $sql_data['surname'] = rcube_charset::clean($object['surname'] . $object['firstname'] . $object['middlename']);
+ $sql_data['email'] = rcube_charset::clean(is_array($object['email']) ? $object['email'][0] : $object['email']);
+
+ if (is_array($sql_data['email'])) {
+ $sql_data['email'] = $sql_data['email']['address'];
+ }
+ // avoid value being null
+ if (empty($sql_data['email'])) {
+ $sql_data['email'] = '';
+ }
+
return $sql_data;
}
}
\ No newline at end of file
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php
similarity index 75%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_event.php
rename to 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
@@ -1,49 +1,49 @@
<?php
/**
* Kolab storage cache class for calendar event objects
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_cache_event extends kolab_storage_cache
{
protected $extra_cols = array('dtstart','dtend');
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*
* @override
*/
protected function _serialize($object)
{
$sql_data = parent::_serialize($object);
- // database runs in server's timezone so using date() is what we want
- $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
- $sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['end']) ? $object['end']->format('U') : $object['end']);
+ $sql_data['dtstart'] = is_object($object['start']) ? $object['start']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['start']);
+ $sql_data['dtend'] = is_object($object['end']) ? $object['end']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['end']);
// extend date range for recurring events
if ($object['recurrence'] && $object['_formatobj']) {
$recurrence = new kolab_date_recurrence($object['_formatobj']);
- $sql_data['dtend'] = date('Y-m-d 23:59:59', $recurrence->end() ?: strtotime('now +10 years'));
+ $dtend = $recurrence->end() ?: new DateTime('now +10 years');
+ $sql_data['dtend'] = $dtend->format(self::DB_DATE_FORMAT);
}
return $sql_data;
}
}
\ No newline at end of file
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache_file.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_cache_file.php
similarity index 100%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_file.php
rename to 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
similarity index 100%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_freebusy.php
rename to 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
similarity index 100%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_journal.php
rename to 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
similarity index 100%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_mongodb.php
rename to 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
similarity index 100%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_note.php
rename to 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
similarity index 80%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage_cache_task.php
rename to 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
@@ -1,44 +1,44 @@
<?php
/**
* Kolab storage cache class for task objects
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_cache_task extends kolab_storage_cache
{
protected $extra_cols = array('dtstart','dtend');
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*
* @override
*/
protected function _serialize($object)
{
$sql_data = parent::_serialize($object) + array('dtstart' => null, 'dtend' => null);
if ($object['start'])
- $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
+ $sql_data['dtstart'] = is_object($object['start']) ? $object['start']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['start']);
if ($object['due'])
- $sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['due']) ? $object['due']->format('U') : $object['due']);
+ $sql_data['dtend'] = is_object($object['due']) ? $object['due']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['due']);
return $sql_data;
}
}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_config.php
new file mode 100644
index 0000000..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
similarity index 88%
rename from lib/kolab/plugins/libkolab/lib/kolab_storage_folder.php
rename to 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
@@ -1,1191 +1,1077 @@
<?php
/**
* The kolab_storage_folder class represents an IMAP folder on the Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-class kolab_storage_folder
+class kolab_storage_folder extends kolab_storage_folder_api
{
- /**
- * The folder name.
- * @var string
- */
- public $name;
-
- /**
- * The type of this folder.
- * @var string
- */
- public $type;
-
- /**
- * Is this folder set to be the default for its type
- * @var boolean
- */
- public $default = false;
-
/**
* The kolab_storage_cache instance for caching operations
* @var object
*/
public $cache;
private $type_annotation;
- private $namespace;
- private $imap;
- private $info;
- private $idata;
- private $owner;
private $resource_uri;
/**
* Default constructor
*/
function __construct($name, $type = null)
{
- $this->imap = rcube::get_instance()->get_storage();
+ parent::__construct($name);
$this->imap->set_options(array('skip_deleted' => true));
$this->set_folder($name, $type);
}
/**
* Set the IMAP folder this instance connects to
*
* @param string The folder name/path
* @param string Optional folder type if known
*/
public function set_folder($name, $type = null)
{
$this->type_annotation = $type ? $type : kolab_storage::folder_type($name);
$oldtype = $this->type;
list($this->type, $suffix) = explode('.', $this->type_annotation);
$this->default = $suffix == 'default';
+ $this->subtype = $this->default ? '' : $suffix;
$this->name = $name;
- $this->resource_uri = null;
+ $this->id = kolab_storage::folder_id($name);
+
+ // reset cached object properties
+ $this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null;
// get a new cache instance of folder type changed
if (!$this->cache || $type != $oldtype)
$this->cache = kolab_storage_cache::factory($this);
$this->imap->set_folder($this->name);
$this->cache->set_folder($this);
}
- /**
- *
- */
- public function get_folder_info()
- {
- if (!isset($this->info))
- $this->info = $this->imap->folder_info($this->name);
-
- return $this->info;
- }
-
- /**
- * Make IMAP folder data available for this folder
- */
- public function get_imap_data()
- {
- if (!isset($this->idata))
- $this->idata = $this->imap->folder_data($this->name);
-
- return $this->idata;
- }
-
- /**
- * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
- *
- * @param array List of metadata keys to read
- * @return array Metadata entry-value hash array on success, NULL on error
- */
- public function get_metadata($keys)
- {
- $metadata = $this->imap->get_metadata($this->name, (array)$keys);
- return $metadata[$this->name];
- }
-
-
- /**
- * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
- *
- * @param array $entries Entry-value array (use NULL value as NIL)
- * @return boolean True on success, False on failure
- */
- public function set_metadata($entries)
- {
- return $this->imap->set_metadata($this->name, $entries);
- }
-
-
- /**
- * Returns the owner of the folder.
- *
- * @return string The owner of this folder.
- */
- public function get_owner()
- {
- // return cached value
- if (isset($this->owner))
- return $this->owner;
-
- $info = $this->get_folder_info();
- $rcmail = rcube::get_instance();
-
- switch ($info['namespace']) {
- case 'personal':
- $this->owner = $rcmail->get_user_name();
- break;
-
- case 'shared':
- $this->owner = 'anonymous';
- break;
-
- default:
- list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
- if (strpos($user, '@') === false) {
- $domain = strstr($rcmail->get_user_name(), '@');
- if (!empty($domain))
- $user .= $domain;
- }
- $this->owner = $user;
- break;
- }
-
- return $this->owner;
- }
-
-
- /**
- * Getter for the name of the namespace to which the IMAP folder belongs
- *
- * @return string Name of the namespace (personal, other, shared)
- */
- public function get_namespace()
- {
- if (!isset($this->namespace))
- $this->namespace = $this->imap->folder_namespace($this->name);
- return $this->namespace;
- }
-
-
- /**
- * Get IMAP ACL information for this folder
- *
- * @return string Permissions as string
- */
- public function get_myrights()
- {
- $rights = $this->info['rights'];
-
- if (!is_array($rights))
- $rights = $this->imap->my_rights($this->name);
-
- return join('', (array)$rights);
- }
-
-
- /**
- * Get the display name value of this folder
- *
- * @return string Folder name
- */
- public function get_name()
- {
- return kolab_storage::object_name($this->name, $this->namespace);
- }
-
-
- /**
- * Get the color value stored in metadata
- *
- * @param string Default color value to return if not set
- * @return mixed Color value from IMAP metadata or $default is not set
- */
- public function get_color($default = null)
- {
- // color is defined in folder METADATA
- $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
- if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
- return $color;
- }
-
- return $default;
- }
-
-
/**
* Compose a unique resource URI for this IMAP folder
*/
public function get_resource_uri()
{
if (!empty($this->resource_uri))
return $this->resource_uri;
// strip namespace prefix from folder name
$ns = $this->get_namespace();
$nsdata = $this->imap->get_namespace($ns);
if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) {
$subpath = substr($this->name, strlen($nsdata[0][0]));
if ($ns == 'other') {
list($user, $suffix) = explode($nsdata[0][1], $subpath, 2);
$subpath = $suffix;
}
}
else {
$subpath = $this->name;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = 'imap://' . urlencode($this->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
return $this->resource_uri;
}
/**
* Helper method to extract folder UID metadata
*
* @return string Folder's UID
*/
public function get_uid()
{
// UID is defined in folder METADATA
$metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_CYRUS);
$metadata = $this->get_metadata($metakeys);
foreach ($metakeys as $key) {
if (($uid = $metadata[$key])) {
return $uid;
}
}
// generate a folder UID and set it to IMAP
- $uid = rtrim(chunk_split(md5($this->name . $this->get_owner()), 12, '-'), '-');
- $this->set_uid($uid);
+ $uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-');
+ if ($this->set_uid($uid)) {
+ return $uid;
+ }
- return $uid;
+ // create hash from folder name if we can't write the UID metadata
+ return md5($this->name . $this->get_owner());
}
/**
* Helper method to set an UID value to the given IMAP folder instance
*
* @param string Folder's UID
* @return boolean True on succes, False on failure
*/
public function set_uid($uid)
{
if (!($success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid)))) {
$success = $this->set_metadata(array(kolab_storage::UID_KEY_PRIVATE => $uid));
}
return $success;
}
/**
* Compose a folder Etag identifier
*/
public function get_ctag()
{
$fdata = $this->get_imap_data();
return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']);
}
/**
* Check activation status of this folder
*
* @return boolean True if enabled, false if not
*/
public function is_active()
{
return kolab_storage::folder_is_active($this->name);
}
/**
* Change activation status of this folder
*
* @param boolean The desired subscription status: true = active, false = not active
*
* @return True on success, false on error
*/
public function activate($active)
{
return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
}
/**
* Check subscription status of this folder
*
* @return boolean True if subscribed, false if not
*/
public function is_subscribed()
{
return kolab_storage::folder_is_subscribed($this->name);
}
/**
* Change subscription status of this folder
*
* @param boolean The desired subscription status: true = subscribed, false = not subscribed
*
* @return True on success, false on error
*/
public function subscribe($subscribed)
{
return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
}
/**
* Get number of objects stored in this folder
*
* @param mixed Pseudo-SQL query as list of filter parameter triplets
* or string with object type (e.g. contact, event, todo, journal, note, configuration)
* @return integer The number of objects of the given type
* @see self::select()
*/
public function count($query = null)
{
// synchronize cache first
$this->cache->synchronize();
return $this->cache->count($this->_prepare_query($query));
}
/**
* List all Kolab objects of the given type
*
* @param string $type Object type (e.g. contact, event, todo, journal, note, configuration)
* @return array List of Kolab data objects (each represented as hash array)
*/
public function get_objects($type = null)
{
if (!$type) $type = $this->type;
// synchronize caches
$this->cache->synchronize();
// fetch objects from cache
return $this->cache->select($this->_prepare_query($type));
}
/**
* Select *some* Kolab objects matching the given query
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* triplet: array('<colname>', '<comparator>', '<value>')
* @return array List of Kolab data objects (each represented as hash array)
*/
public function select($query = array())
{
// check query argument
if (empty($query))
return $this->get_objects();
// synchronize caches
$this->cache->synchronize();
// fetch objects from cache
return $this->cache->select($this->_prepare_query($query));
}
/**
* Getter for object UIDs only
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @return array List of Kolab object UIDs
*/
public function get_uids($query = array())
{
// synchronize caches
$this->cache->synchronize();
// fetch UIDs from cache
return $this->cache->select($this->_prepare_query($query), true);
}
+ /**
+ * Setter for ORDER BY and LIMIT parameters for cache queries
+ *
+ * @param array List of columns to order by
+ * @param integer Limit result set to this length
+ * @param integer Offset row
+ */
+ public function set_order_and_limit($sortcols, $length = null, $offset = 0)
+ {
+ $this->cache->set_order_by($sortcols);
+
+ if ($length !== null) {
+ $this->cache->set_limit($length, $offset);
+ }
+ }
/**
* Helper method to sanitize query arguments
*/
private function _prepare_query($query)
{
// string equals type query
// FIXME: should not be called this way!
if (is_string($query)) {
return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array();
}
foreach ((array)$query as $i => $param) {
if ($param[0] == 'type' && !$this->cache->has_type_col()) {
unset($query[$i]);
}
else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) {
if (is_object($param[2]) && is_a($param[2], 'DateTime'))
$param[2] = $param[2]->format('U');
if (is_numeric($param[2]))
$query[$i][2] = date('Y-m-d H:i:s', $param[2]);
}
}
return $query;
}
/**
* Getter for a single Kolab object, identified by its UID
*
* @param string $uid Object UID
* @param string $type Object type (e.g. contact, event, todo, journal, note, configuration)
* Defaults to folder type
*
* @return array The Kolab object represented as hash array
*/
public function get_object($uid, $type = null)
{
// synchronize caches
$this->cache->synchronize();
$msguid = $this->cache->uid2msguid($uid);
if ($msguid && ($object = $this->cache->get($msguid, $type))) {
return $object;
}
return false;
}
/**
* Fetch a Kolab object attachment which is stored in a separate part
* of the mail MIME message that represents the Kolab record.
*
* @param string Object's UID
* @param string The attachment's mime number
* @param string IMAP folder where message is stored;
* If set, that also implies that the given UID is an IMAP UID
* @param bool True to print the part content
* @param resource File pointer to save the message part
* @param boolean Disables charset conversion
*
* @return mixed The attachment content as binary string
*/
public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false)
{
if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
$this->imap->set_folder($mailbox ? $mailbox : $this->name);
- return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
+
+ if (substr($part, 0, 2) == 'i:') {
+ // attachment data is stored in XML
+ if ($object = $this->cache->get($msguid)) {
+ // load data from XML (attachment content is not stored in cache)
+ if ($object['_formatobj'] && isset($object['_size'])) {
+ $object['_attachments'] = array();
+ $object['_formatobj']->get_attachments($object);
+ }
+
+ foreach ($object['_attachments'] as $k => $attach) {
+ if ($attach['id'] == $part) {
+ if ($print) echo $attach['content'];
+ else if ($fp) fwrite($fp, $attach['content']);
+ else return $attach['content'];
+ return true;
+ }
+ }
+ }
+ }
+ else {
+ // return message part from IMAP directly
+ return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
+ }
}
return null;
}
/**
* Fetch the mime message from the storage server and extract
* the Kolab groupware object from it
*
* @param string The IMAP message UID to fetch
* @param string The object type expected (use wildcard '*' to accept all types)
* @param string The folder name where the message is stored
*
* @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
*/
public function read_object($msguid, $type = null, $folder = null)
{
if (!$type) $type = $this->type;
if (!$folder) $folder = $this->name;
$this->imap->set_folder($folder);
$this->cache->bypass(true);
$message = new rcube_message($msguid);
$this->cache->bypass(false);
// Message doesn't exist?
if (empty($message->headers)) {
return false;
}
// extract the X-Kolab-Type header from the XML attachment part if missing
if (empty($message->headers->others['x-kolab-type'])) {
foreach ((array)$message->attachments as $part) {
if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
$message->headers->others['x-kolab-type'] = $part->mimetype;
break;
}
}
}
// fix buggy messages stating the X-Kolab-Type header twice
else if (is_array($message->headers->others['x-kolab-type'])) {
$message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']);
}
// no object type header found: abort
if (empty($message->headers->others['x-kolab-type'])) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "No X-Kolab-Type information found in message $msguid ($this->name).",
), true);
return false;
}
$object_type = kolab_format::mime2object_type($message->headers->others['x-kolab-type']);
$content_type = kolab_format::KTYPE_PREFIX . $object_type;
// check object type header and abort on mismatch
if ($type != '*' && $object_type != $type)
return false;
$attachments = array();
// get XML part
foreach ((array)$message->attachments as $part) {
- if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
+ if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!', $part->mimetype))) {
$xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
}
else if ($part->filename || $part->content_id) {
$key = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
$size = null;
// Use Content-Disposition 'size' as for the Kolab Format spec.
if (isset($part->d_parameters['size'])) {
$size = $part->d_parameters['size'];
}
// we can trust part size only if it's not encoded
else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') {
$size = $part->size;
}
$attachments[$key] = array(
'id' => $part->mime_id,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'size' => $size,
);
}
}
if (!$xml) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Could not find Kolab data part in message $msguid ($this->name).",
), true);
return false;
}
// check kolab format version
$format_version = $message->headers->others['x-kolab-mime-version'];
if (empty($format_version)) {
list($xmltype, $subtype) = explode('.', $object_type);
$xmlhead = substr($xml, 0, 512);
// detect old Kolab 2.0 format
if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false)
$format_version = '2.0';
else
$format_version = '3.0'; // assume 3.0
}
// get Kolab format handler for the given type
$format = kolab_format::factory($object_type, $format_version);
if (is_a($format, 'PEAR_Error'))
return false;
// load Kolab object from XML part
$format->load($xml);
if ($format->is_valid()) {
$object = $format->to_array(array('_attachments' => $attachments));
$object['_type'] = $object_type;
$object['_msguid'] = $msguid;
$object['_mailbox'] = $this->name;
$object['_formatobj'] = $format;
return $object;
}
else {
// try to extract object UID from XML block
if (preg_match('!<uid>(.+)</uid>!Uims', $xml, $m))
$msgadd = " UID = " . trim(strip_tags($m[1]));
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd,
), true);
}
return false;
}
/**
* Save an object in this folder.
*
* @param array $object The array that holds the data of the object.
* @param string $type The type of the kolab object.
* @param string $uid The UID of the old object if it existed before
* @return boolean True on success, false on error
*/
public function save(&$object, $type = null, $uid = null)
{
if (!$type)
$type = $this->type;
// copy attachments from old message
if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) {
foreach ((array)$old['_attachments'] as $key => $att) {
if (!isset($object['_attachments'][$key])) {
$object['_attachments'][$key] = $old['_attachments'][$key];
}
// unset deleted attachment entries
if ($object['_attachments'][$key] == false) {
unset($object['_attachments'][$key]);
}
// load photo.attachment from old Kolab2 format to be directly embedded in xcard block
else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
if (!isset($object['photo']))
$object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']);
unset($object['_attachments'][$key]);
}
}
}
// save contact photo to attachment for Kolab2 format
if (kolab_storage::$version == '2.0' && $object['photo']) {
$attkey = 'kolab-picture.png'; // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp
$object['_attachments'][$attkey] = array(
'mimetype'=> rcube_mime::image_content_type($object['photo']),
'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']),
);
}
// process attachments
if (is_array($object['_attachments'])) {
$numatt = count($object['_attachments']);
foreach ($object['_attachments'] as $key => $attachment) {
// FIXME: kolab_storage and Roundcube attachment hooks use different fields!
if (empty($attachment['content']) && !empty($attachment['data'])) {
$attachment['content'] = $attachment['data'];
unset($attachment['data'], $object['_attachments'][$key]['data']);
}
// make sure size is set, so object saved in cache contains this info
if (!isset($attachment['size'])) {
if (!empty($attachment['content'])) {
if (is_resource($attachment['content'])) {
// this need to be a seekable resource, otherwise
// fstat() failes and we're unable to determine size
// here nor in rcube_imap_generic before IMAP APPEND
$stat = fstat($attachment['content']);
$attachment['size'] = $stat ? $stat['size'] : 0;
}
else {
$attachment['size'] = strlen($attachment['content']);
}
}
else if (!empty($attachment['path'])) {
$attachment['size'] = filesize($attachment['path']);
}
$object['_attachments'][$key] = $attachment;
}
// generate unique keys (used as content-id) for attachments
if (is_numeric($key) && $key < $numatt) {
// derrive content-id from attachment file name
$ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null;
$basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext)); // to 7bit ascii
if (!$basename) $basename = 'noname';
$cid = $basename . '.' . microtime(true) . $ext;
$object['_attachments'][$cid] = $attachment;
unset($object['_attachments'][$key]);
}
}
}
// save recurrence exceptions as individual objects due to lack of support in Kolab v2 format
if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) {
$this->save_recurrence_exceptions($object, $type);
}
// check IMAP BINARY extension support for 'file' objects
// allow configuration to workaround bug in Cyrus < 2.4.17
$rcmail = rcube::get_instance();
$binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY');
// generate and save object message
if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) {
// resolve old msguid before saving
if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) {
$object['_msguid'] = $msguid;
$object['_mailbox'] = $this->name;
}
$result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);
- // delete old message
- if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
- $this->cache->bypass(true);
- $this->imap->delete_message($object['_msguid'], $object['_mailbox']);
- $this->cache->bypass(false);
- $this->cache->set($object['_msguid'], false, $object['_mailbox']);
- }
-
// update cache with new UID
if ($result) {
+ $old_uid = $object['_msguid'];
+
$object['_msguid'] = $result;
- $this->cache->insert($result, $object);
+ $object['_mailbox'] = $this->name;
- // remove temp file
- if ($body_file) {
- @unlink($body_file);
+ if ($old_uid) {
+ // delete old message
+ $this->cache->bypass(true);
+ $this->imap->delete_message($old_uid, $object['_mailbox']);
+ $this->cache->bypass(false);
}
+
+ // insert/update message in cache
+ $this->cache->save($result, $object, $old_uid);
+ }
+
+ // remove temp file
+ if ($body_file) {
+ @unlink($body_file);
}
}
return $result;
}
/**
* Save recurrence exceptions as individual objects.
* The Kolab v2 format doesn't allow us to save fully embedded exception objects.
*
* @param array Hash array with event properties
* @param string Object type
*/
private function save_recurrence_exceptions(&$object, $type = null)
{
if ($object['recurrence']['EXCEPTIONS']) {
$exdates = array();
foreach ((array)$object['recurrence']['EXDATE'] as $exdate) {
$key = is_a($exdate, 'DateTime') ? $exdate->format('Y-m-d') : strval($exdate);
$exdates[$key] = 1;
}
// save every exception as individual object
foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
$exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd'));
$exception['sequence'] = $object['sequence'] + 1;
if ($exception['thisandfuture']) {
$exception['recurrence'] = $object['recurrence'];
// adjust the recurrence duration of the exception
if ($object['recurrence']['COUNT']) {
$recurrence = new kolab_date_recurrence($object['_formatobj']);
if ($end = $recurrence->end()) {
unset($exception['recurrence']['COUNT']);
- $exception['recurrence']['UNTIL'] = new DateTime('@'.$end);
+ $exception['recurrence']['UNTIL'] = $end;
}
}
// set UNTIL date if we have a thisandfuture exception
$untildate = clone $exception['start'];
$untildate->sub(new DateInterval('P1D'));
$object['recurrence']['UNTIL'] = $untildate;
unset($object['recurrence']['COUNT']);
}
else {
if (!$exdates[$exception['start']->format('Y-m-d')])
$object['recurrence']['EXDATE'][] = clone $exception['start'];
unset($exception['recurrence']);
}
unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']);
$this->save($exception, $type, $exception['uid']);
}
unset($object['recurrence']['EXCEPTIONS']);
}
}
/**
* Generate an object UID with the given recurrence-ID in a way that it is
* unique (the original UID is not a substring) but still recoverable.
*/
private static function recurrence_exception_uid($uid, $recurrence_id)
{
$offset = -2;
return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset);
}
/**
* Delete the specified object from this folder.
*
* @param mixed $object The Kolab object to delete or object UID
* @param boolean $expunge Should the folder be expunged?
*
* @return boolean True if successful, false on error
*/
public function delete($object, $expunge = true)
{
$msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
$success = false;
$this->cache->bypass(true);
if ($msguid && $expunge) {
$success = $this->imap->delete_message($msguid, $this->name);
}
else if ($msguid) {
$success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
}
$this->cache->bypass(false);
if ($success) {
$this->cache->set($msguid, false);
}
return $success;
}
/**
*
*/
public function delete_all()
{
$this->cache->purge();
$this->cache->bypass(true);
$result = $this->imap->clear_folder($this->name);
$this->cache->bypass(false);
return $result;
}
/**
* Restore a previously deleted object
*
* @param string Object UID
* @return mixed Message UID on success, false on error
*/
public function undelete($uid)
{
if ($msguid = $this->cache->uid2msguid($uid, true)) {
$this->cache->bypass(true);
$result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
$this->cache->bypass(false);
if ($result) {
return $msguid;
}
}
return false;
}
/**
* Move a Kolab object message to another IMAP folder
*
* @param string Object UID
* @param string IMAP folder to move object to
* @return boolean True on success, false on failure
*/
public function move($uid, $target_folder)
{
+ if (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) {
$this->cache->move($msguid, $uid, $target_folder);
return true;
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(),
), true);
}
}
return false;
}
/**
* Creates source of the configuration object message
*
* @param array $object The array that holds the data of the object.
* @param string $type The type of the kolab object.
* @param bool $binary Enables use of binary encoding of attachment(s)
* @param string $body_file Reference to filename of message body
*
* @return mixed Message as string or array with two elements
* (one for message file path, second for message headers)
*/
private function build_message(&$object, $type, $binary, &$body_file)
{
// load old object to preserve data we don't understand/process
if (is_object($object['_formatobj']))
$format = $object['_formatobj'];
else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox'])))
$format = $old['_formatobj'];
// create new kolab_format instance
if (!$format)
$format = kolab_format::factory($type, kolab_storage::$version);
if (PEAR::isError($format))
return false;
$format->set($object);
$xml = $format->write(kolab_storage::$version);
$object['uid'] = $format->uid; // read UID from format
$object['_formatobj'] = $format;
if (empty($xml) || !$format->is_valid() || empty($object['uid'])) {
return false;
}
$mime = new Mail_mime("\r\n");
$rcmail = rcube::get_instance();
$headers = array();
$files = array();
$part_id = 1;
$encoding = $binary ? 'binary' : 'base64';
if ($user_email = $rcmail->get_user_email()) {
$headers['From'] = $user_email;
$headers['To'] = $user_email;
}
$headers['Date'] = date('r');
$headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type;
$headers['X-Kolab-Mime-Version'] = kolab_storage::$version;
$headers['Subject'] = $object['uid'];
// $headers['Message-ID'] = $rcmail->gen_message_id();
$headers['User-Agent'] = $rcmail->config->get('useragent');
// Check if we have enough memory to handle the message in it
// It's faster than using files, so we'll do this if we only can
if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) {
$memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
foreach ($object['_attachments'] as $id => $attachment) {
$memory += $attachment['size'];
}
// 1.33 is for base64, we need at least 4x more memory than the message size
if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) {
$marker = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%';
$is_file = true;
$temp_dir = unslashify($rcmail->config->get('temp_dir'));
$mime->setParam('delay_file_io', true);
}
}
$mime->headers($headers);
$mime->setTXTBody("This is a Kolab Groupware object. "
. "To view this object you will need an email client that understands the Kolab Groupware format. "
. "For a list of such email clients please visit http://www.kolab.org/\n\n");
$ctype = kolab_storage::$version == '2.0' ? $format->CTYPEv2 : $format->CTYPE;
// Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines"
// when APPENDing from temp file
$xml = preg_replace('/\r?\n/', "\r\n", $xml);
$mime->addAttachment($xml, // file
$ctype, // content-type
'kolab.xml', // filename
false, // is_file
'8bit', // encoding
'attachment', // disposition
RCUBE_CHARSET // charset
);
$part_id++;
// save object attachments as separate parts
foreach ((array)$object['_attachments'] as $key => $att) {
if (empty($att['content']) && !empty($att['id'])) {
// @TODO: use IMAP CATENATE to skip attachment fetch+push operation
$msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
if ($is_file) {
$att['path'] = tempnam($temp_dir, 'rcmAttmnt');
if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) {
fclose($fp);
}
else {
return false;
}
}
else {
$att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, null, true);
}
}
$headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable'));
$name = !empty($att['name']) ? $att['name'] : $key;
// To store binary files we can use faster method
// without writting full message content to a temporary file but
// directly to IMAP, see rcube_imap_generic::append().
// I.e. use file handles where possible
if (!empty($att['path'])) {
if ($is_file && $binary) {
$files[] = fopen($att['path'], 'r');
$mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
else {
$mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
}
else {
if (is_resource($att['content']) && $is_file && $binary) {
$files[] = $att['content'];
$mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
else {
if (is_resource($att['content'])) {
@rewind($att['content']);
$att['content'] = stream_get_contents($att['content']);
}
$mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
}
$object['_attachments'][$key]['id'] = ++$part_id;
}
if (!$is_file || !empty($files)) {
$message = $mime->getMessage();
}
// parse message and build message array with
// attachment file pointers in place of file markers
if (!empty($files)) {
$message = explode($marker, $message);
$tmp = array();
foreach ($message as $msg_part) {
$tmp[] = $msg_part;
if ($file = array_shift($files)) {
$tmp[] = $file;
}
}
$message = $tmp;
}
// write complete message body into temp file
else if ($is_file) {
// use common temp dir
$body_file = tempnam($temp_dir, 'rcmMsg');
if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) {
self::raise_error(array('code' => 650, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$mime_result->getMessage()),
true, false);
return false;
}
$message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r'));
}
return $message;
}
/**
* Triggers any required updates after changes within the
* folder. This is currently only required for handling free/busy
* information with Kolab.
*
* @return boolean|PEAR_Error True if successfull.
*/
public function trigger()
{
$owner = $this->get_owner();
$result = false;
switch($this->type) {
case 'event':
if ($this->get_namespace() == 'personal') {
$result = $this->trigger_url(
sprintf('%s/trigger/%s/%s.pfb',
kolab_storage::get_freebusy_server(),
urlencode($owner),
urlencode($this->imap->mod_folder($this->name))
),
$this->imap->options['user'],
$this->imap->options['password']
);
}
break;
default:
return true;
}
if ($result && is_object($result) && is_a($result, 'PEAR_Error')) {
return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s",
$this->name, $result->getMessage()));
}
return $result;
}
/**
* Triggers a URL.
*
* @param string $url The URL to be triggered.
* @param string $auth_user Username to authenticate with
* @param string $auth_passwd Password for basic auth
* @return boolean|PEAR_Error True if successfull.
*/
private function trigger_url($url, $auth_user = null, $auth_passwd = null)
{
require_once('HTTP/Request2.php');
try {
$request = libkolab::http_request($url);
// set authentication credentials
if ($auth_user && $auth_passwd)
$request->setAuth($auth_user, $auth_passwd);
$result = $request->send();
// rcube::write_log('trigger', $result->getBody());
}
catch (Exception $e) {
return PEAR::raiseError($e->getMessage());
}
return true;
}
}
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php
new file mode 100644
index 0000000..ea603b1
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -0,0 +1,345 @@
+<?php
+
+/**
+ * Abstract interface class for Kolab storage IMAP folder objects
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+abstract class kolab_storage_folder_api
+{
+ /**
+ * Folder identifier
+ * @var string
+ */
+ public $id;
+
+ /**
+ * The folder name.
+ * @var string
+ */
+ public $name;
+
+ /**
+ * The type of this folder.
+ * @var string
+ */
+ public $type;
+
+ /**
+ * The subtype of this folder.
+ * @var string
+ */
+ public $subtype;
+
+ /**
+ * Is this folder set to be the default for its type
+ * @var boolean
+ */
+ public $default = false;
+
+ /**
+ * List of direct child folders
+ * @var array
+ */
+ public $children = array();
+
+ /**
+ * Name of the parent folder
+ * @var string
+ */
+ public $parent = '';
+
+ protected $imap;
+ protected $owner;
+ protected $info;
+ protected $idata;
+ protected $namespace;
+
+
+ /**
+ * Private constructor
+ */
+ protected function __construct($name)
+ {
+ $this->name = $name;
+ $this->id = kolab_storage::folder_id($name);
+ $this->imap = rcube::get_instance()->get_storage();
+ }
+
+
+ /**
+ * Returns the owner of the folder.
+ *
+ * @return string The owner of this folder.
+ */
+ public function get_owner()
+ {
+ // return cached value
+ if (isset($this->owner))
+ return $this->owner;
+
+ $info = $this->get_folder_info();
+ $rcmail = rcube::get_instance();
+
+ switch ($info['namespace']) {
+ case 'personal':
+ $this->owner = $rcmail->get_user_name();
+ break;
+
+ case 'shared':
+ $this->owner = 'anonymous';
+ break;
+
+ default:
+ list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+ if (strpos($user, '@') === false) {
+ $domain = strstr($rcmail->get_user_name(), '@');
+ if (!empty($domain))
+ $user .= $domain;
+ }
+ $this->owner = $user;
+ break;
+ }
+
+ return $this->owner;
+ }
+
+
+ /**
+ * Getter for the name of the namespace to which the IMAP folder belongs
+ *
+ * @return string Name of the namespace (personal, other, shared)
+ */
+ public function get_namespace()
+ {
+ if (!isset($this->namespace))
+ $this->namespace = $this->imap->folder_namespace($this->name);
+ return $this->namespace;
+ }
+
+
+ /**
+ * Get the display name value of this folder
+ *
+ * @return string Folder name
+ */
+ public function get_name()
+ {
+ return kolab_storage::object_name($this->name, $this->get_namespace());
+ }
+
+
+ /**
+ * Getter for the top-end folder name (not the entire path)
+ *
+ * @return string Name of this folder
+ */
+ public function get_foldername()
+ {
+ $parts = explode('/', $this->name);
+ return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+ }
+
+ /**
+ * Getter for parent folder path
+ *
+ * @return string Full path to parent folder
+ */
+ public function get_parent()
+ {
+ $path = explode('/', $this->name);
+ array_pop($path);
+
+ // don't list top-level namespace folder
+ if (count($path) == 1 && in_array($this->get_namespace(), array('other', 'shared'))) {
+ $path = array();
+ }
+
+ return join('/', $path);
+ }
+
+ /**
+ * Getter for the Cyrus mailbox identifier corresponding to this folder
+ * (e.g. user/john.doe/Calendar/Personal@example.org)
+ *
+ * @return string Mailbox ID
+ */
+ public function get_mailbox_id()
+ {
+ $info = $this->get_folder_info();
+ $owner = $this->get_owner();
+ list($user, $domain) = explode('@', $owner);
+
+ switch ($info['namespace']) {
+ case 'personal':
+ return sprintf('user/%s/%s@%s', $user, $this->name, $domain);
+
+ case 'shared':
+ $ns = $this->imap->get_namespace('shared');
+ $prefix = is_array($ns) ? $ns[0][0] : '';
+ list(, $domain) = explode('@', rcube::get_instance()->get_user_name());
+ return substr($this->name, strlen($prefix)) . '@' . $domain;
+
+ default:
+ $ns = $this->imap->get_namespace('other');
+ $prefix = is_array($ns) ? $ns[0][0] : '';
+ list($user, $folder) = explode($this->imap->get_hierarchy_delimiter(), substr($info['name'], strlen($prefix)), 2);
+ if (strpos($user, '@')) {
+ list($user, $domain) = explode('@', $user);
+ }
+ return sprintf('user/%s/%s@%s', $user, $folder, $domain);
+ }
+ }
+
+ /**
+ * Get the color value stored in metadata
+ *
+ * @param string Default color value to return if not set
+ * @return mixed Color value from IMAP metadata or $default is not set
+ */
+ public function get_color($default = null)
+ {
+ // color is defined in folder METADATA
+ $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
+ if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
+ return $color;
+ }
+
+ return $default;
+ }
+
+
+ /**
+ * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
+ *
+ * @param array List of metadata keys to read
+ * @return array Metadata entry-value hash array on success, NULL on error
+ */
+ public function get_metadata($keys)
+ {
+ $metadata = rcube::get_instance()->get_storage()->get_metadata($this->name, (array)$keys);
+ return $metadata[$this->name];
+ }
+
+
+ /**
+ * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
+ *
+ * @param array $entries Entry-value array (use NULL value as NIL)
+ * @return boolean True on success, False on failure
+ */
+ public function set_metadata($entries)
+ {
+ return $this->imap->set_metadata($this->name, $entries);
+ }
+
+
+ /**
+ *
+ */
+ public function get_folder_info()
+ {
+ if (!isset($this->info))
+ $this->info = $this->imap->folder_info($this->name);
+
+ return $this->info;
+ }
+
+ /**
+ * Make IMAP folder data available for this folder
+ */
+ public function get_imap_data()
+ {
+ if (!isset($this->idata))
+ $this->idata = $this->imap->folder_data($this->name);
+
+ return $this->idata;
+ }
+
+
+ /**
+ * Get IMAP ACL information for this folder
+ *
+ * @return string Permissions as string
+ */
+ public function get_myrights()
+ {
+ $rights = $this->info['rights'];
+
+ if (!is_array($rights))
+ $rights = $this->imap->my_rights($this->name);
+
+ return join('', (array)$rights);
+ }
+
+
+ /**
+ * Check activation status of this folder
+ *
+ * @return boolean True if enabled, false if not
+ */
+ public function is_active()
+ {
+ return kolab_storage::folder_is_active($this->name);
+ }
+
+ /**
+ * Change activation status of this folder
+ *
+ * @param boolean The desired subscription status: true = active, false = not active
+ *
+ * @return True on success, false on error
+ */
+ public function activate($active)
+ {
+ return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
+ }
+
+ /**
+ * Check subscription status of this folder
+ *
+ * @return boolean True if subscribed, false if not
+ */
+ public function is_subscribed()
+ {
+ return kolab_storage::folder_is_subscribed($this->name);
+ }
+
+ /**
+ * Change subscription status of this folder
+ *
+ * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+ *
+ * @return True on success, false on error
+ */
+ public function subscribe($subscribed)
+ {
+ return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
+ }
+
+ /**
+ * Return folder name as string representation of this object
+ *
+ * @return string Full IMAP folder name
+ */
+ public function __toString()
+ {
+ return $this->name;
+ }
+}
+
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_user.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_user.php
new file mode 100644
index 0000000..7c141c5
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_user.php
@@ -0,0 +1,135 @@
+<?php
+
+/**
+ * Class that represents a (virtual) folder in the 'other' namespace
+ * implementing a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class kolab_storage_folder_user extends kolab_storage_folder_virtual
+{
+ protected static $ldapcache = array();
+
+ public $ldaprec;
+ public $type;
+
+ /**
+ * Default constructor
+ */
+ public function __construct($name, $parent = '', $ldaprec = null)
+ {
+ parent::__construct($name, $name, 'other', $parent);
+
+ if (!empty($ldaprec)) {
+ self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
+ }
+ // use value cached in memory for repeated lookups
+ else if (array_key_exists($name, self::$ldapcache)) {
+ $this->ldaprec = self::$ldapcache[$name];
+ }
+ // lookup user in LDAP and set $this->ldaprec
+ else if ($ldap = kolab_storage::ldap()) {
+ // get domain from current user
+ list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
+ $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
+ if (!empty($this->ldaprec)) {
+ $this->ldaprec['kolabtargetfolder'] = $name;
+ }
+ self::$ldapcache[$name] = $this->ldaprec;
+ }
+ }
+
+ /**
+ * Getter for the top-end folder name to be displayed
+ *
+ * @return string Name of this folder
+ */
+ public function get_foldername()
+ {
+ return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
+ parent::get_foldername();
+ }
+
+ /**
+ * Getter for a more informative title of this user folder
+ *
+ * @return string Title for the given user record
+ */
+ public function get_title()
+ {
+ return trim($this->ldaprec['displayname'] . '; ' . $this->ldaprec['mail'], '; ');
+ }
+
+ /**
+ * Returns the owner of the folder.
+ *
+ * @return string The owner of this folder.
+ */
+ public function get_owner()
+ {
+ return $this->ldaprec['mail'];
+ }
+
+ /**
+ * Check subscription status of this folder.
+ * Subscription of a virtual user folder depends on the subscriptions of subfolders.
+ *
+ * @return boolean True if subscribed, false if not
+ */
+ public function is_subscribed()
+ {
+ if (!empty($this->type)) {
+ $children = $subscribed = 0;
+ $delimiter = $this->imap->get_hierarchy_delimiter();
+ foreach ((array)kolab_storage::list_folders($this->name . $delimiter, '*', $this->type, false) as $subfolder) {
+ if (kolab_storage::folder_is_subscribed($subfolder)) {
+ $subscribed++;
+ }
+ $children++;
+ }
+ if ($subscribed > 0) {
+ return $subscribed == $children ? true : 2;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Change subscription status of this folder
+ *
+ * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+ *
+ * @return True on success, false on error
+ */
+ public function subscribe($subscribed)
+ {
+ $success = false;
+
+ // (un)subscribe all subfolders of a given type
+ if (!empty($this->type)) {
+ $delimiter = $this->imap->get_hierarchy_delimiter();
+ foreach ((array)kolab_storage::list_folders($this->name . $delimiter, '*', $this->type, false) as $subfolder) {
+ $success |= ($subscribed ? kolab_storage::folder_subscribe($subfolder) : kolab_storage::folder_unsubscribe($subfolder));
+ }
+ }
+
+ return $success;
+ }
+
+}
\ No newline at end of file
diff --git a/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_virtual.php b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_virtual.php
new file mode 100644
index 0000000..e419ced
--- /dev/null
+++ b/lib/drivers/kolab/plugins/libkolab/lib/kolab_storage_folder_virtual.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * Helper class that represents a virtual IMAP folder
+ * with a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class kolab_storage_folder_virtual extends kolab_storage_folder_api
+{
+ public $virtual = true;
+
+ protected $displayname;
+
+ public function __construct($name, $dispname, $ns, $parent = '')
+ {
+ parent::__construct($name);
+
+ $this->namespace = $ns;
+ $this->parent = $parent;
+ $this->displayname = $dispname;
+ }
+
+ /**
+ * Get the display name value of this folder
+ *
+ * @return string Folder name
+ */
+ public function get_name()
+ {
+ return $this->displayname ?: parent::get_name();
+ }
+
+ /**
+ * Get the color value stored in metadata
+ *
+ * @param string Default color value to return if not set
+ * @return mixed Color value from IMAP metadata or $default is not set
+ */
+ public function get_color($default = null)
+ {
+ return $default;
+ }
+}
\ No newline at end of file
diff --git a/lib/kolab/plugins/libkolab/libkolab.php b/lib/drivers/kolab/plugins/libkolab/libkolab.php
similarity index 90%
rename from lib/kolab/plugins/libkolab/libkolab.php
rename to 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
@@ -1,126 +1,138 @@
<?php
/**
* Kolab core library
*
* Plugin to setup a basic environment for the interaction with a Kolab server.
* Other Kolab-related plugins will depend on it and can use the library classes
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class libkolab extends rcube_plugin
{
static $http_requests = array();
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
// load local config
$this->load_config();
- $this->add_hook('storage_init', array($this, 'storage_init'));
-
// extend include path to load bundled lib classes
$include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
set_include_path($include_path);
+ $this->add_hook('storage_init', array($this, 'storage_init'));
+ $this->add_hook('user_delete', array('kolab_storage', 'delete_user_folders'));
+
$rcmail = rcube::get_instance();
try {
kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
}
catch (Exception $e) {
rcube::raise_error($e, true);
kolab_format::$timezone = new DateTimeZone('GMT');
}
}
/**
* Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers
*/
function storage_init($p)
{
$p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION');
return $p;
}
/**
* Wrapper function to load and initalize the HTTP_Request2 Object
*
* @param string|Net_Url2 Request URL
* @param string Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
* @param array Configuration for this Request instance, that will be merged
* with default configuration
*
* @return HTTP_Request2 Request object
*/
public static function http_request($url = '', $method = 'GET', $config = array())
{
$rcube = rcube::get_instance();
$http_config = (array) $rcube->config->get('kolab_http_request');
// deprecated configuration options
if (empty($http_config)) {
foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
$value = $rcube->config->get('kolab_' . $option, true);
if (is_bool($value)) {
$http_config[$option] = $value;
}
}
}
if (!empty($config)) {
$http_config = array_merge($http_config, $config);
}
$key = md5(serialize($http_config));
if (!($request = self::$http_requests[$key])) {
// load HTTP_Request2
require_once 'HTTP/Request2.php';
try {
$request = new HTTP_Request2();
$request->setConfig($http_config);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
// proxy User-Agent string
$request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
self::$http_requests[$key] = $request;
}
// cleanup
try {
$request->setBody('');
$request->setUrl($url);
$request->setMethod($method);
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
return $request;
}
+
+ /**
+ * Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
+ */
+ public static function html_diff($from, $to)
+ {
+ include_once __dir__ . '/vendor/finediff.php';
+
+ $diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
+ return $diff->renderDiffToHTML();
+ }
}
diff --git a/lib/kolab/plugins/libkolab/package.xml b/lib/drivers/kolab/plugins/libkolab/package.xml
similarity index 100%
rename from lib/kolab/plugins/libkolab/package.xml
rename to 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
Binary files /dev/null and b/lib/drivers/seafile/seafile.png 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
@@ -1,850 +1,637 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
-class file_api
+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();
}
/**
* Process the request and dispatch it to the requested service
*/
public function run()
{
$this->request = strtolower($_GET['method']);
// Check the session, authenticate the user
if (!$this->session_validate()) {
$this->session->destroy(session_id());
if ($this->request == 'authenticate') {
$this->session->regenerate_id(false);
if ($username = $this->authenticate()) {
$_SESSION['user'] = $username;
$_SESSION['time'] = time();
$_SESSION['config'] = $this->config;
$this->output_success(array(
'token' => session_id(),
'capabilities' => $this->capabilities(),
));
}
}
throw new Exception("Invalid session", 403);
}
// Call service method
$result = $this->request_handler($this->request);
// Send success response, errors should be handled by driver class
// by throwing exceptions or sending output by itself
$this->output_success($result);
}
/**
* Session validation check and session start
*/
private function session_validate()
{
$sess_id = rcube_utils::request_header('X-Session-Token') ?: $_REQUEST['token'];
if (empty($sess_id)) {
session_start();
return false;
}
session_id($sess_id);
session_start();
if (empty($_SESSION['user'])) {
return false;
}
$timeout = $this->conf->get('session_lifetime', 0) * 60;
if ($timeout && $_SESSION['time'] && $_SESSION['time'] < time() - $timeout) {
return false;
}
// update session time
$_SESSION['time'] = time();
return true;
}
/**
* Initializes session
*/
private function session_init()
{
$rcube = rcube::get_instance();
$sess_name = $this->conf->get('session_name');
$lifetime = $this->conf->get('session_lifetime', 0) * 60;
if ($lifetime) {
ini_set('session.gc_maxlifetime', $lifetime * 2);
}
ini_set('session.name', $sess_name ? $sess_name : 'file_api_sessid');
ini_set('session.use_cookies', 0);
ini_set('session.serialize_handler', 'php');
// use database for storing session data
$this->session = new rcube_session($rcube->get_dbh(), $this->conf);
$this->session->register_gc_handler(array($rcube, 'gc'));
$this->session->set_secret($this->conf->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
$this->session->set_ip_check($this->conf->get('ip_check'));
// this is needed to correctly close session in shutdown function
$rcube->session = $this->session;
}
/**
* Script shutdown handler
*/
public function shutdown()
{
// write performance stats to logs/console
if ($this->conf->get('devel_mode')) {
if (function_exists('memory_get_peak_usage'))
$mem = memory_get_peak_usage();
else if (function_exists('memory_get_usage'))
$mem = memory_get_usage();
$log = trim($this->request . ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : ''));
if (defined('FILE_API_START')) {
rcube::print_timer(FILE_API_START, $log);
}
else {
rcube::console($log);
}
}
}
/**
* Authentication request handler (HTTP Auth)
*/
private function authenticate()
{
if (isset($_POST['username'])) {
$username = $_POST['username'];
$password = $_POST['password'];
}
else if (!empty($_SERVER['PHP_AUTH_USER'])) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
}
// when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule
else if (!isset($_SERVER['PHP_AUTH_USER'])) {
// "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..."
if (isset($_SERVER["REMOTE_USER"])) {
$basicAuthData = base64_decode(substr($_SERVER["REMOTE_USER"], 6));
}
else if (isset($_SERVER["REDIRECT_REMOTE_USER"])) {
$basicAuthData = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6));
}
else if (isset($_SERVER["Authorization"])) {
$basicAuthData = base64_decode(substr($_SERVER["Authorization"], 6));
}
else if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
$basicAuthData = base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6));
}
if (isset($basicAuthData) && !empty($basicAuthData)) {
list($username, $password) = explode(":", $basicAuthData);
}
}
if (!empty($username)) {
- $this->api_init();
- $result = $this->api->authenticate($username, $password);
+ $backend = $this->get_backend();
+ $result = $backend->authenticate($username, $password);
}
if (empty($result)) {
/*
header('WWW-Authenticate: Basic realm="' . $this->app_name .'"');
header('HTTP/1.1 401 Unauthorized');
exit;
*/
throw new Exception("Invalid password or username", file_api::ERROR_CODE);
}
return $username;
}
/**
* Storage/System method handler
*/
private function request_handler($request)
{
// handle "global" requests that don't require api driver
switch ($request) {
case 'ping':
return array();
case 'quit':
$this->session->destroy(session_id());
return array();
case 'configure':
foreach (array_keys($this->config) as $name) {
if (isset($_GET[$name])) {
$this->config[$name] = $_GET[$name];
}
}
$_SESSION['config'] = $this->config;
return $this->config;
case 'upload_progress':
return $this->upload_progress();
case 'mimetypes':
return $this->supported_mimetypes();
case 'capabilities':
- // this one actually uses api driver, but we put it here
- // because we'd need session for the api driver
return $this->capabilities();
}
- // init API driver
- $this->api_init();
-
- // GET arguments
- $args = &$_GET;
-
- // POST arguments (JSON)
- if ($_SERVER['REQUEST_METHOD'] == 'POST') {
- $post = file_get_contents('php://input');
- $args += (array) json_decode($post, true);
- unset($post);
- }
-
- // disable script execution time limit, so we can handle big files
- @set_time_limit(0);
-
// handle request
- switch ($request) {
- case 'file_list':
- $params = array('reverse' => !empty($args['reverse']) && rcube_utils::get_boolean($args['reverse']));
- if (!empty($args['sort'])) {
- $params['sort'] = strtolower($args['sort']);
- }
-
- if (!empty($args['search'])) {
- $params['search'] = $args['search'];
- if (!is_array($params['search'])) {
- $params['search'] = array('name' => $params['search']);
- }
- }
-
- return $this->api->file_list($args['folder'], $params);
-
- case 'file_upload':
- // for Opera upload frame response cannot be application/json
- $this->output_type = self::OUTPUT_HTML;
-
- if (!isset($args['folder']) || $args['folder'] === '') {
- throw new Exception("Missing folder name", file_api::ERROR_CODE);
- }
-
- $uploads = $this->upload();
- $result = array();
-
- foreach ($uploads as $file) {
- $this->api->file_create($args['folder'] . file_storage::SEPARATOR . $file['name'], $file);
- unset($file['path']);
- $result[$file['name']] = array(
- 'type' => $file['type'],
- 'size' => $file['size'],
- );
- }
-
- return $result;
-
- case 'file_create':
- case 'file_update':
- if (!isset($args['file']) || $args['file'] === '') {
- throw new Exception("Missing file name", file_api::ERROR_CODE);
- }
- if (!isset($args['content'])) {
- throw new Exception("Missing file content", file_api::ERROR_CODE);
- }
-
- $file = array(
- 'content' => $args['content'],
- 'type' => rcube_mime::file_content_type($args['content'], $args['file'], $args['content-type'], true),
- );
-
- $this->api->$request($args['file'], $file);
-
- if (!empty($args['info']) && rcube_utils::get_boolean($args['info'])) {
- return $this->api->file_info($args['file']);
- }
-
- return;
-
- case 'file_delete':
- $files = (array) $args['file'];
-
- if (empty($files)) {
- throw new Exception("Missing file name", file_api::ERROR_CODE);
- }
-
- foreach ($files as $file) {
- $this->api->file_delete($file);
- }
- return;
-
- case 'file_info':
- if (!isset($args['file']) || $args['file'] === '') {
- throw new Exception("Missing file name", file_api::ERROR_CODE);
- }
-
- $info = $this->api->file_info($args['file']);
-
- if (!empty($args['viewer']) && rcube_utils::get_boolean($args['viewer'])) {
- $this->file_viewer_info($args['file'], $info);
- }
+ if ($request && preg_match('/^[a-z0-9_-]+$/', $request)) {
+ // request name aliases for backward compatibility
+ $aliases = array(
+ 'lock' => 'lock_create',
+ 'unlock' => 'lock_delete',
+ 'folder_rename' => 'folder_move',
+ );
+
+ $request = $aliases[$request] ?: $request;
+
+ include_once __DIR__ . "/api/$request.php";
+
+ $class_name = "file_api_$request";
+ if (class_exists($class_name, false)) {
+ $handler = new $class_name($this);
+ return $handler->handle();
+ }
+ }
- return $info;
+ throw new Exception("Unknown method", 501);
+ }
- case 'file_get':
- $this->output_type = self::OUTPUT_HTML;
+ /**
+ * Initialise authentication/configuration backend class
+ *
+ * @return file_storage Main storage driver
+ */
+ public function get_backend()
+ {
+ if ($this->backend) {
+ return $this->backend;
+ }
- if (!isset($args['file']) || $args['file'] === '') {
- header("HTTP/1.0 ".file_api::ERROR_CODE." Missing file name");
- }
+ $driver = $this->conf->get('fileapi_backend', 'kolab');
+ $class = $driver . '_file_storage';
- $params = array(
- 'force-download' => !empty($args['force-download']) && rcube_utils::get_boolean($args['force-download']),
- 'force-type' => $args['force-type'],
- );
+ $include_path = RCUBE_INSTALL_PATH . "/lib/drivers/$driver" . PATH_SEPARATOR;
+ $include_path .= ini_get('include_path');
+ set_include_path($include_path);
- if (!empty($args['viewer'])) {
- $this->file_view($args['file'], $args['viewer'], $args, $params);
- }
+ $this->backend = new $class;
- try {
- $this->api->file_get($args['file'], $params);
- }
- catch (Exception $e) {
- header("HTTP/1.0 " . file_api::ERROR_CODE . " " . $e->getMessage());
- }
- exit;
+ // configure api
+ $this->backend->configure($this->config);
- case 'file_move':
- case 'file_copy':
- if (!isset($args['file']) || $args['file'] === '') {
- throw new Exception("Missing file name", file_api::ERROR_CODE);
- }
+ return $this->backend;
+ }
- if (is_array($args['file'])) {
- if (empty($args['file'])) {
- throw new Exception("Missing file name", file_api::ERROR_CODE);
- }
- }
- else {
- if (!isset($args['new']) || $args['new'] === '') {
- throw new Exception("Missing new file name", file_api::ERROR_CODE);
- }
- $args['file'] = array($args['file'] => $args['new']);
- }
+ /**
+ * Return supported/enabled external storage instances
+ *
+ * @param bool $as_objects Return drivers as objects not config data
+ *
+ * @return array List of storage drivers
+ */
+ public function get_drivers($as_objects = false)
+ {
+ $enabled = $this->conf->get('fileapi_drivers');
+ $preconf = $this->conf->get('fileapi_sources');
+ $result = array();
+ $all = array();
- $overwrite = !empty($args['overwrite']) && rcube_utils::get_boolean($args['overwrite']);
- $files = (array) $args['file'];
- $errors = array();
+ if (!empty($enabled)) {
+ $backend = $this->get_backend();
+ $drivers = $backend->driver_list();
- foreach ($files as $file => $new_file) {
- if ($new_file === '') {
- throw new Exception("Missing new file name", file_api::ERROR_CODE);
- }
- if ($new_file === $file) {
- throw new Exception("Old and new file name is the same", file_api::ERROR_CODE);
- }
+ foreach ($drivers as $item) {
+ $all[] = $item['title'];
- try {
- $this->api->{$request}($file, $new_file);
- }
- catch (Exception $e) {
- if ($e->getCode() == file_storage::ERROR_FILE_EXISTS) {
- // delete existing file and do copy/move again
- if ($overwrite) {
- $this->api->file_delete($new_file);
- $this->api->{$request}($file, $new_file);
- }
- // collect file-exists errors, so the client can ask a user
- // what to do and skip or replace file(s)
- else {
- $errors[] = array(
- 'src' => $file,
- 'dst' => $new_file,
- );
- }
- }
- else {
- throw $e;
- }
- }
+ if ($item['enabled'] && in_array($item['driver'], (array) $enabled)) {
+ $result[] = $as_objects ? $this->get_driver_object($item) : $item;
}
+ }
+ }
- if (!empty($errors)) {
- return array('already_exist' => $errors);
+ if (empty($result) && !empty($preconf)) {
+ foreach ((array) $preconf as $title => $item) {
+ if (!in_array($title, $all)) {
+ $item['title'] = $title;
+ $result[] = $as_objects ? $this->get_driver_object($item) : $item;
}
+ }
+ }
- return;
+ return $result;
+ }
- case 'folder_create':
- if (!isset($args['folder']) || $args['folder'] === '') {
- throw new Exception("Missing folder name", file_api::ERROR_CODE);
- }
- return $this->api->folder_create($args['folder']);
+ /**
+ * Return driver for specified file/folder path
+ *
+ * @param string $path Folder/file path
+ *
+ * @return array Storage driver object and modified path
+ */
+ public function get_driver($path)
+ {
+ $drivers = $this->get_drivers();
- case 'folder_delete':
- if (!isset($args['folder']) || $args['folder'] === '') {
- throw new Exception("Missing folder name", file_api::ERROR_CODE);
- }
- return $this->api->folder_delete($args['folder']);
+ foreach ($drivers as $item) {
+ $prefix = $item['title'] . file_storage::SEPARATOR;
- case 'folder_rename':
- case 'folder_move':
- if (!isset($args['folder']) || $args['folder'] === '') {
- throw new Exception("Missing source folder name", file_api::ERROR_CODE);
- }
- if (!isset($args['new']) || $args['new'] === '') {
- throw new Exception("Missing destination folder name", file_api::ERROR_CODE);
- }
- if ($args['new'] === $args['folder']) {
- return;
- }
- return $this->api->folder_move($args['folder'], $args['new']);
+ 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;
}
/**
* File upload progress handler
*/
protected function upload_progress()
{
if (function_exists('apc_fetch')) {
$prefix = ini_get('apc.rfc1867_prefix');
$uploadid = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET);
$status = apc_fetch($prefix . $uploadid);
if (!empty($status)) {
$status['percent'] = round($status['current']/$status['total']*100);
if ($status['percent'] < 100) {
$diff = time() - intval($status['start_time']);
// calculate time to end of uploading (in seconds)
$status['eta'] = intval($diff * (100 - $status['percent']) / $status['percent']);
// average speed (bytes per second)
$status['rate'] = intval($status['current'] / $diff);
}
}
$status['id'] = $uploadid;
return $status; // id, done, total, current, percent, start_time, eta, rate
}
throw new Exception("Not supported", file_api::ERROR_CODE);
}
- /*
- * Returns API capabilities
- */
- protected function capabilities()
- {
- $this->api_init();
-
- $caps = array();
-
- // check support for upload progress
- if (($progress_sec = $this->conf->get('upload_progress'))
- && ini_get('apc.rfc1867') && function_exists('apc_fetch')
- ) {
- $caps[file_storage::CAPS_PROGRESS_NAME] = ini_get('apc.rfc1867_name');
- $caps[file_storage::CAPS_PROGRESS_TIME] = $progress_sec;
- }
-
- foreach ($this->api->capabilities() as $name => $value) {
- // skip disabled capabilities
- if ($value !== false) {
- $caps[$name] = $value;
- }
- }
-
- return $caps;
- }
-
/**
* Return mimetypes list supported by built-in viewers
*
* @return array List of mimetypes
*/
protected function supported_mimetypes()
{
$mimetypes = array();
$dir = RCUBE_INSTALL_PATH . 'lib/viewers';
if ($handle = opendir($dir)) {
while (false !== ($file = readdir($handle))) {
if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) {
include_once $dir . '/' . $file;
$class = 'file_viewer_' . $matches[1];
$viewer = new $class($this);
$mimetypes = array_merge($mimetypes, $viewer->supported_mimetypes());
}
}
closedir($handle);
}
return $mimetypes;
}
- /**
- * Merge file viewer data into file info
- */
- protected function file_viewer_info($file, &$info)
- {
- if ($viewer = $this->find_viewer($info['type'])) {
- $info['viewer'] = array();
- if ($frame = $viewer->frame($file, $info['type'])) {
- $info['viewer']['frame'] = $frame;
- }
- else if ($href = $viewer->href($file, $info['type'])) {
- $info['viewer']['href'] = $href;
- }
- }
- }
-
- /**
- * File vieweing request handler
- */
- protected function file_view($file, $viewer, &$args, &$params)
- {
- $path = RCUBE_INSTALL_PATH . "lib/viewers/$viewer.php";
- $class = "file_viewer_$viewer";
-
- if (!file_exists($path)) {
- return;
- }
-
- // get file info
- try {
- $info = $this->api->file_info($file);
- }
- catch (Exception $e) {
- header("HTTP/1.0 " . file_api::ERROR_CODE . " " . $e->getMessage());
- exit;
- }
-
- include_once $path;
- $viewer = new $class($this);
-
- // check if specified viewer supports file type
- // otherwise return (fallback to file_get action)
- if (!$viewer->supports($info['type'])) {
- return;
- }
-
- $viewer->output($file, $info['type']);
- exit;
- }
-
- /**
- * Return built-in viewer opbject for specified mimetype
- *
- * @return object Viewer object
- */
- protected function find_viewer($mimetype)
- {
- $dir = RCUBE_INSTALL_PATH . 'lib/viewers';
-
- if ($handle = opendir($dir)) {
- while (false !== ($file = readdir($handle))) {
- if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) {
- include_once $dir . '/' . $file;
- $class = 'file_viewer_' . $matches[1];
- $viewer = new $class($this);
-
- if ($viewer->supports($mimetype)) {
- return $viewer;
- }
- }
- }
- closedir($handle);
- }
- }
-
/**
* Returns complete File URL
*
* @param string $file File name (with path)
*
* @return string File URL
*/
public function file_url($file)
{
return file_utils::script_uri(). '?method=file_get'
. '&file=' . urlencode($file)
. '&token=' . urlencode(session_id());
}
/**
* Returns web browser object
*
* @return rcube_browser Web browser object
*/
public function get_browser()
{
if ($this->browser === null) {
$this->browser = new rcube_browser;
}
return $this->browser;
}
/**
* Send success response
*
* @param mixed $data Data
*/
public function output_success($data)
{
if (!is_array($data)) {
$data = array();
}
$response = array('status' => 'OK', 'result' => $data);
if (!empty($_REQUEST['req_id'])) {
$response['req_id'] = $_REQUEST['req_id'];
}
$this->output_send($response);
}
/**
* Send error response
*
* @param mixed $response Response data
* @param int $code Error code
*/
public function output_error($response, $code = null)
{
if (is_string($response)) {
$response = array('reason' => $response);
}
$response['status'] = 'ERROR';
if ($code) {
$response['code'] = $code;
}
if (!empty($_REQUEST['req_id'])) {
$response['req_id'] = $_REQUEST['req_id'];
}
if (empty($response['code'])) {
$response['code'] = file_api::ERROR_CODE;
}
$this->output_send($response);
}
/**
* Send response
*
* @param mixed $data Data
*/
protected function output_send($data)
{
// Send response
header("Content-Type: {$this->output_type}; charset=utf-8");
echo json_encode($data);
exit;
}
/**
* Create a human readable string for a number of bytes
*
* @param int Number of bytes
*
* @return string Byte string
*/
public function show_bytes($bytes)
{
if ($bytes >= 1073741824) {
$gb = $bytes/1073741824;
$str = sprintf($gb >= 10 ? "%d " : "%.1f ", $gb) . 'GB';
}
else if ($bytes >= 1048576) {
$mb = $bytes/1048576;
$str = sprintf($mb >= 10 ? "%d " : "%.1f ", $mb) . 'MB';
}
else if ($bytes >= 1024) {
$str = sprintf("%d ", round($bytes/1024)) . 'KB';
}
else {
$str = sprintf('%d ', $bytes) . 'B';
}
return $str;
}
}
diff --git a/lib/file_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
@@ -1,240 +1,304 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
interface file_storage
{
// capabilities
const CAPS_ACL = 'ACL';
const CAPS_MAX_UPLOAD = 'MAX_UPLOAD';
const CAPS_PROGRESS_NAME = 'PROGRESS_NAME';
const CAPS_PROGRESS_TIME = 'PROGRESS_TIME';
const CAPS_QUOTA = 'QUOTA';
const CAPS_LOCKS = 'LOCKS';
// config
const SEPARATOR = '/';
// error codes
const ERROR = 500;
const ERROR_LOCKED = 423;
const ERROR_FILE_EXISTS = 550;
const ERROR_UNSUPPORTED = 570;
+ const ERROR_NOAUTH = 580;
// locks
const LOCK_SHARED = 'shared';
const LOCK_EXCLUSIVE = 'exclusive';
const LOCK_INFINITE = 'infinite';
+
/**
* Authenticates a user
*
* @param string $username User name
* @param string $password User password
*
* @return bool True on success, False on failure
*/
public function authenticate($username, $password);
/**
* Configures environment
*
- * @param array $config COnfiguration
+ * @param array $config Configuration
+ * @param string $title Driver instance identifier
+ */
+ public function configure($config, $title = null);
+
+ /**
+ * Returns current instance title
+ *
+ * @return string Instance title (mount point)
*/
- public function configure($config);
+ public function title();
/**
* Storage driver capabilities
*
* @return array List of capabilities
*/
public function capabilities();
+ /**
+ * Save configuration of external driver (mount point)
+ *
+ * @param array $driver Driver data
+ *
+ * @throws Exception
+ */
+ public function driver_create($driver);
+
+ /**
+ * Delete configuration of external driver (mount point)
+ *
+ * @param string $name Driver instance name
+ *
+ * @throws Exception
+ */
+ public function driver_delete($name);
+
+ /**
+ * Return list of registered drivers (mount points)
+ *
+ * @return array List of drivers data
+ * @throws Exception
+ */
+ public function driver_list();
+
+ /**
+ * 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
*/
public function file_create($file_name, $file);
/**
* Update a file.
*
* @param string $file_name Name of a file (with folder path)
* @param array $file File data (path/content, type)
*
* @throws Exception
*/
public function file_update($file_name, $file);
/**
* Delete a file.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_delete($file_name);
/**
* Returns file body.
*
* @param string $file_name Name of a file (with folder path)
* @param array $params Parameters (force-download)
* @param resource $fp Print to file pointer instead (send no headers)
*
* @throws Exception
*/
public function file_get($file_name, $params = array(), $fp = null);
/**
* Move (or rename) a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_move($file_name, $new_name);
/**
* Copy a file.
*
* @param string $file_name Name of a file (with folder path)
* @param string $new_name New name of a file (with folder path)
*
* @throws Exception
*/
public function file_copy($file_name, $new_name);
/**
* Returns file metadata.
*
* @param string $file_name Name of a file (with folder path)
*
* @throws Exception
*/
public function file_info($file_name);
/**
* List files in a folder.
*
* @param string $folder_name Name of a folder with full path
- * @param array $params List parameters ('sort', 'reverse', 'search')
+ * @param array $params List parameters ('sort', 'reverse', 'search', 'prefix')
*
* @return array List of files (file properties array indexed by filename)
* @throws Exception
*/
public function file_list($folder_name, $params = array());
/**
* Create a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_create($folder_name);
/**
* Delete a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_delete($folder_name);
/**
* Move/Rename a folder.
*
* @param string $folder_name Name of a folder with full path
* @param string $new_name New name of a folder with full path
*
* @throws Exception
*/
public function folder_move($folder_name, $new_name);
/**
* Returns list of folders.
*
* @return array List of folders
* @throws Exception
*/
public function folder_list();
/**
* Returns a list of locks
*
* This method should return all the locks for a particular URI, including
* locks that might be set on a parent URI.
*
* If child_locks is set to true, this method should also look for
* any locks in the subtree of the URI for locks.
*
* @param string $uri URI
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
* @throws Exception
*/
public function lock_list($uri, $child_locks = false);
/**
* Locks a URI
*
* @param string $uri URI
* @param array $lock Lock data
* - depth: 0/'infinite'
* - scope: 'shared'/'exclusive'
* - owner: string
* - token: string
* - timeout: int
*
* @throws Exception
*/
public function lock($uri, $lock);
/**
* Removes a lock from a URI
*
* @param string $path URI
* @param array $lock Lock data
*
* @throws Exception
*/
public function unlock($uri, $lock);
/**
* Return disk quota information for specified folder.
*
* @param string $folder_name Name of a folder with full path
*
* @return array Quota
* @throws Exception
*/
public function quota($folder);
}
diff --git a/lib/file_ui.php b/lib/file_ui.php
index 071301b..858030a 100644
--- a/lib/file_ui.php
+++ b/lib/file_ui.php
@@ -1,662 +1,568 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-2012, 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_ui
+class file_ui extends file_locale
{
/**
* @var kolab_client_output
*/
protected $output;
/**
* @var kolab_client_api
*/
public $api;
/**
* @var Conf
*/
protected $config;
protected $ajax_only = false;
protected $page_title = 'Kolab File API';
protected $menu = array();
protected $cache = array();
protected $devel_mode = false;
protected $object_types = array();
- protected static $translation = array();
-
/**
* Class constructor.
*
* @param file_ui_output $output Optional output object
*/
public function __construct($output = null)
{
$rcube = rcube::get_instance();
$rcube->add_shutdown_function(array($this, 'shutdown'));
$this->config_init();
$this->devel_mode = $this->config->get('devel_mode', false);
$this->output_init($output);
$this->api_init();
ini_set('session.use_cookies', 'On');
session_start();
// Initialize locales
$this->locale_init();
$this->auth();
}
- /**
- * 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()
{
$this->config = rcube::get_instance()->config;
}
/**
* Output initialization.
*/
private function output_init($output = null)
{
if ($output) {
$this->output = $output;
return;
}
$skin = $this->config->get('file_api_skin', 'default');
$this->output = new file_ui_output($skin);
// Assign self to template variable
$this->output->assign('engine', $this);
}
/**
* API initialization
*/
private function api_init()
{
$url = $this->config->get('file_api_url', '');
if (!$url) {
$url = rcube_utils::https_check() ? 'https://' : 'http://';
$url .= $_SERVER['SERVER_NAME'];
$url .= preg_replace('/\/?\?.*$/', '', $_SERVER['REQUEST_URI']);
$url .= '/api/';
}
$this->api = new file_ui_api($url);
}
/**
* Initializes User Interface
*/
protected function ui_init()
{
// assign token
$this->output->set_env('token', $_SESSION['user']['token']);
// assign capabilities
$this->output->set_env('capabilities', $_SESSION['caps']);
// add watermark content
$this->output->set_env('watermark', $this->output->get_template('watermark'));
// $this->watermark('taskcontent');
// assign default set of translations
$this->output->add_translation('loading', 'servererror');
// $this->output->assign('tasks', $this->menu);
// $this->output->assign('main_menu', $this->menu());
$this->output->assign('user', $_SESSION['user']);
if ($_SESSION['caps']['MAX_UPLOAD']) {
$this->output->assign('max_upload', $this->show_bytes($_SESSION['caps']['MAX_UPLOAD']));
}
}
- /**
- * 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()
{
if (isset($_POST['login'])) {
$login = $this->get_input('login', 'POST');
if ($login['username']) {
$result = $this->api->login($login['username'], $login['password']);
if ($token = $result->get('token')) {
$user = array(
'token' => $token,
'username' => $login['username'],
);
$this->api->set_session_token($user['token']);
/*
// Find user settings
// Don't call API user.info for non-existing users (#1025)
if (preg_match('/^cn=([a-z ]+)/i', $login['username'], $m)) {
$user['fullname'] = ucwords($m[1]);
}
else {
$res = $this->api->get('user.info', array('user' => $user['id']));
$res = $res->get();
if (is_array($res) && !empty($res)) {
$user['language'] = $res['preferredlanguage'];
$user['fullname'] = $res['cn'];
}
}
*/
// Save capabilities
$_SESSION['caps'] = $result->get('capabilities');
// Save user data
$_SESSION['user'] = $user;
if (($language = $this->get_language()) && $language != 'en_US') {
$_SESSION['user']['language'] = $language;
$session_config['language'] = $language;
}
/*
// Configure API session
if (!empty($session_config)) {
$this->api->post('system.configure', null, $session_config);
}
*/
header('Location: ?');
die;
}
else {
$code = $result->get_error_code();
$str = $result->get_error_str();
$label = 'loginerror';
if ($code == file_ui_api::ERROR_INTERNAL
|| $code == file_ui_api::ERROR_CONNECTION
) {
$label = 'internalerror';
$this->raise_error(500, 'Login failed. ' . $str);
}
$this->output->command('display_message', $label, 'error');
}
}
}
else if (!empty($_SESSION['user']) && !empty($_SESSION['user']['token'])) {
// Validate session
$timeout = $this->config->get('session_timeout', 3600);
if ($timeout && $_SESSION['time'] && $_SESSION['time'] < time() - $timeout) {
$this->action_logout(true);
}
// update session time
$_SESSION['time'] = time();
// Set API session key
$this->api->set_session_token($_SESSION['user']['token']);
}
}
/**
* Main execution.
*/
public function run()
{
// Session check
if (empty($_SESSION['user']) || empty($_SESSION['user']['token'])) {
$this->action_logout();
}
// Run security checks
$this->input_checks();
$this->action = $this->get_input('action', 'GET');
if ($this->action) {
$method = 'action_' . $this->action;
if (method_exists($this, $method)) {
$this->$method();
}
}
else if (method_exists($this, 'action_default')) {
$this->action_default();
}
}
/**
* Security checks and input validation.
*/
public function input_checks()
{
$ajax = $this->output->is_ajax();
// Check AJAX-only tasks
if ($this->ajax_only && !$ajax) {
$this->raise_error(500, 'Invalid request type!', null, true);
}
// CSRF prevention
$token = $ajax ? rcube_utils::request_header('X-Session-Token') : $this->get_input('token');
$task = $this->get_task();
if ($task != 'main' && $token != $_SESSION['user']['token']) {
$this->raise_error(403, 'Invalid request data!', null, true);
}
}
/**
* Logout action.
*/
private function action_logout($sess_expired = false, $stop_sess = true)
{
if (!empty($_SESSION['user']) && !empty($_SESSION['user']['token']) && $stop_sess) {
$this->api->logout();
}
$_SESSION = array();
if ($this->output->is_ajax()) {
if ($sess_expired) {
$args = array('error' => 'session.expired');
}
$this->output->command('main_logout', $args);
if ($sess_expired) {
$this->output->send();
exit;
}
}
else {
$this->output->add_translation('loginerror', 'internalerror', 'session.expired');
}
if ($sess_expired) {
$error = 'session.expired';
}
else {
$error = $this->get_input('error', 'GET');
}
if ($error) {
$this->output->command('display_message', $error, 'error', 60000);
}
$this->output->send('login');
exit;
}
/**
* Error action (with error logging).
*
* @param int $code Error code
* @param string $msg Error message
* @param array $args Optional arguments (type, file, line)
* @param bool $output Enable to send output and finish
*/
public function raise_error($code, $msg, $args = array(), $output = false)
{
$log_line = sprintf("%s Error: %s (%s)",
isset($args['type']) ? $args['type'] : 'PHP',
$msg . (isset($args['file']) ? sprintf(' in %s on line %d', $args['file'], $args['line']) : ''),
$_SERVER['REQUEST_METHOD']);
rcube::write_log('errors', $log_line);
if (!$output) {
return;
}
if ($this->output->is_ajax()) {
header("HTTP/1.0 $code $msg");
die;
}
$this->output->assign('error_code', $code);
$this->output->assign('error_message', $msg);
$this->output->send('error');
exit;
}
/**
* Script shutdown handler
*/
public function shutdown()
{
// write performance stats to logs/console
if ($this->devel_mode) {
if (function_exists('memory_get_peak_usage'))
$mem = memory_get_peak_usage();
else if (function_exists('memory_get_usage'))
$mem = memory_get_usage();
$log = 'ui:' . $this->get_task() . ($this->action ? '/' . $this->action : '');
$log .= ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : '');
if (defined('FILE_API_START')) {
rcube::print_timer(FILE_API_START, $log);
}
else {
rcube::console($log);
}
}
}
/**
* Output sending.
*/
public function send()
{
$task = $this->get_task();
if ($this->page_title) {
$this->output->assign('pagetitle', $this->page_title);
}
$this->output->set_env('task', $task);
$this->output->send($this->task_template ? $this->task_template : $task);
exit;
}
/**
* Returns name of the current task.
*
* @return string Task name
*/
public function get_task()
{
$class_name = get_class($this);
if (preg_match('/^file_ui_client_([a-z]+)$/', $class_name, $m)) {
return $m[1];
}
}
- /**
- * 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
* @param string $type Parameter type (GET|POST|NULL)
* @param bool $allow_html Disables stripping of insecure content (HTML tags)
*
* @see rcube_utils::get_input_value
* @return mixed Input value.
*/
public static function get_input($name, $type = null, $allow_html = false)
{
if ($type == 'GET') {
$type = rcube_utils::INPUT_GET;
}
else if ($type == 'POST') {
$type = rcube_utils::INPUT_POST;
}
else {
$type = rcube_utils::INPUT_GPC;
}
$result = rcube_utils::get_input_value($name, $type, $allow_html);
return $result;
}
/**
* Returns task menu output.
*
* @return string HTML output
*/
protected function menu()
{
}
/**
* Adds watermark page definition into main page.
*/
protected function watermark($name)
{
$this->output->command('set_watermark', $name);
}
/**
* API GET request wrapper
*/
protected function api_get($action, $get = array())
{
return $this->api_call('get', $action, $get);
}
/**
* API POST request wrapper
*/
protected function api_post($action, $get = array(), $post = array())
{
return $this->api_call('post', $action, $get, $post);
}
/**
* API request wrapper with error handling
*/
protected function api_call($type, $action, $get = array(), $post = array())
{
if ($type == 'post') {
$result = $this->api->post($action, $get, $post);
}
else {
$result = $this->api->get($action, $get);
}
// error handling
if ($code = $result->get_error_code()) {
// Invalid session, do logout
if ($code == 403) {
$this->action_logout(true, false);
}
// Log communication errors, other should be logged on API side
if ($code < 400) {
$this->raise_error($code, 'API Error: ' . $result->get_error_str());
}
}
return $result;
}
/**
* Returns execution time in seconds
*
* @param string Execution time
*/
public function gentime()
{
return sprintf('%.4f', microtime(true) - FILE_API_START);
}
/**
* Returns HTML output of login form
*
* @param string HTML output
*/
public function login_form()
{
$post = $this->get_input('login', 'POST');
$user_input = new html_inputfield(array(
'type' => 'text',
'id' => 'login_name',
'name' => 'login[username]',
'autofocus' => true,
));
$pass_input = new html_inputfield(array(
'type' => 'password',
'id' => 'login_pass',
'name' => 'login[password]',
));
$button = new html_inputfield(array(
'type' => 'submit',
'id' => 'login_submit',
'value' => $this->translate('login.login'),
));
$username = html::label(array('for' => 'login_name'), $this->translate('login.username'))
. $user_input->show($post['username']);
$password = html::label(array('for' => 'login_pass'), $this->translate('login.password'))
. $pass_input->show('');
$form = html::tag('form', array(
'id' => 'login_form',
'name' => 'login',
'method' => 'post',
'action' => '?'),
html::span(null, $username) . html::span(null, $password) . $button->show());
return $form;
}
/**
* Create a human readable string for a number of bytes
*
* @param int Number of bytes
*
* @return string Byte string
*/
protected function show_bytes($bytes)
{
if ($bytes >= 1073741824) {
$gb = $bytes/1073741824;
$str = sprintf($gb>=10 ? "%d " : "%.1f ", $gb) . $this->translate('size.GB');
}
else if ($bytes >= 1048576) {
$mb = $bytes/1048576;
$str = sprintf($mb>=10 ? "%d " : "%.1f ", $mb) . $this->translate('size.MB');
}
else if ($bytes >= 1024) {
$str = sprintf("%d ", round($bytes/1024)) . $this->translate('size.KB');
}
else {
$str = sprintf("%d ", $bytes) . $this->translate('size.B');
}
return $str;
}
}
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
@@ -1,290 +1,291 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-2012, 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> |
+--------------------------------------------------------------------------+
*/
/**
* Output functionality for Kolab File API UI
*/
class file_ui_output
{
private $tpl_vars = array();
private $env = array();
private $objects = array();
private $commands = array();
private $labels = array();
private $skin;
/**
* Class constructor.
*
* @param string $skin Interface skin name
*/
public function __construct($skin = null)
{
$this->skin = $skin ? $skin : 'default';
$this->init();
}
/**
* Initialization.
*/
private function init()
{
$conf = rcube::get_instance()->config;
$smarty_path = array('Smarty', 'smarty3', 'smarty');
if ($path = $conf->get('smarty_path')) {
array_unshift($smarty_path, $path);
}
foreach ($smarty_path as $path) {
@include_once "$path/Smarty.class.php";
if (class_exists('Smarty', false)) {
break;
}
}
$SMARTY = new Smarty;
$SMARTY->template_dir = 'skins/' . $this->skin . '/templates';
$SMARTY->compile_dir = RCUBE_INSTALL_PATH . '/cache';
$SMARTY->plugins_dir = RCUBE_INSTALL_PATH . '/lib/ext/Smarty/plugins/';
$SMARTY->debugging = false;
$this->tpl = $SMARTY;
}
/**
* Sends output to the browser.
*
* @param string $template HTML template name
*/
public function send($template = null)
{
if ($this->is_ajax()) {
echo $this->send_json();
}
else {
$this->send_tpl($template);
}
}
/**
* JSON output.
*/
private function send_json()
{
header('Content-Type: application/json');
$response = array(
'objects' => $this->objects,
'env' => array(),
);
foreach ($this->env as $name => $value) {
$response['env'][$name] = $value;
}
foreach ($this->commands as $command) {
$cname = array_shift($command);
$args = array();
foreach ($command as $arg) {
$args[] = json_encode($arg);
}
$commands[] = sprintf('ui.%s(%s);', $cname, implode(',', $args));
}
if (!empty($commands)) {
$response['exec'] = implode("\n", $commands);
}
$this->labels = array_unique($this->labels);
foreach ($this->labels as $label) {
$response['labels'][$label] = file_ui::translate($label);
}
return json_encode($response);
}
/**
* HTML output.
*
* @param string $template HTML template name
*/
private function send_tpl($template)
{
if (!$template) {
return;
}
foreach ($this->tpl_vars as $name => $value) {
$this->tpl->assign($name, $value);
}
+ $this->env['skin_path'] = 'skins/' . $this->skin . '/';
$script = '';
if (!empty($this->env)) {
$script[] = 'ui.set_env(' . json_encode($this->env) . ');';
}
$this->labels = array_unique($this->labels);
if (!empty($this->labels)) {
foreach ($this->labels as $label) {
$labels[$label] = file_ui::translate($label);
}
$script[] = 'ui.tdef(' . json_encode($labels) . ');';
}
foreach ($this->commands as $command) {
$cname = array_shift($command);
$args = array();
foreach ($command as $arg) {
$args[] = json_encode($arg);
}
$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);
}
$this->tpl->display($template . '.html');
}
/**
* Request type checker.
*
* @return bool True on AJAX request, False otherwise
*/
public function is_ajax()
{
return !empty($_REQUEST['remote']);
}
/**
* Assigns value to a template variable.
*
* @param string $name Variable name
* @param mixed $value Variable value
*/
public function assign($name, $value)
{
$this->tpl_vars[$name] = $value;
}
/**
* Get the value from the environment to be sent to the browser.
*
* @param string $name Variable name
*
*/
public function get_env($name)
{
if (empty($this->env[$name])) {
return null;
} else {
return $this->env[$name];
}
}
/**
* Assigns value to browser environment.
*
* @param string $name Variable name
* @param mixed $value Variable value
*/
public function set_env($name, $value)
{
$this->env[$name] = $value;
}
/**
* Sets content of a HTML object.
*
* @param string $name Object's identifier (HTML ID attribute)
* @param string $content Object's content
* @param bool $is_template Set to true if $content is a template name
*/
public function set_object($name, $content, $is_template = false)
{
if ($is_template) {
$content = $this->get_template($content);
}
$this->objects[$name] = $content;
}
/**
* Returns content of a HTML object (set with set_object())
*
* @param string $name Object's identifier (HTML ID attribute)
*
* @return string Object content
*/
public function get_object($name)
{
return $this->objects[$name];
}
/**
* Returns HTML template output.
*
* @param string $name Template name
*
* @return string Template output
*/
public function get_template($name)
{
ob_start();
$this->send_tpl($name);
$content = ob_get_contents();
ob_end_clean();
return $content;
}
/**
* Sets javascript command (to be added to the request).
*/
public function command()
{
$this->commands[] = func_get_args();
}
/**
* Adds one or more translation labels to the browser env.
*/
public function add_translation()
{
$this->labels = array_merge($this->labels, func_get_args());
}
}
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
@@ -1,140 +1,188 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class file_utils
{
static $class_map = array(
'document' => array(
// text
'text/',
'application/rtf',
'application/x-rtf',
'application/xml',
// office
'application/wordperfect',
'application/excel',
'application/msword',
'application/msexcel',
'application/mspowerpoint',
'application/vnd.ms-word',
'application/vnd.ms-excel',
'application/vnd.ms-powerpoint',
'application/vnd.openxmlformats-officedocument',
'application/vnd.oasis.opendocument',
'application/vnd.sun.xml.calc',
'application/vnd.sun.xml.writer',
'application/vnd.stardivision.calc',
'application/vnd.stardivision.writer',
// pdf
'application/pdf',
'application/x-pdf',
'application/acrobat',
'application/vnd.pdf',
),
'audio' => array(
'audio/',
),
'video' => array(
'video/',
),
'image' => array(
'image/',
'application/dxf',
'application/acad',
),
'empty' => array(
'application/x-empty',
),
);
+ // 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
*
* @param string $class Class name
*
* @return array List of mimetype prefixes
*/
static function class2mimetypes($class)
{
return isset(self::$class_map[$class]) ? self::$class_map[$class] : self::$class_map['empty'];
}
/**
* Finds class of specified mimetype
*
* @param string $mimetype File mimetype
*
* @return string Class name
*/
static function mimetype2class($mimetype)
{
$mimetype = strtolower($mimetype);
foreach (self::$class_map as $class => $prefixes) {
foreach ($prefixes as $prefix) {
if (strpos($mimetype, $prefix) === 0) {
return $class;
}
}
}
}
/**
* Apply some fixes on file mimetype string
*
* @param string $mimetype File type
*
* @return string File type
*/
static function real_mimetype($mimetype)
{
if (preg_match('/^text\/(.+)/i', $mimetype, $m)) {
// fix pdf mimetype
if (preg_match('/^(pdf|x-pdf)$/i', $m[1])) {
$mimetype = 'application/pdf';
}
}
return $mimetype;
}
+ /**
+ * 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
*/
static function script_uri()
{
if (!empty($_SERVER['SCRIPT_URI'])) {
return $_SERVER['SCRIPT_URI'];
}
$uri = $_SERVER['SERVER_PORT'] == 443 ? 'https://' : 'http://';
$uri .= $_SERVER['HTTP_HOST'];
$uri .= preg_replace('/\?.*$/', '', $_SERVER['REQUEST_URI']);
return $uri;
}
}
diff --git a/lib/init.php b/lib/init.php
index 6b8e86a..2ff13a1 100644
--- a/lib/init.php
+++ b/lib/init.php
@@ -1,40 +1,40 @@
<?php
/**
+--------------------------------------------------------------------------+
| Kolab File API |
| |
| Copyright (C) 2011-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/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
// 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;
$include_path .= RCUBE_INSTALL_PATH . '/lib/ext' . PATH_SEPARATOR;
$include_path .= RCUBE_INSTALL_PATH . '/lib/client' . PATH_SEPARATOR;
$include_path .= ini_get('include_path');
set_include_path($include_path);
// include global functions from Roundcube Framework
require_once 'Roundcube/bootstrap.php';
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
@@ -1,75 +1,83 @@
<?php
$LANG['about.community'] = 'This is the Community Edition of the <b>Kolab Server</b>.';
$LANG['about.warranty'] = 'Professional support is available from <a href="http://kolabsys.com">Kolab Systems</a>.';
$LANG['about.support'] = 'It comes with absolutely <b>no warranties</b> and is typically run entirely self supported. You can find help &amp; information on the community <a href="http://kolab.org">web site</a> &amp; <a href="http://wiki.kolab.org">wiki</a>.';
$LANG['collection.audio'] = 'Audio';
$LANG['collection.video'] = 'Video';
$LANG['collection.image'] = 'Images';
$LANG['collection.document'] = 'Documents';
$LANG['file.copy'] = 'Copy';
$LANG['file.create'] = 'Create File';
$LANG['file.download'] = 'Download';
$LANG['file.edit'] = 'Edit';
$LANG['file.upload'] = 'Upload File';
$LANG['file.name'] = 'Name';
$LANG['file.move'] = 'Move';
$LANG['file.mtime'] = 'Modified';
$LANG['file.size'] = 'Size';
$LANG['file.open'] = 'Open';
$LANG['file.delete'] = 'Delete';
$LANG['file.rename'] = 'Rename';
$LANG['file.search'] = 'Search file';
$LANG['file.type'] = 'Type';
$LANG['file.save'] = 'Save';
$LANG['file.skip'] = 'Skip';
$LANG['file.skipall'] = 'Skip all';
$LANG['file.overwrite'] = 'Overwrite';
$LANG['file.overwriteall'] = 'Overwrite all';
$LANG['file.moveconfirm'] = 'This action is going to overwrite the destination file: <b>$file</b>.';
$LANG['file.progress'] = 'Uploaded $current of $total ($percent%)';
$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';
$LANG['login.login'] = 'Log in';
$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...';
$LANG['copying'] = 'Copying...';
$LANG['moving'] = 'Moving...';
$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...';
$LANG['search.in_all_folders'] = 'in all folders';
$LANG['search.in_current_folder'] = 'in current folder';
$LANG['size.B'] = 'B';
$LANG['size.KB'] = 'KB';
$LANG['size.MB'] = 'MB';
$LANG['size.GB'] = 'GB';
$LANG['upload.size'] = 'Size:';
$LANG['upload.size.error'] = 'Maximum upload size ($size) exceeded!';
$LANG['upload.progress'] = 'Progress:';
$LANG['upload.rate'] = 'Rate:';
$LANG['upload.eta'] = 'ETA:';
diff --git a/lib/viewers/image.php b/lib/viewers/image.php
index fee9b3e..8b288a4 100644
--- a/lib/viewers/image.php
+++ b/lib/viewers/image.php
@@ -1,109 +1,111 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Class implementing image viewer (with format converter)
*
* NOTE: some formats are supported by browser, don't use viewer when not needed.
*/
class file_viewer_image extends file_viewer
{
protected $mimetypes = array(
'image/bmp',
'image/png',
'image/jpeg',
'image/jpg',
'image/pjpeg',
'image/gif',
'image/tiff',
'image/x-tiff',
);
/**
* Class constructor
*
* @param file_api File API object
*/
public function __construct($api)
{
// @TODO: disable types not supported by some browsers
$this->api = $api;
}
/**
* Return file viewer URL
*
* @param string $file File name
* @param string $mimetype File type
*/
public function href($file, $mimetype = null)
{
$href = file_utils::script_uri() . '?method=file_get'
. '&file=' . urlencode($file)
. '&token=' . urlencode(session_id());
// we redirect to self only images with types unsupported
// by browser
if (in_array($mimetype, $this->mimetypes)) {
$href .= '&viewer=image';
}
return $href;
}
/**
* Print output and exit
*
* @param string $file File name
* @param string $mimetype File type
*/
public function output($file, $mimetype = null)
{
/*
// conversion not needed
if (preg_match('/^image/p?jpe?g$/i', $mimetype)) {
$this->api->api->file_get($file);
return;
}
*/
$rcube = rcube::get_instance();
$temp_dir = unslashify($rcube->config->get('temp_dir'));
$file_path = tempnam($temp_dir, 'rcmImage');
+ list($driver, $file) = $this->api->get_driver($file);
+
// write content to temp file
$fd = fopen($file_path, 'w');
- $this->api->api->file_get($file, array(), $fd);
+ $driver->file_get($file, array(), $fd);
fclose($fd);
// convert image to jpeg and send it to the browser
$image = new rcube_image($file_path);
if ($image->convert(rcube_image::TYPE_JPG, $file_path)) {
header("Content-Type: image/jpeg");
header("Content-Length: " . filesize($file_path));
readfile($file_path);
}
unlink($file_path);
}
}
diff --git a/lib/viewers/text.php b/lib/viewers/text.php
index 718692c..8708f77 100644
--- a/lib/viewers/text.php
+++ b/lib/viewers/text.php
@@ -1,214 +1,216 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Class integrating text editor http://ajaxorg.github.io/ace
*/
class file_viewer_text extends file_viewer
{
/**
* Mimetype to tokenizer map
*
* @var array
*/
protected $mimetypes = array(
'text/plain' => 'text',
'text/html' => 'html',
'text/javascript' => 'javascript',
'text/ecmascript' => 'javascript',
'text/x-c' => 'c_cpp',
'text/css' => 'css',
'text/x-java-source' => 'java',
'text/x-php' => 'php',
'text/x-sh' => 'sh',
'text/xml' => 'xml',
'application/xml' => 'xml',
'application/x-vbscript' => 'vbscript',
'message/rfc822' => 'text',
'application/x-empty' => 'text',
);
/**
* File extension to highligter mode mapping
*
* @var array
*/
protected $extensions = array(
'php' => '/^(php|phpt|inc)$/',
'html' => '/^html?$/',
'css' => '/^css$/',
'xml' => '/^xml$/',
'javascript' => '/^js$/',
'sh' => '/^sh$/',
);
/**
* Returns list of supported mimetype
*
* @return array List of mimetypes
*/
public function supported_mimetypes()
{
// we return only mimetypes not starting with text/
$mimetypes = array();
foreach (array_keys($this->mimetypes) as $type) {
if (strpos($type, 'text/') !== 0) {
$mimetypes[] = $type;
}
}
return $mimetypes;
}
/**
* Check if mimetype is supported by the viewer
*
* @param string $mimetype File type
*
* @return bool
*/
public function supports($mimetype)
{
return $this->mimetypes[$mimetype] || preg_match('/^text\/(?!(pdf|x-pdf))/', $mimetype);
}
/**
* Print file content
*/
protected function print_file($file)
{
$stdout = fopen('php://output', 'w');
stream_filter_register('file_viewer_text', 'file_viewer_content_filter');
stream_filter_append($stdout, 'file_viewer_text');
- $this->api->api->file_get($file, array(), $stdout);
+ list($driver, $file) = $this->api->get_driver($file);
+
+ $driver->file_get($file, array(), $stdout);
}
/**
* Return file viewer URL
*
* @param string $file File name
* @param string $mimetype File type
*/
public function href($file, $mimetype = null)
{
return $this->api->file_url($file) . '&viewer=text';
}
/**
* Print output and exit
*
* @param string $file File name
* @param string $mimetype File type
*/
public function output($file, $mimetype = null)
{
$mode = $this->get_mode($mimetype, $file);
$href = addcslashes($this->api->file_url($file), "'");
echo '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Editor</title>
<script src="viewers/text/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="viewers/text/file_editor.js" type="text/javascript" charset="utf-8"></script>
<style>
#editor { top: 0; right: 0; bottom: 0; left: 0; position: absolute; font-size: 14px; padding: 0; margin: 0; }
.ace_search_options { float: right; }
</style>
</head>
<body>
<pre id="editor">';
$this->print_file($file);
echo "</pre>
<script>
var file_editor = new file_editor;
file_editor.init('editor', '$mode', '$href');
</script>
</body>
</html>";
}
protected function get_mode($mimetype, $filename)
{
$mimetype = strtolower($mimetype);
if ($this->mimetypes[$mimetype]) {
return $this->mimetypes[$mimetype];
}
$filename = explode('.', $filename);
$extension = count($filename) > 1 ? array_pop($filename) : null;
if ($extension) {
foreach ($this->extensions as $mode => $regexp) {
if (preg_match($regexp, $extension)) {
return $mode;
}
}
}
return 'text';
}
}
/**
* PHP stream filter to detect escape html special chars in a file
*/
class file_viewer_content_filter extends php_user_filter
{
private $buffer = '';
private $cutoff = 2048;
function onCreate()
{
$this->cutoff = rand(2048, 3027);
return true;
}
function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$bucket->data = htmlspecialchars($bucket->data, ENT_COMPAT | ENT_HTML401 | ENT_IGNORE);
$this->buffer .= $bucket->data;
// keep buffer small enough
if (strlen($this->buffer) > 4096) {
$this->buffer = substr($this->buffer, $this->cutoff);
}
$consumed += $bucket->datalen; // or strlen($bucket->data)?
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
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
@@ -1,542 +1,552 @@
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
function files_api()
{
var ref = this;
// default config
this.translations = {};
this.env = {
url: 'api/',
directory_separator: '/',
resources_dir: 'resources'
};
/*********************************************************/
/********* Basic utilities *********/
/*********************************************************/
// set environment variable(s)
this.set_env = function(p, value)
{
if (p != null && typeof p === 'object' && !value)
for (var n in p)
this.env[n] = p[n];
else
this.env[p] = value;
};
// add a localized label(s) to the client environment
this.tdef = function(p, value)
{
if (typeof p == 'string')
this.translations[p] = value;
else if (typeof p == 'object')
$.extend(this.translations, p);
};
// return a localized string
this.t = function(label)
{
if (this.translations[label])
return this.translations[label];
else
return label;
};
// print a message into browser console
this.log = function(msg)
{
if (window.console && console.log)
console.log(msg);
};
/********************************************************/
/********* Remote request methods *********/
/********************************************************/
// send a http POST request to the API service
this.post = function(action, data, func)
{
var url = this.env.url + '?method=' + action;
if (!func) func = 'response';
this.set_request_time();
return $.ajax({
type: 'POST', url: url, data: JSON.stringify(data), dataType: 'json',
contentType: 'application/json; charset=utf-8',
success: function(response) { ref[func](response); },
error: function(o, status, err) { ref.http_error(o, status, err); },
cache: false,
beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', ref.env.token); }
});
};
// send a http GET request to the API service
this.get = function(action, data, func)
{
var url = this.env.url;
if (!func) func = 'response';
this.set_request_time();
data.method = action;
return $.ajax({
type: 'GET', url: url, data: data, dataType: 'json',
success: function(response) { ref[func](response); },
error: function(o, status, err) { ref.http_error(o, status, err); },
cache: false,
beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', ref.env.token); }
});
};
// send request with auto-selection of POST/GET method
this.request = function(action, data, func)
{
// Use POST for modification actions with probable big request size
var method = /(create|delete|move|copy|update)/.test(action) ? 'post' : 'get';
return this[method](action, data, func);
};
// handle HTTP request errors
this.http_error = function(request, status, err)
{
var errmsg = request.statusText;
this.set_busy(false);
request.abort();
if (request.status && errmsg)
this.display_message(this.t('servererror') + ' (' + errmsg + ')', 'error');
};
this.response = function(response)
{
this.update_request_time();
this.set_busy(false);
return this.response_parse(response);
};
this.response_parse = function(response)
{
if (!response || response.status != 'OK') {
// Logout on invalid-session error
if (response && response.code == 403)
this.logout(response);
else
this.display_message(response && response.reason ? response.reason : this.t('servererror'), 'error');
return false;
}
return true;
};
/*********************************************************/
/********* Utilities *********/
/*********************************************************/
// Called on "session expired" session
this.logout = function(response) {};
// set state
this.set_busy = function(a, message) {};
// displays error message
this.display_message = function(label) {};
// called when a request timed out
this.request_timed_out = function() {};
// called on start of the request
this.set_request_time = function() {};
// called on request response
this.update_request_time = function() {};
/*********************************************************/
/********* Helpers *********/
/*********************************************************/
// compose a valid url with the given parameters
this.url = function(action, query)
{
var k, param = {},
querystring = typeof query === 'string' ? '&' + query : '';
if (typeof action !== 'string')
query = action;
else if (!query || typeof query !== 'object')
query = {};
// overwrite task name
if (action)
query.method = action;
// remove undefined values
for (k in query) {
if (query[k] !== undefined && query[k] !== null)
param[k] = query[k];
}
return '?' + $.param(param) + querystring;
};
// 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);
items_len = items.length;
for (n=0; n<items_len-1; n++) {
tmp = items.slice(0,n+1);
f = tmp.join(this.env.directory_separator);
if (!folders[f])
folders[f] = {name: tmp.pop(), depth: n, id: 'f'+num++, virtual: 1};
}
folders[folder] = {name: items.pop(), depth: items_len-1, id: 'f'+num++};
}
return folders;
};
// folder structure presentation (structure icons)
this.folder_list_tree = function(folders)
{
var i, n, diff, tree = [], folder;
for (i in folders) {
items = i.split(this.env.directory_separator);
items_len = items.length;
// skip root
if (items_len < 2) {
tree = [];
continue;
}
folders[i].tree = [1];
for (n=0; n<tree.length; n++) {
folder = tree[n];
diff = folders[folder].depth - (items_len - 1);
if (diff >= 0)
folders[folder].tree[diff] = folders[folder].tree[diff] ? folders[folder].tree[diff] + 2 : 2;
}
tree.push(i);
}
for (i in folders) {
if (tree = folders[i].tree) {
var html = '', divs = [];
for (n=0; n<folders[i].depth; n++) {
if (tree[n] > 2)
divs.push({'class': 'l3', width: 15});
else if (tree[n] > 1)
divs.push({'class': 'l2', width: 15});
else if (tree[n] > 0)
divs.push({'class': 'l1', width: 15});
// separator
else if (divs.length && !divs[divs.length-1]['class'])
divs[divs.length-1].width += 15;
else
divs.push({'class': null, width: 15});
}
for (n=divs.length-1; n>=0; n--) {
if (divs[n]['class'])
html += '<span class="tree '+divs[n]['class']+'" />';
else
html += '<span style="width:'+divs[n].width+'px" />';
}
if (html)
$('#' + folders[i].id + ' span.branch').html(html);
}
}
};
// convert content-type string into class name
this.file_type_class = function(type)
{
if (!type)
return '';
type = type.replace(/[^a-z0-9]/g, '_');
return type;
};
// convert bytes into number with size unit
this.file_size = function(size)
{
if (size >= 1073741824)
return parseFloat(size/1073741824).toFixed(2) + ' GB';
if (size >= 1048576)
return parseFloat(size/1048576).toFixed(2) + ' MB';
if (size >= 1024)
return parseInt(size/1024) + ' kB';
return parseInt(size || 0) + ' B';
};
// Extract file name from full path
this.file_name = function(path)
{
var path = path.split(this.env.directory_separator);
return path.pop();
};
// Extract file path from full path
this.file_path = function(path)
{
var path = path.split(this.env.directory_separator);
path.pop();
return path.join(this.env.directory_separator);
};
// compare two sortable objects
this.sort_compare = function(data1, data2)
{
var key = this.env.sort_col || 'name';
if (key == 'mtime')
key = 'modified';
data1 = data1[key];
data2 = data2[key];
if (key == 'size' || key == 'modified')
// numeric comparison
return this.env.sort_reverse ? data2 - data1 : data1 - data2;
else {
// use Array.sort() for string comparison
var arr = [data1, data2];
arr.sort(function (a, b) {
// @TODO: use localeCompare() arguments for better results
return a.localeCompare(b);
});
if (this.env.sort_reverse)
arr.reverse();
return arr[0] === data2 ? 1 : -1;
}
};
// Checks if specified mimetype is supported natively by the browser (return 1)
// or can be displayed in the browser using File API viewer (return 2)
this.file_type_supported = function(type)
{
var i, t, regexps = [], img = 'jpg|jpeg|gif|bmp|png',
caps = this.env.browser_capabilities || {};
if (caps.tif)
img += '|tiff';
if ((new RegExp('^image/(' + img + ')$', 'i')).test(type))
return 1;
// prefer text viewer for any text type
if (/^text\/(?!(pdf|x-pdf))/i.test(type))
return 2;
if (caps.pdf) {
regexps.push(/^application\/(pdf|x-pdf|acrobat|vnd.pdf)/i);
regexps.push(/^text\/(pdf|x-pdf)/i);
}
if (caps.flash)
regexps.push(/^application\/x-shockwave-flash/i);
for (i in regexps)
if (regexps[i].test(type))
return 1;
for (i in navigator.mimeTypes) {
t = navigator.mimeTypes[i].type;
if (t == type && navigator.mimeTypes[i].enabledPlugin)
return 1;
}
// types with viewer support
if ($.inArray(type, this.env.supported_mimetypes) > -1)
return 2;
};
// Return browser capabilities
this.browser_capabilities = function()
{
var i, caps = [], ctypes = ['pdf', 'flash', 'tif'];
for (i in ctypes)
if (this.env.browser_capabilities[ctypes[i]])
caps.push(ctypes[i]);
return caps;
};
// Checks browser capabilities eg. PDF support, TIF support
this.browser_capabilities_check = function()
{
if (!this.env.browser_capabilities)
this.env.browser_capabilities = {};
if (this.env.browser_capabilities.pdf === undefined)
this.env.browser_capabilities.pdf = this.pdf_support_check();
if (this.env.browser_capabilities.flash === undefined)
this.env.browser_capabilities.flash = this.flash_support_check();
if (this.env.browser_capabilities.tif === undefined)
this.tif_support_check();
};
this.tif_support_check = function()
{
var img = new Image(), ref = this;
img.onload = function() { ref.env.browser_capabilities.tif = 1; };
img.onerror = function() { ref.env.browser_capabilities.tif = 0; };
img.src = this.env.resources_dir + '/blank.tif';
};
this.pdf_support_check = function()
{
var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/pdf"] : {},
plugins = navigator.plugins,
len = plugins.length,
regex = /Adobe Reader|PDF|Acrobat/i,
ref = this;
if (plugin && plugin.enabledPlugin)
return 1;
if (window.ActiveXObject) {
try {
if (axObj = new ActiveXObject("AcroPDF.PDF"))
return 1;
}
catch (e) {}
try {
if (axObj = new ActiveXObject("PDF.PdfCtrl"))
return 1;
}
catch (e) {}
}
for (i=0; i<len; i++) {
plugin = plugins[i];
if (typeof plugin === 'String') {
if (regex.test(plugin))
return 1;
}
else if (plugin.name && regex.test(plugin.name))
return 1;
}
return 0;
};
this.flash_support_check = function()
{
var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/x-shockwave-flash"] : {};
if (plugin && plugin.enabledPlugin)
return 1;
if (window.ActiveXObject) {
try {
if (axObj = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
return 1;
}
catch (e) {}
}
return 0;
};
// converts number of seconds into HH:MM:SS format
this.time_format = function(s)
{
s = parseInt(s);
if (s >= 60*60*24)
return '-';
return (new Date(1970, 1, 1, 0, 0, s, 0)).toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1');
};
};
// Add escape() method to RegExp object
// http://dev.rubyonrails.org/changeset/7271
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)
{
if (window.encodeURIComponent)
return encodeURIComponent(str).replace('*', '%2A');
return escape(str)
.replace('+', '%2B')
.replace('*', '%2A')
.replace('/', '%2F')
.replace('@', '%40');
};
function escapeHTML(str)
{
return str === undefined ? '' : String(str)
.replace(/&/g, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;');
};
function object_is_empty(obj)
{
if (obj)
for (var i in obj)
if (i !== null)
return true;
return false;
}
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
@@ -1,1727 +1,1963 @@
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
function files_ui()
{
var ref = this;
this.request_timeout = 300;
this.message_time = 3000;
this.events = {};
this.commands = {};
this.requests = {};
this.uploads = {};
this.ie = document.all && !window.opera;
this.env = {
url: 'api/',
sort_col: 'name',
sort_reverse: 0,
search_threads: 1,
directory_separator: '/',
resources_dir: 'resources'
};
// set jQuery ajax options
$.ajaxSetup({
cache: false,
error: function(request, status, err) { ref.http_error(request, status, err); },
beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', ref.env.token); }
});
/*********************************************************/
/********* basic utilities *********/
/*********************************************************/
// initialize interface
this.init = function()
{
if (!this.env.token)
return;
if (this.env.task == 'main') {
this.enable_command('folder.list', 'folder.create', 'file.search', true);
this.command('folder.list');
}
else if (this.env.task == 'file') {
this.load_file('#file-content', this.env.filedata);
this.enable_command('file.delete', 'file.download', true);
}
if (!this.env.browser_capabilities)
this.browser_capabilities_check();
};
// set environment variable(s)
this.set_env = function(p, value)
{
if (p != null && typeof p === 'object' && !value)
for (var n in p)
this.env[n] = p[n];
else
this.env[p] = value;
};
// execute a specific command on the web client
this.command = function(command, props, obj)
{
if (obj && obj.blur)
obj.blur();
if (this.busy)
return false;
if (!this.commands[command])
return;
var ret = undefined,
func = command.replace(/[^a-z]/g, '_'),
task = command.replace(/\.[a-z-_]+$/g, '');
if (this[func] && typeof this[func] === 'function') {
ret = this[func](props);
}
return ret === false ? false : obj ? false : true;
};
this.set_busy = function(a, message)
{
if (a && this.busy)
return;
if (a && message) {
var msg = this.t(message);
if (msg == message)
msg = 'Loading...';
this.display_message(msg, 'loading');
}
else if (!a) {
this.hide_message('loading');
}
this.busy = a;
// if (this.gui_objects.editform)
// this.lock_form(this.gui_objects.editform, a);
// clear pending timer
if (this.request_timer)
clearTimeout(this.request_timer);
// set timer for requests
if (a && this.request_timeout)
this.request_timer = window.setTimeout(function() { ref.request_timed_out(); }, this.request_timeout * 1000);
};
// called when a request timed out
this.request_timed_out = function()
{
this.set_busy(false);
this.display_message('Request timed out!', 'error');
};
// Add variable to GET string, replace old value if exists
this.add_url = function(url, name, value)
{
value = urlencode(value);
if (/(\?.*)$/.test(url)) {
var urldata = RegExp.$1,
datax = RegExp('((\\?|&)'+RegExp.escape(name)+'=[^&]*)');
if (datax.test(urldata))
urldata = urldata.replace(datax, RegExp.$2 + name + '=' + value);
else
urldata += '&' + name + '=' + value
return url.replace(/(\?.*)$/, urldata);
}
return url + '?' + name + '=' + value;
};
this.trigger_event = function(event, data)
{
if (this.events[event])
for (var i in this.events[event])
this.events[event][i](data);
};
this.add_event_listener = function(event, func)
{
if (!this.events[event])
this.events[event] = [];
this.events[event].push(func);
};
this.buttons = function(p)
{
$.each(p, function(i, v) {
if (!ui.buttons[i])
ui.buttons[i] = [];
if (typeof v == 'object')
ui.buttons[i] = $.merge(ui.buttons[i], v);
else
ui.buttons[i].push(v);
});
};
this.enable_command = function()
{
var i, n, args = Array.prototype.slice.call(arguments),
enable = args.pop(), cmd;
for (n=0; n<args.length; n++) {
cmd = args[n];
// argument of type array
if (typeof cmd === 'string') {
this.commands[cmd] = enable;
if (this.buttons[cmd])
$.each(this.buttons[cmd], function (i, button) {
$('#'+button)[enable ? 'removeClass' : 'addClass']('disabled');
});
this.trigger_event('enable-command', {command: cmd, status: enable});
}
// push array elements into commands array
else {
for (i in cmd)
args.push(cmd[i]);
}
}
};
/*********************************************************/
/********* GUI functionality *********/
/*********************************************************/
// write to the document/window title
this.set_pagetitle = function(title)
{
if (title && document.title)
document.title = title;
};
// display a system message (types: loading, notice, error)
this.display_message = function(msg, type, timeout)
{
var obj, ref = this;
if (!type)
type = 'notice';
if (msg)
msg = this.t(msg);
if (type == 'loading') {
timeout = this.request_timeout * 1000;
if (!msg)
msg = this.t('loading');
}
else if (!timeout)
timeout = this.message_time * (type == 'error' || type == 'warning' ? 2 : 1);
obj = $('<div>');
if (type != 'loading') {
msg = '<div><span>' + msg + '</span></div>';
obj.addClass(type).click(function() { return ref.hide_message(); });
}
if (timeout > 0)
window.setTimeout(function() { ref.hide_message(type, type != 'loading'); }, timeout);
obj.attr('id', type == 'loading' ? 'loading' : 'message')
.appendTo('body').html(msg).show();
};
// make a message to disapear
this.hide_message = function(type, fade)
{
if (type == 'loading')
$('#loading').remove();
else
$('#message').fadeOut('normal', function() { $(this).remove(); });
};
this.set_watermark = function(id)
{
if (this.env.watermark)
$('#'+id).html(this.env.watermark);
};
/********************************************************/
/********* Remote request methods *********/
/********************************************************/
/*
// send a http POST request to the server
this.http_post = function(action, postdata)
{
var url = this.url(action);
if (postdata && typeof postdata === 'object')
postdata.remote = 1;
else {
if (!postdata)
postdata = '';
postdata += '&remote=1';
}
this.set_request_time();
return $.ajax({
type: 'POST', url: url, data: postdata, dataType: 'json',
success: function(response) { ui.http_response(response); },
error: function(o, status, err) { ui.http_error(o, status, err); }
});
};
// handle HTTP response
this.http_response = function(response)
{
var i;
if (!response)
return;
// set env vars
if (response.env)
this.set_env(response.env);
// we have translation labels to add
if (typeof response.labels === 'object')
this.tdef(response.labels);
// HTML page elements
if (response.objects)
for (i in response.objects)
$('#'+i).html(response.objects[i]);
this.update_request_time();
this.set_busy(false);
// if we get javascript code from server -> execute it
if (response.exec)
eval(response.exec);
this.trigger_event('http-response', response);
};
// handle HTTP request errors
this.http_error = function(request, status, err)
{
var errmsg = request.statusText;
this.set_busy(false);
request.abort();
if (request.status && errmsg)
this.display_message(this.t('servererror') + ' (' + errmsg + ')', 'error');
};
*/
/********************************************************/
/********* Helper methods *********/
/********************************************************/
// disable/enable all fields of a form
this.lock_form = function(form, lock)
{
if (!form || !form.elements)
return;
var n, len, elm;
if (lock)
this.disabled_form_elements = [];
for (n=0, len=form.elements.length; n<len; n++) {
elm = form.elements[n];
if (elm.type == 'hidden')
continue;
// remember which elem was disabled before lock
if (lock && elm.disabled)
this.disabled_form_elements.push(elm);
// check this.disabled_form_elements before inArray() as a workaround for FF5 bug
// http://bugs.jquery.com/ticket/9873
else if (lock || (this.disabled_form_elements && $.inArray(elm, this.disabled_form_elements)<0))
elm.disabled = lock;
}
};
this.set_request_time = function()
{
this.env.request_time = (new Date()).getTime();
};
// Update request time element
this.update_request_time = function()
{
if (this.env.request_time) {
var t = ((new Date()).getTime() - this.env.request_time)/1000,
el = $('#reqtime');
el.text(el.text().replace(/[0-9.,]+/, t));
}
};
// position and display popup
this.popup_show = function(e, popup)
{
var popup = $(popup),
pos = this.mouse_pos(e),
win = $(window),
w = popup.width(),
h = popup.height(),
left = pos.left - w + 20,
top = pos.top - 10;
if (top + h > win.height())
top -= h;
if (left + w > win.width())
left -= w;
popup.css({left: left + 'px', top: top + 'px'})
.click(function(e) { e.stopPropagation(); $(this).hide(); }).show();
e.stopPropagation();
};
// Return absolute mouse position of an event
this.mouse_pos = function(e)
{
if (!e) e = window.event;
var mX = e.pageX ? e.pageX : e.clientX,
mY = e.pageY ? e.pageY : e.clientY;
if (document.body && document.all) {
mX += document.body.scrollLeft;
mY += document.body.scrollTop;
}
if (e._offset) {
mX += e._offset.left;
mY += e._offset.top;
}
return {left:mX, top:mY};
};
this.serialize_form = function(id)
{
var i, v, json = {},
form = $(id),
query = form.serializeArray();
for (i in query)
json[query[i].name] = query[i].value;
// serializeArray() doesn't work properly for multi-select
$('select[multiple="multiple"]', form).each(function() {
var name = this.name;
json[name] = [];
$(':selected', this).each(function() {
json[name].push(this.value);
});
});
return json;
};
/*********************************************************/
/********* Commands and response handlers *********/
/*********************************************************/
this.logout = function()
{
this.main_logout();
};
this.main_logout = function(params)
{
location.href = '?task=main&action=logout' + (params ? '&' + $.param(params) : '');
return false;
};
// folder list request
this.folder_list = function()
{
this.set_busy(true, 'loading');
this.request('folder_list', {}, 'folder_list_response');
};
// folder list response handler
this.folder_list_response = function(response)
{
if (!this.response(response))
return;
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();
$.each(this.env.folders, function(i, f) {
var row = ui.folder_list_row(i, f);
table.append(row);
});
// add virtual collections
$.each(['audio', 'video', 'image', 'document'], function(i, n) {
var row = $('<tr><td><span class="name"></span></td></tr>'),
span = $('span.name', row);
row.attr('id', 'folder-collection-' + n);
span.text(ui.t('collection.' + n))
.click(function() { ui.folder_select(n, true); });
if (n == ui.env.collection)
row.addClass('selected');
table.append(row);
});
// 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)
{
this.env.search = null;
this.file_search_stop();
var list = $('#folderlist');
$('tr.selected', list).removeClass('selected');
if (is_collection) {
var found = $('#folder-collection-' + folder, list).addClass('selected');
this.env.folder = null;
this.enable_command('file.list', true);
this.enable_command('folder.delete', 'folder.edit', 'file.upload', false);
this.file_list({collection: folder});
}
else {
var found = $('#' + this.env.folders[folder].id, list).addClass('selected');
this.env.collection = null;
this.enable_command('file.list', 'folder.delete', 'folder.edit', 'file.upload', found.length);
this.file_list({folder: folder});
}
};
this.folder_unselect = function()
{
this.env.search = null;
this.env.folder = null;
this.env.collection = null;
this.file_search_stop();
var list = $('#folderlist');
$('tr.selected', list).removeClass('selected');
this.enable_command('file.list', 'folder.delete', 'folder.edit', 'file.upload', false);
};
// folder create request
this.folder_create = function(folder)
{
if (!folder) {
this.folder_create_start();
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
this.folder_create_response = function(response)
{
if (!this.response(response))
return;
+ this.folder_create_stop();
this.folder_list();
};
// folder rename request
this.folder_edit = function(folder)
{
if (!folder) {
this.folder_edit_start();
return;
}
this.set_busy(true, 'saving');
this.request('folder_move', {folder: folder.folder, 'new': folder['new']}, 'folder_rename_response');
};
// folder rename response handler
this.folder_rename_response = function(response)
{
if (!this.response(response))
return;
this.env.folder = this.env.folder_rename;
this.folder_list();
};
// folder delete request
this.folder_delete = function(folder)
{
if (folder === undefined)
folder = this.env.folder;
if (!folder)
return;
// @todo: confirm
this.set_busy(true, 'saving');
this.request('folder_delete', {folder: folder}, 'folder_delete_response');
};
// folder delete response handler
this.folder_delete_response = function(response)
{
if (!this.response(response))
return;
this.env.folder = null;
$('#filelist tbody').empty();
this.enable_command('folder.delete', 'folder.edit', 'file.list', 'file.search', 'file.upload', false);
this.folder_list();
};
// file list request
this.file_list = function(params)
{
if (!params)
params = {};
var i, req = (new Date).getTime();
// reset all pending list requests
for (i in this.requests) {
this.requests[i].abort();
delete this.requests[i];
}
if (params.all_folders) {
params.collection = null;
params.folder = null;
this.folder_unselect();
}
if (params.collection == undefined)
params.collection = this.env.collection;
if (params.folder == undefined)
params.folder = this.env.folder;
if (params.sort == undefined)
params.sort = this.env.sort_col;
if (params.reverse == undefined)
params.reverse = this.env.sort_reverse;
if (params.search == undefined)
params.search = this.env.search;
this.env.collection = params.collection;
this.env.folder = params.folder;
this.env.sort_col = params.sort;
this.env.sort_reverse = params.reverse;
// empty the list
$('#filelist tbody').empty();
this.env.file_list = [];
this.env.list_shift_start = null;
this.enable_command('file.open', 'file.get', 'file.rename', 'file.delete', 'file.copy', 'file.move', false);
// request
if (params.collection || params.all_folders)
this.file_list_loop(params);
else {
this.set_busy(true, 'loading');
this.requests[req] = this.request('file_list', params, 'file_list_response');
}
};
// call file.list request for every folder (used for search and virt. collections)
this.file_list_loop = function(params)
{
var i, folders = [], req = (new Date).getTime(),
limit = Math.max(this.env.search_threads || 1, 1);
if (params.collection) {
if (!params.search)
params.search = {};
params.search['class'] = params.collection;
delete params['collection'];
}
delete params['all_folders'];
$.each(this.env.folders, function(i, f) {
if (!f.virtual)
folders.push(i);
});
this.env.folders_loop = folders;
this.env.folders_loop_params = params;
this.env.folders_loop_lock = false;
for (i=0; i<folders.length && i<limit; i++) {
this.set_busy(true, 'loading');
params.folder = folders.shift();
this.requests[req+'-'+i] = this.request('file_list', params, 'file_list_loop_response');
}
};
// file list response handler
this.file_list_response = function(response)
{
if (!this.response(response))
return;
var table = $('#filelist'), list = [];
$.each(response.result, function(key, data) {
var row = ui.file_list_row(key, data);
table.append(row);
data.row = row;
list.push(data);
});
this.env.file_list = list;
};
// file list response handler for loop'ed request
this.file_list_loop_response = function(response)
{
var i, folders = this.env.folders_loop,
params = this.env.folders_loop_params,
limit = Math.max(this.env.search_threads || 1, 1),
valid = this.response(response),
req = (new Date).getTime();
for (i=0; i<folders.length && i<limit; i++) {
this.set_busy(true, 'loading');
params.folder = folders.shift();
this.requests[req+'-'+i] = this.request('file_list', params, 'file_list_loop_response');
}
if (!valid)
return;
this.file_list_loop_result_add(response.result);
};
// add files from list request to the table (with sorting)
this.file_list_loop_result_add = function(result)
{
// chack if result (hash-array) is empty
if (!object_is_empty(result))
return;
if (this.env.folders_loop_lock) {
setTimeout(function() { ui.file_list_loop_result_add(result); }, 100);
return;
}
// lock table, other list responses will wait
this.env.folders_loop_lock = true;
var n, i, len, elem, list = [], table = $('#filelist');
for (n=0, len=this.env.file_list.length; n<len; n++) {
elem = this.env.file_list[n];
for (i in result) {
if (this.sort_compare(elem, result[i]) < 0)
break;
var row = this.file_list_row(i, result[i]);
elem.row.before(row);
result[i].row = row;
list.push(result[i]);
delete result[i];
}
list.push(elem);
}
// add the rest of rows
$.each(result, function(key, data) {
var row = ui.file_list_row(key, data);
table.append(row);
result[key].row = row;
list.push(result[key]);
});
this.env.file_list = list;
this.env.folders_loop_lock = false;
};
// sort files list (without API request)
this.file_list_sort = function(col, reverse)
{
var n, len, list = this.env.file_list,
table = $('#filelist'), tbody = $('<tbody>');
this.env.sort_col = col;
this.env.sort_reverse = reverse;
if (!list || !list.length)
return;
// sort the list
list.sort(function (a, b) {
return ui.sort_compare(a, b);
});
// add rows to the new body
for (n=0, len=list.length; n<len; n++) {
tbody.append(list[n].row);
}
// replace table bodies
$('tbody', table).replaceWith(tbody);
};
// file delete request
this.file_delete = function(file)
{
if (!file) {
file = [];
if (this.env.file)
file.push(this.env.file);
else
file = this.file_list_selected();
}
this.set_busy(true, 'deleting');
this.request('file_delete', {file: file}, 'file_delete_response');
};
// file delete response handler
this.file_delete_response = function(response)
{
if (!this.response(response))
return;
if (this.env.file) {
var path = this.file_path(this.env.file);
if (window.opener && opener.ui && (!opener.ui.env.folder || opener.ui.env.folder == path))
opener.ui.file_list();
window.close();
}
else
this.file_list();
};
// file rename request
this.file_rename = function(file, newname)
{
if (file === newname)
return;
this.set_busy(true, 'saving');
this.request('file_move', {file: file, 'new': newname}, 'file_rename_response');
};
// file rename response handler
this.file_rename_response = function(response)
{
if (!this.response(response))
return;
// @TODO: we could update list/file metadata and just sort
this.file_list();
};
// file copy request
this.file_copy = function(folder)
{
var count = 0, list = {}, files = this.file_list_selected();
if (!files || !files.length || !folder)
return;
$.each(files, function(i, v) {
var name = folder + ui.env.directory_separator + ui.file_name(v);
if (name != v) {
list[v] = name;
count++;
}
});
if (!count)
return;
this.set_busy(true, 'copying');
this.request('file_copy', {file: list}, 'file_copy_response');
};
// file copy response handler
this.file_copy_response = function(response)
{
if (!this.response(response))
return;
if (response.result && response.result.already_exist && response.result.already_exist.length)
this.file_move_ask_user(response.result.already_exist);
};
// file move request
this.file_move = function(folder)
{
var count = 0, list = {}, files = this.file_list_selected();
if (!files || !files.length || !folder)
return;
$.each(files, function(i, v) {
var name = folder + ui.env.directory_separator + ui.file_name(v);
if (name != v) {
list[v] = name;
count++;
}
});
if (!count)
return;
this.set_busy(true, 'moving');
this.request('file_move', {file: list}, 'file_move_response');
};
// file move response handler
this.file_move_response = function(response)
{
if (!this.response(response))
return;
if (response.result && response.result.already_exist && response.result.already_exist.length)
this.file_move_ask_user(response.result.already_exist, true);
else
this.file_list();
};
this.file_download = function(file)
{
if (!file)
file = this.env.file;
location.href = this.env.url + this.url('file_get', {token: this.env.token, file: file, 'force-download': 1});
};
// file upload request
this.file_upload = function()
{
var i, size = 0, maxsize = this.env.capabilities.MAX_UPLOAD,
form = $('#uploadform'),
field = $('input[type=file]', form).get(0),
files = field.files ? field.files.length : field.value ? 1 : 0;
if (files) {
// check upload max size
if (field.files && maxsize) {
for (i=0; i < files; i++)
size += field.files[i].size;
if (size > maxsize) {
alert(this.t('upload.size.error').replace('$size', this.file_size(maxsize)));
return;
}
}
// submit form and read server response
this.file_upload_form(form, 'file_upload', function(e, frame, folder) {
var doc, response, res;
try {
doc = frame.contentDocument ? frame.contentDocument : frame.contentWindow.document;
response = doc.body.innerHTML;
// in Opera onload is called twice, once with empty body
if (!response)
return;
// response may be wrapped in <pre> tag
if (response.match(/^<pre[^>]*>(.*)<\/pre>$/i))
response = RegExp.$1;
response = eval('(' + response + ')');
}
catch (err) {
response = {status: 'ERROR'};
}
if ((res = ui.response_parse(response)) && folder == ui.env.folder)
ui.file_list();
return res;
});
}
};
/*********************************************************/
/********* 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)
{
var row = $('<tr><td><span class="branch"></span><span class="name"></span></td></tr>'),
span = $('span.name', row);
span.text(data.name);
row.attr('id', data.id).data('folder', folder);
if (data.depth)
$('span.branch', row).width(15 * data.depth);
if (data.virtual)
row.addClass('virtual');
else {
span.click(function() { ui.folder_select(folder); })
row.mouseenter(function() {
if (ui.drag_active && (!ui.env.folder || ui.env.folder != $(this).data('folder')))
$(this).addClass('droptarget');
})
.mouseleave(function() {
if (ui.drag_active)
$(this).removeClass('droptarget');
});
if (folder == this.env.folder)
row.addClass('selected');
}
return row;
};
// create files table row
this.file_list_row = function(filename, data)
{
var row = $('<tr><td class="filename"></td>'
+' <td class="filemtime"></td><td class="filesize"></td></tr>'),
link = $('<span></span>').text(data.name).click(function(e) { ui.file_menu(e, filename, data.type); });
$('td.filename', row).addClass(ui.file_type_class(data.type)).append(link);
$('td.filemtime', row).text(data.mtime);
$('td.filesize', row).text(ui.file_size(data.size));
row.attr('data-file', filename)
.click(function(e) { ui.file_list_click(e, this); })
.mousedown(function(e) { return ui.file_list_drag(e, this); });
// disables selection in IE
if (document.all)
row.on('selectstart', function() { return false; });
return row;
};
// file row click event handler
this.file_list_click = function(e, row)
{
var list = $('#filelist'), org = row, row = $(row),
found, selected, shift = this.env.list_shift_start;
if (e.shiftKey && shift && org != shift) {
$('tr', list).each(function(i, r) {
if (r == org) {
found = 1;
$(r).addClass('selected');
return;
}
else if (!selected && r == shift) {
selected = 1;
return;
}
if ((!found && selected) || (found && !selected))
$(r).addClass('selected');
else
$(r).removeClass('selected');
});
}
else if (e.ctrlKey)
row.toggleClass('selected');
else {
$('tr.selected', list).removeClass('selected');
$(row).addClass('selected');
this.env.list_shift_start = org;
}
selected = $('tr.selected', list).length;
if (!selected)
this.env.list_shift_start = null;
this.enable_command('file.delete', 'file.move', 'file.copy', selected);
this.enable_command('file.open', 'file.get', 'file.rename', selected == 1);
};
// file row drag start event handler
this.file_list_drag = function(e, row)
{
if (e.shiftKey || e.ctrlKey || $(e.target).is('input'))
return true;
// selects currently unselected row
if (!$(row).hasClass('selected'))
this.file_list_click(e, row);
this.drag_start = true;
this.drag_mouse_start = this.mouse_pos(e);
$(document)
.on('mousemove.draghandler', function(e) { ui.file_list_drag_mouse_move(e); })
.on('mouseup.draghandler', function(e) { ui.file_list_drag_mouse_up(e); });
/*
if (bw.mobile) {
$(document)
.on('touchmove.draghandler', function(e) { ui.file_list_drag_mouse_move(e); })
.on('touchend.draghandler', function(e) { ui.file_list_drag_mouse_up(e); });
}
*/
return false;
};
// file row mouse move event handler
this.file_list_drag_mouse_move = function(e)
{
/*
// convert touch event
if (e.type == 'touchmove') {
if (e.changedTouches.length == 1)
e = rcube_event.touchevent(e.changedTouches[0]);
else
return rcube_event.cancel(e);
}
*/
var max_rows = 10, pos = this.mouse_pos(e);
if (this.drag_start) {
// check mouse movement, of less than 3 pixels, don't start dragging
if (!this.drag_mouse_start || (Math.abs(pos.left - this.drag_mouse_start.left) < 3 && Math.abs(pos.top - this.drag_mouse_start.top) < 3))
return false;
if (!this.draglayer)
this.draglayer = $('<div>').attr('id', 'draglayer')
.css({position:'absolute', display:'none', 'z-index':2000})
.appendTo(document.body);
// reset content
this.draglayer.html('');
// get subjects of selected messages
$('#filelist tr.selected').slice(0, max_rows+1).each(function(i) {
if (i == 0)
ui.drag_start_pos = $(this).offset();
else if (i == max_rows) {
ui.draglayer.append('...');
return;
}
var subject = $('td.filename', this).text();
// truncate filename to 50 characters
if (subject.length > 50)
subject = subject.substring(0, 50) + '...';
ui.draglayer.append($('<div>').text(subject));
});
this.draglayer.show();
this.drag_active = true;
}
if (this.drag_active && this.draglayer)
this.draglayer.css({left:(pos.left+20)+'px', top:(pos.top-5 + (this.ie ? document.documentElement.scrollTop : 0))+'px'});
this.drag_start = false;
return false;
};
// file row mouse up event handler
this.file_list_drag_mouse_up = function(e)
{
document.onmousemove = null;
/*
if (e.type == 'touchend') {
if (e.changedTouches.length != 1)
return rcube_event.cancel(e);
}
*/
$(document).off('.draghandler');
this.drag_active = false;
var got_folder = this.file_list_drag_end(e);
if (this.draglayer && this.draglayer.is(':visible')) {
if (this.drag_start_pos && !got_folder)
this.draglayer.animate(this.drag_start_pos, 300, 'swing').hide(20);
else
this.draglayer.hide();
}
};
// files drag end handler
this.file_list_drag_end = function(e)
{
var folder = $('#folderlist tr.droptarget').removeClass('droptarget');
if (folder.length) {
folder = folder.data('folder');
if (e.shiftKey && this.commands['file.copy']) {
this.file_drag_menu(e, folder);
return true;
}
this.command('file.move', folder);
return true;
}
};
// display file drag menu
this.file_drag_menu = function(e, folder)
{
var menu = $('#file-drag-menu');
$('li.file-copy > a', menu).off('click').click(function() { ui.command('file.copy', folder); });
$('li.file-move > a', menu).off('click').click(function() { ui.command('file.move', folder); });
this.popup_show(e, menu);
};
// display file menu
this.file_menu = function(e, file, type)
{
var href, caps, viewer,
menu = $('#file-menu'),
open_action = $('li.file-open > a', menu);
if (viewer = this.file_type_supported(type)) {
caps = this.browser_capabilities().join();
href = '?' + $.param({task: 'file', action: 'open', token: this.env.token, file: file, caps: caps, viewer: viewer == 2 ? 1 : 0});
open_action.attr({target: '_blank', href: href}).removeClass('disabled').off('click');
}
else
open_action.click(function() { return false; }).addClass('disabled');
$('li.file-download > a', menu)
.attr({href: this.env.url + this.url('file_get', {token: this.env.token, file: file, 'force-download': 1})});
$('li.file-delete > a', menu).off('click').click(function() { ui.file_delete(file); });
$('li.file-rename > a', menu).off('click').click(function() { ui.file_rename_start(e); });
this.popup_show(e, menu);
};
// returns selected files (with paths)
this.file_list_selected = function()
{
var files = [];
$('#filelist tr.selected').each(function() {
files.push($(this).data('file'));
});
return files;
};
this.file_rename_start = function(e)
{
var list = $('#filelist'),
tr = $(e.target).parents('tr'),
td = $('td.filename', tr),
file = tr.data('file'),
name = this.file_name(file),
input = $('<input>').attr({type: 'text', name: 'filename', 'class': 'filerename'})
.val(name).data('file', file)
.click(function(e) { e.stopPropagation(); })
.keydown(function(e) {
switch (e.which) {
case 27: // ESC
ui.file_rename_stop();
break;
case 13: // Enter
var elem = $(this),
newname = elem.val(),
oldname = elem.data('file'),
path = ui.file_path(file);
ui.file_rename(oldname, path + ui.env.directory_separator + newname);
elem.parent().text(newname);
break;
}
});
$('span', td).text('').append(input);
input.focus();
};
this.file_rename_stop = function()
{
$('input.filerename').each(function() {
var elem = $(this), name = ui.file_name(elem.data('file'));
elem.parent().text(name);
});
};
// post the given form to a hidden iframe
this.file_upload_form = function(form, action, onload)
{
var ts = new Date().getTime(),
frame_name = 'fileupload' + ts;
// upload progress supported (and handler exists)
if (this.env.capabilities.PROGRESS_NAME && window.progress_update) {
var fname = this.env.capabilities.PROGRESS_NAME,
field = $('input[name='+fname+']', form);
if (!field.length) {
field = $('<input>').attr({type: 'hidden', name: fname});
field.prependTo(form);
}
field.val(ts);
this.uploads[ts] = this.env.folder;
this.file_upload_progress(ts);
}
// have to do it this way for IE
// otherwise the form will be posted to a new window
if (document.all) {
document.body.insertAdjacentHTML('BeforeEnd',
'<iframe id="'+frame_name+'" name="'+frame_name+'"'
+ ' src="' + this.env.resources_dir + '/blank.gif" '
+ ' style="width:0;height:0;visibility:hidden;"></iframe>');
}
// for standards-compliant browsers
else
$('<iframe>')
.attr({name: frame_name, id: frame_name})
.css({border: 'none', width: 0, height: 0, visibility: 'hidden'})
.appendTo(document.body);
// handle upload errors, parsing iframe content in onload
$('#'+frame_name).load(function(e) {
// hide progressbar on upload error
if (!onload(e, this, ui.uploads[ts]) && window.progress_update)
window.progress_update({id: ts, done: true});
delete ui.uploads[ts];
});
$(form).attr({
target: frame_name,
action: this.env.url + this.url(action, {folder: this.env.folder, token: this.env.token, uploadid:ts}),
method: 'POST'
}).attr(form.encoding ? 'encoding' : 'enctype', 'multipart/form-data')
.submit();
};
// upload progress requests
this.file_upload_progress = function(id)
{
setTimeout(function() {
if (id && ui.uploads[id])
ui.request('upload_progress', {id: id}, 'file_upload_progress_response');
}, this.env.capabilities.PROGRESS_TIME * 1000);
};
// upload progress response
this.file_upload_progress_response = function(response)
{
if (!this.response(response))
return;
var param = response.result;
if (param.id && window.progress_update)
window.progress_update(param);
if (!param.done)
this.file_upload_progress(param.id);
};
// Display file search form
this.file_search = function()
{
var form = this.form_show('file-search'),
has_folder = this.env.folder || this.env.collection,
radio1 = $('input[name="all_folders"][value="0"]', form);
$('input[name="name"]', form).val('').focus();
if (has_folder)
radio1.prop('disabled', false).click();
else {
radio1.prop('disabled', true);
$('input[name="all_folders"][value="1"]', form).click();
}
};
// Hide file search form
this.file_search_stop = function()
{
if (this.env.search)
this.file_list(null, {search: null});
this.form_hide('file-search');
this.env.search = null;
};
// Execute file search
this.file_search_submit = function()
{
var form = this.form_show('file-search'),
value = $('input[name="name"]', form).val(),
all = $('input[name="all_folders"]:checked', form).val();
if (value) {
this.env.search = {name: value};
this.file_list({search: this.env.search, all_folders: all == 1});
}
else
this.file_search_stop();
};
// 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
this.folder_create_stop = function()
{
this.form_hide('folder-create');
};
// 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
this.folder_edit_start = function()
{
var form = this.form_show('folder-edit'),
arr = this.env.folder.split(this.env.directory_separator),
name = arr.pop();
this.env.folder_edit_path = arr.join(this.env.directory_separator);
$('input[name="name"]', form).val(name).focus();
};
// Hide folder edit form
this.folder_edit_stop = function()
{
this.form_hide('folder-edit');
};
// Submit folder edit form
this.folder_edit_submit = function()
{
var folder = '', data = this.serialize_form('#folder-edit-form');
if (!data.name)
return;
if (this.env.folder_edit_path)
folder = this.env.folder_edit_path + this.env.directory_separator;
folder += data.name;
this.env.folder_rename = folder;
this.folder_edit_stop();
this.command('folder.edit', {folder: this.env.folder, 'new': folder});
};
// when file move/copy operation returns file-exists error
// this displays a dialog where user can decide to skip
// or overwrite destination file(s)
this.file_move_ask_user = function(list, move)
{
var file = list[0], buttons = {},
label = this.t('file.moveconfirm').replace('$file', file.dst);
buttons['file.overwrite'] = function() {
var file = list.shift(), f = {},
action = move ? 'file_move' : 'file_copy';
f[file.src] = file.dst;
ui.file_move_ask_list = list;
ui.file_move_ask_mode = move;
this.hide();
ui.set_busy(true, move ? 'moving' : 'copying');
ui.request(action, {file: f, overwrite: 1}, 'file_move_ask_user_response');
};
if (list.length > 1)
buttons['file.overwriteall'] = function() {
var f = {}, action = move ? 'file_move' : 'file_copy';
$.each(list, function() { f[this.src] = this.dst; });
this.hide();
ui.set_busy(true, move ? 'moving' : 'copying');
ui.request(action, {file: f, overwrite: 1}, action + '_response');
};
buttons['file.skip'] = function() {
list.shift();
this.hide();
if (list.length)
ui.file_move_ask_user(list, move);
else if (move)
ui.file_list();
};
if (list.length > 1)
buttons['file.skipall'] = function() {
this.hide();
if (move)
ui.file_list();
};
this.modal_dialog(label, buttons);
};
// file move (with overwrite) response handler
this.file_move_ask_user_response = function(response)
{
var mode = this.file_move_ask_mode, list = this.file_move_ask_list;
this.response(response);
if (list && list.length)
this.file_move_ask_user(list, mode);
else if (mode)
this.file_list();
};
this.file_edit = function()
{
if (this.file_editor) {
this.file_editor.enable();
this.enable_command('file.save', true);
}
};
this.file_save = function()
{
if (!this.file_editor)
return;
var content = this.file_editor.getContent();
this.file_editor.disable();
// because we currently can edit only text file
// and we do not expect them to be very big, we save
// file in a very simple way, no upload progress, etc.
this.set_busy(true, 'saving');
this.request('file_update', {file: this.env.file, content: content, info: 1}, 'file_save_response');
};
this.file_save_response = function(response)
{
this.file_editor.enable();
if (!this.response(response))
return;
// update file properties table
var table = $('#filedata table'), file = response.result;
if (response.result) {
$('td.filetype', table).text(file.type);
$('td.filesize', table).text(this.file_size(file.size));
$('td.filemtime', table).text(file.mtime);
}
};
/*********************************************************/
/********* Utilities *********/
/*********************************************************/
// modal dialog popup
this.modal_dialog = function(content, buttons, opts)
{
var settings = {position: 'cm', btns: {}, fxShow: 'fade'},
dialog = $('<div class="_wModal"></div>'),
body = $('<div class="_wModal"></div>'),
head, foot, footer = [];
// title bar
if (opts && opts.title)
$('<div class="_wModal_header"></div>')
.append($('<span>').text(opts.title))
.appendTo(body);
// dialog content
if (typeof content != 'object')
content = $('<div></div>').html(content);
content.addClass('_wModal_msg').appendTo(body);
// buttons
$.each(buttons, function(i, v) {
var n = i.replace(/[^a-z0-9_]/ig, '');
settings.btns[n] = v;
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(); };
if (footer.length) {
foot = $('<div class="_wModal_btns"></div>');
$.each(footer, function() {
$('<div></div>').addClass('_wModal_btn_' + this.name).text(this.label).appendTo(foot);
});
body.append(foot);
}
// configure and display dialog
dialog.append(body).wModal(settings).wModal('show');
};
// Display folder creation form
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;
};
// Display folder creation form
this.form_hide = function(name)
{
var form = $('#' + name + '-form');
form.hide();
$('#taskcontent').css('top', 10);
};
// loads a file content into an iframe (with loading image)
this.load_file = function(content, filedata)
{
var iframe = $(content);
if (!iframe.length)
return;
var href = filedata.href,
div = iframe.parent(),
loader = $('#loader'),
offset = div.offset(),
w = loader.width(), h = loader.height(),
width = div.width(), height = div.height();
loader.css({
top: offset.top + height/2 - h/2 - 20,
left: offset.left + width/2 - w/2
}).show();
iframe.css('opacity', 0.1)
.load(function() { ui.loader_hide(this); })
.attr('src', href);
// some content, e.g. movies or flash doesn't execute onload on iframe
// let's wait some time and check document ready state
if (!/^text/i.test(filedata.mimetype))
setTimeout(function() {
// there sometimes "Permission denied to access propert document", use try/catch
try {
$(iframe.get(0).contentWindow.document).ready(function() {
parent.ui.loader_hide(content);
});
} catch (e) {};
}, 1000);
};
// hide content loader element, show content element
this.loader_hide = function(content)
{
$('#loader').hide();
$(content).css('opacity', 1);
var win = content.contentWindow;
this.file_editor = null;
try {
// it throws "permission denied" sometimes
if (win.file_editor && win.file_editor.editable)
this.file_editor = win.file_editor;
}
catch (e) {}
if (this.file_editor)
ui.enable_command('file.edit', true);
};
};
// Initialize application object (don't change var name!)
var ui = $.extend(new files_api(), new files_ui());
// general click handler
$(document).click(function() {
$('.popup').hide();
ui.file_rename_stop();
}).ready(function() {
ui.init();
});
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
@@ -1,312 +1,312 @@
/******************************************
* Websanova.com
*
* Resources for web entrepreneurs
*
* @author Websanova
* @copyright Copyright (c) 2012 Websanova.
* @license This websanova jQuery modal plugin is dual licensed under the MIT and GPL licenses.
* @link http://www.websanova.com
* @github http://github.com/websanova/modal
* @version 1.1.1
*
******************************************/
(function($)
{
$.fn.wModal = function(option, settings)
{
if(typeof option === 'object')
{
settings = option;
}
else if(typeof option === 'string')
{
var values = [];
var elements = this.each(function()
{
var data = $(this).data('_wModal');
if(data)
{
if(option === 'show') data.show(settings || {});
else if(option === 'hide') data.hide(settings || {});
else if($.fn.wModal.defaultSettings[option] !== undefined)
{
if(settings !== undefined) { data.settings[option] = settings; }
else { values.push(data.settings[option]); }
}
}
});
if(values.length === 1) { return values[0]; }
else if(values.length > 0) { return values; }
else { return elements; }
}
return this.each(function()
{
var _settings = $.extend({}, $.fn.wModal.defaultSettings, settings || {});
var modal = new Modal(_settings, $(this));
var $el = modal.generate();
modal.pixel.append($el);
$(this).data('_wModal', modal);
});
}
$.fn.wModal.defaultSettings = {
position : 'cm', // position of modal (lt, ct, rt, rm, rb, cb, lb, lm, cm)
offset : '10', // offset of modal from edges if not in "m" or "c" position
fxShow : 'none', // show effects (fade, slideUp, slideDown, slideLeft, slideRight)
fxHide : 'none', // hide effects (fade, slideUp, slideDown, slideLeft, slideRight)
btns : {}, // button callbacks
msg : null // optional message to set if "_wModal_msg" class is set
};
function Modal(settings, elem)
{
this.modal = null;
this.settings = settings;
this.elem = elem;
this.tempButtons = {};
this.rotationTimer = null;
return this;
}
Modal.prototype =
{
generate: function()
{
var _this = this;
if(this.modal) return this.modal;
// bg - check if bg already exists
if($('#_wModal_bg').length)
{
this.bg = $('#_wModal_bg');
this.pixel = $('#_wModal_pixel');
}
else
{
this.bg = $('<div id="_wModal_bg"></div>').css({position:'fixed', left:'0', top:'0', display:'none'});
$('body').append(this.bg);
$(window).resize(function(){ if(_this.bg.is(':visible')) _this.resetBg.apply(_this); });
// positioning pixel setting to body produces some weird effects wtih scrollbars when doing sliding effects
this.pixel = $('<div id="_wModal_pixel"></div>').css({position:'fixed', left:'0', top:'0', width:'0', height:'0', lineHeight:'0', fontSize:'0'});
$('body').append(this.pixel);
}
// modal
this.modal = $('<div class="_wModal_holder"></div>').css({position:'absolute', display:'none'});
this.modal.html(this.elem.html());
// set message - if set we use it, otherwise we try to pull from the _wModal_message container
var msg = this.modal.find('._wModal_msg');
if(msg.length)
{
if(this.settings.msg && this.settings.msg !== '') msg.html(this.settings.msg);
else this.settings.msg = msg.html();
}
$(window).resize(function(){ if(_this.modal.is(':visible')) _this.resetModal.apply(_this); });
this.resetBtns();
return this.modal;
},
resetModal: function()
{
var left = null, top = null, right = null, bottom = null;
var position = this.tempPosition || this.settings.position;
var offset = this.tempOffset || this.settings.offset;
var modalWidth = this.modal.outerWidth(true);
var modalHeight = this.modal.outerHeight(true);
var viewWidth = $(window).width();
var viewHeight = $(window).height();
this.outerX = viewWidth;
this.outerY = viewHeight;
var cX = (viewWidth/2) - (modalWidth/2);
var cY = (viewHeight/2) - (modalHeight/2);
var rX = viewWidth - modalWidth - offset;
var bY = viewHeight - modalHeight - offset;
switch(position)
{
case 'cm': left = cX; top = cY; break;
case 'lt': left = offset; top = offset; break;
case 'ct': left = cX; top = offset; break;
case 'rt': left = rX; top = offset; break;
case 'rm': left = rX; top = cY; break;
case 'rb': left = rX; top = bY; break;
case 'cb': left = cX; top = bY; break;
case 'lb': left = offset; top = bY; break;
case 'lm': left = offset; top = cY; break;
}
this.modal.css({left:(left ? left + 'px' : 'auto'), top:(top ? top + 'px' : 'auto'), bottom:(bottom ? bottom + 'px' : 'auto'), right: (right ? right + 'px' : 'auto') });
},
resetBg: function()
{
this.bg.css({width:$(window).width(), height:$(window).height()});
},
resetBtns: function(btns)
{
var btns = btns || this.settings.btns;
var _this = this;
for(var btn in btns)
{
(function(btn){
_this.modal.find('._wModal_btn_' + btn).unbind('click');
_this.modal.find('._wModal_btn_' + btn).click(function()
{
if(_this.tempBtns[btn]) _this.tempBtns[btn].apply(_this);
else btns[btn].apply(_this);
});
})(btn);
}
},
show: function(settings)
{
this.tempBtns = settings.btns || {};
this.tempPosition = settings.position || null;
this.tempOffset = settings.offset || null;
this.tempFxShow = settings.fxShow || this.settings.fxShow;
this.tempFxHide = settings.fxHide || this.settings.fxHide;
var msg = this.modal.find('._wModal_msg');
if(msg.length) msg.html(settings.msg || this.settings.msg);
this.resetBg();
this.resetModal();
this.pixel.children('._wModal_holder').hide();
this['fxShow' + this.tempFxShow.charAt(0).toUpperCase() + this.tempFxShow.substring(1)].apply(this);
},
hide: function()
{
this['fxHide' + this.tempFxHide.charAt(0).toUpperCase() + this.tempFxHide.substring(1)].apply(this);
},
/************************************************
* Effects
************************************************/
/*** none ***/
fxShowNone: function()
{
this.bg.show();
this.modal.show();
},
fxHideNone: function()
{
this.modal.hide();
this.bg.hide();
},
/*** fade ***/
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()
{
var _this = this;
this.modal.fadeOut(300, function(){ _this.bg.fadeOut(100); });
},
/*** slideUp ***/
fxShowSlideUp: function(){ this.fxSlide('show', 'top', this.outerY); },
fxHideSlideUp: function(){ this.fxSlide('hide', 'top', -1*this.outerY); },
/*** slideDown ***/
fxShowSlideDown: function(){ this.fxSlide('show', 'top', -1*this.outerY); },
fxHideSlideDown: function(){ this.fxSlide('hide', 'top', this.outerY); },
/*** slideLeft ***/
fxShowSlideLeft: function(){ this.fxSlide('show', 'left', this.outerX); },
fxHideSlideLeft: function(){ this.fxSlide('hide', 'left', -1*this.outerX); },
/*** slideRight ***/
fxShowSlideRight: function(){ this.fxSlide('show', 'left', -1*this.outerX); },
fxHideSlideRight: function(){ this.fxSlide('hide', 'left', this.outerX); },
/*** slide helper ***/
fxSlide: function(state, position, value)
{
var _this = this;
var css = {};
css[position] = value;
if(state === 'show')
{
this.modal.show();
var offset = this.modal.offset();
this.modal.hide();
this.modal.css(css);
this.bg.show();
this.modal.show();
css[position] = offset[position];
this.modal.animate(css);
}
else
{
var _this = this;
this.modal.animate(css, function(){ _this.modal.hide(); _this.bg.hide(); });
}
},
fxShowRotateUp: function(){ this.fxRotate(); this.fxShowSlideUp(); },
fxHideRotateUp: function(){ this.fxRotate(); this.fxHideSlideUp(); },
fxShowRotateDown: function(){ this.fxRotate(); this.fxShowSlideDown(); },
fxHideRotateDown: function(){ this.fxRotate(); this.fxHideSlideDown(); },
fxShowRotateLeft: function(){ this.fxRotate(); this.fxShowSlideLeft(); },
fxHideRotateLeft: function(){ this.fxRotate(); this.fxHideSlideLeft(); },
fxShowRotateRight: function(){ this.fxRotate(); this.fxShowSlideRight(); },
fxHideRotateRight: function(){ this.fxRotate(); this.fxHideSlideRight(); },
/*** rotate helper ***/
fxRotate: function()
{
var _this = this;
this.rotationDegree = 0;
this.rotationTimer = setInterval(function(){
_this.rotationDegree += 60;
_this.modal.css('-webkit-transform', 'rotate(' + _this.rotationDegree + 'deg)');
_this.modal.css('-moz-transform', 'rotate(' + _this.rotationDegree + 'deg)');
if(_this.rotationDegree % 360 === 0) clearInterval(_this.rotationTimer);
}, 80);
}
}
})(jQuery);
\ No newline at end of file
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
@@ -1,1423 +1,1492 @@
body {
font-family: Verdana, Arial, Helvetica, sans-serif;
font-size: 12px;
color: #333;
margin: 0;
color: #514949;
background: url(images/body.png) top repeat-x #f0f0f0;
}
a {
color: #1E90FF;
text-decoration: none;
}
h1, h2, h3, h4, h5, h6 {
margin-top: 5px;
color: #ff9900;
}
input[type="text"],
input[type="password"],
select[multiple="multiple"],
textarea {
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
padding-left: 2px;
color: black;
}
select[multiple="multiple"] {
padding-left: 0;
}
table.list {
width: 100%;
table-layout: fixed;
border-spacing: 0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
}
table.list td {
padding: 2px 4px;
border: 1px solid white;
border-left: none;
border-top: none;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
}
table.list thead tr {
background-color: #e0e0e0;
font-weight: bold;
}
table.list tbody tr {
background-color: #f0f0f0;
}
table.list tfoot tr {
background-color: #e0e0e0;
}
table.list tfoot tr td {
padding: 3px 3px;
font-size: 10px;
text-shadow: white 1px 1px;
}
table.list tfoot tr td {
border-top: solid 1px white;
border-bottom: none;
}
table.list td:last-child {
border-right: none;
}
table.list tbody tr.selectable td {
cursor: default;
}
table.list tbody tr.selectable:hover {
background-color: #d6efff;
}
table.list tbody tr td.empty-body {
height: 150px;
color: #ff9900;
text-align: center;
}
fieldset {
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
margin-top: 10px;
}
legend {
padding-left: 0;
color: #909090;
}
table.form {
width: 100%;
}
table.form td {
padding: 1px 5px;
}
table.form tr.required input,
table.form tr.required select,
table.form tr.required textarea {
background-color: #f0f9ff;
}
table.form tr input.error,
table.form tr select.error,
table.form tr textarea.error {
background-color: #f5e3e3;
}
td.label {
width: 1%;
min-width: 120px;
text-align: right;
font-weight: bold;
white-space: nowrap;
}
table tbody tr.selected {
background-color: #d6efff;
}
/**** Common UI elements ****/
#topmenu {
text-align: right;
height: 20px;
padding: 0 10px;
margin: 0;
white-space: nowrap;
background: url(images/linen_header.jpg) 0 0 repeat-x;
}
#topmenu > span {
color: #aaa;
font-size: 11px;
padding-top: 2px;
display: inline-block;
height: 18px;
}
#navigation {
margin: 0 15px;
text-align: right;
height: 36px;
z-index: 2;
white-space: nowrap;
}
#task_navigation {
margin: 0 15px;
text-align: right;
height: 21px;
z-index: 2;
white-space: nowrap;
}
#message {
position: absolute;
top: 80px;
left: 20px;
width: 350px;
height: 60px;
z-index: 20;
}
#message div {
opacity: 0.97;
padding-left: 70px;
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
-moz-box-shadow: 1px 1px 3px #999;
-webkit-box-shadow: #999 1px 1px 3px;
width: 100%;
height: 100%;
display: table;
}
#message.notice div {
border-color: #aec1db;
color: #3465a4;
background: url(images/info.png) 10px center no-repeat #c0e0ff;
}
#message.error div {
border-color: #db9999;
color: #a40000;
background: url(images/error.png) 10px center no-repeat #edcccc;
}
#message div span {
vertical-align: middle;
display: table-cell;
line-height: 20px;
}
#logo {
position: absolute;
top: 0px;
left: 10px;
width: 198px;
height: 74px;
cursor: pointer;
background: url(images/logo.png) 0 0 no-repeat;
}
#content {
position: absolute;
left: 0;
right: 0;
top: 74px;
bottom: 55px;
margin: 0 15px;
padding: 10px;
background-color: #f5f5f5;
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
}
#footer {
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 50px;
margin: 2px 15px;
color: #b0b0b0;
font-size: 9px;
}
#loading {
position: absolute;
display: none;
top: 2px;
left: 15px;
width: 150px;
height: 18px;
padding-left: 86px;
color: #aaa;
font-size: 11px;
z-index: 1;
background: url(images/loading.gif) 0 3px no-repeat;
white-space: nowrap;
}
#topmenu .logout {
background: url(images/buttons.png) -1px -100px no-repeat;
padding-left: 20px;
margin-right: 10px;
color: white;
}
#topmenu .login {
padding-left: 20px;
margin-right: 20px;
background: url(images/user_ico.png) 0 2px no-repeat;
}
#topmenu .domain {
padding-left: 20px;
margin-right: 10px;
background: url(images/domain_ico.png) 0 3px no-repeat;
}
#navigation ul {
list-style-type: none;
margin: 0;
padding: 8px 0;
}
#navigation ul li {
display: inline;
font-size: 13px;
font-weight: bold;
padding: 8px 0;
}
#navigation ul li a {
padding: 8px 10px;
text-decoration: none;
color: #514949;
}
#navigation ul li.active {
background: url(images/navbg.png) 0 0 repeat-x;
}
#navigation ul li.active a {
text-shadow: white 1px 1px;
color: #ff9900;
}
#task_navigation ul {
list-style-type: none;
margin: 0;
padding: 2px 0;
}
#task_navigation ul li {
padding: 1px 0;
display: inline;
font-size: 12px;
font-weight: bold;
text-shadow: white 1px 1px;
}
#task_navigation ul li a {
padding: 1px 10px;
text-decoration: none;
color: #808080;
}
#navigation ul li a:active,
#task_navigation ul li a:active {
outline: none;
}
#search {
padding: 7px;
margin-bottom: 10px;
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
background-color: #e0e0e0;
}
#searchinput {
border: none;
background-color: white;
margin-top: 2px;
}
#searchinput:focus {
outline: none;
}
.searchinput {
height: 20px;
margin: 0;
padding: 0;
background-color: white;
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
overflow: hidden;
}
.searchactions {
float: left;
padding: 1px;
margin-left: -1px;
height: 18px;
width: 37px;
background-color: #f0f0f0;
cursor: pointer;
border-right: 1px solid #e0e0e0;
border-top-left-radius: 3px;
border-bottom-left-radius: 3px;
-moz-border-radius-topleft: 3px;
-moz-border-radius-bottomleft: 3px;
-webkit-border-top-left-radius: 3px;
-webkit-border-left-right-radius: 3px;
}
#search-reset,
#search-details {
display: block;
float: left;
width: 18px;
height: 18px;
background: url(images/buttons.png) -1px 0 no-repeat;
}
#search-reset:hover,
#search-details:hover {
background-color: white;
}
#search-reset {
border-left: 1px solid #e0e0e0;
}
#search-details {
background-position: -1px 20px;
}
.searchdetails {
display: none;
}
.searchfilter {
color: #909090;
font-weight: bold;
margin-top: 5px;
}
#search fieldset {
margin: 0;
color: #909090;
margin-top: 5px;
}
#search td.label {
min-width: 0;
}
div.vsplitter {
float: left;
width: 10px;
min-height: 400px;
}
/* fix cursor on upload button */
input[type="file"]::-webkit-file-upload-button {
cursor: pointer;
}
#draglayer {
min-width: 300px;
width: auto !important;
width: 300px;
border: 1px solid #999999;
background-color: #fff;
padding-left: 8px;
padding-right: 8px;
padding-top: 3px;
padding-bottom: 3px;
white-space: nowrap;
opacity: 0.9;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
-moz-box-shadow: 1px 1px 12px #999;
-webkit-box-shadow: #999 1px 1px 12px;
}
/**** Common classes ****/
a.disabled {
opacity: 0.5;
filter: alpha(opacity=50);
}
.nowrap {
white-space: nowrap;
}
.clear {
clear: both;
}
.watermark {
padding-top: 40px;
text-align: center;
width: 100%;
}
.link {
cursor: pointer;
}
.icon {
width: 16px;
height: 16px;
}
input.inactive {
color: #d0d0d0;
}
.formtitle {
color: #ff9900;
font-size: 18px;
font-weight: bold;
margin-left: 5px;
}
.formbuttons {
text-align: center;
white-space: nowrap;
}
.formbuttons input {
margin: 5px;
}
div.scroller {
left: 0;
top: 0;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
position: absolute;
bottom: 19px;
}
.listnav {
width: 100%;
text-align: right;
}
.listnav a {
float: left;
display: block;
width: 16px;
height: 16px;
background: url(images/arrow_left.png) center no-repeat;
}
.listnav a.next {
background: url(images/arrow_right.png) center no-repeat;
}
.listnav a.disabled {
opacity: .3;
cursor: default;
}
.listnav span span {
float: left;
display: block;
height: 14px;
padding: 1px 5px;
}
.disabled,
.readonly,
.select.readonly option {
color: #a0a0a0;
}
input.disabled,
input.readonly,
select.disabled,
select.readonly,
textarea.disabled,
textarea.readonly {
background-color: #f5f5f5;
color: #a0a0a0;
}
input.maxsize {
width: 368px; /* span.listarea width - 2px */
}
pre.debug {
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
background-color: white;
padding: 2px;
width: 500px;
height: 200px;
margin: 0;
overflow: auto;
}
.popup {
display: none;
position: absolute;
border: 1px solid #d0d0d0;
border-radius: 3px;
box-shadow: 0 2px 6px 0 #d0d0d0;
-moz-box-shadow: 0 2px 6px 0 #d0d0d0;
-webkit-box-shadow: 0 2px 6px 0 #d0d0d0;
-o-box-shadow: 0 2px 6px 0 #d0d0d0;
background-color: #f0f0f0;
}
a.button {
display: inline-block;
width: 18px;
height: 18px;
background: url(images/buttons.png) 0 0 no-repeat;
}
a.button.edit {
background-position: -1px -81px;
}
a.button.add {
background-position: -1px -41px;
}
a.button.delete {
background-position: -1px -1px;
}
.popup ul {
list-style-type: none;
margin: 0;
padding: 0;
}
.popup ul li {
padding: 2px 4px;
min-width: 100px;
cursor: default;
}
.popup ul li a {
cursor: default;
display: block;
}
.popup ul li:hover {
background-color: #d6efff;
}
div.boxfooter {
position: absolute;
height: 18px;
left: 0;
right: 0;
bottom: 0;
background-color: #e0e0e0;
border-top: 1px solid #d0d0d0;
}
div.boxfooter a.button {
width: auto;
white-space: nowrap;
color: #514949;
display: inline;
line-height: 18px;
padding: 0 5px 0 20px;
}
/********* Form smart inputs *********/
span.listarea {
display: block;
width: 370px;
max-height: 209px;
overflow-y: auto;
overflow-x: hidden;
margin: 0;
padding: 0;
background-color: white;
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
text-align: left;
text-shadow: none;
color: black;
}
span.listelement {
display: block;
padding: 0;
margin: 0;
height: 18px;
overflow: hidden;
border-top: 1px solid #d0d0d0;
white-space: nowrap;
}
span.listelement:first-child {
border: none;
}
span.listelement input {
border: none;
background-color: transparent;
padding-left: 2px;
width: 328px;
height: 16px;
}
span.listarea.disabled span.listelement input,
span.listarea.readonly span.listelement input {
width: 370px;
}
span.listelement input:focus {
outline: none;
}
span.listelement span.actions {
float: left;
padding: 1px 0;
margin-left: -1px;
margin-top: -1px;
height: 18px;
width: 37px;
border: 1px solid #d0d0d0;
background-color: #f0f0f0;
cursor: pointer;
}
span.listelement span.actions span {
display: block;
float: left;
width: 18px;
height: 18px;
background: url(images/buttons.png) 0 0 no-repeat;
}
span.listelement span.actions span:hover {
background-color: white;
}
span.listelement span.actions span.reset {
background-position: -1px -1px;
border-left: 1px solid #e0e0e0;
}
span.listelement span.actions span.add {
background-position: -41px -2px;
}
span.listelement span.actions span.search {
background-position: -61px -1px;
cursor: default;
}
span.listarea.disabled,
span.listarea.readonly {
background-color: #f5f5f5;
}
input.disabled,
input.readonly,
span.listarea.disabled span.listelement input,
span.listarea.readonly span.listelement input {
color: #a0a0a0;
cursor: default;
}
span.listarea.autocomplete span.listelement input {
color: #514949;
}
span.listarea.autocomplete span.listelement input.autocomplete {
color: black;
}
.autocomplete > span.listelement input {
width: 346px;
}
.autocomplete > span.listelement span.actions {
width: 18px;
}
.autocomplete > span.listelement span.actions span.reset {
border-left: none;
}
.autocomplete > span.listelement span.actions span.search:hover {
background-color: #f0f0f0;
}
span.listarea.select {
width: 200px;
}
span.listarea.select > span.listelement input {
width: 180px;
}
span.listcontent {
display: block;
padding: 0;
margin: 0;
overflow: hidden;
max-height: 94px;
overflow-x: hidden;
overflow-y: auto;
border-top: 1px solid #d0d0d0;
background-color: #f5f5f5;
cursor: default;
}
span.listcontent span.listelement {
padding-left: 3px;
}
span.listcontent span.listelement:hover {
background-color: #d6efff;
}
span.listcontent span.listelement.selected {
background-color: #d6efff;
}
span.form_error {
color: #FF0000;
font-weight: bold;
font-size: 90%;
padding-left: 5px;
}
/***** progress bar ****/
table.progress {
width: 100%;
height: 5px;
background-color: #f0f0f0;
border: 1px solid #d0d0d0;
border-spacing: 0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
margin-bottom: 3px;
}
table.progress td.bar {
background-color: #71b9e1;
}
table.progressinfo {
font-size: 9px;
border: 1px solid #d0d0d0;
width: 5%;
background-color: #f0f0f0;
z-index: 100;
border-spacing: 0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
padding: 3px;
}
table.progressinfo td {
padding: 0 1px;
white-space: nowrap;
}
table.progressinfo td.label {
text-align: left;
font-weight: normal;
padding-right: 10px;
min-width: 50px;
width: 1%;
}
table.progressinfo td.value {
text-align: right;
width: 99%;
}
/***** autocomplete list *****/
#autocompletepane
{
background-color: white;
border: 1px solid #d0d0d0;
min-width: 351px;
}
#autocompletepane ul
{
margin: 0px;
padding: 2px;
list-style-image: none;
list-style-type: none;
}
#autocompletepane ul li
{
display: block;
height: 16px;
font-size: 11px;
padding-left: 6px;
padding-top: 2px;
padding-right: 6px;
white-space: nowrap;
cursor: pointer;
}
#autocompletepane ul li.selected
{
background-color: #d6efff;
}
/***** tabbed interface elements *****/
div.tabsbar
{
height: 22px;
border-bottom: 1px solid #d0d0d0;
white-space: nowrap;
margin: 10px 5px 0 5px;
}
span.tablink,
span.tablink-selected
{
float: left;
height: 23px !important;
height: 22px;
overflow: hidden;
background: url(images/tabs-left.gif) top left no-repeat;
font-weight: bold;
}
span.tablink
{
cursor: pointer;
text-shadow: white 1px 1px;
}
span.tablink-selected
{
cursor: default;
background-position: 0px -23px;
}
span.tablink a,
span.tablink-selected a
{
display: inline-block;
padding: 4px 10px 0 5px;
margin-left: 5px;
height: 23px;
color: #808080;
max-width: 185px;
text-decoration: none;
overflow: hidden;
text-overflow: ellipsis;
-o-text-overflow: ellipsis;
background: url(images/tabs-right.gif) top right no-repeat;
outline: none;
}
span.tablink-selected a
{
cursor: inherit;
color: #514949;
background-position: right -23px;
}
fieldset.tabbed
{
margin-top: 0;
padding-top: 12px;
border-top: none;
}
/***** Dialog windows *****/
#_wModal_bg {
background-color: #000;
z-index: 10000;
opacity: 0.2;
filter: alpha(opacity=20);
}
#_wModal_pixel {
z-index: 10001;
}
._wModal {
position: relative;
min-width: 350px;
overflow: hidden;
line-height: 15px;
background-color: #FFF;
color: #333;
border: 1px solid rgba(51, 51, 51, 0.5);
border-radius: 4px;
box-shadow: 0 3px 7px rgba(0, 0, 0, 0.3);
}
._wModal_header {
padding: 10px;
font-size: 14px;
font-weight: bold;
border-bottom: solid 1px #DDD;
}
._wModal_close {
position: absolute;
right: 10px;
top: 10px;
font-weight: normal;
font-size: 12px;
cursor: pointer;
color: #BABABA;
}
._wModal_msg {
font-size: 12px;
padding: 20px;
color: #3A3A3A;
text-shadow: rgba(255, 255, 255, 0.75) 0 1px 1px;
}
._wModal_btns {
padding: 10px;
font-size: 10px;
font-weight: bold;
border-top: solid 1px #DDD;
background-color: #EFEFEF;
text-align: right;
white-space: nowrap;
}
._wModal_btns div {
display: inline-block;
min-width: 40px;
padding: 0 10px;
height: 25px;
line-height: 25px;
margin-left: 10px;
text-align: center;
cursor: pointer;
border-radius: 4px;
box-shadow: rgba(255, 255, 255, 0.2) 0px 1px 0px 0px inset, rgba(0, 0, 0, 0.0470588) 0px 1px 2px 0px;
text-shadow: rgba(255, 255, 255, 0.75) 0 1px 1px;
border: 1px solid rgba(0, 0, 0, 0.14902);
border-bottom-color: rgba(0, 0, 0, 0.247059);
background-color: #F5F5F5;
color: #333;
}
._wModal_btns div:hover {
background-color: #E6E6E6;
}
._wModal_btns div.default {
text-shadow: rgba(0, 0, 0, 0.247059) 0px -1px 0px;
border: 1px solid rgba(0, 0, 0, 0.0980392);
background-color: #006DCC;
color: #FFF;
}
._wModal_btns div.default:hover {
background-color: #0044CC
}
/**** Login form elements ****/
#login_form {
margin: auto;
margin-top: 75px;
padding: 20px;
width: 330px;
background-color: #e0e0e0;
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
text-align: center;
}
#login_form span {
display: block;
line-height: 24px;
width: 330px;
text-align: left;
}
#login_form label {
display: block;
width: 80px;
text-align: right;
float: left;
margin-right: 10px;
}
#login_form select,
#login_form input[type="text"],
#login_form input[type="password"] {
width: 210px;
}
#login_submit {
margin-top: 5px;
}
/**** Main screen elements ****/
#main {
padding: 5px 30px;
}
#footer .foot {
white-space: nowrap;
vertical-align: top;
text-align: right;
}
/***** tree indicators *****/
td span.branch span
{
float: left;
height: 16px;
}
td span.branch span.tree
{
height: 17px;
width: 15px;
background: url(images/tree.gif) 0 0 no-repeat;
}
td span.branch span.l1
{
background-position: 0px 0px; /* L */
}
td span.branch span.l2
{
background-position: -30px 0px; /* | */
}
td span.branch span.l3
{
background-position: -15px 0px; /* |- */
}
/**** File manager elements ****/
#taskcontent {
position: absolute;
left: 310px;
right: 10px;
bottom: 10px;
top: 10px;
background-color: #f0f0f0;
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
overflow-y: auto;
}
#actionbar {
position: absolute;
width: 82px;
top: 10px;
bottom: 10px;
overflow: hidden;
}
#forms {
position: absolute;
left: 310px;
right: 10px;
top: 10px;
}
#forms form {
display: none;
}
#forms fieldset {
margin: 0;
background-color: #f0f0f0;
}
#forms table {
border-spacing: 0;
margin: 0;
padding: 0;
}
#forms table td.buttons {
width: 1%;
white-space: nowrap;
}
#actionbar a {
display: block;
width: 80px;
height: 55px;
border: 1px solid #d0d0d0;
border-spacing: 0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
background-color: #f0f0f0;
margin-bottom: 6px;
cursor: pointer;
color: #333;
}
#actionbar a span {
display: block;
position: absolute;
width: 100%;
text-align: center;
padding-top: 40px;
font-size: 9px;
}
#folder-create-button {
background: url(images/folder_new.png) center 6px no-repeat;
}
#file-create-button {
background: url(images/file_new.png) center 6px no-repeat;
}
#file-upload-button {
background: url(images/file_new.png) center 6px no-repeat;
}
#file-search-button {
background: url(images/search.png) center 6px no-repeat;
}
#file-save-button {
background: url(images/save.png) center 6px no-repeat;
}
#actionbar #file-save-button {
display: none;
}
#folderlist,
#filedata {
position: absolute;
width: 200px;
top: 10px;
bottom: 10px;
left: 98px;
border: 1px solid #d0d0d0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
background-color: #f0f0f0;
padding: 2px;
}
#folderlist table,
#filelist table {
width: 100%;
border-spacing: 0;
}
#filelist td {
white-space: nowrap;
cursor: default;
width: 70px;
overflow: hidden;
}
#filelist td.filename {
width: 98%;
height: 20px;
padding: 0 4px;
text-overflow: ellipsis;
}
#filelist td.filesize {
text-align: right;
}
#filelist td.filemtime {
width: 115px;
}
#filelist tbody td.filename span {
background: url(images/mimetypes/unknown.png) 0 0 no-repeat;
padding: 0 0 0 20px;
height: 16px;
cursor: pointer;
}
#filelist tbody td.filename span input {
padding: 0 2px;
height: 18px;
}
#filelist thead td {
cursor: pointer;
}
#filelist thead td.sorted {
padding-right: 16px;
text-decoration: underline;
background: url(images/buttons.png) right -140px no-repeat;
}
#filelist thead td.sorted.reverse {
background-position: right -120px;
}
#folderlist td span.name {
background: url(images/folder.png) 0 0 no-repeat;
height: 18px;
padding-left: 20px;
margin-left: 3px;
cursor: pointer;
}
#folderlist tr.selected td span.name {
background-image: url(images/folder_open.png);
}
#folderlist tr.selected {
background-color: inherit;
}
#folderlist tr.selected td span.name {
font-weight: bold;
}
#folderlist tr.virtual td span.name {
color: #bbb;
cursor: default;
}
#folderlist tr.droptarget {
background-color: #e0e0e0;
}
#folder-collection-audio td span.name,
#folderlist #folder-collection-audio.selected td span.name {
background: url(images/audio.png) 1px 0 no-repeat;
}
#folder-collection-video td span.name,
#folderlist #folder-collection-video.selected td span.name {
background: url(images/video.png) 0 0 no-repeat;
}
#folder-collection-image td span.name,
#folderlist #folder-collection-image.selected td span.name {
background: url(images/image.png) 0 0 no-repeat;
}
#folder-collection-document td span.name,
#folderlist #folder-collection-document.selected td span.name {
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 {
background: url(images/edit.png) center 6px no-repeat #f0f0f0;
}
#actionbar #file-delete-button {
background: url(images/trash.png) center 6px no-repeat #f0f0f0;
}
#actionbar #file-download-button {
background: url(images/download.png) center 6px no-repeat #f0f0f0;
}
#taskcontent iframe {
border: none;
width: 100%;
height: 100%;
background-color: white;
overflow: auto;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
opacity: 0.1;
}
.fileopen #taskcontent {
overflow: hidden;
background-color: white;
}
#filedata table {
width: 200px;
}
#filedata table td.label {
min-width: 30px;
}
#filedata table td.data {
/*
text-overflow: ellipsis;
overflow: hidden;
*/
}
#filedata table td.data.filename {
font-weight: bold;
}
#loader {
display: none;
z-index: 10;
width: 100px;
background-color: #fafafa;
color: #a0a0a0;
position: absolute;
border: 1px solid #e0e0e0;
border-radius: 3px;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
text-align: center;
padding: 10px;
font-weight: bold;
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 6:42 AM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196984
Default Alt Text
(833 KB)

Event Timeline