diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist index c27fb09..8059f37 100644 --- a/config/config.inc.php.dist +++ b/config/config.inc.php.dist @@ -1,75 +1,79 @@ array( 'driver' => 'seafile', 'host' => 'seacloud.cc', // when username is set to '%u' current user name and password // will be used to authenticate to this storage source 'username' => '%u', ), 'Public-Files' => array( 'driver' => 'webdav', 'baseuri' => 'https://some.host.tld/Files', 'username' => 'admin', 'password' => 'pass', ), ); */ +// Manticore service URL. Enables use of WebODF collaborative editor. +// Note: this URL should be accessible from Chwala host and Roundcube host as well. +$config['fileapi_manticore'] = null; + // Name of the user interface skin. $config['file_api_skin'] = 'default'; // Chwala UI communicates with Chwala API via HTTP protocol // The URL here is a location of Chwala API service. By default // the UI location is used with addition of /api/ suffix. $config['file_api_url'] = ''; // ------------------------------------------------ // SeaFile driver settings // ------------------------------------------------ // Enables SeaFile Web API conversation log $config['fileapi_seafile_debug'] = true; // Enables caching of some SeaFile information e.g. folders list // Note: 'db', 'apc' and 'memcache' are supported $config['fileapi_seafile_cache'] = 'db'; // Expiration time of SeaFile cache entries $config['fileapi_seafile_cache_ttl'] = '7d'; // Default SeaFile Web API host $config['fileapi_seafile_host'] = 'localhost'; // Enables SSL certificates validation when connecting // with any SeaFile server $config['fileapi_seafile_ssl_verify_host'] = false; $config['fileapi_seafile_ssl_verify_peer'] = false; // ------------------------------------------------ // WebDAV driver settings // ------------------------------------------------ // Default URI location for WebDAV storage $config['fileapi_webdav_baseuri'] = 'https://localhost/iRony'; diff --git a/doc/SQL/mysql.initial.sql b/doc/SQL/mysql.initial.sql index 36e17c0..e294311 100644 --- a/doc/SQL/mysql.initial.sql +++ b/doc/SQL/mysql.initial.sql @@ -1,14 +1,24 @@ 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 */; -INSERT INTO `system` (`name`, `value`) VALUES ('chwala-version', '2013111300'); +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/doc/chwala.conf b/doc/chwala.conf index 30d7833..9c87403 100644 --- a/doc/chwala.conf +++ b/doc/chwala.conf @@ -1,17 +1,28 @@ # A suggested default configuration for chwala under httpd Alias /chwala /usr/share/chwala/public_html AllowOverride All # Apache 2.4 Require all granted # Apache 2.2 Order Allow,Deny Allow from All + + + RewriteEngine on + # NOTE: This needs to point to the base uri of your installation. + RewriteBase /chwala/ + + # Rewrite document URLs of the form api/document/:id to api/index.php?method=document&id=:id + RewriteCond %{REQUEST_FILENAME} !-f + RewriteCond %{REQUEST_FILENAME} !-d + RewriteRule ^api/document/(.*)$ api/index.php?method=document&id=$1 [L,QSA] + diff --git a/lib/api/document.php b/lib/api/document.php new file mode 100644 index 0000000..3ff6fa8 --- /dev/null +++ b/lib/api/document.php @@ -0,0 +1,115 @@ + | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak | + +--------------------------------------------------------------------------+ +*/ + +class file_api_document extends file_api_common +{ + /** + * Request handler + */ + public function handle() + { + if (empty($_GET['id'])) { + throw new Exception("Missing document ID.", file_api_core::ERROR_CODE); + } + + $method = $_SERVER['REQUEST_METHOD']; + + if ($method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) { + $method = $_SERVER['HTTP_X_HTTP_METHOD']; + } + + $file = $this->get_file_path($_GET['id']); + + if ($method == 'PUT' || $method == 'GET') { + return $this->{'document_' . strtolower($method)}($file); + } + } + + /** + * Get file path from manticore session identifier + */ + protected function get_file_path($id) + { + $manticore = new file_manticore($this->api); + + return $manticore->session_file($id); + } + + /** + * 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 { + $driver->file_get($path); + } + 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 aa1bcc3..8c04783 100644 --- a/lib/api/file_info.php +++ b/lib/api/file_info.php @@ -1,64 +1,91 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_file_info extends file_api_common { /** * Request handler */ public function handle() { parent::handle(); if (!isset($this->args['file']) || $this->args['file'] === '') { throw new Exception("Missing file name", file_api_core::ERROR_CODE); } list($driver, $path) = $this->api->get_driver($this->args['file']); $info = $driver->file_info($path); + // 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) + if (rcube_utils::get_boolean((string) $this->args['viewer'])) { $this->file_viewer_info($this->args['file'], $info); + + if ((intval($this->args['viewer']) & 4) && $this->rc->config->get('fileapi_manticore')) { + $this->file_manticore_handler($this->args['file'], $info); + } } return $info; } /** * Merge file viewer data into file info */ protected function file_viewer_info($file, &$info) { if ($viewer = $this->find_viewer($info['type'])) { $info['viewer'] = array(); if ($frame = $viewer->frame($file, $info['type'])) { $info['viewer']['frame'] = $frame; } else if ($href = $viewer->href($file, $info['type'])) { $info['viewer']['href'] = $href; } } } + + /** + * Merge manticore session data into file info + */ + protected function file_manticore_handler($file, &$info) + { + // check if file type is supported by webodf editor? + if (strtolower($info['type']) != 'application/vnd.oasis.opendocument.text') { + return; + } + + $manticore = new file_manticore($this->api); + + if ($uri = $manticore->viewer_uri($file)) { + $info['viewer']['href'] = $uri; + $info['viewer']['manticore'] = true; + } + } } diff --git a/lib/file_api.php b/lib/file_api.php index 62d522f..365aaa6 100644 --- a/lib/file_api.php +++ b/lib/file_api.php @@ -1,438 +1,437 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api extends file_api_core { public $session; + public $config; + public $browser; public $output_type = file_api_core::OUTPUT_JSON; - private $conf; - private $browser; - public function __construct() { $rcube = rcube::get_instance(); $rcube->add_shutdown_function(array($this, 'shutdown')); - $this->conf = $rcube->config; + $this->config = $rcube->config; $this->session_init(); - if ($_SESSION['config']) { - $this->config = $_SESSION['config']; + 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 ($this->request == 'authenticate') { - $this->session->regenerate_id(false); - - if ($username = $this->authenticate()) { - $_SESSION['user'] = $username; - $_SESSION['time'] = time(); - $_SESSION['config'] = $this->config; + 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']; - } + // 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(), )); } } - - throw new Exception("Invalid session", 403); + 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(); return false; } session_id($sess_id); session_start(); if (empty($_SESSION['user'])) { return false; } - $timeout = $this->conf->get('session_lifetime', 0) * 60; + $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->conf->get('session_name'); - $lifetime = $this->conf->get('session_lifetime', 0) * 60; + $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->conf); + $this->session = rcube_session::factory($this->config); } // Rouncube Framework < 1.2 else { - $this->session = new rcube_session($rcube->get_dbh(), $this->conf); - $this->session->set_secret($this->conf->get('des_key') . dirname($_SERVER['SCRIPT_NAME'])); - $this->session->set_ip_check($this->conf->get('ip_check')); + $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->conf->get('devel_mode')) { + 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)) { + if (empty($result)) { /* - header('WWW-Authenticate: Basic realm="' . $this->app_name .'"'); - header('HTTP/1.1 401 Unauthorized'); - exit; + 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); + 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->config) as $name) { + foreach (array_keys($this->env) as $name) { if (isset($_GET[$name])) { - $this->config[$name] = $_GET[$name]; + $this->env[$name] = $_GET[$name]; } } - $_SESSION['config'] = $this->config; + $_SESSION['env'] = $this->env; - return $this->config; + 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)) { // request name aliases for backward compatibility $aliases = array( 'lock' => 'lock_create', 'unlock' => 'lock_delete', 'folder_rename' => 'folder_move', ); $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 6034a23..97b45ea 100644 --- a/lib/file_api_core.php +++ b/lib/file_api_core.php @@ -1,324 +1,329 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ 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 $config = array( + public $env = array( 'date_format' => 'Y-m-d H:i', 'language' => 'en_US', ); protected $app_name = 'Kolab File API'; protected $drivers = 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->config); + $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 // It does not work when the API is used where // some SabreDAV classes are redefined if ($iRony && $item['driver'] == 'webdav') { 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->config), $key); + $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 * * @return array Capabilities */ public function capabilities() { $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; + } + // 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; } /** * 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_api_lib.php b/lib/file_api_lib.php index 940fc50..41feeca 100644 --- a/lib/file_api_lib.php +++ b/lib/file_api_lib.php @@ -1,187 +1,187 @@ | +--------------------------------------------------------------------------+ | 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->config) as $name) { + foreach (array_keys($this->env) as $name) { if (isset($arguments[0][$name])) { - $this->config[$name] = $arguments[0][$name]; + $this->env[$name] = $arguments[0][$name]; } } - return $this->config; + 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': $args = array( 'folder' => $arguments[0], ); 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 new file mode 100644 index 0000000..9e33c56 --- /dev/null +++ b/lib/file_manticore.php @@ -0,0 +1,246 @@ + | + +--------------------------------------------------------------------------+ + | 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 + } + } + + if (empty($path)) { + throw new Exception("Document session ID not found.", file_api_core::ERROR_CODE); + } + } + + /** + * 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; + } + } + + /** + * 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']; + } + + /** + * 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_ui_api.php b/lib/file_manticore_api.php similarity index 57% copy from lib/file_ui_api.php copy to lib/file_manticore_api.php index 2c2b2a7..6553c8d 100644 --- a/lib/file_ui_api.php +++ b/lib/file_manticore_api.php @@ -1,286 +1,342 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** - * Helper class to connect to the API + * Helper class to connect to the Manticore API */ -class file_ui_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 = 200; + const ERROR_CONNECTION = 500; const ACCEPT_HEADER = "application/json,text/javascript,*/*"; + /** * Class constructor. * * @param string $base_url Base URL of the Kolab API */ public function __construct($base_url) - { - $this->base_url = $base_url; - $this->init(); - } - - /** - * Initializes HTTP Request object. - */ - public function init() { require_once 'HTTP/Request2.php'; - $this->request = new HTTP_Request2(); + + $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()); + 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 - * @param array $get Additional GET parameters (e.g. 'version') * - * @return file_ui_api_result Request response + * @return string Session token (on success) */ - public function login($username, $password, $get = null) + public function login($username, $password) { $query = array( - 'username' => $username, + 'email' => $username, 'password' => $password, ); - $response = $this->post('authenticate', $get, $query); + // 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 $response; + return $token; } /** - * Logs specified user out of the API + * Sets request session token. * - * @return bool True on success, False on failure + * @param string $token Session token. + * @param bool $validate Enables token validatity check + * + * @return bool Token validity status */ - public function logout() + public function set_session_token($token, $validate = false) { - $response = $this->get('quit'); + $this->request->setHeader('Authorization', "Bearer $token"); - return $response->get_error_code() ? false : true; + if ($validate) { + $result = $this->get('api/user/me'); + + return $result->get_error_code() == 200; + } + + return true; } /** - * Sets session token value. + * Delete document editing session * - * @param string $token Token string + * @param array $id Session identifier + * + * @return bool True on success, False on failure */ - public function set_session_token($token) + public function session_delete($id) { - $this->request->setHeader('X-Session-Token', $token); + $res = $this->delete('api/documents/' . $id); + + return $res->get_error_code() == 200; } /** - * Gets capabilities of the API (according to logged in user). + * Create document editing session * - * @return kolab_client_api_result Capabilities response + * @param array $params Session parameters + * + * @return bool True on success, False on failure */ - public function get_capabilities() + public function session_create($params) { - $this->get('capabilities'); + $res = $this->post('api/documents', $params); + + // @TODO: 422? + return $res->get_error_code() == 201 || $res->get_error_code() == 422; } /** * API's GET request. * - * @param string $action Action name - * @param array $args Request arguments + * @param string $action Action name + * @param array $get Request arguments * - * @return file_ui_api_result Response + * @return file_ui_api_result Response */ - public function get($action, $args = array()) + public function get($action, $get = array()) { - $url = $this->build_url($action, $args); + $url = $this->build_url($action, $get); -// Log::trace("Calling API GET: $url"); + 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 $url_args URL arguments - * @param array $post POST arguments + * @param string $action Action name + * @param array $post POST arguments * - * @return kolab_client_api_result Response + * @return kolab_client_api_result Response */ - public function post($action, $url_args = array(), $post = array()) + public function post($action, $post = array()) { - $url = $this->build_url($action, $url_args); + $url = $this->build_url($action); -// Log::trace("Calling API POST: $url"); + if ($this->debug) { + rcube::write_log('manticore', "POST: $url " . json_encode($post)); + } $this->request->setMethod(HTTP_Request2::METHOD_POST); - $this->request->addPostParameter($post); + $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) + private function build_url($action, $args = array()) { - $url = new Net_URL2($this->base_url); - - $args['method'] = $action; + $url = new Net_URL2($this->base_url . $action); - $url->setQueryVariables($args); + $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()); } - $body = @json_decode($body, true); - $err_code = null; - $err_str = null; + $code = $response->getStatus(); - if (is_array($body) && (empty($body['status']) || $body['status'] != 'OK')) { - $err_code = !empty($body['code']) ? $body['code'] : self::ERROR_INTERNAL; - $err_str = !empty($body['reason']) ? $body['reason'] : 'Unknown error'; + if ($this->debug) { + rcube::write_log('manticore', "Response [$code]: $body"); } - else if (!is_array($body)) { - $err_code = self::ERROR_INTERNAL; - $err_str = 'Unable to decode response'; + + if ($code < 300) { + $result = $body ? json_decode($body, true) : array(); } + else { + if ($code != 401) { + rcube::raise_error("Error $code on $url", true, false); + } - return new file_ui_api_result($body, $err_code, $err_str); - } + $error = $body; + } + return new file_ui_api_result($result, $code, $error); + } } diff --git a/lib/file_ui_api.php b/lib/file_ui_api.php index 2c2b2a7..542f803 100644 --- a/lib/file_ui_api.php +++ b/lib/file_ui_api.php @@ -1,286 +1,289 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Helper class to connect to the API */ class file_ui_api { /** * @var HTTP_Request2 */ private $request; /** * @var string */ private $base_url; const ERROR_INTERNAL = 100; const ERROR_CONNECTION = 200; const ACCEPT_HEADER = "application/json,text/javascript,*/*"; /** * Class constructor. * * @param string $base_url Base URL of the Kolab API */ public function __construct($base_url) { $this->base_url = $base_url; $this->init(); } /** * Initializes HTTP Request object. */ public function init() { require_once 'HTTP/Request2.php'; $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); } /** * 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 * @param array $get Additional GET parameters (e.g. 'version') * * @return file_ui_api_result Request response */ public function login($username, $password, $get = null) { $query = array( 'username' => $username, 'password' => $password, ); $response = $this->post('authenticate', $get, $query); return $response; } /** * Logs specified user out of the API * * @return bool True on success, False on failure */ public function logout() { $response = $this->get('quit'); return $response->get_error_code() ? false : true; } /** * Sets session token value. * * @param string $token Token string */ public function set_session_token($token) { $this->request->setHeader('X-Session-Token', $token); } /** * Gets capabilities of the API (according to logged in user). * * @return kolab_client_api_result Capabilities response */ public function get_capabilities() { $this->get('capabilities'); } /** * API's GET request. * * @param string $action Action name * @param array $args Request arguments * * @return file_ui_api_result Response */ public function get($action, $args = array()) { $url = $this->build_url($action, $args); // Log::trace("Calling API GET: $url"); $this->request->setMethod(HTTP_Request2::METHOD_GET); return $this->get_response($url); } /** * API's POST request. * * @param string $action Action name * @param array $url_args URL arguments * @param array $post POST arguments * * @return kolab_client_api_result Response */ public function post($action, $url_args = array(), $post = array()) { $url = $this->build_url($action, $url_args); // Log::trace("Calling API POST: $url"); $this->request->setMethod(HTTP_Request2::METHOD_POST); $this->request->addPostParameter($post); 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) { $url = new Net_URL2($this->base_url); $args['method'] = $action; $url->setQueryVariables($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()); } $body = @json_decode($body, true); $err_code = null; $err_str = null; if (is_array($body) && (empty($body['status']) || $body['status'] != 'OK')) { $err_code = !empty($body['code']) ? $body['code'] : self::ERROR_INTERNAL; $err_str = !empty($body['reason']) ? $body['reason'] : 'Unknown error'; } else if (!is_array($body)) { $err_code = self::ERROR_INTERNAL; $err_str = 'Unable to decode response'; } + if (!$err_code && array_key_exists('result', (array) $body)) { + $body = $body['result']; + } + return new file_ui_api_result($body, $err_code, $err_str); } - } diff --git a/lib/file_ui_api_result.php b/lib/file_ui_api_result.php index 1688266..2c7533d 100644 --- a/lib/file_ui_api_result.php +++ b/lib/file_ui_api_result.php @@ -1,91 +1,91 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * API result wrapper */ class file_ui_api_result { /** * @var array */ private $data = array(); private $error_code; private $error_str; /** * Class constructor. * * @param array $data Result data * @param int $error_code Error code * @param string $error_str Error message */ public function __construct($data = array(), $error_code = null, $error_str = null) { - if (is_array($data) && isset($data['result'])) { - $this->data = $data['result']; + if (is_array($data)) { + $this->data = $data; } $this->error_code = $error_code; - $this->error_str = $error_str; + $this->error_str = $error_str; } /** * Error code getter. * * @return int Error code */ public function get_error_code() { return $this->error_code; } /** * Error message getter. * * @return string Error message */ public function get_error_str() { return $this->error_str; } /** * Response data getter. * * @param string $name Response member name * * @return array|string Data member or complete response data (when $name is null) */ public function get($name = null) { if ($name !== null) { return isset($this->data[$name]) ? $this->data[$name] : null; } return $this->data; } } diff --git a/public_html/api/.htaccess b/public_html/api/.htaccess index cdd72da..d8e752e 100644 --- a/public_html/api/.htaccess +++ b/public_html/api/.htaccess @@ -1,5 +1,6 @@ php_flag session.auto_start Off php_flag session.use_cookies Off php_flag display_errors Off php_flag log_errors On php_value error_log ../../logs/errors +