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());
}