Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F2533598
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Size
73 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/api/document.php b/lib/api/document.php
index 8886a38..2f22d29 100644
--- a/lib/api/document.php
+++ b/lib/api/document.php
@@ -1,211 +1,242 @@
<?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'];
}
- // Sessions and invitations management
- if (strpos($this->args['method'], 'document_') === 0) {
+ // Invitation notifications
+ if ($this->args['method'] == 'invitations') {
+ return $this->invitations();
+ }
+ // Session and invitations management
+ else 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':
return $this->document_delete($this->args['id']);
case 'document_invite':
return $this->document_invite($this->args['id']);
// case 'document_request':
// case 'document_decline':
// case 'document_accept':
case 'document_remove':
return $this->document_remove($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);
+ }
+
+ $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,
+ );
+ }
+
/**
* 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'];
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);
$result[] = array(
'session_id' => $id,
'user' => $user['user'],
// 'name' => $user['name'],
'status' => file_manticore::STATUS_INVITED,
);
}
}
return array(
'list' => $result,
);
}
/**
* Remove a session participant(s)
*/
protected function document_remove($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 e5706ac..f9f9a8c 100644
--- a/lib/file_api.php
+++ b/lib/file_api.php
@@ -1,442 +1,442 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2013, 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->session_validate()) {
$this->session->destroy(session_id());
$this->session->regenerate_id(false);
if ($username = $this->authenticate()) {
$_SESSION['user'] = $username;
$_SESSION['time'] = time();
$_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)) {
session_start();
return false;
}
session_id($sess_id);
session_start();
if (empty($_SESSION['user'])) {
return false;
}
$timeout = $this->config->get('session_lifetime', 0) * 60;
if ($timeout && $_SESSION['time'] && $_SESSION['time'] < time() - $timeout) {
return false;
}
// update session time
$_SESSION['time'] = time();
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 (strpos($request, 'document_') === 0) {
+ if (preg_match('/^(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 7f10db3..46b04e7 100644
--- a/lib/file_manticore.php
+++ b/lib/file_manticore.php
@@ -1,567 +1,624 @@
<?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 $sessions_table = 'chwala_sessions';
protected $invitations_table = 'chwala_invitations';
const STATUS_INVITED = 'invited';
const STATUS_REQUESTED = 'requested';
const STATUS_ACCEPTED = 'accepted';
const STATUS_DECLINED = 'declined';
/**
* Class constructor
*
* @param file_api Chwala API app instance
*/
public function __construct($api)
{
$this->rc = rcube::get_instance();
$this->api = $api;
$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)
{
list($driver, $path) = $this->api->get_driver($file);
$backend = $this->api->get_backend();
$uri = $driver->path2uri($path);
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'] != $_SESSION['user']) {
// check if the user was invited
- $invitations = $this->invitations_list($session_id);
+ $invitations = $this->invitations_find(array('session_id' => $session_id, 'user' => $_SESSION['user']));
$states = array(self::STATUS_DECLINED, self::STATUS_REQUESTED);
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, $_SESSION['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 {
// @TODO: to prevent from creating a new sessions for the same file+user
// (e.g. when user uses F5 to refresh the page), we should check
// if such a session exist
$session_id = rcube_utils::bin2ascii(md5(time() . $uri, true));
$data = array();
$owner = $_SESSION['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);
}
}
return $this->frame_uri($session_id);
}
/**
* Get file path (not URI) from session.
*
* @param string $id Session ID
*
* @return string File path
* @throws Exception
*/
public function session_file($id)
{
$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)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
// @TODO: check permissions to the session
return $path;
}
/**
* Get editing session info
*
* @param string $id Session identifier
* @param bool $with_invitations Return invitations list
*/
public function session_info($id, $with_invitations = false)
{
$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);
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', '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_list();
+ $invitations = $this->invitations_find(array('user' => $_SESSION['user']));
$states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED);
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, $_SESSION['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)
{
// 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`, `data`) VALUES (?, ?, ?, ?)",
$id, $uri, $owner, 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 invitations for current user
- *
+ * 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($session_id = null)
+ public function invitations_list($filter = array())
{
- $invitations = array();
- $db = $this->rc->get_dbh();
- $result = $db->query("SELECT * FROM `{$this->invitations_table}`"
- . " WHERE `user`= ?"
- . ($session_id ? " AND `session_id` = " . $db->quote($session_id) : "")
- . " ORDER BY `changed`", $_SESSION['user']);
+ $filter['user'] = $_SESSION['user'];
- while ($row = $db->fetch_assoc($result)) {
- $invitations[] = $row;
+ // list of invitations to the user or requested by him
+ $result = $this->invitations_find($filter, true);
+
+ unset($filter['user']);
+ $filter['owner'] = $_SESSION['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 $invitations;
+ return $result;
}
/**
- * Find invitations for specified session_id
+ * 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)
+ public function invitations_find($filter, $extended = false)
{
- $invitations = array();
- $db = $this->rc->get_dbh();
+ $db = $this->rc->get_dbh();
+ $query = '';
+ $select = "i.*";
foreach ($filter as $column => $value) {
- $filter[$column] = "`$column` = " . $db->quote($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);
+ }
}
- $where = implode(' AND ', $filter);
- $result = $db->query("SELECT * FROM `{$this->invitations_table}`"
- . " WHERE $where ORDER BY `changed`");
+ if ($extended) {
+ $select .= ", s.`uri`";
+ $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`");
+
+ 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;
+ unset($row['uri']);
+ }
+
$invitations[] = $row;
}
return $invitations;
}
/**
* Create an invitation
*
* @param string $session_id Document session identifier
* @param string $user User identifier
* @param string $status Invitation status (invited, requested)
*
* @throws Exception
*/
public function invitation_create($session_id, $user, $status = 'invited')
{
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'] != $_SESSION['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);
}
}
// insert invitation
$db = $this->rc->get_dbh();
$result = $db->query("INSERT INTO `{$this->invitations_table}`"
. " (`session_id`, `user`, `status`, `changed`)"
. " VALUES (?, ?, ?, " . $db->now() . ")",
$session_id, $user, $status);
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, $_SESSION['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
* @param string $status Invitation status (accepted, declined)
*
* @throws Exception
*/
public function invitation_update($session_id, $user, $status)
{
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 != $_SESSION['user'] && $session['owner'] != $_SESSION['user']) {
throw new Exception("No permission to update an invitation.", file_api_core::ERROR_CODE);
}
$db = $this->rc->get_dbh();
$result = $db->query("UPDATE `{$this->invitations_table}`"
. " SET `status` = ?, `changed` = " . $db->now()
. " WHERE `session_id` = ? AND `user` = ?",
$status, $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 && $_SESSION['user'] == $session['owner']) {
// @todo
}
}
/**
* 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');
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'] == $_SESSION['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
}
}
}
/**
* Return Manticore user/session info
*/
public function user_info()
{
$req = $this->get_request();
$res = $req->get('api/users/me');
return $res->get();
}
/**
* 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;
}
}
diff --git a/public_html/js/files_api.js b/public_html/js/files_api.js
index 8c9501a..aa7c0ae 100644
--- a/public_html/js/files_api.js
+++ b/public_html/js/files_api.js
@@ -1,927 +1,993 @@
-/*
+/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
- | Copyright (C) 2012-2013, Kolab Systems AG |
+ | 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> |
+--------------------------------------------------------------------------+
*/
function files_api()
{
var ref = this;
// default config
this.sessions = {};
this.translations = {};
this.env = {
url: 'api/',
directory_separator: '/',
resources_dir: 'resources'
};
/*********************************************************/
/********* Basic utilities *********/
/*********************************************************/
// set environment variable(s)
this.set_env = function(p, value)
{
if (p != null && typeof p === 'object' && !value)
for (var n in p)
this.env[n] = p[n];
else
this.env[p] = value;
};
// add a localized label(s) to the client environment
this.tdef = function(p, value)
{
if (typeof p == 'string')
this.translations[p] = value;
else if (typeof p == 'object')
$.extend(this.translations, p);
};
// return a localized string
this.t = function(label)
{
if (this.translations[label])
return this.translations[label];
else
return label;
};
// print a message into browser console
this.log = function(msg)
{
if (window.console && console.log)
console.log(msg);
};
/********************************************************/
/********* Remote request methods *********/
/********************************************************/
// send a http POST request to the API service
this.post = function(action, data, func)
{
var url = this.env.url + '?method=' + action;
if (!func) func = 'response';
this.set_request_time();
return $.ajax({
type: 'POST', url: url, data: JSON.stringify(data), dataType: 'json',
contentType: 'application/json; charset=utf-8',
- success: function(response) { ref[func](response); },
+ success: function(response) { if (typeof func == 'function') func(response); else ref[func](response); },
error: function(o, status, err) { ref.http_error(o, status, err); },
cache: false,
beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', ref.env.token); }
});
};
// send a http GET request to the API service
this.get = function(action, data, func)
{
var url = this.env.url;
if (!func) func = 'response';
this.set_request_time();
data.method = action;
return $.ajax({
type: 'GET', url: url, data: data, dataType: 'json',
- success: function(response) { ref[func](response); },
+ success: function(response) { if (typeof func == 'function') func(response); else ref[func](response); },
error: function(o, status, err) { ref.http_error(o, status, err); },
cache: false,
beforeSend: function(xmlhttp) { xmlhttp.setRequestHeader('X-Session-Token', ref.env.token); }
});
};
// send request with auto-selection of POST/GET method
this.request = function(action, data, func)
{
// Use POST for modification actions with probable big request size
var method = /_(create|delete|move|copy|update|auth|subscribe|unsubscribe|invite|decline|request|accept|remove)$/.test(action) ? 'post' : 'get';
return this[method](action, data, func);
};
// handle HTTP request errors
this.http_error = function(request, status, err)
{
var errmsg = request.statusText;
this.set_busy(false);
request.abort();
if (request.status && errmsg)
this.display_message(this.t('servererror') + ' (' + errmsg + ')', 'error');
};
this.response = function(response)
{
this.update_request_time();
this.set_busy(false);
return this.response_parse(response);
};
this.response_parse = function(response)
{
if (!response || response.status != 'OK') {
// Logout on invalid-session error
if (response && response.code == 403)
this.logout(response);
else
this.display_message(response && response.reason ? response.reason : this.t('servererror'), 'error');
return false;
}
return true;
};
/*********************************************************/
/********* Utilities *********/
/*********************************************************/
// Called on "session expired" session
this.logout = function(response) {};
// set state
this.set_busy = function(state, message) {};
// displays error message
this.display_message = function(label, type) {};
// called when a request timed out
this.request_timed_out = function() {};
// called on start of the request
this.set_request_time = function() {};
// called on request response
this.update_request_time = function() {};
/*********************************************************/
/********* Helpers *********/
/*********************************************************/
// compose a valid url with the given parameters
this.url = function(action, query)
{
var k, param = {},
querystring = typeof query === 'string' ? '&' + query : '';
if (typeof action !== 'string')
query = action;
else if (!query || typeof query !== 'object')
query = {};
// overwrite task name
if (action)
query.method = action;
// remove undefined values
for (k in query) {
if (query[k] !== undefined && query[k] !== null)
param[k] = query[k];
}
return '?' + $.param(param) + querystring;
};
// fill folder selector with options
this.folder_select_element = function(select, params)
{
var options = [],
selected = params && params.selected ? params.selected : this.env.folder;
if (params && params.empty)
options.push($('<option>').val('').text('---'));
$.each(this.env.folders, function(i, f) {
var n, name = escapeHTML(f.name);
// skip read-only folders
if (params && params.writable && (f.readonly || f.virtual)) {
var folder, found = false, prefix = i + ref.env.directory_separator;
// for virtual folders check if there's any writable subfolder
for (n in ref.env.folders) {
if (n.indexOf(prefix) === 0) {
folder = ref.env.folders[n];
if (!folder.virtual && !folder.readonly) {
found = true;
break;
}
}
}
if (!found)
return;
}
for (n=0; n<f.depth; n++)
name = ' ' + name;
options.push($('<option>').val(i).html(name));
});
select.empty().append(options);
if (selected)
select.val(selected);
};
// Folder list parser, converts it into structure
this.folder_list_parse = function(list, num, subscribed)
{
var i, n, j, items, items_len, f, tmp, folder, readonly,
subs_support, subs_prefixes = {}, found,
separator = this.env.directory_separator,
len = list ? list.length : 0, folders = {};
if (!num) num = 1;
if (subscribed === undefined)
subscribed = true;
// prepare subscriptions support detection
if (len && this.env.caps) {
subs_support = !!this.env.caps.SUBSCRIPTIONS;
$.each(this.env.caps.MOUNTPOINTS || [], function(i, v) {
subs_prefixes[i] = !!v.SUBSCRIPTIONS;
});
}
for (i=0; i<len; i++) {
folder = list[i];
readonly = false;
// in extended format folder is an object
if (typeof folder !== 'string') {
readonly = folder.readonly;
folder = folder.folder;
}
items = folder.split(separator);
items_len = items.length;
for (n=0; n<items_len-1; n++) {
tmp = items.slice(0, n+1);
f = tmp.join(separator);
if (!folders[f])
folders[f] = {name: tmp.pop(), depth: n, id: 'f'+num++, virtual: 1};
}
folders[folder] = {
name: items.pop(),
depth: items_len-1,
id: 'f' + num++,
readonly: readonly
};
// set subscription flag, leave undefined if the source does not support subscriptions
found = false;
for (j in subs_prefixes) {
if (folder === j) {
// this is a mount point
found = true;
break;
}
if (folder.indexOf(j + separator) === 0) {
if (subs_prefixes[j])
folders[folder].subscribed = subscribed;
found = true;
break;
}
}
if (!found && subs_support)
folders[folder].subscribed = subscribed;
}
return folders;
};
// folder structure presentation (structure icons)
this.folder_list_tree = function(folders)
{
var i, n, diff, tree = [], folder;
for (i in folders) {
items = i.split(this.env.directory_separator);
items_len = items.length;
// skip root
if (items_len < 2) {
tree = [];
continue;
}
folders[i].tree = [1];
for (n=0; n<tree.length; n++) {
folder = tree[n];
diff = folders[folder].depth - (items_len - 1);
if (diff >= 0)
folders[folder].tree[diff] = folders[folder].tree[diff] ? folders[folder].tree[diff] + 2 : 2;
}
tree.push(i);
}
for (i in folders) {
if (tree = folders[i].tree) {
var html = '', divs = [];
for (n=0; n<folders[i].depth; n++) {
if (tree[n] > 2)
divs.push({'class': 'l3', width: 15});
else if (tree[n] > 1)
divs.push({'class': 'l2', width: 15});
else if (tree[n] > 0)
divs.push({'class': 'l1', width: 15});
// separator
else if (divs.length && !divs[divs.length-1]['class'])
divs[divs.length-1].width += 15;
else
divs.push({'class': null, width: 15});
}
for (n=divs.length-1; n>=0; n--) {
if (divs[n]['class'])
html += '<span class="tree '+divs[n]['class']+'" />';
else
html += '<span style="width:'+divs[n].width+'px" />';
}
if (html)
$('#' + folders[i].id + ' span.branch').html(html);
}
}
};
// Get editing sessions on the specified file
this.file_sessions = function(file)
{
var sessions = [], folder = this.file_path(file);
$.each(this.sessions[folder] || {}, function(session_id, session) {
if (session.file == file) {
session.id = session_id;
sessions.push(session);
}
});
return sessions;
};
// convert content-type string into class name
this.file_type_class = function(type)
{
if (!type)
return '';
type = type.replace(/[^a-z0-9]/g, '_');
return type;
};
// convert bytes into number with size unit
this.file_size = function(size)
{
if (size >= 1073741824)
return parseFloat(size/1073741824).toFixed(2) + ' GB';
if (size >= 1048576)
return parseFloat(size/1048576).toFixed(2) + ' MB';
if (size >= 1024)
return parseInt(size/1024) + ' kB';
return parseInt(size || 0) + ' B';
};
// Extract file name from full path
this.file_name = function(path)
{
var path = path.split(this.env.directory_separator);
return path.pop();
};
// Extract file path from full path
this.file_path = function(path)
{
var path = path.split(this.env.directory_separator);
path.pop();
return path.join(this.env.directory_separator);
};
// compare two sortable objects
this.sort_compare = function(data1, data2)
{
var key = this.env.sort_col || 'name';
if (key == 'mtime')
key = 'modified';
data1 = data1[key];
data2 = data2[key];
if (key == 'size' || key == 'modified')
// numeric comparison
return this.env.sort_reverse ? data2 - data1 : data1 - data2;
else {
// use Array.sort() for string comparison
var arr = [data1, data2];
arr.sort(function (a, b) {
// @TODO: use localeCompare() arguments for better results
return a.localeCompare(b);
});
if (this.env.sort_reverse)
arr.reverse();
return arr[0] === data2 ? 1 : -1;
}
};
// Checks if specified mimetype is supported natively by the browser (return 1)
// or can be displayed in the browser using File API viewer (return 2)
// or using Manticore - WebODF collaborative editor (return 4)
this.file_type_supported = function(type, capabilities)
{
var i, t, res = 0, regexps = [], img = 'jpg|jpeg|gif|bmp|png',
caps = this.env.browser_capabilities || {},
doc = /^application\/vnd.oasis.opendocument.(text)$/i;
// Manticore?
if (capabilities && capabilities.MANTICORE && doc.test(type))
res |= 4;
if (caps.tif)
img += '|tiff';
if ((new RegExp('^image/(' + img + ')$', 'i')).test(type))
res |= 1;
// prefer text viewer for any text type
if (/^text\/(?!(pdf|x-pdf))/i.test(type))
res |= 2;
if (caps.pdf) {
regexps.push(/^application\/(pdf|x-pdf|acrobat|vnd.pdf)/i);
regexps.push(/^text\/(pdf|x-pdf)/i);
}
if (caps.flash)
regexps.push(/^application\/x-shockwave-flash/i);
for (i in regexps)
if (regexps[i].test(type))
res |= 1;
for (i in navigator.mimeTypes) {
t = navigator.mimeTypes[i].type;
if (t == type && navigator.mimeTypes[i].enabledPlugin)
res |= 1;
}
// types with viewer support
if ($.inArray(type, this.env.supported_mimetypes) > -1)
res |= 2;
return res;
};
// Return browser capabilities
this.browser_capabilities = function()
{
var i, caps = [], ctypes = ['pdf', 'flash', 'tif'];
for (i in ctypes)
if (this.env.browser_capabilities[ctypes[i]])
caps.push(ctypes[i]);
return caps;
};
// Checks browser capabilities eg. PDF support, TIF support
this.browser_capabilities_check = function()
{
if (!this.env.browser_capabilities)
this.env.browser_capabilities = {};
if (this.env.browser_capabilities.pdf === undefined)
this.env.browser_capabilities.pdf = this.pdf_support_check();
if (this.env.browser_capabilities.flash === undefined)
this.env.browser_capabilities.flash = this.flash_support_check();
if (this.env.browser_capabilities.tif === undefined)
this.tif_support_check();
};
this.tif_support_check = function()
{
var img = new Image(), ref = this;
img.onload = function() { ref.env.browser_capabilities.tif = 1; };
img.onerror = function() { ref.env.browser_capabilities.tif = 0; };
img.src = this.env.resources_dir + '/blank.tif';
};
this.pdf_support_check = function()
{
var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/pdf"] : {},
plugins = navigator.plugins,
len = plugins.length,
regex = /Adobe Reader|PDF|Acrobat/i,
ref = this;
if (plugin && plugin.enabledPlugin)
return 1;
if (window.ActiveXObject) {
try {
if (axObj = new ActiveXObject("AcroPDF.PDF"))
return 1;
}
catch (e) {}
try {
if (axObj = new ActiveXObject("PDF.PdfCtrl"))
return 1;
}
catch (e) {}
}
for (i=0; i<len; i++) {
plugin = plugins[i];
if (typeof plugin === 'String') {
if (regex.test(plugin))
return 1;
}
else if (plugin.name && regex.test(plugin.name))
return 1;
}
return 0;
};
this.flash_support_check = function()
{
var plugin = navigator.mimeTypes ? navigator.mimeTypes["application/x-shockwave-flash"] : {};
if (plugin && plugin.enabledPlugin)
return 1;
if (window.ActiveXObject) {
try {
if (axObj = new ActiveXObject("ShockwaveFlash.ShockwaveFlash"))
return 1;
}
catch (e) {}
}
return 0;
};
// converts number of seconds into HH:MM:SS format
this.time_format = function(s)
{
s = parseInt(s);
if (s >= 60*60*24)
return '-';
return (new Date(1970, 1, 1, 0, 0, s, 0)).toTimeString().replace(/.*(\d{2}:\d{2}:\d{2}).*/, '$1');
};
// same as str.split(delimiter) but it ignores delimiters within quoted strings
this.explode_quoted_string = function(str, delimiter)
{
var result = [],
strlen = str.length,
q, p, i, chr, last;
for (q = p = i = 0; i < strlen; i++) {
chr = str.charAt(i);
if (chr == '"' && last != '\\') {
q = !q;
}
else if (!q && chr == delimiter) {
result.push(str.substring(p, i));
p = i + 1;
}
last = chr;
}
result.push(str.substr(p));
return result;
};
};
/**
* Class implementing Manticore Client API
*
* Configuration:
* iframe - manticore iframe element
* title_input - document title element
* export_menu - export formats list
* members_list - collaborators list
* photo_url - <img> src for a collaborator
* photo_default_url - default image of a collaborator
- * set_busy, display_message, hide_message, gettext - methods
+ *
+ * set_busy, display_message, hide_message, gettext - common methods
+ *
+ * api - Chwala files_api instance
+ * interval - how often to check for invitations in seconds (default: 60)
+ * owner - user identifier
+ * invitationMore - add "more" link into invitation notices
*/
function manticore_api(conf)
{
- var domain,
+ var domain, manticore,
locks = {},
callbacks = {},
members = {},
- self = this,
- manticore = conf.iframe.contentWindow;
-
- if (/^(https?:\/\/[^/]+)/i.test(conf.iframe.src))
- domain = RegExp.$1;
-
- // Register 'message' event to receive messages from Manticore iframe
- window.addEventListener('message', function(event) {
- if (event.source == manticore && event.origin == domain) {
- self.message_handler(event.data);
- }
- });
-
- // Bind for document title changes
- if (conf.title_input)
- $(conf.title_input).change(function() { self.set_title($(this).val()); });
+ self = this;
// Sets state
this.set_busy = function(state, message)
{
if (conf.set_busy)
return conf.set_busy(state, message);
};
// Displays error/notification message
- this.display_message = function(label, type)
+ this.display_message = function(label, type, is_txt)
{
if (conf.display_message)
- return conf.display_message(label, type);
+ return conf.display_message(label, type, is_txt, 10);
if (type == 'error')
- alert(this.gettext(label));
+ alert(is_txt ? label : this.gettext(label));
};
// Hides the error/notification message
this.hide_message = function(id)
{
if (conf.hide_message)
return conf.hide_message(id);
};
// Localization method
this.gettext = function(label)
{
if (conf.gettext)
return conf.gettext(label);
return label;
};
- // Display loading message
- this.init_lock = this.set_busy(true, 'loading');
-
// Handle messages from Manticore
this.message_handler = function(data)
{
var result;
console.log(data);
if (callbacks[data.id])
result = callbacks[data.id](data);
if (result !== false && data.name && conf[data.name])
result = conf[data.name](data);
delete callbacks[data.id];
if (locks[data.id]) {
this.set_busy(false);
this.hide_message(data.id);
delete locks[data.id];
}
if (result === false)
return;
switch (data.name) {
case 'ready':
this.ready();
break;
case 'titleChanged':
if (conf.title_input)
$(conf.title_input).val(data.value);
break;
case 'memberAdded':
// @TODO: display notification?
if (conf.members_list)
$(conf.members_list).append(this.member_item(data));
break;
case 'memberRemoved':
// @TODO: display notification?
if (conf.members_list) {
$('#' + members[data.memberId].id, conf.members_list).remove();
delete members[data.memberId];
}
break;
}
};
this.post = function(action, data, callback, lock_label)
{
if (!data) data = {};
if (lock_label) {
data.id = this.set_busy(true, this.gettext(lock_label));
locks[data.id] = true;
}
if (!data.id)
data.id = (new Date).getTime();
// make sure the id is not in use
while (callbacks[data.id])
data.id++;
data.name = action;
callbacks[data.id] = callback;
console.log(data);
manticore.postMessage(data, domain);
};
this.ready = function()
{
if (this.init_lock) {
this.set_busy(false);
this.hide_message(this.init_lock);
delete this.init_lock;
}
if (conf.export_menu)
this.export_menu(conf.export_menu);
if (conf.members_list)
this.get_members(function(data) {
var images = [], id = (new Date).getTime();
$.each(data.value || [], function() {
images.push(self.member_item(this, id++));
});
$(conf.members_list).html('').append(images);
});
if (conf.title_input)
this.get_title(function(data) {
$(conf.title_input).val(data.value);
});
};
// Save current document
this.save = function(callback)
{
this.post('actionSave', {}, callback, 'saving');
};
// Export/download current document
this.export = function(type, callback)
{
this.post('actionExport', {value: type}, callback);
};
// Get supported export formats and create content of menu element
this.export_menu = function(menu)
{
this.post('getExportFormats', {}, function(data) {
var items = [];
$.each(data.value || [], function(i, v) {
items.push($('<li>').attr({role: 'menuitem'}).append(
$('<a>').attr({href: '#', role: 'button', tabindex: 0, 'aria-disabled': false, 'class': 'active'})
.text(v.label).click(function() { self.export(v.format); })
));
});
$(menu).html('').append(items);
});
};
// Get document title
this.get_title = function(callback)
{
this.post('getTitle', {}, callback);
};
// Set document title
this.set_title = function(title, callback)
{
this.post('setTitle', {value: title}, callback);
};
// Get document session members
this.get_members = function(callback)
{
this.post('getMembers', {}, callback);
};
// Creates session member image element
this.member_item = function(member, id)
{
member.id = 'member' + (id || (new Date).getTime());
member.name = member.fullName + ' (' + member.email + ')';
members[member.memberId] = member;
var img = $('<img>').attr({title: member.name, id: member.id, 'class': 'photo', src: conf.photo_default_url})
.css({'border-color': member.color})
.text(name);
if (conf.photo_url) {
img.attr('src', conf.photo_url.replace(/%email/, urlencode(member.email)));
if (conf.photo_default_url)
img.error(function() { this.src = conf.photo_default_url; });
}
return img;
};
+
+ // track changes in invitations
+ this.track_invitations = function()
+ {
+ conf.api.request('invitations', {timestamp: this.invitations_timestamp || -1}, this.parse_invitations);
+ this.invitations_timeout = setTimeout(function() { self.track_invitations(); }, (conf.interval || 60) * 1000);
+ };
+
+ // parse 'invitations' response
+ this.parse_invitations = function(response)
+ {
+ if (!conf.api.response(response) || !response.result)
+ return;
+
+ var invitation_change = function(invitation, suffix) {
+ var msg = self.gettext(invitation.status + 'notice' + (suffix || ''));
+
+ msg = msg.replace('$user', invitation.user).replace('$file', invitation.filename);
+
+ if (conf.invitationMore)
+ msg = $('<div>').append($('<span>').text(msg))
+ .append($('<a>').text(self.gettext('more')).attr('id', invitation.id)).html();
+
+ self.display_message(msg, 'notice', true);
+
+ if (conf.invitationChange)
+ conf.invitationChange(invitation);
+ }
+
+ $.each(response.result.list || [], function(i, invitation) {
+ invitation.id = 'i' + (response.result.timestamp + i);
+ invitation.is_session_owner = invitation.user == conf.owner;
+
+ // display notifications
+ if (invitation.is_session_owner) {
+ // @todo: add Accept, Decline buttons for 'invited' invitations
+ if (invitation.status == 'invited' || invitation.status == 'declined') {
+ invitation_change(invitation);
+ }
+ }
+ else {
+ // @todo: add Accept, Decline buttons for 'requested' invitations
+ if (invitation.status == 'accepted' || invitation.status == 'declined' || invitation.status == 'requested') {
+ invitation_change(invitation, 'owner');
+ }
+ }
+ });
+
+ self.invitations_timestamp = response.result.timestamp;
+ };
+
+ if (!conf)
+ conf = {};
+
+ // Got Manticore iframe, use Client API
+ if (conf.iframe) {
+ manticore = conf.iframe.contentWindow;
+
+ if (/^(https?:\/\/[^/]+)/i.test(conf.iframe.src))
+ domain = RegExp.$1;
+
+ // Register 'message' event to receive messages from Manticore iframe
+ window.addEventListener('message', function(event) {
+ if (event.source == manticore && event.origin == domain) {
+ self.message_handler(event.data);
+ }
+ });
+
+ // Bind for document title changes
+ if (conf.title_input)
+ $(conf.title_input).change(function() { self.set_title($(this).val()); });
+
+ // Display loading message
+ this.init_lock = this.set_busy(true, 'loading');
+ }
+
+ if (conf.api)
+ this.track_invitations();
};
// Add escape() method to RegExp object
// http://dev.rubyonrails.org/changeset/7271
RegExp.escape = function(str)
{
return String(str).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1');
};
// define String's startsWith() method for old browsers
if (!String.prototype.startsWith) {
String.prototype.startsWith = function(search, position) {
position = position || 0;
return this.slice(position, search.length) === search;
};
};
// make a string URL safe (and compatible with PHP's rawurlencode())
function urlencode(str)
{
if (window.encodeURIComponent)
return encodeURIComponent(str).replace('*', '%2A');
return escape(str)
.replace('+', '%2B')
.replace('*', '%2A')
.replace('/', '%2F')
.replace('@', '%40');
};
function escapeHTML(str)
{
return str === undefined ? '' : String(str)
.replace(/&/g, '&')
.replace(/>/g, '>')
.replace(/</g, '<');
};
function object_is_empty(obj)
{
if (obj)
for (var i in obj)
if (i !== null)
return true;
return false;
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Feb 5, 7:05 PM (7 h, 13 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
427983
Default Alt Text
(73 KB)
Attached To
Mode
R26 chwala
Attached
Detach File
Event Timeline
Log In to Comment