diff --git a/doc/SQL/mysql.initial.sql b/doc/SQL/mysql.initial.sql index e294311..87c03aa 100644 --- a/doc/SQL/mysql.initial.sql +++ b/doc/SQL/mysql.initial.sql @@ -1,24 +1,23 @@ 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, - `expires` datetime DEFAULT NULL, `data` mediumtext, PRIMARY KEY (`id`), UNIQUE INDEX `uri_index` (`uri`(255)), INDEX `expires_index` (`expires`) ) /*!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 4d5b3f6..8f7d5e7 100644 --- a/lib/api/common.php +++ b/lib/api/common.php @@ -1,143 +1,166 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ 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); } /** * 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/folder_rights.php b/lib/api/folder_info.php similarity index 73% rename from lib/api/folder_rights.php rename to lib/api/folder_info.php index 9894f93..c1667f6 100644 --- a/lib/api/folder_rights.php +++ b/lib/api/folder_info.php @@ -1,58 +1,61 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -class file_api_folder_rights extends file_api_common +class file_api_folder_info 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); } - list($driver, $path) = $this->api->get_driver($this->args['folder']); - - $rights = $driver->folder_rights($path); - $result = array(); - $map = array( - file_storage::ACL_READ => 'read', - file_storage::ACL_WRITE => 'write', + $result = array( + 'folder' => $this->args['folder'], ); - foreach ($map as $key => $value) { - if ($rights & $key) { - $result[] = $value; - } + if (!empty($this->args['rights']) && rcube_utils::get_boolean((string) $this->args['rights'])) { + $result['rights'] = $this->folder_rights($this->args['folder']); } - return array( - 'folder' => $this->args['folder'], - 'rights' => $result, - ); + if (!empty($this->args['sessions']) && rcube_utils::get_boolean((string) $this->args['sessions'])) { + $result['sessions'] = $this->folder_sessions($this->args['folder']); + } + + return $result; + } + + /** + * Get editing sessions + */ + protected function folder_sessions($folder) + { + $manticore = new file_manticore($this->api); + return $manticore->sessions_find($folder); } } diff --git a/lib/file_api_lib.php b/lib/file_api_lib.php index bda3f20..3747141 100644 --- a/lib/file_api_lib.php +++ b/lib/file_api_lib.php @@ -1,188 +1,195 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * This class gives access to Chwala API as a library */ class file_api_lib extends file_api_core { /** * API methods handler */ public function __call($name, $arguments) { $this->init(); switch ($name) { case 'configure': foreach (array_keys($this->env) as $name) { if (isset($arguments[0][$name])) { $this->env[$name] = $arguments[0][$name]; } } return $this->env; case 'mimetypes': return $this->supported_mimetypes(); case 'file_list': $args = array( 'folder' => $arguments[0], ); break; case 'file_create': case 'file_update': $args = array( 'file' => $arguments[0], 'path' => $arguments[1]['path'], 'content' => $arguments[1]['content'], 'content-type' => $arguments[1]['type'], ); break; case 'file_delete': case 'file_info': $args = array( 'file' => $arguments[0], ); break; case 'file_copy': case 'file_move': $args = array( 'file' => array($arguments[0] => $arguments[1]), ); break; case 'file_get': // override default action, we need only to support // writes to file handle list($driver, $path) = $this->get_driver($arguments[0]); $driver->file_get($path, $arguments[1], $arguments[2]); return; case 'folder_list': // no arguments $args = array(); break; case 'folder_create': case 'folder_subscribe': case 'folder_unsubscribe': case 'folder_delete': - case 'folder_rights': $args = array( 'folder' => $arguments[0], ); break; + case 'folder_info': + $args = array( + 'folder' => $arguments[0], + 'rights' => 1, + 'sessions' => 1, + ); + break; + case 'folder_move': $args = array( 'folder' => $arguments[0], 'new' => $arguments[1], ); break; case 'lock_create': case 'lock_delete': $args = $arguments[1]; $args['uri'] = $arguments[0]; break; case 'lock_list': $args = array( 'uri' => $arguments[0], 'child_locks' => $arguments[1], ); break; default: throw new Exception("Invalid method name", \file_storage::ERROR_UNSUPPORTED); } require_once __DIR__ . "/api/$name.php"; $class = "file_api_$name"; $handler = new $class($this, $args); return $handler->handle(); } /** * Configure environment (this is to be overriden by implementation class) */ protected function init() { } } /** * Common handler class, from which action handler classes inherit */ class file_api_common { protected $api; protected $rc; protected $args; public function __construct($api, $args) { $this->rc = rcube::get_instance(); $this->api = $api; $this->args = $args; } /** * Request handler */ public function handle() { // disable script execution time limit, so we can handle big files @set_time_limit(0); } /** * Parse driver metadata information */ protected function parse_metadata($metadata, $default = false) { if ($default) { unset($metadata['form']); $metadata['name'] .= ' (' . $this->api->translate('localstorage') . ')'; } // localize form labels foreach ($metadata['form'] as $key => $val) { $label = $this->api->translate('form.' . $val); if (strpos($label, 'form.') !== 0) { $metadata['form'][$key] = $label; } } return $metadata; } } diff --git a/lib/file_manticore.php b/lib/file_manticore.php index 9e33c56..85a4110 100644 --- a/lib/file_manticore.php +++ b/lib/file_manticore.php @@ -1,246 +1,288 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Document editing sessions handling */ class file_manticore { protected $api; protected $rc; protected $request; protected $table = 'chwala_sessions'; /** * Class constructor * * @param file_api Chwala API app instance */ public function __construct($api) { $this->rc = rcube::get_instance(); $this->api = $api; } /** * Return viewer URI for specified file. This creates * a new collaborative editing session when needed * * @param string $file File path * * @return string Manticore URI * @throws Exception */ public function viewer_uri($file) { list($driver, $path) = $this->api->get_driver($file); $backend = $this->api->get_backend(); $uri = $driver->path2uri($path); $id = rcube_utils::bin2ascii(md5(time() . $uri, true)); $data = array( 'user' => $_SESSION['user'], ); // @TODO: check if session exists and is valid (?) // 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; } // Do this before starting the session in Manticore, // it will immediately call api/document to get the file body $res = $this->session_create($id, $uri, $data); if (!$res) { throw new Exception("Failed creating document editing session", file_api_core::ERROR_CODE); } // get filename $path = explode(file_storage::SEPARATOR, $path); $filename = $path[count($path)-1]; // create the session in Manticore $req = $this->get_request(); $res = $req->session_create(array( 'id' => $id, 'title' => $filename, 'access' => array( array( 'identity' => $data['user'], 'permission' => 'write', ), ), )); if (!$res) { $this->session_delete($id); throw new Exception("Failed creating document editing session", file_api_core::ERROR_CODE); } return $this->frame_uri($id); } /** * Get file path (not URI) from session. * * @param string $id Session ID * * @return string File path * @throws Exception */ public function session_file($id) { - $backend = $this->api->get_backend(); $session = $this->session_info($id); if (empty($session)) { throw new Exception("Document session ID not found.", file_api_core::ERROR_CODE); } - try { - return $backend->uri2path($session['uri']); - } - catch (Exception $e) { - // do nothing - } - - foreach ($this->api->get_drivers(true) as $driver) { - try { - $path = $driver->uri2path($session['uri']); - $title = $driver->title(); - - if ($title) { - $path = $title . file_storage::SEPARATOR . $path; - } - - return $path; - } - catch (Exception $e) { - // do nothing - } - } + $path = $this->uri2path($session['uri']); if (empty($path)) { throw new Exception("Document session ID not found.", file_api_core::ERROR_CODE); } + + return $path; } /** * Get editing session info */ public function session_info($id) { $db = $this->rc->get_dbh(); $result = $db->query("SELECT * FROM `{$this->table}`" . " WHERE `id` = ?", $id); if ($row = $db->fetch_assoc($result)) { $row['data'] = json_decode($row['data'], true); return $row; } } + /** + * Find editing sessions for specified path + */ + public function session_find($path) + { + // create an URI for specified path + list($driver, $path) = $this->api->get_driver($path); + + $uri = trim($driver->path2uri($path), '/') . '/'; + + // get existing sessions + $db = $this->rc->get_dbh(); + $sessions = array(); + $result = $db->query("SELECT * FROM `{$this->table}`" + . " WHERE `uri` LIKE '" . $db->escape($uri) . "%'"); + + if ($row = $db->fetch_assoc($result)) { + if ($path = $this->uri2path($row['uri'])) { + $data = json_decode($row['data'], true); + + $sessions[$row['id']] = array( + 'file' => $path, + 'owner' => $data['user'], + // @TODO: invitated?, last_modified? + ); + } + } + + return $sessions; + } + /** * Create editing session */ protected function session_create($id, $uri, $data) { $db = $this->rc->get_dbh(); $result = $db->query("INSERT INTO `{$this->table}`" . " (`id`, `uri`, `data`) VALUES (?, ?, ?)", $id, $uri, json_encode($data)); return $db->affected_rows($result) > 0; } /** * Delete editing session */ protected function session_delete($id) { $db = $this->rc->get_dbh(); $result = $db->query("DELETE FROM `{$this->table}`" . " WHERE `id` = ?", $id); return $db->affected_rows($result) > 0; } /** * 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; } }