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 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api_document extends file_api_common { /** * Request handler */ public function handle() { $method = $_SERVER['REQUEST_METHOD']; $this->args = $_GET; if ($method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) { $method = $_SERVER['HTTP_X_HTTP_METHOD']; } // Invitation notifications if ($this->args['method'] == 'invitations') { return $this->invitations(); } + + // Sessions list + if ($this->args['method'] == 'sessions') { + return $this->sessions(); + } + // Session and invitations management - 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 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ class file_api extends file_api_core { public $session; public $config; public $browser; public $output_type = file_api_core::OUTPUT_JSON; public function __construct() { $rcube = rcube::get_instance(); $rcube->add_shutdown_function(array($this, 'shutdown')); $this->config = $rcube->config; $this->session_init(); if ($_SESSION['env']) { $this->env = $_SESSION['env']; } $this->locale_init(); } /** * Process the request and dispatch it to the requested service */ public function run() { $this->request = strtolower($_GET['method']); // Check the session, authenticate the user if ($this->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 @@ | +--------------------------------------------------------------------------+ | Author: Aleksander Machniak | +--------------------------------------------------------------------------+ */ /** * 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; } }