diff --git a/config/config.inc.php.dist b/config/config.inc.php.dist index bfee9fb..9526422 100644 --- a/config/config.inc.php.dist +++ b/config/config.inc.php.dist @@ -1,117 +1,113 @@ 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', ), ); */ // Default values for sources configuration dialog. // Note: use driver names as the array keys. // Note: %u variable will be resolved to the current username. /* $config['fileapi_presets'] = array( 'seafile' => array( 'host' => 'seacloud.cc', 'username' => '%u', ), 'webdav' => array( 'baseuri' => 'https://some.host.tld/Files', 'username' => '%u', ), ); */ // 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; // WOPI/Office service URL. Enables use of collaborative editor supporting WOPI. // Note: this URL should be accessible from Chwala host and Roundcube host as well. $config['fileapi_wopi_office'] = null; -// Kolab WOPI service URL. Enables use of collaborative editor supporting WOPI. -// Note: this URL should be accessible from Chwala host and Office host as well. -$config['fileapi_wopi_service'] = 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'] = ''; // Type of Chwala cache. Supported values: 'db', 'apc' and 'memcache'. // Note: This is only for some additional data like WOPI capabilities. $config['fileapi_cache'] = 'db'; // lifetime of Chwala cache // possible units: s, m, h, d, w $config['fileapi_cache_ttl'] = '1d'; // ------------------------------------------------ // SeaFile driver settings // ------------------------------------------------ // Enables SeaFile Web API conversation log $config['fileapi_seafile_debug'] = false; // 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 // Note: http:// and https:// (default) prefixes can be used here $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; // To support various Seafile configurations when fetching a file // from Seafile server we proxy it via Chwala server. // Enable this option to allow direct downloading of files // from Seafile server to user browser. $config['fileapi_seafile_allow_redirects'] = false; // ------------------------------------------------ // WebDAV driver settings // ------------------------------------------------ // Default URI location for WebDAV storage $config['fileapi_webdav_baseuri'] = 'https://localhost/iRony'; diff --git a/doc/chwala.conf b/doc/chwala.conf index c6820b7..5c2fe69 100644 --- a/doc/chwala.conf +++ b/doc/chwala.conf @@ -1,35 +1,35 @@ # A suggested default configuration for chwala under httpd Alias /chwala /usr/share/chwala/public_html AllowOverride None php_flag session.auto_start Off php_flag display_errors Off php_flag log_errors On php_flag suhosin.session.encrypt Off php_value error_log /var/log/chwala/errors # 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] + RewriteRule ^api/wopi/(.*)$ api/index.php?wopi=1&method=$1 [L,QSA] - diff --git a/lib/api/document.php b/lib/api/document.php index a572114..d33809c 100644 --- a/lib/api/document.php +++ b/lib/api/document.php @@ -1,342 +1,345 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ 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']; } // Invitation notifications if ($this->args['method'] == 'invitations') { return $this->invitations(); } // Sessions list if ($this->args['method'] == 'sessions') { return $this->sessions(); } // Session and invitations management 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': case 'document_invite': case 'document_request': case 'document_decline': case 'document_accept': case 'document_cancel': case 'document_info': 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); + return $this->{'document_' . strtolower($method)}($this->args['id']); } throw new Exception("Unknown method", file_api_core::ERROR_INVALID); } /** * Get file path from manticore session identifier */ protected function get_file_path($id) { $document = new file_document($this->api); $file = $document->session_file($id); return $file['file']; } /** * Get invitations list */ protected function invitations() { $timestamp = new DateTime('now', new DateTimeZone('UTC')); $timestamp = $timestamp->format('U'); // 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 } $document = new file_document($this->api); $filter = array(); if ($this->args['timestamp']) { $filter['timestamp'] = $this->args['timestamp']; } $list = $document->invitations_list($filter); return array( 'list' => $list, 'timestamp' => $timestamp, ); } /** * Get sessions list */ protected function sessions() { $document = new file_document($this->api); $params = array( 'reverse' => rcube_utils::get_boolean((string) $this->args['reverse']), ); if (!empty($this->args['sort'])) { $params['sort'] = strtolower($this->args['sort']); } return $document->sessions_list($params); } /** * Close (delete) manticore session */ protected function document_delete($id) { $document = file_document::get_handler($this->api, $id); if (!$document->session_delete($id)) { throw new Exception("Failed deleting the document session.", file_api_core::ERROR_CODE); } } /** * Invite/add a session participant(s) */ protected function document_invite($id) { $document = file_document::get_handler($this->api, $id); $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'])) { $document->invitation_create($id, $user['user'], file_document::STATUS_INVITED, $comment, $user['name']); $result[] = array( 'session_id' => $id, 'user' => $user['user'], 'user_name' => $user['name'], 'status' => file_document::STATUS_INVITED, ); } } return array( 'list' => $result, ); } /** * Request an invitation to a session */ protected function document_request($id) { $document = file_document::get_handler($this->api, $id); $document->invitation_create($id, null, file_document::STATUS_REQUESTED, $this->args['comment']); } /** * Decline an invitation to a session */ protected function document_decline($id) { $document = file_document::get_handler($this->api, $id); $document->invitation_update($id, $this->args['user'], file_document::STATUS_DECLINED, $this->args['comment']); } /** * Accept an invitation to a session */ protected function document_accept($id) { $document = file_document::get_handler($this->api, $id); $document->invitation_update($id, $this->args['user'], file_document::STATUS_ACCEPTED, $this->args['comment']); } /** * Remove a session participant(s) - cancel invitations */ protected function document_cancel($id) { $document = file_document::get_handler($this->api, $id); $users = $this->args['users']; if (empty($users)) { throw new Exception("Invalid arguments.", file_api_core::ERROR_CODE); } foreach ((array) $users as $user) { $document->invitation_delete($id, $user); $result[] = $user; } return array( 'list' => $result, ); } /** * Return document informations */ - protected function document_info($id) + protected function document_info($id, $extended = true) { $document = file_document::get_handler($this->api, $id); $file = $document->session_file($id); - $session = $document->session_info($id); $rcube = rcube::get_instance(); try { list($driver, $path) = $this->api->get_driver($file['file']); $result = $driver->file_info($path); } catch (Exception $e) { // invited users may have no permission, // use file data from the session $result = array( 'size' => $file['size'], 'name' => $file['name'], 'modified' => $file['modified'], 'type' => $file['type'], ); } - $result['owner'] = $session['owner']; - $result['owner_name'] = $session['owner_name']; - $result['user'] = $rcube->user->get_username(); - $result['readonly'] = !empty($session['readonly']); - $result['origin'] = $session['origin']; + if ($extended) { + $session = $document->session_info($id); - if ($result['owner'] == $result['user']) { - $result['user_name'] = $result['owner_name']; - } - else { - $result['user_name'] = $this->api->resolve_user($result['user']) ?: ''; + $result['owner'] = $session['owner']; + $result['owner_name'] = $session['owner_name']; + $result['user'] = $rcube->user->get_username(); + $result['readonly'] = !empty($session['readonly']); + $result['origin'] = $session['origin']; + + if ($result['owner'] == $result['user']) { + $result['user_name'] = $result['owner_name']; + } + else { + $result['user_name'] = $this->api->resolve_user($result['user']) ?: ''; + } } return $result; } /** * Update document file content */ - protected function document_put($file) + protected function document_put($id) { + $file = $this->get_file_path($id); 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_data = array( 'path' => $tmp_path, 'type' => rcube_mime::file_content_type($tmp_path, $file), ); $driver->file_update($path, $file_data); // remove the temp file unlink($tmp_path); // Update the file metadata in session $file_data = $driver->file_info($file); $document = file_document::get_handler($this->api, $this->args['id']); $document->session_update($this->args['id'], $file_data); } /** * Return document file content */ - protected function document_get($file) + protected function document_get($id) { + $file = $this->get_file_path($id); 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; + $this->api->output_send(); } } diff --git a/lib/file_api.php b/lib/file_api.php index 648692b..c2232d2 100644 --- a/lib/file_api.php +++ b/lib/file_api.php @@ -1,500 +1,531 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api extends file_api_core { public $session; public $config; public $browser; public $output_type = file_api_core::OUTPUT_JSON; + /** + * Class factory. + */ + public static function factory() + { + $class = 'file_api' . (!empty($_GET['wopi']) ? '_wopi' : ''); + + return new $class; + } + + /** + * Class constructor. + */ public function __construct() { $rcube = rcube::get_instance(); $rcube->add_shutdown_function(array($this, 'shutdown')); $this->config = $rcube->config; $this->session_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->request == 'authenticate')) { + if (!$this->session_validate($this->request == 'authenticate', $_REQUEST['token'])) { $this->session->destroy(session_id()); $this->session->regenerate_id(false); if ($username = $this->authenticate()) { // Init locale after the session started $this->locale_init(); $this->env['language'] = $this->language; $_SESSION['user'] = $username; $_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); + throw new Exception("Invalid session", file_api_core::ERROR_UNAUTHORIZED); } } else { // Init locale after the session started $this->locale_init(); } // 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($new_session = false) + protected function session_validate($new_session = false, $token = null) { if (!$new_session) { - $sess_id = rcube_utils::request_header('X-Session-Token') ?: $_REQUEST['token']; + $sess_id = rcube_utils::request_header('X-Session-Token') ?: $token; } if (empty($sess_id)) { $this->session->start(); return false; } session_id($sess_id); $this->session->start(); if (empty($_SESSION['user'])) { return false; } - // Document-only session - if (($doc_id = $_SESSION['document_session']) + // Single-document session? + if (!($this instanceof file_api_wopi) + && ($doc_id = $_SESSION['document_session']) && (strpos($this->request, 'document') !== 0 || $doc_id != $_GET['id']) ) { - throw new Exception("Access denied", 403); + throw new Exception("Access denied", file_api_core::ERROR_UNAUTHORIZED); } if ($_SESSION['env']) { $this->env = $_SESSION['env']; } return true; } /** * Initializes session */ - private function session_init() + protected 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')) + if ($this->config->get('devel_mode') || $this->config->get('performance_stats')) { + // make sure logged numbers use unified format + setlocale(LC_NUMERIC, 'en_US.utf8', 'en_US.UTF-8', 'en_US', 'C'); + + if (function_exists('memory_get_peak_usage')) { $mem = memory_get_peak_usage(); - else if (function_exists('memory_get_usage')) + } + else if (function_exists('memory_get_usage')) { $mem = memory_get_usage(); + } + + $request = ($this instanceof file_api_wopi ? 'wopi/' : '') + . $this->request + . (!empty($this->path) ? '/' . implode($this->path, '/') : ''); + + $log = trim(sprintf('%s: %s %s', + $this->method ?: $_SERVER['REQUEST_METHOD'], + $request, + $mem ? sprintf('[%.1f MB]', $mem/1024/1024) : '' + )); - $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() + protected 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); + throw new Exception("Invalid password or username", file_api_core::ERROR_UNAUTHORIZED); } } return $username; } /** * Storage/System method handler */ - private function request_handler($request) + protected 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 (preg_match('/^(sessions|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 $this->api_url() . '?method=file_get' . '&file=' . urlencode($file) . '&token=' . urlencode(session_id()); } /** * Returns API URL * * @return string API URL */ public function api_url() { $api_url = $this->config->get('file_api_url', ''); if (!preg_match('|^https?://|', $api_url)) { $schema = rcube_utils::https_check() ? 'https' : 'http'; $port = $schema == 'http' ? 80 : 443; $url = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']); if ($_SERVER['SERVER_PORT'] != $port && $_SERVER['SERVER_PORT'] != 80) { $url .= ':' . $_SERVER['SERVER_PORT']; } if ($api_url) { $api_url = $url . '/' . trim($api_url, '/ '); } else { $url .= preg_replace('/\/?\?.*$/', '', $_SERVER['REQUEST_URI']); $url = preg_replace('/\/api$/', '', $url); $api_url = $url . '/api'; } } return rtrim($api_url, '/ '); } /** * 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']; header("X-Chwala-Request-ID: " . $_REQUEST['req_id']); } if (empty($response['code'])) { $response['code'] = file_api_core::ERROR_CODE; } header("X-Chwala-Error: " . $response['code']); // When binary response is expected return real // HTTP error instaead of JSON response with code 200 if ($this->is_binary_request()) { header(sprintf("HTTP/1.0 %d %s", $response['code'], $response ?: "Server error")); exit; } $this->output_send($response); } /** * Send response * * @param mixed $data Data */ - protected function output_send($data) + public function output_send($data = null) { // Send response - header("Content-Type: {$this->output_type}; charset=utf-8"); - echo json_encode($data); + if ($data !== null) { + header("Content-Type: {$this->output_type}; charset=utf-8"); + echo rcube_output::json_serialize($data); + } + exit; } /** * Find out if current request expects binary output */ protected function is_binary_request() { - return preg_match('/^(file_get|document)$/', $this->request) - && $_SERVER['REQUEST_METHOD'] == 'GET'; + return $_SERVER['REQUEST_METHOD'] == 'GET' && + ($this->request == 'file_get' || $this->request == 'document'); } /** * 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 55a5f7c..7e79b80 100644 --- a/lib/file_api_core.php +++ b/lib/file_api_core.php @@ -1,415 +1,419 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_core extends file_locale { const API_VERSION = 3; - const ERROR_CODE = 500; - const ERROR_INVALID = 501; + const ERROR_UNAUTHORIZED = 401; + const ERROR_NOT_FOUND = 404; + const ERROR_PRECONDITION_FAILED = 412; + const ERROR_CODE = 500; + const ERROR_INVALID = 501; + const ERROR_NOT_IMPLEMENTED = 501; const OUTPUT_JSON = 'application/json'; const OUTPUT_HTML = 'text/html'; public $env = array( 'date_format' => 'Y-m-d H:i', 'language' => 'en_US', 'timezone' => 'UTC', ); 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(); $backend = $this->get_backend(); $enabled = $rcube->config->get('fileapi_drivers'); $preconf = $rcube->config->get('fileapi_sources'); $result = array(); $all = array(); $iRony = defined('KOLAB_DAV_ROOT'); if (!empty($enabled)) { $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($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 ($rcube->config->get('fileapi_manticore')) { $caps['MANTICORE'] = true; } // WOPI support if ($rcube->config->get('fileapi_wopi_office')) { $caps['WOPI'] = true; } if (!$full) { return $caps; } if ($caps['MANTICORE']) { $manticore = new file_manticore($this); $caps['MANTICORE_EDITABLE'] = $manticore->supported_filetypes(true); } if ($caps['WOPI']) { $wopi = new file_wopi($this); $caps['WOPI_EDITABLE'] = $wopi->supported_filetypes(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; } /** * 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() { $rcube = rcube::get_instance(); $mimetypes = array(); $mimetypes_c = array(); $dir = __DIR__ . '/viewers'; // make sure Kolab backend is initialized so kolab_auth can modify config $backend = $this->get_backend(); if ($handle = opendir($dir)) { while (false !== ($file = readdir($handle))) { if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) { include_once $dir . '/' . $file; $class = 'file_viewer_' . $matches[1]; $viewer = new $class($this); if ($supported = $viewer->supported_mimetypes()) { $mimetypes = array_merge($mimetypes, $supported); } } } closedir($handle); } // Here we return mimetypes supported for editing and creation of files // @TODO: maybe move this to viewers if ($rcube->config->get('fileapi_wopi_office')) { $mimetypes_c['application/vnd.oasis.opendocument.text'] = array('ext' => 'odt'); $mimetypes_c['application/vnd.oasis.opendocument.presentation'] = array('ext' => 'odp'); $mimetypes_c['application/vnd.oasis.opendocument.spreadsheet'] = array('ext' => 'ods'); } else if ($rcube->config->get('fileapi_manticore')) { $mimetypes_c['application/vnd.oasis.opendocument.text'] = array('ext' => 'odt'); } $mimetypes_c['text/plain'] = array('ext' => 'txt'); $mimetypes_c['text/html'] = array('ext' => 'html'); foreach (array_keys($mimetypes_c) as $type) { list ($app, $label) = explode('/', $type); $label = preg_replace('/[^a-z]/', '', $label); $mimetypes_c[$type]['label'] = $this->translate('type.' . $label); } return array( 'view' => $mimetypes, 'edit' => $mimetypes_c, ); } /** * 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_wopi.php b/lib/file_api_wopi.php new file mode 100644 index 0000000..b980939 --- /dev/null +++ b/lib/file_api_wopi.php @@ -0,0 +1,122 @@ + | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak | + +--------------------------------------------------------------------------+ +*/ + +class file_api_wopi extends file_api +{ + public $output_type = file_api_core::OUTPUT_JSON; + public $path = array(); + public $args = array(); + public $method = 'GET'; + + + /** + * Session validation check and session start + */ + protected function session_validate($new_session = false, $token = null) + { + if (empty($_GET['access_token'])) { + throw new Exception("Token missing", file_api_core::ERROR_UNAUTHORIZED); + } + + return parent::session_validate($new_session = false, $_GET['access_token']); + } + + /** + * Storage/System method handler + */ + protected function request_handler($request) + { + // header("X-WOPI-HostEndpoint: " . $endpoint_desc); + // header("X-WOPI-MachineName: " . $machine_name); + header("X-WOPI-ServerVersion: " . file_api_core::API_VERSION); + + $request = $_GET['method']; // file_api uses strtolower(), we don't want that + + // handle request + if ($request && preg_match('/^[a-z]+\/*[a-zA-Z0-9_\/-]*$/', $request)) { + $path = explode('/', $request); + $request = array_shift($path); + $method = $_SERVER['REQUEST_METHOD']; + + if ($_method = rcube_utils::request_header('X-WOPI-Override')) { + $method = $_method; + } + else if ($method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) { + $method = $_SERVER['HTTP_X_HTTP_METHOD']; + } + + $this->path = $path; + $this->args = $_GET; + $this->method = $method; + + include_once __DIR__ . "/wopi/$request.php"; + + $class_name = "file_wopi_$request"; + if (class_exists($class_name, false)) { + $handler = new $class_name($this); + return $handler->handle(); + } + } + + throw new Exception("Unknown method", file_api_core::ERROR_NOT_FOUND); + } + + /** + * Send success response + * + * @param mixed $data Data + */ + public function output_success($data) + { + $this->output_send($data); + } + + /** + * Send error response + * + * @param mixed $response Response data + * @param int $code Error code + */ + public function output_error($response, $code = null) + { + header(sprintf("HTTP/1.0 %d %s", $code ?: file_api_core::ERROR_CODE, $response)); + + $this->output_send(); + } + + /** + * Send response + * + * @param mixed $data Data + */ + public function output_send($data = null) + { + // Remove NULL data according to WOPI spec. + if (is_array($data)) { + $data = array_filter($data, function($v) { return $v !== null; }); + } + + parent::output_send($data); + } +} diff --git a/lib/file_document.php b/lib/file_document.php index cb994e8..cfd69ff 100644 --- a/lib/file_document.php +++ b/lib/file_document.php @@ -1,880 +1,880 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Document editing sessions handling */ class file_document { protected $api; protected $rc; protected $user; protected $sessions_table = 'chwala_sessions'; protected $invitations_table = 'chwala_invitations'; protected $icache = array(); protected $file_meta_items = array('type', 'name', 'size', 'modified'); 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 const DB_DATE_FORMAT = 'Y-m-d H:i:s'; /** * Class constructor * * @param file_api $api Chwala API app instance */ public function __construct($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); } /** * Detect type of file_document class to use for specified session * * @param file_api $api Chwala API app instance * @param string $session_id Document session ID * * @return file_document Document object */ public static function get_handler($api, $session_id) { // we add "w-" prefix to wopi session identifiers, // so we can distinguish it from manticore sessions if (strpos($session_id, 'w-') === 0) { return new file_wopi($api); } return new file_manticore($api); } /** * Return viewer URI for specified file/session. This creates * a new collaborative editing session when needed. * * @param string $file File path * @param array &$file_info File metadata (e.g. type) * @param string &$session_id Optional session ID to join to * @param string $readonly Create readonly (one-time) session * * @return string An URI for specified file/session * @throws Exception */ public function session_start($file, &$file_info, &$session_id = null, $readonly = false) { if ($file !== null) { $uri = $this->path2uri($file, $driver); } $backend = $this->api->get_backend(); 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'] != $this->user) { // check if the user was invited $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)) { 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, $this->user, self::STATUS_ACCEPTED); } } $file_info['type'] = $session['type']; } 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` = ? AND `readonly` = ?", $this->user, $uri, intval($readonly)); 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)); $owner = $this->user; $data = array('origin' => $this->get_origin()); // store some file data, they will be used // by invited users that has no access to the storage foreach ($this->file_meta_items as $item) { if (isset($file_info[$item])) { $data[$item] = $file_info[$item]; } } // bind the session ID with editor type (see file_document::get_handler()) if ($this instanceof file_wopi) { $session_id = 'w-' . $session_id; } // 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, $readonly); } 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); } // Implementations should return real URI return ''; } /** * Get file path (not URI) from session. * * @param string $id Session ID * @param bool $join_mode Throw exception only if session does not exist * * @return array File info (file, type, size) * @throws Exception */ 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) && (!$join_mode || $session['owner'] == $this->user)) { throw new Exception("Document session not found.", file_api_core::ERROR_CODE); } // 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); } } $result = array('file' => $path); foreach ($this->file_meta_items as $item) { if (isset($session[$item])) { $result[$item] = $session[$item]; } } return $result; } /** * 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) { $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 ($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; } /** * Find editing sessions for specified path */ public function session_find($path, $invitations = true) { // create an URI for specified path $uri = trim($this->path2uri($path), '/') . '/'; // get existing sessions $sessions = array(); $filter = array('file', 'owner', 'owner_name', 'is_owner'); $db = $this->rc->get_dbh(); $result = $db->query("SELECT * FROM `{$this->sessions_table}`" . " WHERE `readonly` = 0 AND `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_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 */ public function session_delete($id) { $db = $this->rc->get_dbh(); $result = $db->query("DELETE FROM `{$this->sessions_table}`" . " WHERE `id` = ? AND `owner` = ?", $id, $this->user); return $db->affected_rows($result) > 0; } /** * Update editing session * * @param string $id Session ID * @param array $data Session metadata */ public function session_update($id, $data) { $db = $this->rc->get_dbh(); $result = $db->query("SELECT `data` FROM `{$this->sessions_table}`" . " WHERE `id` = ?", $id); if ($row = $db->fetch_assoc($result)) { // merge only relevant information $data = array_intersect_key($data, array_flip($this->file_meta_items)); if (empty($data)) { return true; } $sess_data = json_decode($row['data'], true); $sess_data = array_merge($sess_data, $data); $result = $db->query("UPDATE `{$this->sessions_table}`" . " SET `data` = ? WHERE `id` = ?", json_encode($sess_data), $id); return $db->affected_rows($result) > 0; } return false; } /** * Create editing session */ protected function session_create($id, $uri, $owner, $data, $readonly = false) { // get user name $owner_name = $this->api->resolve_user($owner) ?: ''; $db = $this->rc->get_dbh(); $result = $db->query("INSERT INTO `{$this->sessions_table}`" . " (`id`, `uri`, `owner`, `owner_name`, `data`, `readonly`)" . " VALUES (?, ?, ?, ?, ?, ?)", $id, $uri, $owner, $owner_name, json_encode($data), intval($readonly)); return $db->affected_rows($result) > 0; } /** * Find sessions, including: * 1. to which the user has access (is a creator or has been invited) * 2. to which the user is considered eligible to request authorization * to participate in the session by already having access to the file * Note: Readonly sessions are ignored here. * * @param array $param List parameters * * @return array Sessions list */ public function sessions_list($param = array()) { $db = $this->rc->get_dbh(); $sessions = array(); // 1. Get sessions user has access to $result = $db->query("SELECT * FROM `{$this->sessions_table}` s" . " WHERE s.`readonly` = 0 AND (s.`owner` = ? OR s.`id` IN (" . "SELECT i.`session_id` FROM `{$this->invitations_table}` i" . " WHERE i.`user` = ?" . "))", $this->user, $this->user); if ($db->is_error($result)) { throw new Exception("Internal error.", file_api_core::ERROR_CODE); } while ($row = $db->fetch_assoc($result)) { if ($path = $this->uri2path($row['uri'], true)) { $sessions[$row['id']] = $this->session_info_parse($row, $path); } } // 2. Get sessions user is eligible // - get list of all folder URIs and find sessions for files in these locations // @FIXME: in corner cases (user has many folders) this may produce a big query, // maybe fetching all sessions and then comparing with list of locations would be faster? $uris = $this->all_folder_locations(); if (!empty($uris)) { $where = array_map(function($uri) use ($db) { return 's.`uri` LIKE ' . $db->quote(str_replace('%', '_', $uri) . '/%'); }, $uris); $result = $db->query("SELECT * FROM `{$this->sessions_table}` s" . " WHERE s.`readonly` = 0 AND (" . join(' OR ', $where) . ")"); if ($db->is_error($result)) { throw new Exception("Internal error.", file_api_core::ERROR_CODE); } while ($row = $db->fetch_assoc($result)) { if (empty($sessions[$row['id']])) { // remove filename (and anything after it) so we have the folder URI // to check if it's on the folders list we have $uri = substr($row['uri'], 0, strrpos($row['uri'], '/')); if (in_array($uri, $uris) && ($path = $this->uri2path($row['uri'], true))) { $sessions[$row['id']] = $this->session_info_parse($row, $path); } } } } // set 'is_invited' flag if (!empty($sessions)) { $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; } } } // Sorting $sort = !empty($params['sort']) ? $params['sort'] : 'name'; $index = array(); if (in_array($sort, array('name', 'file', 'owner'))) { foreach ($sessions as $key => $val) { if ($sort == 'name' || $sort == 'file') { $path = explode(file_storage::SEPARATOR, $val['file']); $index[$key] = $path[count($path) - 1]; continue; } $index[$key] = $val[$sort]; } array_multisort($index, SORT_ASC, SORT_LOCALE_STRING, $sessions); } if ($params['reverse']) { $sessions = array_reverse($sessions, true); } return $sessions; } /** * Retern extra editor parameters to post the the viewer iframe * * @param array $info File info * * @return array POST parameters */ public function editor_post_params($info) { return array(); } /** * 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($filter = array()) { $filter['user'] = $this->user; // 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 $result; } /** * 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, $extended = false) { $db = $this->rc->get_dbh(); $query = ''; $select = "i.*"; foreach ($filter as $column => $value) { if ($column == 'timestamp') { $where[] = "i.`changed` > " . $db->quote($this->db_datetime($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); } } 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 i.`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 (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', $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'] != $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); } // 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`, `user_name`, `status`, `comment`, `changed`)" . " VALUES (?, ?, ?, ?, ?, ?)", $session_id, $user, $user_name, $status, $comment ?: '', $this->db_datetime()); 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 * @param bool $local Remove invitation only from local database * * @throws Exception */ public function invitation_delete($session_id, $user, $local = false) { $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, $this->user); if (!$db->affected_rows($result)) { throw new Exception("Failed to delete an invitation.", file_api_core::ERROR_CODE); } } /** * Update an invitation status * * @param string $session_id Session 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, $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 != $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` = ?, `comment` = ?, `changed` = ?" . " WHERE `session_id` = ? AND `user` = ?", $status, $comment ?: '', $this->db_datetime(), $session_id, $user); if (!$db->affected_rows($result)) { throw new Exception("Failed to update an invitation status.", file_api_core::ERROR_CODE); } } /** * Update a session URI (e.g. on file/folder move) * * @param string $from Source file/folder path * @param string $to Destination file/folder path * @param bool $is_folder True if the path is a folder */ public function session_uri_update($from, $to, $is_folder = false) { $db = $this->rc->get_dbh(); // Resolve paths $from = $this->path2uri($from); $to = $this->path2uri($to); if ($is_folder) { $set = "`uri` = REPLACE(`uri`, " . $db->quote($from . '/') . ", " . $db->quote($to .'/') . ")"; $where = "`uri` LIKE " . $db->quote(str_replace('%', '_', $from) . '/%'); } else { $set = "`uri` = " . $db->quote($to); $where = "`uri` = " . $db->quote($from); } $db->query("UPDATE `{$this->sessions_table}` SET $set WHERE $where"); } /** * Parse session info data */ protected function session_info_parse($record, $path = null, $filter = array()) { $session = array(); $fields = array('id', 'uri', 'owner', 'owner_name', 'readonly'); foreach ($fields as $field) { if (isset($record[$field])) { $session[$field] = $record[$field]; } } if ($path) { $session['file'] = $path; } if (!empty($record['data'])) { $data = json_decode($record['data'], true); $fields = array_merge($this->file_meta_items, array('origin')); foreach ($fields as $field) { if (empty($filter) || in_array($field, $filter)) { $session[$field] = $data[$field]; } } } // @TODO: is_invited?, last_modified? if ($session['owner'] == $this->user) { $session['is_owner'] = true; } if (!empty($filter)) { $session = array_intersect_key($session, array_flip($filter)); } return $session; } /** * Get file URI from path */ protected function path2uri($path, &$driver = null) { list($driver, $path) = $this->api->get_driver($path); return $driver->path2uri($path); } /** * Get file path from the URI */ protected function uri2path($uri, $use_fallback = false) { $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 } } // likely user has no access to the file, but has been invited, // extract filename from the URI if ($use_fallback && $uri) { $path = parse_url($uri, PHP_URL_PATH); $path = explode('/', $path); $path = $path[count($path) - 1]; return $path; } } /** * Get URI of all user folders (with shared locations) */ protected function all_folder_locations() { $locations = array(); foreach (array_merge(array($this->api->get_backend()), $this->api->get_drivers(true)) as $driver) { // Performance optimization: We're interested here in shared folders, // Kolab is the only driver that currently supports them, ignore others if (get_class($driver) != 'kolab_file_storage') { continue; } try { foreach ($driver->folder_list() as $folder) { if ($uri = $driver->path2uri($folder)) { $locations[] = $uri; } } } catch (Exception $e) { // do nothing } } return $locations; } /** * Get request origin, use Referer header if specified */ protected function get_origin() { if (!empty($_SERVER['HTTP_REFERER'])) { $url = parse_url($_SERVER['HTTP_REFERER']); return $url['scheme'] . '://' . $url['host'] . ($url['port'] ?: ''); } - return $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST']; + return (rcube_utils::https_check() ? 'https' : 'http') . '://' . $_SERVER['HTTP_HOST']; } /** * Return datetime in UTC timezone in SQL format */ protected function db_datetime($dt = null) { $timezone = new DateTimeZone('UTC'); $datetime = new DateTime($dt ? '@'.$dt : 'now', $timezone); return $datetime->format(self::DB_DATE_FORMAT); } } diff --git a/lib/file_wopi.php b/lib/file_wopi.php index 5f69e24..85149e7 100644 --- a/lib/file_wopi.php +++ b/lib/file_wopi.php @@ -1,357 +1,356 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * Document editing sessions handling (WOPI) */ class file_wopi extends file_document { protected $cache; // Mimetypes supported by CODE, but not advertised by all possible names protected $mimetype_aliases = array( 'application/vnd.corel-draw' => 'image/x-coreldraw', ); // Mimetypes supported by other Chwala viewers or ones we don't want to be editable protected $mimetype_exceptions = array( 'text/plain', 'image/bmp', 'image/png', 'image/jpeg', 'image/jpg', 'image/pjpeg', 'image/gif', 'image/tiff', 'image/x-tiff', ); /** * Return viewer URI for specified file/session. This creates * a new collaborative editing session when needed. * * @param string $file File path * @param array &$file_info File metadata (e.g. type) * @param string &$session_id Optional session ID to join to * @param string $readonly Create readonly (one-time) session * * @return string WOPI URI for specified document * @throws Exception */ public function session_start($file, &$file_info, &$session_id = null, $readonly = false) { parent::session_start($file, $file_info, $session_id, $readonly); if ($session_id) { // Create Chwala session for use as WOPI access_token // This session will have access to this one document session only $keys = array('env', 'user_id', 'user', 'username', 'password', 'storage_host', 'storage_port', 'storage_ssl'); $data = array_intersect_key($_SESSION, array_flip($keys)); $data['document_session'] = $session_id; $this->token = $this->api->session->create($data); $this->log_login($session_id); } return $this->frame_uri($session_id, $file_info['type']); } /** * Generate URI of WOPI editing session (WOPIsrc) */ protected function frame_uri($id, $mimetype) { $capabilities = $this->capabilities(); if (empty($capabilities) || empty($mimetype)) { return; } $metadata = $capabilities[strtolower($mimetype)]; if (empty($metadata)) { return; } - $office_url = rtrim($metadata['urlsrc'], ' /?'); // collabora - $service_url = rtrim($this->rc->config->get('fileapi_wopi_service'), ' /'); // kolab-wopi - $service_url .= '/wopi/files/' . $id; + $office_url = rtrim($metadata['urlsrc'], ' /?'); // collabora + $service_url = $this->api->api_url() . '/wopi/files/' . $id; // @TODO: Parsing and replacing placeholder values // https://wopi.readthedocs.io/en/latest/discovery.html#action-urls $args = array('WOPISrc' => $service_url); // We could also set: title, closebutton, revisionhistory // @TODO: do it in editor_post_params() when supported by the editor if ($lang = $this->api->env['language']) { $args['lang'] = str_replace('_', '-', $lang); } return $office_url . '?' . http_build_query($args, '', '&'); } /** * Retern extra viewer parameters to post to the viewer iframe * * @param array $info File info * * @return array POST parameters */ public function editor_post_params($info) { // Access token TTL (number of milliseconds since January 1, 1970 UTC) if ($ttl = $this->rc->config->get('session_lifetime', 0) * 60) { $now = new DateTime('now', new DateTimeZone('UTC')); $ttl = ($ttl + $now->format('U')) . '000'; } $params = array( 'access_token' => $this->token, 'access_token_ttl' => $ttl ?: 0, ); return $params; } /** * List supported mimetypes * * @param bool $editable Return only editable mimetypes * * @return array List of supported mimetypes */ public function supported_filetypes($editable = false) { $caps = $this->capabilities(); if ($editable) { $editable = array(); foreach ($caps as $mimetype => $c) { if ($c['name'] == 'edit') { $editable[] = $mimetype; } } return $editable; } return array_keys($caps); } /** * Uses WOPI discovery to get Office capabilities * https://wopi.readthedocs.io/en/latest/discovery.html */ protected function capabilities() { $cache_key = 'wopi.capabilities'; if ($result = $this->get_from_cache($cache_key)) { return $this->apply_aliases_and_exceptions($result); } $office_url = rtrim($this->rc->config->get('fileapi_wopi_office'), ' /'); $office_url .= '/hosting/discovery'; try { $request = $this->http_request(); $request->setMethod(HTTP_Request2::METHOD_GET); $request->setBody(''); $request->setUrl($office_url); $response = $request->send(); $body = $response->getBody(); $code = $response->getStatus(); if (empty($body) || $code != 200) { throw new Exception("Unexpected WOPI discovery response"); } } catch (Exception $e) { rcube::raise_error($e, true, false); // Don't bail out here, it would make the kolab_files UI broken return array(); } // parse XML output // // // // // // ... $node = new DOMDocument('1.0', 'UTF-8'); $node->loadXML($body); $result = array(); foreach ($node->getElementsByTagName('app') as $app) { if ($mimetype = $app->getAttribute('name')) { if ($action = $app->getElementsByTagName('action')->item(0)) { foreach ($action->attributes as $attr) { $result[$mimetype][$attr->name] = $attr->value; } } } } if (empty($result)) { rcube::raise_error("Failed to parse WOPI discovery response: $body", true, false); // Don't bail out here, it would make the kolab_files UI broken return array(); } $this->save_in_cache($cache_key, $result); return $this->apply_aliases_and_exceptions($result); } /** * Initializes HTTP request object */ protected function http_request() { require_once 'HTTP/Request2.php'; $request = new HTTP_Request2(); // Configure connection options $config = $this->rc->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', "application/json,text/javascript,*/*"); return $request; } /** * Get cached data */ protected function get_from_cache($key) { if ($cache = $this->get_cache()) { return $cache->get($key); } } /** * Store data in cache */ protected function save_in_cache($key, $value) { if ($cache = $this->get_cache()) { $cache->set($key, $value); } } /** * Getter for the shared cache engine object */ protected function get_cache() { if ($this->cache === null) { $this->cache = $this->rc->get_cache_shared('fileapi') ?: false; } return $this->cache; } /** * Support more mimetypes in CODE capabilities */ protected function apply_aliases_and_exceptions($caps) { foreach ($this->mimetype_aliases as $type => $alias) { if (isset($caps[$type]) && !isset($caps[$alias])) { $caps[$alias] = $caps[$type]; } } foreach ($this->mimetype_exceptions as $type) { unset($caps[$type]); } return $caps; } /** * Write login data (name, ID, IP address) to the 'userlogins' log file. */ protected function log_login($session_id) { if (!$this->api->config->get('log_logins')) { return; } $rcube = rcube::get_instance(); $user_name = $rcube->get_user_name(); $user_id = $rcube->get_user_id(); $message = sprintf('CODE access for %s (ID: %d) from %s in session %s; %s', $user_name, $user_id, rcube_utils::remote_ip(), session_id(), $session_id); // log login rcube::write_log('userlogins', $message); } } diff --git a/public_html/api/index.php b/lib/wopi/containers.php similarity index 78% copy from public_html/api/index.php copy to lib/wopi/containers.php index 7026885..5a442cb 100644 --- a/public_html/api/index.php +++ b/lib/wopi/containers.php @@ -1,35 +1,34 @@ | + | Copyright (C) 2012-2018, 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 | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -// environment initialization -require_once '../../lib/init.php'; - -try { - $API = new file_api; - $API->run(); -} catch (Exception $e) { - //rcube::console('API Error: ' . $e->getMessage()); - $API->output_error($e->getMessage(), $e->getCode()); +class file_wopi_containers +{ + /** + * Request handler + */ + public function handle() + { + throw new Exception("Not implemented", file_api_core::ERROR_NOT_IMPLEMENTED); + } } diff --git a/public_html/api/index.php b/lib/wopi/ecosystem.php similarity index 78% copy from public_html/api/index.php copy to lib/wopi/ecosystem.php index 7026885..105ee14 100644 --- a/public_html/api/index.php +++ b/lib/wopi/ecosystem.php @@ -1,35 +1,34 @@ | + | Copyright (C) 2012-2018, 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 | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ -// environment initialization -require_once '../../lib/init.php'; - -try { - $API = new file_api; - $API->run(); -} catch (Exception $e) { - //rcube::console('API Error: ' . $e->getMessage()); - $API->output_error($e->getMessage(), $e->getCode()); +class file_wopi_ecosystem +{ + /** + * Request handler + */ + public function handle() + { + throw new Exception("Not implemented", file_api_core::ERROR_NOT_IMPLEMENTED); + } } diff --git a/lib/wopi/files.php b/lib/wopi/files.php new file mode 100644 index 0000000..3d280fb --- /dev/null +++ b/lib/wopi/files.php @@ -0,0 +1,181 @@ + | + +--------------------------------------------------------------------------+ + | Author: Aleksander Machniak | + +--------------------------------------------------------------------------+ +*/ + +require_once __DIR__ . "/../api/common.php"; +require_once __DIR__ . "/../api/document.php"; + +class file_wopi_files extends file_api_document +{ + /** + * Request handler + */ + public function handle() + { + if (empty($this->api->path)) { + throw new Exception("File ID not specified", file_api_core::ERROR_NOT_FOUND); + } + + $file_id = $this->api->path[0]; + $command = $this->api->path[1]; + + if ($file_id != $_SESSION['document_session']) { + throw new Exception("File ID not specified", file_api_core::ERROR_UNAUTHORIZED); + } + + if ($this->api->method == 'GET') { + if (empty($command)) { + return $this->document_info($file_id); + } + + if ($command == 'contents') { + return $this->document_get($file_id); + } + } + else if ($this->api->method == 'PUT') { + if ($command == 'contents') { + return $this->document_put($file_id); + } + } +/* + else if (empty($command)) { + switch ($api->method) + // TODO case 'UNLOCK': + // TODO case 'LOCK': + // TODO case 'GET_LOCK': + // TODO case 'REFRESH_LOCK': + // TODO case 'PUT_RELATIVE': + // TODO case 'RENAME_FILE': + // TODO case 'DELETE': + // TODO case 'PUT_USER_INFO': + // TODO case 'GET_SHARE_URL': + } + } +*/ + throw new Exception("Unknown method", file_api_core::ERROR_NOT_IMPLEMENTED); + } + + /** + * Return document informations + * + * JSON Response (only required attributes listed): + * - BaseFileName: The string name of the file without a path. Used for + * display in user interface (UI), and determining the extension + * of the file. + * - OwnerId: A string that uniquely identifies the owner of the file. + * - Size: The size of the file in bytes, expressed as a long, + * a 64-bit signed integer. + * - UserId: A string value uniquely identifying the user currently + * accessing the file. + * - Version: The current version of the file based on the server’s file + * version schema, as a string. This value must change when the file changes. + */ + protected function document_info($id) + { + $info = parent::document_info($id); + + // Convert file metadata to Wopi format + // TODO: support more properties from + // https://wopirest.readthedocs.io/en/latest/files/CheckFileInfo.html + + $result = array( + 'BaseFileName' => $info['name'], + 'Size' => $info['size'], + 'Version' => $info['modified'], + 'OwnerId' => $info['owner'], + 'UserId' => $info['user'], + 'UserFriendlyName' => $info['user_name'], + 'UserCanWrite' => empty($info['readonly']), + 'PostMessageOrigin' => $info['origin'], + // Tell the client we do not support PutRelativeFile yet + 'UserCanNotWriteRelative' => true, + // Properties specific to Collabora Online + 'HideSaveOption' => true, + 'HideExportOption' => true, + 'HidePrintOption' => true, + 'EnableOwnerTermination' => true, + // TODO: 'UserExtraInfo' => ['avatar' => 'http://url/to/user/avatar', 'mail' => 'user@server.com'] + ); + + return $result; + } + + /** + * Update document file content + * + * Request Headers: + * - X-WOPI-Lock: A string provided by the WOPI client in a previous Lock request. + * Note that this header will not be included during document creation. + * Collabora-specific Request Headers: + * - X-LOOL-WOPI-IsModifiedByUser: true/false indicates whether the document + * was modified by the user when they saved it. + * - X-LOOL-WOPI-IsAutosave: true/false indicates whether the PutFile + * is a result of autosave or the user pressing the Save button. + * Response Headers: + * - X-WOPI-Lock: A string value identifying the current lock on the file. + * This header must always be included when responding to the request with 409. + * It should not be included when responding to the request with 200 OK. + * - X-WOPI-LockFailureReason: An optional string value indicating the cause + * of a lock failure. + * - X-WOPI-ItemVersion: An optional string value indicating the version of the file. + * Its value should be the same as Version value in CheckFileInfo. + * Status Codes: + * - 409 Conflict: Lock mismatch/locked by another interface + * - 413 Request Entity Too Large: File is too large; Host limit exceeded. + */ + protected function document_put($file_id) + { + // TODO: Locking + + parent::document_put($file_id); + } + + /** + * Return document file content + * + * Request Headers: + * - X-WOPI-MaxExpectedSize: An integer specifying the upper bound + * of the expected size of the file being requested. Optional. + * The host should use the maximum value of a 4-byte integer + * if this value is not set in the request. + * Response Headers: + * - X-WOPI-ItemVersion: An optional string value indicating the version of the file. + * Its value should be the same as Version value in CheckFileInfo. + * Status Codes: + * - 412 File is larger than X-WOPI-MaxExpectedSize + */ + protected function document_get($id) + { + $doc_info = parent::document_info($id, false); + $max_size = rcube_utils::request_header('X-WOPI-MaxExpectedSize') ?: 1024 * 1024 * 1024; + + // Check max file size + if ($doc_info['size'] > $max_size) { + throw new Exception("File exceeds max expected size", file_api_core::ERROR_PRECONDITION_FAILED); + } + + header("X-WOPI-ItemVersion: " . $doc_info['modified']); + + parent::document_get($id); + } +} diff --git a/public_html/api/index.php b/public_html/api/index.php index 7026885..6da8865 100644 --- a/public_html/api/index.php +++ b/public_html/api/index.php @@ -1,35 +1,35 @@ | + | Copyright (C) 2011-2018, 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 | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ // environment initialization require_once '../../lib/init.php'; try { - $API = new file_api; + $API = file_api::factory(); $API->run(); } catch (Exception $e) { //rcube::console('API Error: ' . $e->getMessage()); $API->output_error($e->getMessage(), $e->getCode()); }