Page MenuHomePhorge

No OneTemporary

diff --git a/lib/api/file_get.php b/lib/api/file_get.php
index 451d1d6..249584c 100644
--- a/lib/api/file_get.php
+++ b/lib/api/file_get.php
@@ -1,103 +1,103 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2014, 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class file_api_file_get extends file_api_common
{
protected $driver;
/**
* Request handler
*/
public function handle()
{
parent::handle();
$this->api->output_type = file_api_core::OUTPUT_HTML;
if (!isset($this->args['file']) || $this->args['file'] === '') {
header("HTTP/1.0 ".file_api_core::ERROR_CODE." Missing file name");
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) {
$method = $_SERVER['HTTP_X_HTTP_METHOD'];
}
$params = array(
- 'force-download' => rcube_utils::get_boolean((string) $this->args['force-download']),
- 'force-type' => $this->args['force-type'],
- 'head' => $this->args['head'] ?: $method == 'HEAD',
+ 'force-download' => rcube_utils::get_boolean((string) $this->args['force-download'] ?? ""),
+ 'force-type' => $this->args['force-type'] ?? null,
+ 'head' => ($this->args['head'] ?? null) ?: $method == 'HEAD',
);
list($this->driver, $path) = $this->api->get_driver($this->args['file']);
if (!empty($this->args['viewer'])) {
$this->file_view($path, $this->args, $params);
}
try {
$this->driver->file_get($path, $params);
}
catch (Exception $e) {
header("HTTP/1.0 " . file_api_core::ERROR_CODE . " " . $e->getMessage());
}
exit;
}
/**
* File vieweing request handler
*/
protected function file_view($file, $args, $params)
{
$viewer = $args['viewer'];
$path = __DIR__ . "/../viewers/$viewer.php";
$class = "file_viewer_$viewer";
if (!file_exists($path)) {
return;
}
// get file info
try {
$info = $this->driver->file_info($file);
}
catch (Exception $e) {
header("HTTP/1.0 " . file_api_core::ERROR_CODE . " " . $e->getMessage());
exit;
}
include_once $path;
$viewer = new $class($this->api);
// check if specified viewer supports file type
// otherwise return (fallback to file_get action)
if (!$viewer->supports($info['type'])) {
return;
}
$viewer->output($args['file'], $info);
exit;
}
}
diff --git a/lib/file_api.php b/lib/file_api.php
index c9b8782..a2d8db8 100644
--- a/lib/file_api.php
+++ b/lib/file_api.php
@@ -1,541 +1,541 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2015, 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
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();
register_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', $_REQUEST['token'] ?? null)) {
$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'] ?? null)) {
$_SESSION['version'] = $_GET['version'];
}
if ($this->request == 'authenticate') {
$this->output_success(array(
'token' => session_id(),
'capabilities' => $this->capabilities(),
));
}
}
else {
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
*/
protected function session_validate($new_session = false, $token = null)
{
if (!$new_session) {
$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;
}
// Single-document session?
if (!($this instanceof file_api_wopi)
&& ($doc_id = ($_SESSION['document_session'] ?? null))
&& (strpos($this->request, 'document') !== 0 || $doc_id != $_GET['id'])
) {
throw new Exception("Access denied", file_api_core::ERROR_UNAUTHORIZED);
}
if ($_SESSION['env']) {
$this->env = $_SESSION['env'];
}
return true;
}
/**
* Initializes session
*/
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 {
/* @phpstan-ignore-next-line */
$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') || $this->config->get('performance_stats')) {
// we have to disable per_user_logging to make sure stats end up in the main console log
$this->config->set('per_user_logging', false);
// 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_usage')) {
$mem = round(memory_get_usage() / 1024 /1024, 1);
}
if (function_exists('memory_get_peak_usage')) {
$mem .= '/'. round(memory_get_peak_usage() / 1024 / 1024, 1);
}
- $path = !empty($this->path) ? '/' . implode($this->path, '/') : '';
+ $path = !empty($this->path) ? '/' . implode('/', $this->path) : '';
$request = ($this instanceof file_api_wopi ? 'wopi/' : '') . $this->request;
if ($path !== '' && substr_compare($this->request, $path, -1 * strlen($path), strlen($path), true) != 0) {
$request .= $path;
}
$log = sprintf('%s: %s [%s]', property_exists($this, 'method') ? $this->method : $_SERVER['REQUEST_METHOD'], trim($request) ?: '/', $mem);
if (defined('FILE_API_START')) {
rcube::print_timer(FILE_API_START, $log);
}
else {
rcube::console($log);
}
}
}
/**
* Authentication request handler (HTTP Auth)
*/
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'])) {
$tokens = array(
$_SERVER['REMOTE_USER'],
$_SERVER['REDIRECT_REMOTE_USER'],
$_SERVER['HTTP_AUTHORIZATION'],
rcube_utils::request_header('Authorization'),
);
foreach ($tokens as $token) {
if (!empty($token)) {
if (stripos($token, 'Basic ') === 0) {
$basicAuthData = base64_decode(substr($token, 6));
list($username, $password) = explode(':', $basicAuthData, 2);
if ($username) {
break;
}
}
else if (stripos($token, 'Bearer ') === 0) {
$username = base64_decode(substr($token, 7));
if ($username) {
break;
}
}
}
}
}
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_UNAUTHORIZED);
}
}
return $username;
}
/**
* Storage/System method handler
*/
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
*/
public function output_send($data = null)
{
// Send response
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 $_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 40c258d..d1a669a 100644
--- a/lib/file_api_core.php
+++ b/lib/file_api_core.php
@@ -1,448 +1,448 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2014, 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class file_api_core extends file_locale
{
const API_VERSION = 5;
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 ERROR_UNSUPPORTED = 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;
protected $admin_drivers = array();
/**
* 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
* @param array &$admin_drivers List of admin-configured drivers
*
* @return array List of storage drivers
*/
public function get_drivers($as_objects = false, &$admin_drivers = null)
{
$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');
// Disable webdav sources/drivers in iRony that point to the
// same host to prevent infinite recursion
$is_valid_source = function($source) {
if ($source['driver'] == 'webdav') {
$self_url = parse_url($_SERVER['SCRIPT_URI']);
$item_url = parse_url($source['baseuri'] ?: $source['host']);
$hosts = array($self_url['host'], $_SERVER['SERVER_NAME'], $_SERVER['SERVER_ADDR']);
if (in_array($item_url['host'], $hosts)) {
return false;
}
}
return true;
};
if (!empty($enabled)) {
$drivers = $backend->driver_list();
if ($iRony) {
$drivers = array_filter($drivers, $is_valid_source);
}
foreach ($drivers as $item) {
$all[] = $item['title'];
if ($item['enabled'] && in_array($item['driver'], (array) $enabled)) {
$result[] = $as_objects ? $this->get_driver_object($item) : $item;
}
}
}
$admin_drivers = array();
if (!empty($preconf)) {
if ($iRony) {
$preconf = array_filter($preconf, $is_valid_source);
}
foreach ($preconf as $title => $item) {
if (!in_array($title, $all)) {
$item['title'] = $title;
$item['admin'] = true;
$result[] = $as_objects ? $this->get_driver_object($item) : $item;
$admin_drivers[] = $title;
}
}
}
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)) {
$rcube = rcube::get_instance();
if ($rcube->config->get('fileapi_backend_storage_disabled')) {
throw new Exception("Failed to find a driver for specified folder/file.", self::ERROR_NOT_FOUND);
}
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('VERSION' => self::API_VERSION);
// 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 ($rcube->config->get('fileapi_backend_storage_disabled')) {
$caps['NOROOT'] = true;
}
if (!$full) {
return $caps;
}
if ($caps['MANTICORE'] ?? false) {
$manticore = new file_manticore($this);
$caps['MANTICORE_EDITABLE'] = $manticore->supported_filetypes(true);
}
if ($caps['WOPI'] ?? false) {
$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|null 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
+ if (($this->icache[$key] ?? null) === 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]) {
+ if ($this->icache[$key] ?? null) {
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_document.php b/lib/file_document.php
index df4c4e2..502dc22 100644
--- a/lib/file_document.php
+++ b/lib/file_document.php
@@ -1,879 +1,879 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2016, 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* 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"];
+ $session = $this->icache["session:$id"] ?? null;
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($params = 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 $url['scheme'] . '://' . $url['host'] . (($url['port'] ?? null) ?: '');
}
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/wopi/files.php b/lib/wopi/files.php
index 341b1d8..1022974 100644
--- a/lib/wopi/files.php
+++ b/lib/wopi/files.php
@@ -1,199 +1,199 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| 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 <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
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];
+ $command = $this->api->path[1] ?? null;
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, $extended = true)
{
$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'] ?: preg_replace('/@.*$/', '', $info['user']),
'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,
'WatermarkText' => '', // ??
'DisablePrint' => false,
'DisableExport' => false,
'DisableCopy' => false,
'DisableInactiveMessages' => true,
'DisableChangeTrackingRecord' => true,
'DisableChangeTrackingShow' => true,
'HideChangeTrackingControls' => true,
// TODO: 'UserExtraInfo' => ['avatar' => 'http://url/to/user/avatar', 'mail' => $info['user']]
'UserExtraInfo' => array(),
);
if ($info['modified']) {
try {
$dt = new DateTime('@' . $info['modified'], new DateTimeZone('UTC'));
$result['LastModifiedTime'] = $dt->format('Y-m-d\TH:i:s') . '.0000000Z';
}
catch (Exception $e) {
}
}
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);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 5:56 AM (1 d, 14 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196966
Default Alt Text
(76 KB)

Event Timeline