diff --git a/lib/file_api.php b/lib/file_api.php index 3764d23..70f0dd9 100644 --- a/lib/file_api.php +++ b/lib/file_api.php @@ -1,462 +1,468 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api extends file_api_core { public $session; public $config; public $browser; public $output_type = file_api_core::OUTPUT_JSON; public function __construct() { $rcube = rcube::get_instance(); $rcube->add_shutdown_function(array($this, 'shutdown')); $this->config = $rcube->config; $this->session_init(); - - if ($_SESSION['env']) { - $this->env = $_SESSION['env']; - } - - $this->locale_init(); } /** * Process the request and dispatch it to the requested service */ public function run() { $this->request = strtolower($_GET['method']); // Check the session, authenticate the user if (!$this->session_validate($this->request == 'authenticate')) { $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); } } + 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) { if (!$new_session) { $sess_id = rcube_utils::request_header('X-Session-Token') ?: $_REQUEST['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']) && (strpos($this->request, 'document') !== 0 || $doc_id != $_GET['id']) ) { throw new Exception("Access denied", 403); } + if ($_SESSION['env']) { + $this->env = $_SESSION['env']; + } + return true; } /** * Initializes session */ private function session_init() { $rcube = rcube::get_instance(); $sess_name = $this->config->get('session_name'); $lifetime = $this->config->get('session_lifetime', 0) * 60; if ($lifetime) { ini_set('session.gc_maxlifetime', $lifetime * 2); } ini_set('session.name', $sess_name ? $sess_name : 'file_api_sessid'); ini_set('session.use_cookies', 0); ini_set('session.serialize_handler', 'php'); // Roundcube Framework >= 1.2 if (in_array('factory', get_class_methods('rcube_session'))) { $this->session = rcube_session::factory($this->config); } // Rouncube Framework < 1.2 else { $this->session = new rcube_session($rcube->get_dbh(), $this->config); $this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME'])); $this->session->set_ip_check($this->config->get('ip_check')); } $this->session->register_gc_handler(array($rcube, 'gc')); // this is needed to correctly close session in shutdown function $rcube->session = $this->session; } /** * Script shutdown handler */ public function shutdown() { // write performance stats to logs/console if ($this->config->get('devel_mode')) { if (function_exists('memory_get_peak_usage')) $mem = memory_get_peak_usage(); else if (function_exists('memory_get_usage')) $mem = memory_get_usage(); $log = trim($this->request . ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : '')); if (defined('FILE_API_START')) { rcube::print_timer(FILE_API_START, $log); } else { rcube::console($log); } } } /** * Authentication request handler (HTTP Auth) */ private function authenticate() { if (isset($_POST['username'])) { $username = $_POST['username']; $password = $_POST['password']; } else if (!empty($_SERVER['PHP_AUTH_USER'])) { $username = $_SERVER['PHP_AUTH_USER']; $password = $_SERVER['PHP_AUTH_PW']; } // when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule else if (!isset($_SERVER['PHP_AUTH_USER'])) { // "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..." if (isset($_SERVER["REMOTE_USER"])) { $basicAuthData = base64_decode(substr($_SERVER["REMOTE_USER"], 6)); } else if (isset($_SERVER["REDIRECT_REMOTE_USER"])) { $basicAuthData = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6)); } else if (isset($_SERVER["Authorization"])) { $basicAuthData = base64_decode(substr($_SERVER["Authorization"], 6)); } else if (isset($_SERVER["HTTP_AUTHORIZATION"])) { $basicAuthData = base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6)); } if (isset($basicAuthData) && !empty($basicAuthData)) { list($username, $password) = explode(":", $basicAuthData); } } if (!empty($username)) { $backend = $this->get_backend(); $result = $backend->authenticate($username, $password); if (empty($result)) { /* header('WWW-Authenticate: Basic realm="' . $this->app_name .'"'); header('HTTP/1.1 401 Unauthorized'); exit; */ throw new Exception("Invalid password or username", file_api_core::ERROR_CODE); } } return $username; } /** * Storage/System method handler */ private function request_handler($request) { // handle "global" requests that don't require api driver switch ($request) { case 'ping': return array(); case 'quit': $this->session->destroy(session_id()); return array(); case 'configure': foreach (array_keys($this->env) as $name) { if (isset($_GET[$name])) { $this->env[$name] = $_GET[$name]; } } $_SESSION['env'] = $this->env; return $this->env; case 'upload_progress': return $this->upload_progress(); case 'mimetypes': return $this->supported_mimetypes(); case 'capabilities': return $this->capabilities(); } // handle request if ($request && preg_match('/^[a-z0-9_-]+$/', $request)) { $aliases = array( // request name aliases for backward compatibility 'lock' => 'lock_create', 'unlock' => 'lock_delete', 'folder_rename' => 'folder_move', ); // Redirect all document_* actions into 'document' action if (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 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']; 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) { // Send response header("Content-Type: {$this->output_type}; charset=utf-8"); echo json_encode($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'; } /** * 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_locale.php b/lib/file_locale.php index 7db48a0..41b7f65 100644 --- a/lib/file_locale.php +++ b/lib/file_locale.php @@ -1,122 +1,127 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | | Author: Jeroen van Meeuwen | +--------------------------------------------------------------------------+ */ class file_locale { protected static $translation = array(); - + protected $language; /** * Localization initialization. */ protected function locale_init() { $language = $this->get_language(); $LANG = array(); if (!$language) { $language = 'en_US'; } + $this->language = $language; + @include __DIR__ . "/locale/en_US.php"; if ($language != 'en_US') { @include __DIR__ . "/locale/$language.php"; } setlocale(LC_ALL, $language . '.utf8', $language . 'UTF-8', 'en_US.utf8', 'en_US.UTF-8'); self::$translation = $LANG; } /** * Returns system language (locale) setting. * * @return string Language code */ protected function get_language() { $aliases = array( 'de' => 'de_DE', 'en' => 'en_US', 'pl' => 'pl_PL', ); // UI language $langs = !empty($_SERVER['HTTP_ACCEPT_LANGUAGE']) ? $_SERVER['HTTP_ACCEPT_LANGUAGE'] : ''; $langs = explode(',', $langs); if (!empty($_SESSION['user']) && !empty($_SESSION['user']['language'])) { array_unshift($langs, $_SESSION['user']['language']); } + if (!empty($_SESSION['env']) && !empty($_SESSION['env']['language'])) { + array_unshift($langs, $_SESSION['env']['language']); + } - while ($lang = array_shift($langs)) { + foreach (array_unique($langs) as $lang) { $lang = explode(';', $lang); $lang = $lang[0]; $lang = str_replace('-', '_', $lang); if (file_exists(__DIR__ . "/locale/$lang.php")) { return $lang; } if (isset($aliases[$lang]) && ($alias = $aliases[$lang]) && file_exists(__DIR__ . "/locale/$alias.php") ) { return $alias; } } return null; } /** * Returns translation of defined label/message. * * @return string Translated string. */ public static function translate() { $args = func_get_args(); if (is_array($args[0])) { $args = $args[0]; } $label = $args[0]; if (isset(self::$translation[$label])) { $content = trim(self::$translation[$label]); } else { $content = $label; } for ($i = 1, $len = count($args); $i < $len; $i++) { $content = str_replace('$'.$i, $args[$i], $content); } return $content; } } diff --git a/lib/file_wopi.php b/lib/file_wopi.php index 7382aed..af74b1b 100644 --- a/lib/file_wopi.php +++ b/lib/file_wopi.php @@ -1,307 +1,313 @@ | +--------------------------------------------------------------------------+ | 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 $aliases = array( 'application/vnd.corel-draw' => 'image/x-coreldraw', ); /** * 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('language', 'user_id', 'user', 'username', 'password', + $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); } 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; // @TODO: Parsing and replacing placeholder values // https://wopi.readthedocs.io/en/latest/discovery.html#action-urls - return $office_url . '?WOPISrc=' . urlencode($service_url); + $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, '', '&', PHP_QUERY_RFC3986); } /** * 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, ); - // @TODO: we should/could also add: - // lang, title, timestamp, closebutton, revisionhistory 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($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, true); } // 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, true); } $this->save_in_cache($cache_key, $result); return $this->apply_aliases($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) { $cache = $this->rc->get_cache_shared('chwala'); $this->cache = $cache ?: false; } return $this->cache; } /** * Support more mimetypes in CODE capabilities */ protected function apply_aliases($caps) { foreach ($this->aliases as $type => $alias) { if (isset($caps[$type]) && !isset($caps[$alias])) { $caps[$alias] = $caps[$type]; } } return $caps; } }