Page MenuHomePhorge

No OneTemporary

Size
175 KB
Referenced Files
None
Subscribers
None
diff --git a/doc/SQL/mysql.initial.sql b/doc/SQL/mysql.initial.sql
index 6e90f24..810bff5 100644
--- a/doc/SQL/mysql.initial.sql
+++ b/doc/SQL/mysql.initial.sql
@@ -1,35 +1,38 @@
CREATE TABLE IF NOT EXISTS `chwala_locks` (
`uri` varchar(512) BINARY NOT NULL,
`owner` varchar(256),
`timeout` integer unsigned,
`expires` datetime DEFAULT NULL,
`token` varchar(256),
`scope` tinyint,
`depth` tinyint,
INDEX `uri_index` (`uri`, `depth`),
INDEX `expires_index` (`expires`),
INDEX `token_index` (`token`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE IF NOT EXISTS `chwala_sessions` (
- `id` varchar(40) BINARY NOT NULL,
- `uri` varchar(1024) BINARY NOT NULL,
- `owner` varchar(255) BINARY NOT NULL,
- `data` mediumtext,
+ `id` varchar(40) BINARY NOT NULL,
+ `uri` varchar(1024) BINARY NOT NULL,
+ `owner` varchar(255) BINARY NOT NULL,
+ `owner_name` varchar(255) DEFAULT NULL,
+ `data` mediumtext,
PRIMARY KEY (`id`),
INDEX `uri_index` (`uri`(255)),
INDEX `owner` (`owner`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE IF NOT EXISTS `chwala_invitations` (
`session_id` varchar(40) BINARY NOT NULL,
`user` varchar(255) BINARY NOT NULL,
+ `user_name` varchar(255) DEFAULT NULL,
`status` varchar(16) NOT NULL,
`changed` datetime DEFAULT NULL,
+ `comment` mediumtext,
CONSTRAINT `session_id_fk_chwala_invitations` FOREIGN KEY (`session_id`)
REFERENCES `chwala_sessions`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
INDEX `session_id` (`session_id`),
UNIQUE INDEX `user_session_id` (`user`, `session_id`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
INSERT INTO `system` (`name`, `value`) VALUES ('chwala-version', '2015110400');
diff --git a/lib/api/common.php b/lib/api/common.php
index 8f7d5e7..69d2fc7 100644
--- a/lib/api/common.php
+++ b/lib/api/common.php
@@ -1,166 +1,166 @@
<?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 $rc;
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);
+ @set_time_limit(360);
}
/**
* 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_core::ERROR_CODE);
}
throw new Exception("File upload failed", file_api_core::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_core::ERROR_CODE);
}
throw new Exception("File upload failed", file_api_core::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;
}
/**
* Get folder rights
*/
protected function folder_rights($folder)
{
list($driver, $path) = $this->api->get_driver($folder);
$rights = $driver->folder_rights($path);
$result = array();
$map = array(
file_storage::ACL_READ => 'read',
file_storage::ACL_WRITE => 'write',
);
foreach ($map as $key => $value) {
if ($rights & $key) {
$result[] = $value;
}
}
return $result;
}
}
diff --git a/lib/api/document.php b/lib/api/document.php
index 3b89943..fa5d7df 100644
--- a/lib/api/document.php
+++ b/lib/api/document.php
@@ -1,188 +1,268 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2015, 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_document extends file_api_common
{
/**
* Request handler
*/
public function handle()
{
$method = $_SERVER['REQUEST_METHOD'];
$this->args = $_GET;
if ($method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) {
$method = $_SERVER['HTTP_X_HTTP_METHOD'];
}
- // Sessions and invitations management
- if (strpos($this->args['method'], 'document_') === 0) {
- if ($method == 'POST') {
+ // Invitation notifications
+ if ($this->args['method'] == 'invitations') {
+ return $this->invitations();
+ }
+ // Session and invitations management
+ else if (strpos($this->args['method'], 'document_') === 0) {
+ if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$post = file_get_contents('php://input');
$this->args += (array) json_decode($post, true);
unset($post);
}
if (empty($this->args['id'])) {
throw new Exception("Missing document ID.", file_api_core::ERROR_CODE);
}
-
+
switch ($this->args['method']) {
case 'document_delete':
- return $this->document_delete($this->args['id']);
-
case 'document_invite':
- return $this->document_invite($this->args['id']);
-
-// case 'document_request':
-// case 'document_decline':
-// case 'document_accept':
-// case 'document_remove':
+ case 'document_request':
+ case 'document_decline':
+ case 'document_accept':
+ case 'document_cancel':
+ return $this->{$this->args['method']}($this->args['id']);
}
}
// Document content actions for Manticore
else if ($method == 'PUT' || $method == 'GET') {
if (empty($this->args['id'])) {
throw new Exception("Missing document ID.", file_api_core::ERROR_CODE);
}
$file = $this->get_file_path($this->args['id']);
return $this->{'document_' . strtolower($method)}($file);
}
throw new Exception("Unknown method", file_api_core::ERROR_INVALID);
}
/**
* Get file path from manticore session identifier
*/
protected function get_file_path($id)
{
$manticore = new file_manticore($this->api);
return $manticore->session_file($id);
}
+ /**
+ * Get invitations list
+ */
+ protected function invitations()
+ {
+ $timestamp = time();
+
+ // Initial tracking request, return just the current timestamp
+ if ($this->args['timestamp'] == -1) {
+ return array('timestamp' => $timestamp);
+ // @TODO: in this mode we should likely return all invitations
+ // that require user action, otherwise we may skip some unintentionally
+ }
+
+ $manticore = new file_manticore($this->api);
+ $filter = array();
+
+ if ($this->args['timestamp']) {
+ $filter['timestamp'] = $this->args['timestamp'];
+ }
+
+ $list = $manticore->invitations_list($filter);
+
+ return array(
+ 'list' => $list,
+ 'timestamp' => $timestamp,
+ );
+ }
+
/**
* Close (delete) manticore session
*/
protected function document_delete($id)
{
$manticore = new file_manticore($this->api);
if (!$manticore->session_delete($id)) {
throw new Exception("Failed deleting the document session.", file_api_core::ERROR_CODE);
}
}
/**
- * Invite/add a session participant
+ * Invite/add a session participant(s)
*/
protected function document_invite($id)
{
$manticore = new file_manticore($this->api);
$users = $this->args['users'];
+ $comment = $this->args['comment'];
if (empty($users)) {
throw new Exception("Invalid arguments.", file_api_core::ERROR_CODE);
}
foreach ((array) $users as $user) {
- if (empty($user['user']) || !$manticore->invitation_create($id, $user['user'], file_manticore::STATUS_INVITED)) {
- throw new Exception("Failed adding a session participant.", file_api_core::ERROR_CODE);
+ if (!empty($user['user'])) {
+ $manticore->invitation_create($id, $user['user'], file_manticore::STATUS_INVITED, $comment, $user['name']);
+
+ $result[] = array(
+ 'session_id' => $id,
+ 'user' => $user['user'],
+ 'user_name' => $user['name'],
+ 'status' => file_manticore::STATUS_INVITED,
+ );
}
+ }
+
+ return array(
+ 'list' => $result,
+ );
+ }
+
+ /**
+ * Request an invitation to a session
+ */
+ protected function document_request($id)
+ {
+ $manticore = new file_manticore($this->api);
+ $manticore->invitation_create($id, null, file_manticore::STATUS_REQUESTED, $this->args['comment']);
+ }
+
+ /**
+ * Decline an invitation to a session
+ */
+ protected function document_decline($id)
+ {
+ $manticore = new file_manticore($this->api);
+ $manticore->invitation_update($id, $this->args['user'], file_manticore::STATUS_DECLINED, $this->args['comment']);
+ }
- $result = array(
- 'session_id' => $id,
- 'user' => $user['user'],
-// 'name' => $user['name'],
- 'status' => file_manticore::STATUS_INVITED,
- );
+ /**
+ * Accept an invitation to a session
+ */
+ protected function document_accept($id)
+ {
+ $manticore = new file_manticore($this->api);
+ $manticore->invitation_update($id, $this->args['user'], file_manticore::STATUS_ACCEPTED, $this->args['comment']);
+ }
+
+ /**
+ * Remove a session participant(s) - cancel invitations
+ */
+ protected function document_cancel($id)
+ {
+ $manticore = new file_manticore($this->api);
+ $users = $this->args['users'];
+
+ if (empty($users)) {
+ throw new Exception("Invalid arguments.", file_api_core::ERROR_CODE);
+ }
+
+ foreach ((array) $users as $user) {
+ $manticore->invitation_delete($id, $user);
+ $result[] = $user;
}
return array(
'list' => $result,
);
}
/**
* Update document file content
*/
protected function document_put($file)
{
list($driver, $path) = $this->api->get_driver($file);
$length = rcube_utils::request_header('Content-Length');
$tmp_dir = unslashify($this->api->config->get('temp_dir'));
$tmp_path = tempnam($tmp_dir, 'chwalaUpload');
// Create stream to copy input into a temp file
$input = fopen('php://input', 'r');
$tmp_file = fopen($tmp_path, 'w');
if (!$input || !$tmp_file) {
throw new Exception("Failed opening input or temp file stream.", file_api_core::ERROR_CODE);
}
// Create temp file from the input
$copied = stream_copy_to_stream($input, $tmp_file);
fclose($input);
fclose($tmp_file);
if ($copied < $length) {
throw new Exception("Failed writing to temp file.", file_api_core::ERROR_CODE);
}
$file = array(
'path' => $tmp_path,
'type' => rcube_mime::file_content_type($tmp_path, $file),
);
$driver->file_update($path, $file);
// remove the temp file
unlink($tmp_path);
}
/**
* Return document file content
*/
protected function document_get($file)
{
list($driver, $path) = $this->api->get_driver($file);
try {
$params = array('force-type' => 'application/vnd.oasis.opendocument.text');
$driver->file_get($path, $params);
}
catch (Exception $e) {
header("HTTP/1.0 " . file_api_core::ERROR_CODE . " " . $e->getMessage());
}
exit;
}
}
diff --git a/lib/api/file_info.php b/lib/api/file_info.php
index b5d3053..304066c 100644
--- a/lib/api/file_info.php
+++ b/lib/api/file_info.php
@@ -1,98 +1,143 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2015, 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_file_info extends file_api_common
{
/**
* Request handler
*/
public function handle()
{
parent::handle();
+ // check Manticore support. Note: we don't use config->get('fileapi_manticore')
+ // here as it may be not properly set if backend driver wasn't initialized yet
+ $capabilities = $this->api->capabilities(false);
+ $manticore = $capabilities['MANTICORE'];
+
+ // support file_info by session ID
if (!isset($this->args['file']) || $this->args['file'] === '') {
- throw new Exception("Missing file name", file_api_core::ERROR_CODE);
+ if ($manticore && !empty($this->args['session'])) {
+ $this->args['file'] = $this->file_manticore_file($this->args['session']);
+ }
+ else {
+ throw new Exception("Missing file name", file_api_core::ERROR_CODE);
+ }
}
- list($driver, $path) = $this->api->get_driver($this->args['file']);
+ if ($this->args['file'] !== null) {
+ list($driver, $path) = $this->api->get_driver($this->args['file']);
- $info = $driver->file_info($path);
+ $info = $driver->file_info($path);
+ $info['file'] = $this->args['file'];
+ }
+ else {
+ $info = array(
+ // @TODO: session exists, invitation exists, assume ODF format
+ // however, this should be done in a different way,
+ // e.g. this info should be stored in sessions database
+ 'type' => 'application/vnd.oasis.opendocument.text',
+ 'writable' => false,
+ );
+ }
// Possible 'viewer' types are defined in files_api.js:file_type_supported()
// 1 - Native browser support
// 2 - Chwala viewer exists
- // 4 - Manticore (WebODF collaborative editor)
+ // 4 - Editor exists
if (rcube_utils::get_boolean((string) $this->args['viewer'])) {
- $this->file_viewer_info($info);
+ if ($this->args['file'] !== null) {
+ $this->file_viewer_info($info);
+ }
// check if file type is supported by webodf editor?
- if ($this->rc->config->get('fileapi_manticore')) {
+ if ($manticore) {
if (strtolower($info['type']) == 'application/vnd.oasis.opendocument.text') {
$info['viewer']['manticore'] = true;
}
}
if ((intval($this->args['viewer']) & 4) && $info['viewer']['manticore']) {
$this->file_manticore_handler($info);
}
}
+ // check writable flag
+ if ($this->args['file'] !== null) {
+ $path = explode(file_storage::SEPARATOR, $path);
+ array_pop($path);
+ $path = implode(file_storage::SEPARATOR, $path);
+ $acl = $driver->folder_rights($path);
+
+ $info['writable'] = ($acl & file_storage::ACL_WRITE) != 0;
+ }
+
return $info;
}
/**
* Merge file viewer data into file info
*/
protected function file_viewer_info(&$info)
{
$file = $this->args['file'];
$viewer = $this->find_viewer($info['type']);
if ($viewer) {
$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;
}
}
}
/**
* Merge manticore session data into file info
*/
protected function file_manticore_handler(&$info)
{
$manticore = new file_manticore($this->api);
$file = $this->args['file'];
$session = $this->args['session'];
if ($uri = $manticore->session_start($file, $session)) {
$info['viewer']['href'] = $uri;
$info['session'] = $manticore->session_info($session, true);
}
}
+
+ /**
+ * Get file from manticore session
+ */
+ protected function file_manticore_file($session_id)
+ {
+ $manticore = new file_manticore($this->api);
+
+ return $manticore->session_file($session_id, true);
+ }
}
diff --git a/lib/api/folder_create.php b/lib/api/folder_create.php
index 843bbe6..4c77c58 100644
--- a/lib/api/folder_create.php
+++ b/lib/api/folder_create.php
@@ -1,94 +1,94 @@
<?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_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_core::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_core::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']);
+ $driver->configure($this->api->env, $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;
// optionally store (encrypted) passwords
if (!empty($data['password']) && rcube_utils::get_boolean((string) $this->args['store_passwords'])) {
$data['password'] = $this->api->encrypt($data['password']);
}
else {
unset($data['password']);
}
// save the mount point info in config
$backend->driver_create($data);
}
}
diff --git a/lib/drivers/kolab/kolab_file_storage.php b/lib/drivers/kolab/kolab_file_storage.php
index 43ee14c..f51e09e 100644
--- a/lib/drivers/kolab/kolab_file_storage.php
+++ b/lib/drivers/kolab/kolab_file_storage.php
@@ -1,1412 +1,1446 @@
<?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 = array();
/**
* @var string
*/
protected $title;
+ /**
+ * @var array
+ */
+ protected $icache = array();
/**
* 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', 'kolab_folders'));
$plugins = array_unique(array_merge($plugins, 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 way we're compatible with Roundcube Framework 1.2
// we can't use load_plugins() here
foreach ($plugins as $plugin) {
$this->rc->plugins->load_plugin($plugin, true);
}
$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'],
));
}
/**
* Get password and name of authenticated user
*
* @return array Authenticated user data
*/
public function auth_info()
{
return array(
'username' => $this->config['username'] ?: $_SESSION['username'],
'password' => $this->config['password'] ?: $this->rc->decrypt($_SESSION['password']),
);
}
/**
* 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($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)
{
+ $this->rc->plugins->exec_hook('startup');
+
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 string $title Source identifier
*/
public function configure($config, $title = null)
{
$this->config = array_merge($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,
file_storage::CAPS_SUBSCRIPTIONS => 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);
}
$this->driver_list = null;
}
/**
* 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);
}
$this->driver_list = null;
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()
{
// use internal cache, this is specifically for iRony
// which may call this code path many times in one request
if ($this->driver_list !== null) {
return $this->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 $this->driver_list = $result;
}
/**
* Update configuration of external driver (mount point)
*
* @param string $title Driver instance title
* @param array $driver Driver data
*
* @throws Exception
*/
public function driver_update($title, $driver)
{
$drivers = $this->driver_list();
if (!$drivers[$title]) {
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);
}
$this->driver_list = null;
}
/**
* 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);
+ $file = $this->get_file_object($file_name, $folder, true);
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'] && empty($params['head'])) {
$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);
+ $file = $this->get_file_object($file_name, $folder, true);
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())
{
$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);
+ $files = $this->get_files($folder_name, $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 = $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' => $new_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', true);
if (!$success) {
throw new Exception("Storage error. Unable to create the 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 the 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 the folder", file_storage::ERROR);
}
}
/**
* Subscribe a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_subscribe($folder_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$storage = $this->rc->get_storage();
if (!$storage->subscribe($folder_name)) {
throw new Exception("Storage error. Unable to subscribe the folder", file_storage::ERROR);
}
}
/**
* Unsubscribe a folder.
*
* @param string $folder_name Name of a folder with full path
*
* @throws Exception
*/
public function folder_unsubscribe($folder_name)
{
$folder_name = rcube_charset::convert($folder_name, RCUBE_CHARSET, 'UTF7-IMAP');
$storage = $this->rc->get_storage();
if (!$storage->unsubscribe($folder_name)) {
throw new Exception("Storage error. Unable to unsubsribe the folder", file_storage::ERROR);
}
}
/**
* Returns list of folders.
*
* @param array $params List parameters ('type', 'search', 'extended', 'permissions')
*
* @return array List of folders
* @throws Exception
*/
public function folder_list($params = array())
{
$unsubscribed = $params['type'] & file_storage::FILTER_UNSUBSCRIBED;
$rights = ($params['type'] & file_storage::FILTER_WRITABLE) ? 'w' : null;
$imap = $this->rc->get_storage();
$folders = $imap->list_folders_subscribed('', '*', 'file', $rights);
if (!is_array($folders)) {
throw new Exception("Storage error. Unable to get folders list.", file_storage::ERROR);
}
// create/subscribe 'Files' folder in case there's no folder of type 'file'
if (empty($folders) && !$unsubscribed) {
$default = 'Files';
// the folder may exist but be unsubscribed
if (!$imap->folder_exists($default)) {
if (kolab_storage::folder_create($default, 'file', true)) {
$folders[] = $default;
}
}
else if (kolab_storage::folder_type($default) == 'file') {
if ($imap->subscribe($default)) {
$folders[] = $default;
}
}
}
else {
if ($unsubscribed) {
$subscribed = $folders;
$folders = $imap->list_folders('', '*', 'file', $rights);
$folders = array_diff($folders, $subscribed);
}
// convert folder names to UTF-8
$callback = function($folder) {
if (strpos($folder, '&') !== false) {
return rcube_charset::convert($folder, 'UTF7-IMAP', RCUBE_CHARSET);
}
return $folder;
};
$folders = array_map($callback, $folders);
}
// searching
if (isset($params['search'])) {
$search = mb_strtoupper($params['search']);
$prefix = null;
$ns = $imap->get_namespace('other');
if (!empty($ns)) {
$prefix = rcube_charset::convert($ns[0][0], 'UTF7-IMAP', RCUBE_CHARSET);
}
$folders = array_filter($folders, function($folder) use ($search, $prefix) {
$path = explode('/', $folder);
// search in folder name not the full path
if (strpos(mb_strtoupper($path[count($path)-1]), $search) !== false) {
return true;
}
// if it is an other user folder, we'll match the user name
// and return all folders of the matching user
else if (strpos($folder, $prefix) === 0 && strpos(mb_strtoupper($path[1]), $search) !== false) {
return true;
}
return false;
});
}
$folders = array_values($folders);
// In extended format we return array of arrays
if ($params['extended']) {
if (!$rights && $params['permissions']) {
// get list of known writable folders from cache
$cache_key = 'mailboxes.permissions';
$permissions = (array) $imap->get_cache($cache_key);
}
foreach ($folders as $idx => $folder_name) {
$folder = array('folder' => $folder_name);
// check if folder is readonly
if (isset($permissions)) {
if (!array_key_exists($folder_name, $permissions)) {
$acl = $this->folder_rights($folder_name);
$permissions[$folder_name] = $acl;
}
if (!($permissions[$folder_name] & file_storage::ACL_WRITE)) {
$folder['readonly'] = true;
}
}
$folders[$idx] = $folder;
}
if ($cache_key) {
$imap->update_cache($cache_key, $permissions);
}
}
return $folders;
}
/**
* Check folder rights.
*
* @param string $folder Folder name
*
* @return int Folder rights (sum of file_storage::ACL_*)
*/
public function folder_rights($folder)
{
$storage = $this->rc->get_storage();
$folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP');
$rights = file_storage::ACL_READ;
// get list of known writable folders from cache
$cache_key = 'mailboxes.permissions';
$permissions = (array) $storage->get_cache($cache_key);
if (array_key_exists($folder, $permissions)) {
return $permissions[$folder];
}
// For better performance, assume personal folders are writeable
if ($storage->folder_namespace($folder) == 'personal') {
$rights |= file_storage::ACL_WRITE;
}
else {
$myrights = $storage->my_rights($folder);
if (in_array('t', (array) $myrights)) {
$rights |= file_storage::ACL_WRITE;
}
$permissions[$folder] = $rights;
$storage->update_cache($cache_key, $permissions);
}
return $rights;
}
/**
* 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 $path File/folder path
* @param bool $child_locks Enables subtree checks
*
* @return array List of locks
* @throws Exception
*/
public function lock_list($path, $child_locks = false)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->path2uri($path);
// 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->uri2path($lock['uri']);
}
return $list;
}
/**
* Locks a URI
*
* @param string $path File/folder path
* @param array $lock Lock data
* - depth: 0/'infinite'
* - scope: 'shared'/'exclusive'
* - owner: string
* - token: string
* - timeout: int
*
* @throws Exception
*/
public function lock($path, $lock)
{
$this->init_lock_db();
// convert URI to global resource string
$uri = $this->path2uri($path);
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 File/folder path
* @param array $lock Lock data
*
* @throws Exception
*/
public function unlock($path, $lock)
{
$this->init_lock_db();
// convert path to global resource string
$uri = $this->path2uri($path);
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 files from a folder (with performance fix)
+ */
+ protected function get_files($folder, $filter, $all = true)
+ {
+ if (!($folder instanceof kolab_storage_folder)) {
+ $folder = $this->get_folder_object($folder);
+ }
+
+ // for better performance it's good to assume max. number of records
+ $folder->set_order_and_limit(null, $all ? 0 : 1);
+
+ return $folder->select($filter);
+ }
+
/**
* Get file object.
*
* @param string $file_name Name of a file (with folder path)
* @param kolab_storage_folder $folder Reference to folder object
+ * @param bool $cache Use internal cache
*
* @return array File data
* @throws Exception
*/
- protected function get_file_object(&$file_name, &$folder = null)
+ protected function get_file_object(&$file_name, &$folder = null, $cache = false)
{
+ $original_name = $file_name;
+
// 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(
+
+ if ($cache && !empty($this->icache[$original_name])) {
+ return $this->icache[$original_name];
+ }
+
+ $filter = array(
array('type', '=', 'file'),
array('filename', '=', $file_name)
- ));
+ );
+
+ $files = $this->get_files($folder, $filter, false);
+ $file = $files[0];
- return $files[0];
+ if ($cache) {
+ $this->icache[$original_name] = $file;
+ }
+
+ return $file;
}
/**
* 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, 'file');
if (!$folder || !$folder->valid) {
$error = $folder->get_error();
if ($error === kolab_storage::ERROR_IMAP_CONN || $error === kolab_storage::ERROR_CACHE_DB) {
throw new Exception("The storage is temporarily unavailable.", file_storage::ERROR_UNAVAILABLE);
}
else if ($error === kolab_storage::ERROR_NO_PERMISSION) {
throw new Exception("Storage error. Access not permitted", file_storage::ERROR_FORBIDDEN);
}
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;
}
/**
* Convert file/folder path into a global URI.
*
* @param string $path File/folder path
*
* @return string URI
* @throws Exception
*/
public function path2uri($path)
{
$storage = $this->rc->get_storage();
$namespace = $storage->get_namespace();
$separator = $storage->get_hierarchy_delimiter();
$path = str_replace(file_storage::SEPARATOR, $separator, $path);
$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($path, $root[0]) === 0) {
$path = substr($path, 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, $path) = explode($separator, $path, 2);
if (strpos($user, '@') === false) {
$domain = strstr($owner, '@');
if (!empty($domain)) {
$user .= $domain;
}
}
$owner = $user;
break;
}
break 2;
}
}
}
return 'imap://' . rawurlencode($owner) . '@' . $storage->options['host']
. '/' . file_utils::encode_path($path);
}
/**
* Convert global URI into file/folder path.
*
* @param string $uri URI
*
* @return string File/folder path
* @throws Exception
*/
public function uri2path($uri)
{
if (!preg_match('|^imap://([^@]+)@([^/]+)/(.*)$|', $uri, $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 = rawurldecode($matches[1]);
$path = file_utils::decode_path($matches[3]);
// 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)) {
$path = $matches[1] . $path;
}
// 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
$path = $namespace[0][0] . $local . $separator . $path;
}
return str_replace($separator, file_storage::SEPARATOR, $path);
}
/**
* Initializes file_locks object
*/
protected function init_lock_db()
{
if (!$this->lock_db) {
$this->lock_db = new file_locks;
}
}
}
diff --git a/lib/file_api.php b/lib/file_api.php
index e5706ac..21abbd4 100644
--- a/lib/file_api.php
+++ b/lib/file_api.php
@@ -1,442 +1,434 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
- | Copyright (C) 2012-2013, Kolab Systems AG |
+ | Copyright (C) 2012-2015, 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 extends file_api_core
{
public $session;
public $config;
public $browser;
public $output_type = file_api_core::OUTPUT_JSON;
public function __construct()
{
$rcube = rcube::get_instance();
$rcube->add_shutdown_function(array($this, 'shutdown'));
$this->config = $rcube->config;
$this->session_init();
if ($_SESSION['env']) {
$this->env = $_SESSION['env'];
}
$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());
$this->session->regenerate_id(false);
if ($username = $this->authenticate()) {
$_SESSION['user'] = $username;
- $_SESSION['time'] = time();
$_SESSION['env'] = $this->env;
// remember client API version
if (is_numeric($_GET['version'])) {
$_SESSION['version'] = $_GET['version'];
}
if ($this->request == 'authenticate') {
$this->output_success(array(
'token' => session_id(),
'capabilities' => $this->capabilities(),
));
}
}
else {
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();
+ $this->session->start();
return false;
}
session_id($sess_id);
- session_start();
+ $this->session->start();
if (empty($_SESSION['user'])) {
return false;
}
- $timeout = $this->config->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->config->get('session_name');
$lifetime = $this->config->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');
// Roundcube Framework >= 1.2
if (in_array('factory', get_class_methods('rcube_session'))) {
$this->session = rcube_session::factory($this->config);
}
// Rouncube Framework < 1.2
else {
$this->session = new rcube_session($rcube->get_dbh(), $this->config);
$this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
$this->session->set_ip_check($this->config->get('ip_check'));
}
$this->session->register_gc_handler(array($rcube, 'gc'));
// 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->config->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)) {
$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_core::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->env) as $name) {
if (isset($_GET[$name])) {
$this->env[$name] = $_GET[$name];
}
}
$_SESSION['env'] = $this->env;
return $this->env;
case 'upload_progress':
return $this->upload_progress();
case 'mimetypes':
return $this->supported_mimetypes();
case 'capabilities':
return $this->capabilities();
}
// handle request
if ($request && preg_match('/^[a-z0-9_-]+$/', $request)) {
$aliases = array(
// request name aliases for backward compatibility
'lock' => 'lock_create',
'unlock' => 'lock_delete',
'folder_rename' => 'folder_move',
);
// Redirect all document_* actions into 'document' action
- if (strpos($request, 'document_') === 0) {
+ if (preg_match('/^(invitations|document_[a-z]+)$/', $request)) {
$request = 'document';
}
$request = $aliases[$request] ?: $request;
require_once __DIR__ . "/api/common.php";
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();
}
}
throw new Exception("Unknown method", file_api_core::ERROR_INVALID);
}
/**
* 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 = max(1, 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_core::ERROR_CODE);
}
/**
* 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_core::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;
}
/**
* Returns API version supported by the client
*/
public function client_version()
{
return $_SESSION['version'];
}
/**
* 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_api_core.php b/lib/file_api_core.php
index f2dce01..6839b7f 100644
--- a/lib/file_api_core.php
+++ b/lib/file_api_core.php
@@ -1,333 +1,369 @@
<?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_core extends file_locale
{
const API_VERSION = 2;
const ERROR_CODE = 500;
const ERROR_INVALID = 501;
const OUTPUT_JSON = 'application/json';
const OUTPUT_HTML = 'text/html';
public $env = array(
'date_format' => 'Y-m-d H:i',
'language' => 'en_US',
);
protected $app_name = 'Kolab File API';
protected $drivers = array();
+ protected $icache = array();
protected $backend;
/**
* Returns API version
*/
public function client_version()
{
return self::API_VERSION;
}
/**
* Initialise authentication/configuration backend class
*
* @return file_storage Main storage driver
*/
public function get_backend()
{
if ($this->backend) {
return $this->backend;
}
$rcube = rcube::get_instance();
$driver = $rcube->config->get('fileapi_backend', 'kolab');
$this->backend = $this->load_driver_object($driver);
// configure api
$this->backend->configure($this->env);
return $this->backend;
}
/**
* 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)
{
$rcube = rcube::get_instance();
$enabled = $rcube->config->get('fileapi_drivers');
$preconf = $rcube->config->get('fileapi_sources');
$result = array();
$all = array();
$iRony = defined('KOLAB_DAV_ROOT');
if (!empty($enabled)) {
$backend = $this->get_backend();
$drivers = $backend->driver_list();
foreach ($drivers as $item) {
// Disable webdav sources/drivers in iRony that point to the
// same host to prevent infinite recursion
if ($iRony && $item['driver'] == 'webdav') {
$self_url = parse_url($_SERVER['SCRIPT_URI']);
$item_url = parse_url($item['host']);
if ($self_url['host'] == $item_url['host']) {
continue;
}
}
$all[] = $item['title'];
if ($item['enabled'] && in_array($item['driver'], (array) $enabled)) {
$result[] = $as_objects ? $this->get_driver_object($item) : $item;
}
}
}
if (empty($result) && !empty($preconf)) {
foreach ((array) $preconf as $title => $item) {
if (!in_array($title, $all)) {
$item['title'] = $title;
$item['admin'] = true;
$result[] = $as_objects ? $this->get_driver_object($item) : $item;
}
}
}
return $result;
}
/**
* Return driver for specified file/folder path
*
* @param string $path Folder/file path
*
* @return array Storage driver object, modified path, driver config
*/
public function get_driver($path)
{
$drivers = $this->get_drivers();
foreach ($drivers as $item) {
$prefix = $item['title'] . file_storage::SEPARATOR;
if ($path == $item['title'] || strpos($path, $prefix) === 0) {
$selected = $item;
break;
}
}
if (empty($selected)) {
return array($this->get_backend(), $path);
}
$path = substr($path, strlen($selected['title']) + 1);
return array($this->get_driver_object($selected), $path, $selected);
}
/**
* Initialize driver instance
*
* @param array $config Driver config
*
* @return file_storage Storage driver instance
*/
public function get_driver_object($config)
{
$key = $config['title'];
if (empty($this->drivers[$key])) {
$this->drivers[$key] = $driver = $this->load_driver_object($config['driver']);
if ($config['username'] == '%u') {
$backend = $this->get_backend();
$auth_info = $backend->auth_info();
$config['username'] = $auth_info['username'];
$config['password'] = $auth_info['password'];
}
else if (!empty($config['password']) && empty($config['admin']) && !empty($key)) {
$config['password'] = $this->decrypt($config['password']);
}
// configure api
$driver->configure(array_merge($config, $this->env), $key);
}
return $this->drivers[$key];
}
/**
* Loads a driver
*/
public function load_driver_object($name)
{
$class = $name . '_file_storage';
if (!class_exists($class, false)) {
$include_path = __DIR__ . "/drivers/$name" . PATH_SEPARATOR;
$include_path .= ini_get('include_path');
set_include_path($include_path);
}
return new $class;
}
/**
* Returns storage(s) capabilities
*
+ * @param bool $full Return all drivers' capabilities
+ *
* @return array Capabilities
*/
- public function capabilities()
+ public function capabilities($full = true)
{
$rcube = rcube::get_instance();
$backend = $this->get_backend();
$caps = array();
// check support for upload progress
if (($progress_sec = $rcube->config->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;
}
// get capabilities of main storage module
foreach ($backend->capabilities() as $name => $value) {
// skip disabled capabilities
if ($value !== false) {
$caps[$name] = $value;
}
}
// Manticore support
if ($manticore = $rcube->config->get('fileapi_manticore')) {
$caps['MANTICORE'] = true;
}
+ if (!$full) {
+ return $caps;
+ }
+
// 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['MOUNTPOINTS'][$title][$name] = $value;
}
}
}
}
return $caps;
}
+ /**
+ * Get user name from user identifier (email address) using LDAP lookup
+ *
+ * @param string $email User identifier
+ *
+ * @return string User name
+ */
+ public function resolve_user($email)
+ {
+ $key = "user:$email";
+
+ // make sure Kolab backend is initialized so kolab_storage can be found
+ $this->get_backend();
+
+ // @todo: Move this into drivers
+ if ($this->icache[$key] === null
+ && class_exists('kolab_storage')
+ && ($ldap = kolab_storage::ldap())
+ ) {
+ $user = $ldap->get_user_record($email, $_SESSION['imap_host']);
+
+ $this->icache[$key] = $user ?: false;
+ }
+
+ if ($this->icache[$key]) {
+ return $this->icache[$key]['displayname'] ?: $this->icache[$key]['name'];
+ }
+ }
+
/**
* Return mimetypes list supported by built-in viewers
*
* @return array List of mimetypes
*/
protected function supported_mimetypes()
{
$mimetypes = array();
$dir = __DIR__ . '/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;
}
/**
* Encrypts data with current user password
*
* @param string $str A string to encrypt
*
* @return string Encrypted string (and base64-encoded)
*/
public function encrypt($str)
{
$rcube = rcube::get_instance();
$key = $this->get_crypto_key();
return $rcube->encrypt($str, $key, true);
}
/**
* Decrypts data encrypted with encrypt() method
*
* @param string $str Encrypted string (base64-encoded)
*
* @return string Decrypted string
*/
public function decrypt($str)
{
$rcube = rcube::get_instance();
$key = $this->get_crypto_key();
return $rcube->decrypt($str, $key, true);
}
/**
* Set encryption password
*/
protected function get_crypto_key()
{
$key = 'chwala_crypto_key';
$rcube = rcube::get_instance();
$backend = $this->get_backend();
$user = $backend->auth_info();
$password = $user['password'] . $user['username'];
// encryption password must be 24 characters, no less, no more
if (($len = strlen($password)) > 24) {
$password = substr($password, 0, 24);
}
else {
$password = $password . substr($rcube->config->get('des_key'), 0, 24 - $len);
}
$rcube->config->set($key, $password);
return $key;
}
}
diff --git a/lib/file_manticore.php b/lib/file_manticore.php
index f608f5d..440624b 100644
--- a/lib/file_manticore.php
+++ b/lib/file_manticore.php
@@ -1,555 +1,689 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2015, 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> |
+--------------------------------------------------------------------------+
*/
/**
* Document editing sessions handling
*/
class file_manticore
{
protected $api;
protected $rc;
protected $request;
+ protected $user;
protected $sessions_table = 'chwala_sessions';
protected $invitations_table = 'chwala_invitations';
+ protected $icache = array();
const STATUS_INVITED = 'invited';
const STATUS_REQUESTED = 'requested';
const STATUS_ACCEPTED = 'accepted';
const STATUS_DECLINED = 'declined';
+ const STATUS_DECLINED_OWNER = 'declined-owner'; // same as 'declined' but done by the session owner
+ const STATUS_ACCEPTED_OWNER = 'accepted-owner'; // same as 'accepted' but done by the session owner
/**
* Class constructor
*
* @param file_api Chwala API app instance
*/
public function __construct($api)
{
- $this->rc = rcube::get_instance();
- $this->api = $api;
+ $this->rc = rcube::get_instance();
+ $this->api = $api;
+ $this->user = $_SESSION['user'];
$db = $this->rc->get_dbh();
$this->sessions_table = $db->table_name($this->sessions_table);
$this->invitations_table = $db->table_name($this->invitations_table);
}
/**
* Return viewer URI for specified file/session. This creates
* a new collaborative editing session when needed.
*
* @param string $file File path
* @param string &$session_id Optional session ID to join to
*
* @return string Manticore URI
* @throws Exception
*/
public function session_start($file, &$session_id = null)
{
- list($driver, $path) = $this->api->get_driver($file);
+ if ($file !== null) {
+ list($driver, $path) = $this->api->get_driver($file);
+ $uri = $driver->path2uri($path);
+ }
$backend = $this->api->get_backend();
- $uri = $driver->path2uri($path);
if ($session_id) {
$session = $this->session_info($session_id);
if (empty($session)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
// check session ownership
- if ($session['owner'] != $_SESSION['user']) {
+ if ($session['owner'] != $this->user) {
// check if the user was invited
- $invitations = $this->invitations_list($session_id);
- $states = array(self::STATUS_DECLINED, self::STATUS_REQUESTED);
+ $invitations = $this->invitations_find(array('session_id' => $session_id, 'user' => $this->user));
+ $states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
- if (empty($invitations) || in_array($invitations[0]['status'], $states)) {
+ if (empty($invitations) || !in_array($invitations[0]['status'], $states)) {
throw new Exception("No permission to join the editing session.", file_api_core::ERROR_CODE);
}
// automatically accept the invitation, if not done yet
if ($invitations[0]['status'] == self::STATUS_INVITED) {
- $this->invitation_update($session_id, $_SESSION['user'], self::STATUS_ACCEPTED);
+ $this->invitation_update($session_id, $this->user, self::STATUS_ACCEPTED);
}
}
+ // authenticate to Manticore, we need auth token for frame_uri
+ $req = $this->get_request();
+
// @TODO: make sure the session exists in Manticore?
}
- else {
- $session_id = rcube_utils::bin2ascii(md5(time() . $uri, true));
- $data = array();
- $owner = $_SESSION['user'];
-
- // we'll store user credentials if the file comes from
- // an external source that requires authentication
- if ($backend != $driver) {
- $auth = $driver->auth_info();
- $auth['password'] = $this->rc->encrypt($auth['password']);
- $data['auth_info'] = $auth;
+ else if (!empty($uri)) {
+ // To prevent from creating new sessions for the same file+user
+ // (e.g. when user uses F5 to refresh the page), we check first
+ // if such a session exist and continue with it
+ $db = $this->rc->get_dbh();
+ $res = $db->query("SELECT `id` FROM `{$this->sessions_table}`"
+ . " WHERE `owner` = ? AND `uri` = ?", $this->user, $uri);
+
+ if ($row = $db->fetch_assoc($res)) {
+ $session_id = $row['id'];
+ $res = true;
}
+ else if (!$db->is_error($res)) {
+ $session_id = rcube_utils::bin2ascii(md5(time() . $uri, true));
+ $data = array();
+ $owner = $this->user;
+
+ // we'll store user credentials if the file comes from
+ // an external source that requires authentication
+ if ($backend != $driver) {
+ $auth = $driver->auth_info();
+ $auth['password'] = $this->rc->encrypt($auth['password']);
+ $data['auth_info'] = $auth;
+ }
- $res = $this->session_create($session_id, $uri, $owner, $data);
+ $res = $this->session_create($session_id, $uri, $owner, $data);
+ }
if (!$res) {
throw new Exception("Failed creating document editing session", file_api_core::ERROR_CODE);
}
}
+ else {
+ throw new Exception("Failed creating document editing session (unknown file)", file_api_core::ERROR_CODE);
+ }
return $this->frame_uri($session_id);
}
/**
* Get file path (not URI) from session.
*
- * @param string $id Session ID
+ * @param string $id Session ID
+ * @param bool $join_mode Throw exception only if session does not exist
*
* @return string File path
* @throws Exception
*/
- public function session_file($id)
+ public function session_file($id, $join_mode = false)
{
$session = $this->session_info($id);
if (empty($session)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
$path = $this->uri2path($session['uri']);
- if (empty($path)) {
+ if (empty($path) && (!$join_mode || $session['owner'] == $this->user)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
- // @TODO: check permissions to the session
+ // check permissions to the session
+ if ($session['owner'] != $this->user) {
+ $invitations = $this->invitations_find(array('session_id' => $id, 'user' => $this->user));
+ $states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
+
+ if (empty($invitations) || !in_array($invitations[0]['status'], $states)) {
+ throw new Exception("No permission to join the editing session.", file_api_core::ERROR_CODE);
+ }
+ }
return $path;
}
/**
* Get editing session info
*
* @param string $id Session identifier
* @param bool $with_invitations Return invitations list
+ *
+ * @return array Session data
*/
public function session_info($id, $with_invitations = false)
{
- $db = $this->rc->get_dbh();
- $result = $db->query("SELECT * FROM `{$this->sessions_table}`"
- . " WHERE `id` = ?", $id);
+ $session = $this->icache["session:$id"];
+
+ if (!$session) {
+ $db = $this->rc->get_dbh();
+ $result = $db->query("SELECT * FROM `{$this->sessions_table}`"
+ . " WHERE `id` = ?", $id);
+
+ if ($row = $db->fetch_assoc($result)) {
+ $session = $this->session_info_parse($row);
+
+ $this->icache["session:$id"] = $session;
+ }
+ }
- if ($row = $db->fetch_assoc($result)) {
- $session = $this->session_info_parse($row);
+ if ($session) {
+ if ($session['owner'] == $this->user) {
+ $session['is_owner'] = true;
+ }
if ($with_invitations && $session['is_owner']) {
$session['invitations'] = $this->invitations_find(array('session_id' => $id));
}
-
- return $session;
}
+
+ return $session;
}
/**
* Find editing sessions for specified path
*/
public function session_find($path, $invitations = true)
{
// create an URI for specified path
list($driver, $path) = $this->api->get_driver($path);
$uri = trim($driver->path2uri($path), '/') . '/';
// get existing sessions
$sessions = array();
- $filter = array('file', 'owner', 'is_owner');
+ $filter = array('file', 'owner', 'owner_name', 'is_owner');
$db = $this->rc->get_dbh();
$result = $db->query("SELECT * FROM `{$this->sessions_table}`"
. " WHERE `uri` LIKE '" . $db->escape($uri) . "%'");
while ($row = $db->fetch_assoc($result)) {
if ($path = $this->uri2path($row['uri'])) {
$sessions[$row['id']] = $this->session_info_parse($row, $path, $filter);
}
}
// set 'is_invited' flag
if ($invitations && !empty($sessions)) {
- $invitations = $this->invitations_list();
- $states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED);
+ $invitations = $this->invitations_find(array('user' => $this->user));
+ $states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
foreach ($invitations as $invitation) {
if (!empty($sessions[$invitation['session_id']]) && in_array($invitation['status'], $states)) {
$sessions[$invitation['session_id']]['is_invited'] = true;
}
}
}
return $sessions;
}
/**
* Delete editing session (only owner can do that)
*
* @param string $id Session identifier
* @param bool $local Remove session only from local database
*/
public function session_delete($id, $local = false)
{
$db = $this->rc->get_dbh();
$result = $db->query("DELETE FROM `{$this->sessions_table}`"
. " WHERE `id` = ? AND `owner` = ?",
- $id, $_SESSION['user']);
+ $id, $this->user);
$success = $db->affected_rows($result) > 0;
// Send document delete to Manticore
if ($success && !$local) {
$req = $this->get_request();
$res = $req->document_delete($id);
}
return $success;
}
/**
* Create editing session
*/
protected function session_create($id, $uri, $owner, $data)
{
+ // get user name
+ $owner_name = $this->api->resolve_user($owner) ?: '';
+
// Do this before starting the session in Manticore,
// it will immediately call api/document to get the file body
$db = $this->rc->get_dbh();
$result = $db->query("INSERT INTO `{$this->sessions_table}`"
- . " (`id`, `uri`, `owner`, `data`) VALUES (?, ?, ?, ?)",
- $id, $uri, $owner, json_encode($data));
+ . " (`id`, `uri`, `owner`, `owner_name`, `data`)"
+ . " VALUES (?, ?, ?, ?, ?)",
+ $id, $uri, $owner, $owner_name, json_encode($data));
$success = $db->affected_rows($result) > 0;
// create the session in Manticore
if ($success) {
$req = $this->get_request();
$res = $req->document_create(array(
'id' => $id,
'title' => '', // @TODO: maybe set to a file path without extension?
'access' => array(
array(
'identity' => $owner,
'permission' => file_manticore_api::ACCESS_WRITE,
),
),
));
if (!$res) {
$this->session_delete($id, true);
return false;
}
}
return $success;
}
/**
- * Find invitations for current user
- *
+ * Find invitations for current user. This will return all
+ * invitations related to the user including his sessions.
*
+ * @param array $filter Search filter (see self::invitations_find())
*
* @return array Invitations list
*/
- public function invitations_list($session_id = null)
+ public function invitations_list($filter = array())
{
- $invitations = array();
- $db = $this->rc->get_dbh();
- $result = $db->query("SELECT * FROM `{$this->invitations_table}`"
- . " WHERE `user`= ?"
- . ($session_id ? " AND `session_id` = " . $db->quote($session_id) : "")
- . " ORDER BY `changed`", $_SESSION['user']);
+ $filter['user'] = $this->user;
- while ($row = $db->fetch_assoc($result)) {
- $invitations[] = $row;
+ // list of invitations to the user or requested by him
+ $result = $this->invitations_find($filter, true);
+
+ unset($filter['user']);
+ $filter['owner'] = $this->user;
+
+ // other invitations that belong to the sessions owned by the user
+ if ($other = $this->invitations_find($filter, true)) {
+ $result = array_merge($result, $other);
}
- return $invitations;
+ return $result;
}
/**
- * Find invitations for specified session_id
+ * Find invitations for specified filter
+ *
+ * @param array $filter Search filter (see self::invitations_find())
+ * - session_id: session identifier
+ * - timestamp: "changed > ?" filter
+ * - user: Invitation user identifier
+ * - owner: Session owner identifier
+ * @param bool $extended Return session file names
+ *
+ * @return array Invitations list
*/
- public function invitations_find($filter)
+ public function invitations_find($filter, $extended = false)
{
- $invitations = array();
- $db = $this->rc->get_dbh();
+ $db = $this->rc->get_dbh();
+ $query = '';
+ $select = "i.*";
foreach ($filter as $column => $value) {
- $filter[$column] = "`$column` = " . $db->quote($value);
+ if ($column == 'timestamp') {
+ $where[] = "i.`changed` > " . $db->fromunixtime($value);
+ }
+ else if ($column == 'owner') {
+ $join[] = "`{$this->sessions_table}` s ON (i.`session_id` = s.`id`)";
+ $where[] = "s.`owner` = " . $db->quote($value);
+ }
+ else {
+ $where[] = "i.`$column` = " . $db->quote($value);
+ }
}
- $where = implode(' AND ', $filter);
- $result = $db->query("SELECT * FROM `{$this->invitations_table}`"
- . " WHERE $where ORDER BY `changed`");
+ if ($extended) {
+ $select .= ", s.`uri`, s.`owner`, s.`owner_name`";
+ $join[] = "`{$this->sessions_table}` s ON (i.`session_id` = s.`id`)";
+ }
+
+ if (!empty($join)) {
+ $query .= ' JOIN ' . implode(' JOIN ', array_unique($join));
+ }
+
+ if (!empty($where)) {
+ $query .= ' WHERE ' . implode(' AND ', array_unique($where));
+ }
+
+ $result = $db->query("SELECT $select FROM `{$this->invitations_table}` i"
+ . "$query ORDER BY `changed`");
+
+ if ($db->is_error($result)) {
+ throw new Exception("Internal error.", file_api_core::ERROR_CODE);
+ }
+
+ $invitations = array();
while ($row = $db->fetch_assoc($result)) {
+ if ($extended) {
+ try {
+ // add unix-timestamp of the `changed` date to the result
+ $dt = new DateTime($row['changed']);
+ $row['timestamp'] = $dt->format('U');
+ }
+ catch(Exception $e) { }
+
+ // add filename to the result
+ $filename = parse_url($row['uri'], PHP_URL_PATH);
+ $filename = pathinfo($filename, PATHINFO_BASENAME);
+ $filename = rawurldecode($filename);
+
+ $row['filename'] = $filename;
+
+ if ($path = $this->uri2path($row['uri'])) {
+ $row['file'] = $path;
+ }
+
+ unset($row['uri']);
+ }
+
$invitations[] = $row;
}
return $invitations;
}
/**
* Create an invitation
*
* @param string $session_id Document session identifier
- * @param string $user User identifier
+ * @param string $user User identifier (use null for current user)
* @param string $status Invitation status (invited, requested)
+ * @param string $comment Invitation description/comment
+ * @param string &$user_name Optional user name
*
* @throws Exception
*/
- public function invitation_create($session_id, $user, $status = 'invited')
+ public function invitation_create($session_id, $user, $status = 'invited', $comment = '', &$user_name = '')
{
+ if (empty($user)) {
+ $user = $this->user;
+ }
+
if ($status != self::STATUS_INVITED && $status != self::STATUS_REQUESTED) {
throw new Exception("Invalid invitation status.", file_api_core::ERROR_CODE);
}
// get session information
$session = $this->session_info($session_id);
if (empty($session)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
// check session ownership, only owner can create 'new' invitations
- if ($status == self::STATUS_INVITED && $session['owner'] != $_SESSION['user']) {
+ if ($status == self::STATUS_INVITED && $session['owner'] != $this->user) {
throw new Exception("No permission to create an invitation.", file_api_core::ERROR_CODE);
}
if ($session['owner'] == $user) {
throw new Exception("Not possible to create an invitation for the session creator.", file_api_core::ERROR_CODE);
}
// Update Manticore 'access' array
if ($status == self::STATUS_INVITED) {
$req = $this->get_request();
$res = $req->editor_add($session_id, $user, file_manticore_api::ACCESS_WRITE);
if (!$res) {
throw new Exception("Failed to create an invitation.", file_api_core::ERROR_CODE);
}
}
+ // get user name
+ if (empty($user_name)) {
+ $user_name = $this->api->resolve_user($user) ?: '';
+ }
+
// insert invitation
$db = $this->rc->get_dbh();
$result = $db->query("INSERT INTO `{$this->invitations_table}`"
- . " (`session_id`, `user`, `status`, `changed`)"
- . " VALUES (?, ?, ?, " . $db->now() . ")",
- $session_id, $user, $status);
+ . " (`session_id`, `user`, `user_name`, `status`, `comment`, `changed`)"
+ . " VALUES (?, ?, ?, ?, ?, " . $db->now() . ")",
+ $session_id, $user, $user_name, $status, $comment ?: '');
if (!$db->affected_rows($result)) {
throw new Exception("Failed to create an invitation.", file_api_core::ERROR_CODE);
}
}
/**
* Delete an invitation (only session owner can do that)
*
* @param string $session_id Session identifier
* @param string $user User identifier
*
* @throws Exception
*/
public function invitation_delete($session_id, $user)
{
$db = $this->rc->get_dbh();
$result = $db->query("DELETE FROM `{$this->invitations_table}`"
. " WHERE `session_id` = ? AND `user` = ?"
. " AND EXISTS (SELECT 1 FROM `{$this->sessions_table}` WHERE `id` = ? AND `owner` = ?)",
- $session_id, $user, $session_id, $_SESSION['user']);
+ $session_id, $user, $session_id, $this->user);
if (!$db->affected_rows($result)) {
throw new Exception("Failed to delete an invitation.", file_api_core::ERROR_CODE);
}
// Update Manticore 'access' array
- // @todo
+ $req = $this->get_request();
+ $res = $req->editor_delete($session_id, $user);
+
+ if (!$res) {
+ throw new Exception("Failed to remove an invitation.", file_api_core::ERROR_CODE);
+ }
}
/**
* Update an invitation status
*
* @param string $session_id Session identifier
- * @param string $user User identifier
+ * @param string $user User identifier (use null for current user)
* @param string $status Invitation status (accepted, declined)
+ * @param string $comment Invitation description/comment
*
* @throws Exception
*/
- public function invitation_update($session_id, $user, $status)
+ public function invitation_update($session_id, $user, $status, $comment = '')
{
+ if (empty($user)) {
+ $user = $this->user;
+ }
+
if ($status != self::STATUS_ACCEPTED && $status != self::STATUS_DECLINED) {
throw new Exception("Invalid invitation status.", file_api_core::ERROR_CODE);
}
// get session information
$session = $this->session_info($session_id);
if (empty($session)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
// check session ownership
- if ($user != $_SESSION['user'] && $session['owner'] != $_SESSION['user']) {
+ if ($user != $this->user && $session['owner'] != $this->user) {
throw new Exception("No permission to update an invitation.", file_api_core::ERROR_CODE);
}
+ if ($session['owner'] == $this->user) {
+ $status = $status . '-owner';
+ }
+
$db = $this->rc->get_dbh();
$result = $db->query("UPDATE `{$this->invitations_table}`"
- . " SET `status` = ?, `changed` = " . $db->now()
+ . " SET `status` = ?, `comment` = ?, `changed` = " . $db->now()
. " WHERE `session_id` = ? AND `user` = ?",
- $status, $session_id, $user);
+ $status, $comment ?: '', $session_id, $user);
if (!$db->affected_rows($result)) {
throw new Exception("Failed to update an invitation status.", file_api_core::ERROR_CODE);
}
// Update Manticore 'access' array if an owner accepted an invitation request
- if ($status == self::STATUS_ACCEPTED && $_SESSION['user'] == $session['owner']) {
- // @todo
+ if ($status == self::STATUS_ACCEPTED_OWNER) {
+ $req = $this->get_request();
+ $res = $req->editor_add($session_id, $user, file_manticore_api::ACCESS_WRITE);
+
+ if (!$res) {
+ throw new Exception("Failed to update an invitation status.", file_api_core::ERROR_CODE);
+ }
}
}
/**
* Parse session info data
*/
protected function session_info_parse($record, $path = null, $filter = array())
{
/*
if (is_string($data) && !empty($data)) {
$data = json_decode($data, true);
}
*/
$session = array();
- $fields = array('id', 'uri', 'owner');
+ $fields = array('id', 'uri', 'owner', 'owner_name');
foreach ($fields as $field) {
if (isset($record[$field])) {
$session[$field] = $record[$field];
}
}
if ($path) {
$session['file'] = $path;
}
// @TODO: is_invited?, last_modified?
- if ($session['owner'] == $_SESSION['user']) {
+ if ($session['owner'] == $this->user) {
$session['is_owner'] = true;
}
if (!empty($filter)) {
$session = array_intersect_key($session, array_flip($filter));
}
return $session;
}
/**
* Generate URI of Manticore editing session
*/
protected function frame_uri($id)
{
$base_url = rtrim($this->rc->config->get('fileapi_manticore'), ' /');
return $base_url . '/document/' . $id . '/' . $_SESSION['manticore_token'];
}
/**
* Get file path from the URI
*/
protected function uri2path($uri)
{
$backend = $this->api->get_backend();
try {
return $backend->uri2path($uri);
}
catch (Exception $e) {
// do nothing
}
foreach ($this->api->get_drivers(true) as $driver) {
try {
$path = $driver->uri2path($uri);
$title = $driver->title();
if ($title) {
$path = $title . file_storage::SEPARATOR . $path;
}
return $path;
}
catch (Exception $e) {
// do nothing
}
}
}
- /**
- * Return Manticore user/session info
- */
- public function user_info()
- {
- $req = $this->get_request();
- $res = $req->get('api/users/me');
-
- return $res->get();
- }
-
/**
* Initialize Manticore API request handler
*/
protected function get_request()
{
if (!$this->request) {
$uri = rcube_utils::resolve_url($this->rc->config->get('fileapi_manticore'));
$this->request = new file_manticore_api($uri);
// Use stored session token, check if it's still valid
if ($_SESSION['manticore_token']) {
$is_valid = $this->request->set_session_token($_SESSION['manticore_token'], true);
if ($is_valid) {
return $this->request;
}
}
$backend = $this->api->get_backend();
$auth = $backend->auth_info();
$_SESSION['manticore_token'] = $this->request->login($auth['username'], $auth['password']);
if (empty($_SESSION['manticore_token'])) {
throw new Exception("Unable to login to Manticore server.", file_api_core::ERROR_CODE);
}
}
return $this->request;
}
}
diff --git a/lib/file_manticore_api.php b/lib/file_manticore_api.php
index 2a6abd5..b590062 100644
--- a/lib/file_manticore_api.php
+++ b/lib/file_manticore_api.php
@@ -1,394 +1,440 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-2015, 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> |
+--------------------------------------------------------------------------+
*/
/**
* Helper class to connect to the Manticore API
*/
class file_manticore_api
{
/**
* @var HTTP_Request2
*/
private $request;
/**
* @var string
*/
private $base_url;
/**
* @var bool
*/
private $debug = false;
const ERROR_INTERNAL = 100;
const ERROR_CONNECTION = 500;
const ACCEPT_HEADER = "application/json,text/javascript,*/*";
const ACCESS_WRITE = 'write';
const ACCESS_READ = 'read';
const ACCESS_DENY = 'deny';
/**
* Class constructor.
*
* @param string $base_url Base URL of the Kolab API
*/
public function __construct($base_url)
{
require_once 'HTTP/Request2.php';
$config = rcube::get_instance()->config;
$this->debug = rcube_utils::get_boolean($config->get('fileapi_manticore_debug'));
$this->base_url = rtrim($base_url, '/') . '/';
$this->request = new HTTP_Request2();
self::configure($this->request);
}
/**
* Configure HTTP_Request2 object
*
* @param HTTP_Request2 $request Request object
*/
public static function configure($request)
{
// Configure connection options
$config = rcube::get_instance()->config;
$http_config = (array) $config->get('http_request', $config->get('kolab_http_request'));
// Deprecated config, all options are separated variables
if (empty($http_config)) {
$options = array(
'ssl_verify_peer',
'ssl_verify_host',
'ssl_cafile',
'ssl_capath',
'ssl_local_cert',
'ssl_passphrase',
'follow_redirects',
);
foreach ($options as $optname) {
if (($optvalue = $config->get($optname)) !== null
|| ($optvalue = $config->get('kolab_' . $optname)) !== null
) {
$http_config[$optname] = $optvalue;
}
}
}
if (!empty($http_config)) {
try {
$request->setConfig($http_config);
}
catch (Exception $e) {
rcube::log_error("HTTP: " . $e->getMessage());
}
}
// proxy User-Agent
$request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
// some HTTP server configurations require this header
$request->setHeader('accept', self::ACCEPT_HEADER);
$request->setHeader('Content-Type', 'application/json; charset=UTF-8');
}
/**
* Return API's base URL
*
* @return string Base URL
*/
public function base_url()
{
return $this->base_url;
}
/**
* Return HTTP_Request2 object
*
* @return HTTP_Request2 Request object
*/
public function request()
{
return $this->request;
}
/**
* Logs specified user into the API
*
* @param string $username User name
* @param string $password User password
*
* @return string Session token (on success)
*/
public function login($username, $password)
{
$query = array(
'email' => $username,
'password' => $password,
);
// remove current token if any
$this->request->setHeader('Authorization');
// authenticate the user
$response = $this->post('auth/local', $query);
if ($token = $response->get('token')) {
$this->set_session_token($token);
}
return $token;
}
/**
* Sets request session token.
*
* @param string $token Session token.
* @param bool $validate Enables token validatity check
*
* @return bool Token validity status
*/
public function set_session_token($token, $validate = false)
{
$this->request->setHeader('Authorization', "Bearer $token");
if ($validate) {
- $result = $this->get('api/user/me');
+ $result = $this->get('api/users/me');
return $result->get_error_code() == 200;
}
return true;
}
/**
* Delete document editing session
*
* @param array $id Session identifier
*
* @return bool True on success, False on failure
*/
public function document_delete($id)
{
$res = $this->delete('api/documents/' . $id);
return $res->get_error_code() == 204;
}
/**
* Create document editing session
*
* @param array $params Session parameters
*
* @return bool True on success, False on failure
*/
public function document_create($params)
{
$res = $this->post('api/documents', $params);
- // @TODO: 422?
+ // @FIXME: 422?
return $res->get_error_code() == 201 || $res->get_error_code() == 422;
}
/**
* Add document editor (update 'access' array)
*
* @param array $session_id Session identifier
* @param array $identity User identifier
*
* @return bool True on success, False on failure
*/
- public function editor_add($session_id, $idenity, $permission)
+ public function editor_add($session_id, $identity, $permission)
{
$res = $this->get("api/documents/$session_id/access");
-rcube::console($req);
if ($res->get_error_code() != 200) {
return false;
}
- // @todo add editor to the 'access' array
-
+ $access = $res->get();
- $res = $this->put("api/documents/$session_id/access", $params);
+ // sanity check, this should never be empty
+ if (empty($access)) {
+ return false;
+ }
+
+ // add editor to the 'access' array
+ foreach ($access as $entry) {
+ if ($entry['identity'] == $identity) {
+ return true;
+ }
+ }
+
+ $access[] = array('identity' => $identity, 'permission' => $permission);
+
+ $res = $this->put("api/documents/$session_id/access", $access);
+
+ return $res->get_error_code() == 200;
+ }
+
+ /**
+ * Remove document editor (update 'access' array)
+ *
+ * @param array $session_id Session identifier
+ * @param array $identity User identifier
+ *
+ * @return bool True on success, False on failure
+ */
+ public function editor_delete($session_id, $identity)
+ {
+ $res = $this->get("api/documents/$session_id/access");
+
+ if ($res->get_error_code() != 200) {
+ return false;
+ }
+
+ $access = $res->get();
+ $found = true;
+
+ // remove editor from the 'access' array
+ foreach ((array) $access as $idx => $entry) {
+ if ($entry['identity'] == $identity) {
+ unset($access[$idx]);
+ }
+ }
+
+ if (!$found) {
+ return false;
+ }
+
+ $res = $this->put("api/documents/$session_id/access", $access);
-rcube::console($req);
return $res->get_error_code() == 200;
}
/**
* API's GET request.
*
* @param string $action Action name
* @param array $get Request arguments
*
* @return file_ui_api_result Response
*/
public function get($action, $get = array())
{
$url = $this->build_url($action, $get);
if ($this->debug) {
rcube::write_log('manticore', "GET: $url " . json_encode($get));
}
$this->request->setMethod(HTTP_Request2::METHOD_GET);
$this->request->setBody('');
return $this->get_response($url);
}
/**
* API's POST request.
*
* @param string $action Action name
* @param array $post POST arguments
*
* @return kolab_client_api_result Response
*/
public function post($action, $post = array())
{
$url = $this->build_url($action);
if ($this->debug) {
rcube::write_log('manticore', "POST: $url " . json_encode($post));
}
$this->request->setMethod(HTTP_Request2::METHOD_POST);
$this->request->setBody(json_encode($post));
return $this->get_response($url);
}
/**
* API's PUT request.
*
* @param string $action Action name
* @param array $post POST arguments
*
* @return kolab_client_api_result Response
*/
public function put($action, $post = array())
{
$url = $this->build_url($action);
if ($this->debug) {
rcube::write_log('manticore', "PUT: $url " . json_encode($post));
}
$this->request->setMethod(HTTP_Request2::METHOD_PUT);
$this->request->setBody(json_encode($post));
return $this->get_response($url);
}
/**
* API's DELETE request.
*
* @param string $action Action name
* @param array $get Request arguments
*
* @return file_ui_api_result Response
*/
public function delete($action, $get = array())
{
$url = $this->build_url($action, $get);
if ($this->debug) {
rcube::write_log('manticore', "DELETE: $url " . json_encode($get));
}
$this->request->setMethod(HTTP_Request2::METHOD_DELETE);
$this->request->setBody('');
return $this->get_response($url);
}
/**
* @param string $action Action GET parameter
* @param array $args GET parameters (hash array: name => value)
*
* @return Net_URL2 URL object
*/
private function build_url($action, $args = array())
{
$url = new Net_URL2($this->base_url . $action);
$url->setQueryVariables((array) $args);
return $url;
}
/**
* HTTP Response handler.
*
* @param Net_URL2 $url URL object
*
* @return kolab_client_api_result Response object
*/
private function get_response($url)
{
try {
$this->request->setUrl($url);
$response = $this->request->send();
}
catch (Exception $e) {
return new file_ui_api_result(null,
self::ERROR_CONNECTION, $e->getMessage());
}
try {
$body = $response->getBody();
}
catch (Exception $e) {
return new file_ui_api_result(null,
self::ERROR_INTERNAL, $e->getMessage());
}
$code = $response->getStatus();
if ($this->debug) {
rcube::write_log('manticore', "Response [$code]: $body");
}
if ($code < 300) {
$result = $body ? json_decode($body, true) : array();
}
else {
if ($code != 401) {
rcube::raise_error("Error $code on $url", true, false);
}
$error = $body;
}
return new file_ui_api_result($result, $code, $error);
}
}
diff --git a/public_html/js/files_api.js b/public_html/js/files_api.js
index 7d9262e..39f76e1 100644
--- a/public_html/js/files_api.js
+++ b/public_html/js/files_api.js
@@ -1,928 +1,1076 @@
-/*
+/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
- | Copyright (C) 2012-2013, Kolab Systems AG |
+ | Copyright (C) 2012-2015, 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.sessions = {};
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); },
+ success: function(response) { if (typeof func == 'function') func(response); else 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); },
+ success: function(response) { if (typeof func == 'function') func(response); else 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|auth|subscribe|unsubscribe|invite|decline|request|accept|remove)$/.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(state, message) {};
// displays error message
this.display_message = function(label, type) {};
// 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;
};
// fill folder selector with options
this.folder_select_element = function(select, params)
{
var options = [],
selected = params && params.selected ? params.selected : this.env.folder;
if (params && params.empty)
options.push($('<option>').val('').text('---'));
$.each(this.env.folders, function(i, f) {
var n, name = escapeHTML(f.name);
// skip read-only folders
if (params && params.writable && (f.readonly || f.virtual)) {
var folder, found = false, prefix = i + ref.env.directory_separator;
// for virtual folders check if there's any writable subfolder
for (n in ref.env.folders) {
if (n.indexOf(prefix) === 0) {
folder = ref.env.folders[n];
if (!folder.virtual && !folder.readonly) {
found = true;
break;
}
}
}
if (!found)
return;
}
for (n=0; n<f.depth; n++)
name = '&nbsp;&nbsp;&nbsp;' + name;
options.push($('<option>').val(i).html(name));
});
select.empty().append(options);
if (selected)
select.val(selected);
};
// Folder list parser, converts it into structure
this.folder_list_parse = function(list, num, subscribed)
{
var i, n, j, items, items_len, f, tmp, folder, readonly,
subs_support, subs_prefixes = {}, found,
separator = this.env.directory_separator,
len = list ? list.length : 0, folders = {};
if (!num) num = 1;
if (subscribed === undefined)
subscribed = true;
// prepare subscriptions support detection
if (len && this.env.caps) {
subs_support = !!this.env.caps.SUBSCRIPTIONS;
$.each(this.env.caps.MOUNTPOINTS || [], function(i, v) {
subs_prefixes[i] = !!v.SUBSCRIPTIONS;
});
}
for (i=0; i<len; i++) {
folder = list[i];
readonly = false;
// in extended format folder is an object
if (typeof folder !== 'string') {
readonly = folder.readonly;
folder = folder.folder;
}
items = folder.split(separator);
items_len = items.length;
for (n=0; n<items_len-1; n++) {
tmp = items.slice(0, n+1);
f = tmp.join(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++,
readonly: readonly
};
// set subscription flag, leave undefined if the source does not support subscriptions
found = false;
for (j in subs_prefixes) {
if (folder === j) {
// this is a mount point
found = true;
break;
}
if (folder.indexOf(j + separator) === 0) {
if (subs_prefixes[j])
folders[folder].subscribed = subscribed;
found = true;
break;
}
}
if (!found && subs_support)
folders[folder].subscribed = subscribed;
}
return folders;
};
// folder structure presentation (structure icons)
this.folder_list_tree = function(folders)
{
var i, n, diff, prefix, 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];
prefix = items.slice(0, items_len-1).join(this.env.directory_separator) + this.env.directory_separator;
for (n=0; n<tree.length; n++) {
folder = tree[n];
diff = folders[folder].depth - (items_len - 1);
if (diff >= 0 && folder.indexOf(prefix) === 0)
folders[folder].tree[diff] |= 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);
}
}
};
// Get editing sessions on the specified file
this.file_sessions = function(file)
{
var sessions = [], folder = this.file_path(file);
$.each(this.sessions[folder] || {}, function(session_id, session) {
if (session.file == file) {
session.id = session_id;
sessions.push(session);
}
});
return sessions;
};
// 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)
- // or using Manticore - WebODF collaborative editor (return 4)
+ // or is editable (using File API viewer or Manticore) (return 4)
this.file_type_supported = function(type, capabilities)
{
var i, t, res = 0, regexps = [], img = 'jpg|jpeg|gif|bmp|png',
caps = this.env.browser_capabilities || {},
doc = /^application\/vnd.oasis.opendocument.(text)$/i;
// Manticore?
if (capabilities && capabilities.MANTICORE && doc.test(type))
res |= 4;
if (caps.tif)
img += '|tiff';
if ((new RegExp('^image/(' + img + ')$', 'i')).test(type))
res |= 1;
// prefer text viewer for any text type
if (/^text\/(?!(pdf|x-pdf))/i.test(type))
- res |= 2;
+ res |= 2 | 4;
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))
res |= 1;
for (i in navigator.mimeTypes) {
t = navigator.mimeTypes[i].type;
if (t == type && navigator.mimeTypes[i].enabledPlugin)
res |= 1;
}
// types with viewer support
if ($.inArray(type, this.env.supported_mimetypes) > -1)
res |= 2;
return res;
};
// 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');
};
// same as str.split(delimiter) but it ignores delimiters within quoted strings
this.explode_quoted_string = function(str, delimiter)
{
var result = [],
strlen = str.length,
q, p, i, chr, last;
for (q = p = i = 0; i < strlen; i++) {
chr = str.charAt(i);
if (chr == '"' && last != '\\') {
q = !q;
}
else if (!q && chr == delimiter) {
result.push(str.substring(p, i));
p = i + 1;
}
last = chr;
}
result.push(str.substr(p));
return result;
};
};
/**
* Class implementing Manticore Client API
*
* Configuration:
* iframe - manticore iframe element
* title_input - document title element
* export_menu - export formats list
* members_list - collaborators list
* photo_url - <img> src for a collaborator
* photo_default_url - default image of a collaborator
- * set_busy, display_message, hide_message, gettext - methods
+ *
+ * set_busy, display_message, hide_message, gettext - common methods
+ *
+ * api - Chwala files_api instance
+ * interval - how often to check for invitations in seconds (default: 60)
+ * owner - user identifier
+ * invitationMore - add "more" link into invitation notices
+ * invitationChange - method to handle invitation state updates
+ * invitationSave - method to handle invitation state update
*/
function manticore_api(conf)
{
- var domain,
+ var domain, manticore,
locks = {},
callbacks = {},
members = {},
- self = this,
- manticore = conf.iframe.contentWindow;
-
- if (/^(https?:\/\/[^/]+)/i.test(conf.iframe.src))
- domain = RegExp.$1;
-
- // Register 'message' event to receive messages from Manticore iframe
- window.addEventListener('message', function(event) {
- if (event.source == manticore && event.origin == domain) {
- self.message_handler(event.data);
- }
- });
-
- // Bind for document title changes
- if (conf.title_input)
- $(conf.title_input).change(function() { self.set_title($(this).val()); });
+ self = this;
// Sets state
this.set_busy = function(state, message)
{
if (conf.set_busy)
return conf.set_busy(state, message);
};
// Displays error/notification message
- this.display_message = function(label, type)
+ this.display_message = function(label, type, is_txt, timeout)
{
if (conf.display_message)
- return conf.display_message(label, type);
+ return conf.display_message(label, type, is_txt, timeout);
if (type == 'error')
- alert(this.gettext(label));
+ alert(is_txt ? label : this.gettext(label));
};
// Hides the error/notification message
this.hide_message = function(id)
{
if (conf.hide_message)
return conf.hide_message(id);
};
// Localization method
this.gettext = function(label)
{
if (conf.gettext)
return conf.gettext(label);
return label;
};
- // Display loading message
- this.init_lock = this.set_busy(true, 'loading');
-
// Handle messages from Manticore
this.message_handler = function(data)
{
var result;
console.log(data);
if (callbacks[data.id])
result = callbacks[data.id](data);
if (result !== false && data.name && conf[data.name])
result = conf[data.name](data);
delete callbacks[data.id];
if (locks[data.id]) {
this.set_busy(false);
this.hide_message(data.id);
delete locks[data.id];
}
if (result === false)
return;
switch (data.name) {
case 'ready':
this.ready();
break;
case 'titleChanged':
if (conf.title_input)
$(conf.title_input).val(data.value);
break;
case 'memberAdded':
// @TODO: display notification?
if (conf.members_list)
$(conf.members_list).append(this.member_item(data));
break;
case 'memberRemoved':
// @TODO: display notification?
if (conf.members_list) {
$('#' + members[data.memberId].id, conf.members_list).remove();
delete members[data.memberId];
}
break;
}
};
this.post = function(action, data, callback, lock_label)
{
if (!data) data = {};
if (lock_label) {
data.id = this.set_busy(true, this.gettext(lock_label));
locks[data.id] = true;
}
if (!data.id)
data.id = (new Date).getTime();
// make sure the id is not in use
while (callbacks[data.id])
data.id++;
data.name = action;
callbacks[data.id] = callback;
console.log(data);
manticore.postMessage(data, domain);
};
this.ready = function()
{
if (this.init_lock) {
this.set_busy(false);
this.hide_message(this.init_lock);
delete this.init_lock;
}
if (conf.export_menu)
this.export_menu(conf.export_menu);
if (conf.members_list)
this.get_members(function(data) {
var images = [], id = (new Date).getTime();
$.each(data.value || [], function() {
images.push(self.member_item(this, id++));
});
$(conf.members_list).html('').append(images);
});
if (conf.title_input)
this.get_title(function(data) {
$(conf.title_input).val(data.value);
});
};
// Save current document
this.save = function(callback)
{
this.post('actionSave', {}, callback, 'saving');
};
// Export/download current document
this.export = function(type, callback)
{
this.post('actionExport', {value: type}, callback);
};
// Get supported export formats and create content of menu element
this.export_menu = function(menu)
{
this.post('getExportFormats', {}, function(data) {
var items = [];
$.each(data.value || [], function(i, v) {
items.push($('<li>').attr({role: 'menuitem'}).append(
$('<a>').attr({href: '#', role: 'button', tabindex: 0, 'aria-disabled': false, 'class': 'active'})
.text(v.label).click(function() { self.export(v.format); })
));
});
$(menu).html('').append(items);
});
};
// Get document title
this.get_title = function(callback)
{
this.post('getTitle', {}, callback);
};
// Set document title
this.set_title = function(title, callback)
{
this.post('setTitle', {value: title}, callback);
};
// Get document session members
this.get_members = function(callback)
{
this.post('getMembers', {}, callback);
};
// Creates session member image element
this.member_item = function(member, id)
{
member.id = 'member' + (id || (new Date).getTime());
member.name = member.fullName + ' (' + member.email + ')';
members[member.memberId] = member;
var img = $('<img>').attr({title: member.name, id: member.id, 'class': 'photo', src: conf.photo_default_url})
.css({'border-color': member.color})
.text(name);
if (conf.photo_url) {
img.attr('src', conf.photo_url.replace(/%email/, urlencode(member.email)));
if (conf.photo_default_url)
img.error(function() { this.src = conf.photo_default_url; });
}
return img;
};
+
+ // track changes in invitations
+ this.track_invitations = function()
+ {
+ conf.api.request('invitations', {timestamp: this.invitations_timestamp || -1}, this.parse_invitations);
+ this.invitations_timeout = setTimeout(function() { self.track_invitations(); }, (conf.interval || 60) * 1000);
+ };
+
+ // parse 'invitations' response
+ this.parse_invitations = function(response)
+ {
+ if (!conf.api.response(response) || !response.result)
+ return;
+
+ var invitation_change = function(invitation) {
+ var msg = self.invitation_msg(invitation);
+
+ if (conf.invitationMore)
+ msg = $('<div>')
+ .append($('<span>').text(msg + ' '))
+ .append($('<a>').text(self.gettext('more')).attr('id', invitation.id)).html();
+
+ self.display_message(msg, 'notice', true, 30);
+
+ // update existing sessions info
+ if (conf.api && conf.api.sessions && invitation.file) {
+ var session, folder = conf.api.file_path(invitation.file),
+ is_invited = function() {
+ return !invitation.is_session_owner && /^(invited|accepted)/.test(invitation.status);
+ };
+
+ $.each(conf.api.sessions[folder] || {}, function(i, s) {
+ if (i == invitation.session_id || s.file == invitation.file) {
+ if (is_invited())
+ conf.api.sessions[folder][i].is_invited = true;
+ if (s.id == invitation.session_id)
+ session = conf.api.sessions[folder][i];
+ }
+ });
+
+ if (!session) {
+ if (!conf.api.sessions[folder])
+ conf.api.sessions[folder] = {};
+ conf.api.sessions[folder][invitation.session_id] = {
+ owner: invitation.owner,
+ owner_name: invitation.owner_name,
+ is_owner: invitation.is_session_owner,
+ is_invited: is_invited(),
+ file: invitation.file
+ };
+ }
+ }
+
+ if (conf.invitationChange)
+ conf.invitationChange(invitation);
+ }
+
+ $.each(response.result.list || [], function(i, invitation) {
+ invitation.id = 'i' + (response.result.timestamp + i);
+ invitation.is_session_owner = invitation.user != conf.owner;
+
+ // display notifications
+ if (!invitation.is_session_owner) {
+ if (invitation.status == 'invited' || invitation.status == 'declined-owner' || invitation.status == 'accepted-owner') {
+ invitation_change(invitation);
+ }
+ }
+ else {
+ if (invitation.status == 'accepted' || invitation.status == 'declined' || invitation.status == 'requested') {
+ invitation_change(invitation);
+ }
+ }
+ });
+
+ self.invitations_timestamp = response.result.timestamp;
+ };
+
+ this.invitation_msg = function(invitation)
+ {
+ return self.gettext(invitation.status.replace('-', '') + 'notice')
+ .replace('$user', invitation.user_name ? invitation.user_name : invitation.user)
+ .replace('$owner', invitation.owner_name ? invitation.owner_name : invitation.owner)
+ .replace('$file', invitation.filename);
+ };
+
+ // Request access to the editing session
+ this.invitation_request = function(invitation)
+ {
+ var params = {id: invitation.session_id, user: invitation.user || ''};
+
+ conf.api.req = this.set_busy(true, 'invitationrequesting');
+ conf.api.request('document_request', params, function(response) {
+ self.invitation_response(response, invitation, 'requested');
+ });
+ };
+
+ // Accept an invitations to the editing session
+ this.invitation_accept = function(invitation)
+ {
+ var params = {id: invitation.session_id, user: invitation.user || ''};
+
+ conf.api.req = this.set_busy(true, 'invitationaccepting');
+ conf.api.request('document_accept', params, function(response) {
+ self.invitation_response(response, invitation, 'accepted');
+ });
+ };
+
+ // Decline an invitations to the editing session
+ this.invitation_decline = function(invitation)
+ {
+ var params = {id: invitation.session_id, user: invitation.user || ''};
+
+ conf.api.req = this.set_busy(true, 'invitationdeclining');
+ conf.api.request('document_decline', params, function(response) {
+ self.invitation_response(response, invitation, 'declined');
+ });
+ };
+
+ // document_decline response handler
+ this.invitation_response = function(response, invitation, status)
+ {
+ if (!conf.api.response(response))
+ return;
+
+ invitation.status = status;
+
+ if (conf.invitationSaved)
+ conf.invitationSaved(invitation);
+ };
+
+
+ if (!conf)
+ conf = {};
+
+ // Got Manticore iframe, use Client API
+ if (conf.iframe) {
+ manticore = conf.iframe.contentWindow;
+
+ if (/^(https?:\/\/[^/]+)/i.test(conf.iframe.src))
+ domain = RegExp.$1;
+
+ // Register 'message' event to receive messages from Manticore iframe
+ window.addEventListener('message', function(event) {
+ if (event.source == manticore && event.origin == domain) {
+ self.message_handler(event.data);
+ }
+ });
+
+ // Bind for document title changes
+ if (conf.title_input)
+ $(conf.title_input).change(function() { self.set_title($(this).val()); });
+
+ // Display loading message
+ this.init_lock = this.set_busy(true, 'loading');
+ }
+
+ if (conf.api)
+ this.track_invitations();
};
// 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;
}

File Metadata

Mime Type
text/x-diff
Expires
Thu, Feb 5, 4:20 PM (4 h, 8 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
427951
Default Alt Text
(175 KB)

Event Timeline