Page MenuHomePhorge

No OneTemporary

diff --git a/lib/api/document.php b/lib/api/document.php
index fa5d7df..064524d 100644
--- a/lib/api/document.php
+++ b/lib/api/document.php
@@ -1,268 +1,292 @@
<?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_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
- else if (strpos($this->args['method'], 'document_') === 0) {
+ 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':
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);
}
throw new Exception("Unknown method", file_api_core::ERROR_INVALID);
}
/**
* Get file path from manticore session identifier
*/
protected function get_file_path($id)
{
$manticore = new file_manticore($this->api);
return $manticore->session_file($id);
}
/**
* Get invitations list
*/
protected function invitations()
{
$timestamp = time();
// 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
}
$manticore = new file_manticore($this->api);
$filter = array();
if ($this->args['timestamp']) {
$filter['timestamp'] = $this->args['timestamp'];
}
$list = $manticore->invitations_list($filter);
return array(
'list' => $list,
'timestamp' => $timestamp,
);
}
+ /**
+ * Get sessions list
+ */
+ protected function sessions()
+ {
+ $manticore = new file_manticore($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 $manticore->sessions_list($params);
+ }
+
/**
* Close (delete) manticore session
*/
protected function document_delete($id)
{
$manticore = new file_manticore($this->api);
if (!$manticore->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)
{
$manticore = new file_manticore($this->api);
$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'])) {
$manticore->invitation_create($id, $user['user'], file_manticore::STATUS_INVITED, $comment, $user['name']);
$result[] = array(
'session_id' => $id,
'user' => $user['user'],
'user_name' => $user['name'],
'status' => file_manticore::STATUS_INVITED,
);
}
}
return array(
'list' => $result,
);
}
/**
* Request an invitation to a session
*/
protected function document_request($id)
{
$manticore = new file_manticore($this->api);
$manticore->invitation_create($id, null, file_manticore::STATUS_REQUESTED, $this->args['comment']);
}
/**
* Decline an invitation to a session
*/
protected function document_decline($id)
{
$manticore = new file_manticore($this->api);
$manticore->invitation_update($id, $this->args['user'], file_manticore::STATUS_DECLINED, $this->args['comment']);
}
/**
* Accept an invitation to a session
*/
protected function document_accept($id)
{
$manticore = new file_manticore($this->api);
$manticore->invitation_update($id, $this->args['user'], file_manticore::STATUS_ACCEPTED, $this->args['comment']);
}
/**
* Remove a session participant(s) - cancel invitations
*/
protected function document_cancel($id)
{
$manticore = new file_manticore($this->api);
$users = $this->args['users'];
if (empty($users)) {
throw new Exception("Invalid arguments.", file_api_core::ERROR_CODE);
}
foreach ((array) $users as $user) {
$manticore->invitation_delete($id, $user);
$result[] = $user;
}
return array(
'list' => $result,
);
}
/**
* Update document file content
*/
protected function document_put($file)
{
list($driver, $path) = $this->api->get_driver($file);
$length = rcube_utils::request_header('Content-Length');
$tmp_dir = unslashify($this->api->config->get('temp_dir'));
$tmp_path = tempnam($tmp_dir, 'chwalaUpload');
// Create stream to copy input into a temp file
$input = fopen('php://input', 'r');
$tmp_file = fopen($tmp_path, 'w');
if (!$input || !$tmp_file) {
throw new Exception("Failed opening input or temp file stream.", file_api_core::ERROR_CODE);
}
// Create temp file from the input
$copied = stream_copy_to_stream($input, $tmp_file);
fclose($input);
fclose($tmp_file);
if ($copied < $length) {
throw new Exception("Failed writing to temp file.", file_api_core::ERROR_CODE);
}
$file = array(
'path' => $tmp_path,
'type' => rcube_mime::file_content_type($tmp_path, $file),
);
$driver->file_update($path, $file);
// remove the temp file
unlink($tmp_path);
}
/**
* Return document file content
*/
protected function document_get($file)
{
list($driver, $path) = $this->api->get_driver($file);
try {
$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;
}
}
diff --git a/lib/file_api.php b/lib/file_api.php
index 7132044..3679eb1 100644
--- a/lib/file_api.php
+++ b/lib/file_api.php
@@ -1,434 +1,434 @@
<?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;
public function __construct()
{
$rcube = rcube::get_instance();
$rcube->add_shutdown_function(array($this, 'shutdown'));
$this->config = $rcube->config;
$this->session_init();
if ($_SESSION['env']) {
$this->env = $_SESSION['env'];
}
$this->locale_init();
}
/**
* Process the request and dispatch it to the requested service
*/
public function run()
{
$this->request = strtolower($_GET['method']);
// Check the session, authenticate the user
if ($this->request == 'authenticate' || !$this->session_validate()) {
$this->session->destroy(session_id());
$this->session->regenerate_id(false);
if ($username = $this->authenticate()) {
$_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);
}
}
// Call service method
$result = $this->request_handler($this->request);
// Send success response, errors should be handled by driver class
// by throwing exceptions or sending output by itself
$this->output_success($result);
}
/**
* Session validation check and session start
*/
private function session_validate()
{
$sess_id = rcube_utils::request_header('X-Session-Token') ?: $_REQUEST['token'];
if (empty($sess_id)) {
$this->session->start();
return false;
}
session_id($sess_id);
$this->session->start();
if (empty($_SESSION['user'])) {
return false;
}
return true;
}
/**
* Initializes session
*/
private function session_init()
{
$rcube = rcube::get_instance();
$sess_name = $this->config->get('session_name');
$lifetime = $this->config->get('session_lifetime', 0) * 60;
if ($lifetime) {
ini_set('session.gc_maxlifetime', $lifetime * 2);
}
ini_set('session.name', $sess_name ? $sess_name : 'file_api_sessid');
ini_set('session.use_cookies', 0);
ini_set('session.serialize_handler', 'php');
// Roundcube Framework >= 1.2
if (in_array('factory', get_class_methods('rcube_session'))) {
$this->session = rcube_session::factory($this->config);
}
// Rouncube Framework < 1.2
else {
$this->session = new rcube_session($rcube->get_dbh(), $this->config);
$this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
$this->session->set_ip_check($this->config->get('ip_check'));
}
$this->session->register_gc_handler(array($rcube, 'gc'));
// this is needed to correctly close session in shutdown function
$rcube->session = $this->session;
}
/**
* Script shutdown handler
*/
public function shutdown()
{
// write performance stats to logs/console
if ($this->config->get('devel_mode')) {
if (function_exists('memory_get_peak_usage'))
$mem = memory_get_peak_usage();
else if (function_exists('memory_get_usage'))
$mem = memory_get_usage();
$log = trim($this->request . ($mem ? sprintf(' [%.1f MB]', $mem/1024/1024) : ''));
if (defined('FILE_API_START')) {
rcube::print_timer(FILE_API_START, $log);
}
else {
rcube::console($log);
}
}
}
/**
* Authentication request handler (HTTP Auth)
*/
private function authenticate()
{
if (isset($_POST['username'])) {
$username = $_POST['username'];
$password = $_POST['password'];
}
else if (!empty($_SERVER['PHP_AUTH_USER'])) {
$username = $_SERVER['PHP_AUTH_USER'];
$password = $_SERVER['PHP_AUTH_PW'];
}
// when used with (f)cgi no PHP_AUTH* variables are available without defining a special rewrite rule
else if (!isset($_SERVER['PHP_AUTH_USER'])) {
// "Basic didhfiefdhfu4fjfjdsa34drsdfterrde..."
if (isset($_SERVER["REMOTE_USER"])) {
$basicAuthData = base64_decode(substr($_SERVER["REMOTE_USER"], 6));
}
else if (isset($_SERVER["REDIRECT_REMOTE_USER"])) {
$basicAuthData = base64_decode(substr($_SERVER["REDIRECT_REMOTE_USER"], 6));
}
else if (isset($_SERVER["Authorization"])) {
$basicAuthData = base64_decode(substr($_SERVER["Authorization"], 6));
}
else if (isset($_SERVER["HTTP_AUTHORIZATION"])) {
$basicAuthData = base64_decode(substr($_SERVER["HTTP_AUTHORIZATION"], 6));
}
if (isset($basicAuthData) && !empty($basicAuthData)) {
list($username, $password) = explode(":", $basicAuthData);
}
}
if (!empty($username)) {
$backend = $this->get_backend();
$result = $backend->authenticate($username, $password);
if (empty($result)) {
/*
header('WWW-Authenticate: Basic realm="' . $this->app_name .'"');
header('HTTP/1.1 401 Unauthorized');
exit;
*/
throw new Exception("Invalid password or username", file_api_core::ERROR_CODE);
}
}
return $username;
}
/**
* Storage/System method handler
*/
private function request_handler($request)
{
// handle "global" requests that don't require api driver
switch ($request) {
case 'ping':
return array();
case 'quit':
$this->session->destroy(session_id());
return array();
case 'configure':
foreach (array_keys($this->env) as $name) {
if (isset($_GET[$name])) {
$this->env[$name] = $_GET[$name];
}
}
$_SESSION['env'] = $this->env;
return $this->env;
case 'upload_progress':
return $this->upload_progress();
case 'mimetypes':
return $this->supported_mimetypes();
case 'capabilities':
return $this->capabilities();
}
// handle request
if ($request && preg_match('/^[a-z0-9_-]+$/', $request)) {
$aliases = array(
// request name aliases for backward compatibility
'lock' => 'lock_create',
'unlock' => 'lock_delete',
'folder_rename' => 'folder_move',
);
// Redirect all document_* actions into 'document' action
- if (preg_match('/^(invitations|document_[a-z]+)$/', $request)) {
+ if (preg_match('/^(sessions|invitations|document_[a-z]+)$/', $request)) {
$request = 'document';
}
$request = $aliases[$request] ?: $request;
require_once __DIR__ . "/api/common.php";
include_once __DIR__ . "/api/$request.php";
$class_name = "file_api_$request";
if (class_exists($class_name, false)) {
$handler = new $class_name($this);
return $handler->handle();
}
}
throw new Exception("Unknown method", file_api_core::ERROR_INVALID);
}
/**
* File upload progress handler
*/
protected function upload_progress()
{
if (function_exists('apc_fetch')) {
$prefix = ini_get('apc.rfc1867_prefix');
$uploadid = rcube_utils::get_input_value('id', rcube_utils::INPUT_GET);
$status = apc_fetch($prefix . $uploadid);
if (!empty($status)) {
$status['percent'] = round($status['current']/$status['total']*100);
if ($status['percent'] < 100) {
$diff = max(1, time() - intval($status['start_time']));
// calculate time to end of uploading (in seconds)
$status['eta'] = intval($diff * (100 - $status['percent']) / $status['percent']);
// average speed (bytes per second)
$status['rate'] = intval($status['current'] / $diff);
}
}
$status['id'] = $uploadid;
return $status; // id, done, total, current, percent, start_time, eta, rate
}
throw new Exception("Not supported", file_api_core::ERROR_CODE);
}
/**
* Returns complete File URL
*
* @param string $file File name (with path)
*
* @return string File URL
*/
public function file_url($file)
{
return file_utils::script_uri(). '?method=file_get'
. '&file=' . urlencode($file)
. '&token=' . urlencode(session_id());
}
/**
* Returns web browser object
*
* @return rcube_browser Web browser object
*/
public function get_browser()
{
if ($this->browser === null) {
$this->browser = new rcube_browser;
}
return $this->browser;
}
/**
* Send success response
*
* @param mixed $data Data
*/
public function output_success($data)
{
if (!is_array($data)) {
$data = array();
}
$response = array('status' => 'OK', 'result' => $data);
if (!empty($_REQUEST['req_id'])) {
$response['req_id'] = $_REQUEST['req_id'];
}
$this->output_send($response);
}
/**
* Send error response
*
* @param mixed $response Response data
* @param int $code Error code
*/
public function output_error($response, $code = null)
{
if (is_string($response)) {
$response = array('reason' => $response);
}
$response['status'] = 'ERROR';
if ($code) {
$response['code'] = $code;
}
if (!empty($_REQUEST['req_id'])) {
$response['req_id'] = $_REQUEST['req_id'];
}
if (empty($response['code'])) {
$response['code'] = file_api_core::ERROR_CODE;
}
$this->output_send($response);
}
/**
* Send response
*
* @param mixed $data Data
*/
protected function output_send($data)
{
// Send response
header("Content-Type: {$this->output_type}; charset=utf-8");
echo json_encode($data);
exit;
}
/**
* Returns API version supported by the client
*/
public function client_version()
{
return $_SESSION['version'];
}
/**
* Create a human readable string for a number of bytes
*
* @param int Number of bytes
*
* @return string Byte string
*/
public function show_bytes($bytes)
{
if ($bytes >= 1073741824) {
$gb = $bytes/1073741824;
$str = sprintf($gb >= 10 ? "%d " : "%.1f ", $gb) . 'GB';
}
else if ($bytes >= 1048576) {
$mb = $bytes/1048576;
$str = sprintf($mb >= 10 ? "%d " : "%.1f ", $mb) . 'MB';
}
else if ($bytes >= 1024) {
$str = sprintf("%d ", round($bytes/1024)) . 'KB';
}
else {
$str = sprintf('%d ', $bytes) . 'B';
}
return $str;
}
}
diff --git a/lib/file_manticore.php b/lib/file_manticore.php
index 440624b..cd21e43 100644
--- a/lib/file_manticore.php
+++ b/lib/file_manticore.php
@@ -1,689 +1,770 @@
<?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> |
+--------------------------------------------------------------------------+
*/
/**
* Document editing sessions handling
*/
class file_manticore
{
protected $api;
protected $rc;
protected $request;
protected $user;
protected $sessions_table = 'chwala_sessions';
protected $invitations_table = 'chwala_invitations';
protected $icache = array();
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
/**
* Class constructor
*
* @param file_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);
}
/**
* Return viewer URI for specified file/session. This creates
* a new collaborative editing session when needed.
*
* @param string $file File path
* @param string &$session_id Optional session ID to join to
*
* @return string Manticore URI
* @throws Exception
*/
public function session_start($file, &$session_id = null)
{
if ($file !== null) {
list($driver, $path) = $this->api->get_driver($file);
$uri = $driver->path2uri($path);
}
$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);
}
}
// authenticate to Manticore, we need auth token for frame_uri
$req = $this->get_request();
// @TODO: make sure the session exists in Manticore?
}
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` = ?", $this->user, $uri);
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));
$data = array();
$owner = $this->user;
// 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);
}
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);
}
return $this->frame_uri($session_id);
}
/**
* 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 string File path
* @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);
}
}
return $path;
}
/**
* 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
list($driver, $path) = $this->api->get_driver($path);
$uri = trim($driver->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 `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
* @param bool $local Remove session only from local database
*/
public function session_delete($id, $local = false)
{
$db = $this->rc->get_dbh();
$result = $db->query("DELETE FROM `{$this->sessions_table}`"
. " WHERE `id` = ? AND `owner` = ?",
$id, $this->user);
$success = $db->affected_rows($result) > 0;
// Send document delete to Manticore
if ($success && !$local) {
$req = $this->get_request();
$res = $req->document_delete($id);
}
return $success;
}
/**
* Create editing session
*/
protected function session_create($id, $uri, $owner, $data)
{
// get user name
$owner_name = $this->api->resolve_user($owner) ?: '';
// Do this before starting the session in Manticore,
// it will immediately call api/document to get the file body
$db = $this->rc->get_dbh();
$result = $db->query("INSERT INTO `{$this->sessions_table}`"
. " (`id`, `uri`, `owner`, `owner_name`, `data`)"
. " VALUES (?, ?, ?, ?, ?)",
$id, $uri, $owner, $owner_name, json_encode($data));
$success = $db->affected_rows($result) > 0;
// create the session in Manticore
if ($success) {
$req = $this->get_request();
$res = $req->document_create(array(
'id' => $id,
'title' => '', // @TODO: maybe set to a file path without extension?
'access' => array(
array(
'identity' => $owner,
'permission' => file_manticore_api::ACCESS_WRITE,
),
),
));
if (!$res) {
$this->session_delete($id, true);
return false;
}
}
return $success;
}
+ /**
+ * 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
+ *
+ * @param array $param List parameters
+ *
+ * @return array Sessions list
+ */
+ public function sessions_list($param = array())
+ {
+ $db = $this->rc->get_dbh();
+ $sessions = array();
+
+ // Get sessions 1. (user has access)
+ $result = $db->query("SELECT s.`id`, s.`uri`, s.`owner`, s.`owner_name`"
+ . " FROM `{$this->sessions_table}` s"
+ . " WHERE 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)) {
+ $path = $this->uri2path($row['uri']);
+
+ if (!$path) {
+ // likely user has no access to the file, but has been invited,
+ // extract filename from the URI
+ $path = parse_url($row['uri'], PHP_URL_PATH);
+ $path = explode('/', $path);
+ $path = $path[count($path) - 1];
+ }
+
+ if ($path) {
+ $sessions[$row['id']] = $this->session_info_parse($row, $path);
+ // For performance reasons we don't want to fetch info of every file
+ // on the list. As we support only ODT files here...
+ $sessions[$row['id']]['type'] = 'application/vnd.oasis.opendocument.text';
+ }
+ }
+
+ // TODO: Get sessions 2. (user is eligible)
+ // - get list of folders and find sessions for files in these locations
+
+ // 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;
+ }
+
/**
* 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->fromunixtime($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 `changed`");
+ . "$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);
}
// Update Manticore 'access' array
if ($status == self::STATUS_INVITED) {
$req = $this->get_request();
$res = $req->editor_add($session_id, $user, file_manticore_api::ACCESS_WRITE);
if (!$res) {
throw new Exception("Failed to create an invitation.", 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 (?, ?, ?, ?, ?, " . $db->now() . ")",
$session_id, $user, $user_name, $status, $comment ?: '');
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
*
* @throws Exception
*/
public function invitation_delete($session_id, $user)
{
$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 Manticore 'access' array
$req = $this->get_request();
$res = $req->editor_delete($session_id, $user);
if (!$res) {
throw new Exception("Failed to remove 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` = " . $db->now()
. " WHERE `session_id` = ? AND `user` = ?",
$status, $comment ?: '', $session_id, $user);
if (!$db->affected_rows($result)) {
throw new Exception("Failed to update an invitation status.", file_api_core::ERROR_CODE);
}
// Update Manticore 'access' array if an owner accepted an invitation request
if ($status == self::STATUS_ACCEPTED_OWNER) {
$req = $this->get_request();
$res = $req->editor_add($session_id, $user, file_manticore_api::ACCESS_WRITE);
if (!$res) {
throw new Exception("Failed to update an invitation status.", file_api_core::ERROR_CODE);
}
}
}
/**
* Parse session info data
*/
protected function session_info_parse($record, $path = null, $filter = array())
{
-/*
- if (is_string($data) && !empty($data)) {
- $data = json_decode($data, true);
- }
-*/
$session = array();
$fields = array('id', 'uri', 'owner', 'owner_name');
foreach ($fields as $field) {
if (isset($record[$field])) {
$session[$field] = $record[$field];
}
}
if ($path) {
$session['file'] = $path;
}
// @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;
}
/**
* Generate URI of Manticore editing session
*/
protected function frame_uri($id)
{
$base_url = rtrim($this->rc->config->get('fileapi_manticore'), ' /');
return $base_url . '/document/' . $id . '/' . $_SESSION['manticore_token'];
}
/**
* Get file path from the URI
*/
protected function uri2path($uri)
{
$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
}
}
}
/**
* Initialize Manticore API request handler
*/
protected function get_request()
{
if (!$this->request) {
$uri = rcube_utils::resolve_url($this->rc->config->get('fileapi_manticore'));
$this->request = new file_manticore_api($uri);
// Use stored session token, check if it's still valid
if ($_SESSION['manticore_token']) {
$is_valid = $this->request->set_session_token($_SESSION['manticore_token'], true);
if ($is_valid) {
return $this->request;
}
}
$backend = $this->api->get_backend();
$auth = $backend->auth_info();
$_SESSION['manticore_token'] = $this->request->login($auth['username'], $auth['password']);
if (empty($_SESSION['manticore_token'])) {
throw new Exception("Unable to login to Manticore server.", file_api_core::ERROR_CODE);
}
}
return $this->request;
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Sep 14, 11:13 PM (1 d, 1 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
287430
Default Alt Text
(51 KB)

Event Timeline