Page MenuHomePhorge

No OneTemporary

Size
107 KB
Referenced Files
None
Subscribers
None
diff --git a/doc/SQL/mysql.initial.sql b/doc/SQL/mysql.initial.sql
index 810bff5..8d9dafd 100644
--- a/doc/SQL/mysql.initial.sql
+++ b/doc/SQL/mysql.initial.sql
@@ -1,38 +1,39 @@
CREATE TABLE IF NOT EXISTS `chwala_locks` (
`uri` varchar(512) BINARY NOT NULL,
`owner` varchar(256),
`timeout` integer unsigned,
`expires` datetime DEFAULT NULL,
`token` varchar(256),
`scope` tinyint,
`depth` tinyint,
INDEX `uri_index` (`uri`, `depth`),
INDEX `expires_index` (`expires`),
INDEX `token_index` (`token`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE IF NOT EXISTS `chwala_sessions` (
`id` varchar(40) BINARY NOT NULL,
`uri` varchar(1024) BINARY NOT NULL,
`owner` varchar(255) BINARY NOT NULL,
`owner_name` varchar(255) DEFAULT NULL,
`data` mediumtext,
+ `readonly` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
INDEX `uri_index` (`uri`(255)),
INDEX `owner` (`owner`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE IF NOT EXISTS `chwala_invitations` (
`session_id` varchar(40) BINARY NOT NULL,
`user` varchar(255) BINARY NOT NULL,
`user_name` varchar(255) DEFAULT NULL,
`status` varchar(16) NOT NULL,
`changed` datetime DEFAULT NULL,
`comment` mediumtext,
CONSTRAINT `session_id_fk_chwala_invitations` FOREIGN KEY (`session_id`)
REFERENCES `chwala_sessions`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
INDEX `session_id` (`session_id`),
UNIQUE INDEX `user_session_id` (`user`, `session_id`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
-INSERT INTO `system` (`name`, `value`) VALUES ('chwala-version', '2015110400');
+INSERT INTO `system` (`name`, `value`) VALUES ('chwala-version', '2016101700');
diff --git a/doc/SQL/mysql/2016101700.sql b/doc/SQL/mysql/2016101700.sql
new file mode 100644
index 0000000..9d5403c
--- /dev/null
+++ b/doc/SQL/mysql/2016101700.sql
@@ -0,0 +1 @@
+ALTER TABLE `chwala_sessions` ADD COLUMN `readonly` tinyint(1) NOT NULL DEFAULT 0;
diff --git a/doc/SQL/oracle.initial.sql b/doc/SQL/oracle.initial.sql
index 104839b..0451b6a 100644
--- a/doc/SQL/oracle.initial.sql
+++ b/doc/SQL/oracle.initial.sql
@@ -1,42 +1,43 @@
CREATE TABLE "chwala_locks" (
"uri" varchar(512) NOT NULL,
"owner" varchar(256),
"timeout" integer,
"expires" timestamp DEFAULT NULL,
"token" varchar(256),
"scope" smallint,
"depth" smallint
);
CREATE INDEX "uri_index" ON "chwala_locks" ("uri", "depth");
CREATE INDEX "expires_index" ON "chwala_locks" ("expires");
CREATE INDEX "token_index" ON "chwala_locks" ("token");
CREATE TABLE "chwala_sessions" (
"id" varchar(40) NOT NULL,
"uri" varchar(1024) NOT NULL,
"owner" varchar(255) NOT NULL,
"owner_name" varchar(255) DEFAULT NULL,
"data" long,
+ "readonly" smallint DEFAULT 0 NOT NULL,
PRIMARY KEY ("id")
);
CREATE INDEX "chwala_sessions_uri_idx" ON "chwala_sessions" ("uri");
CREATE INDEX "chwala_sessions_owner_idx" ON "chwala_sessions" ("owner");
CREATE TABLE "chwala_invitations" (
"session_id" varchar(40) NOT NULL
REFERENCES "chwala_sessions" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
"user" varchar(255) NOT NULL,
"user_name" varchar(255) DEFAULT NULL,
"status" varchar(16) NOT NULL,
"changed" timestamp DEFAULT NULL,
"comment" long
);
CREATE INDEX "chwala_invitations_session_id_idx" ON "chwala_invitations" ("session_id");
CREATE UNIQUE INDEX "chwala_invitations_user_idx" ON "chwala_invitations" ("user", "session_id");
-INSERT INTO "system" ("name", "value") VALUES ('chwala-version', '2015110400');
+INSERT INTO "system" ("name", "value") VALUES ('chwala-version', '2016101700');
diff --git a/doc/SQL/oracle/2016101700.sql b/doc/SQL/oracle/2016101700.sql
new file mode 100644
index 0000000..892f977
--- /dev/null
+++ b/doc/SQL/oracle/2016101700.sql
@@ -0,0 +1 @@
+ALTER TABLE "chwala_sessions" ADD COLUMN "readonly" smallint DEFAULT 0 NOT NULL;
diff --git a/lib/api/common.php b/lib/api/common.php
index b60bc66..f09eb35 100644
--- a/lib/api/common.php
+++ b/lib/api/common.php
@@ -1,182 +1,190 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2014, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class file_api_common
{
protected $api;
protected $rc;
protected $args = array();
public function __construct($api)
{
$this->rc = rcube::get_instance();
$this->api = $api;
}
/**
* Request handler
*/
public function handle()
{
// GET arguments
$this->args = &$_GET;
// POST arguments (JSON)
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$post = file_get_contents('php://input');
$this->args += (array) json_decode($post, true);
unset($post);
}
// disable script execution time limit, so we can handle big files
@set_time_limit(360);
}
/**
* File uploads handler
*/
protected function upload()
{
$files = array();
if (is_array($_FILES['file']['tmp_name'])) {
foreach ($_FILES['file']['tmp_name'] as $i => $filepath) {
if ($err = $_FILES['file']['error'][$i]) {
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$maxsize = ini_get('upload_max_filesize');
$maxsize = $this->show_bytes(parse_bytes($maxsize));
throw new Exception("Maximum file size ($maxsize) exceeded", file_api_core::ERROR_CODE);
}
throw new Exception("File upload failed", file_api_core::ERROR_CODE);
}
$files[] = array(
'path' => $filepath,
'name' => $_FILES['file']['name'][$i],
'size' => filesize($filepath),
'type' => rcube_mime::file_content_type($filepath, $_FILES['file']['name'][$i], $_FILES['file']['type']),
);
}
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// if filesize exceeds post_max_size then $_FILES array is empty,
if ($maxsize = ini_get('post_max_size')) {
$maxsize = $this->show_bytes(parse_bytes($maxsize));
throw new Exception("Maximum file size ($maxsize) exceeded", file_api_core::ERROR_CODE);
}
throw new Exception("File upload failed", file_api_core::ERROR_CODE);
}
return $files;
}
/**
* Return built-in viewer opbject for specified mimetype
*
* @return object Viewer object
*/
protected function find_viewer($mimetype)
{
- $dir = RCUBE_INSTALL_PATH . 'lib/viewers';
+ $dir = RCUBE_INSTALL_PATH . 'lib/viewers';
+ $files = array();
+ // First get viewers and sort by name to get priority
if ($handle = opendir($dir)) {
while (false !== ($file = readdir($handle))) {
if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) {
- include_once $dir . '/' . $file;
- $class = 'file_viewer_' . $matches[1];
- $viewer = new $class($this->api);
-
- if ($viewer->supports($mimetype)) {
- return $viewer;
- }
+ $files[$matches[1]] = $dir . '/' . $file;
}
}
closedir($handle);
}
+
+ ksort($files);
+
+ foreach ($files as $name => $file) {
+ include_once $file;
+ $class = 'file_viewer_' . $name;
+ $viewer = new $class($this->api);
+
+ if ($viewer->supports($mimetype)) {
+ return $viewer;
+ }
+ }
}
/**
* Parse driver metadata information
*/
protected function parse_metadata($metadata, $default = false)
{
if ($default) {
unset($metadata['form']);
$metadata['name'] .= ' (' . $this->api->translate('localstorage') . ')';
}
// localize form labels
foreach ($metadata['form'] as $key => $val) {
$label = $this->api->translate('form.' . $val);
if (strpos($label, 'form.') !== 0) {
$metadata['form'][$key] = $label;
}
}
return $metadata;
}
/**
* Get folder rights
*/
protected function folder_rights($folder)
{
list($driver, $path) = $this->api->get_driver($folder);
$rights = $driver->folder_rights($path);
$result = array();
$map = array(
file_storage::ACL_READ => 'read',
file_storage::ACL_WRITE => 'write',
);
foreach ($map as $key => $value) {
if ($rights & $key) {
$result[] = $value;
}
}
return $result;
}
/**
* Update manticore session on file/folder move
*/
protected function session_uri_update($from, $to, $is_folder = false)
{
// check Manticore support. Note: we don't use config->get('fileapi_manticore')
// here as it may be not properly set if backend driver wasn't initialized yet
$capabilities = $this->api->capabilities(false);
if (empty($capabilities['MANTICORE'])) {
return;
}
$manticore = new file_manticore($this->api);
$manticore->session_uri_update($from, $to, $is_folder);
}
}
diff --git a/lib/file_api_core.php b/lib/file_api_core.php
index 896e833..e51552d 100644
--- a/lib/file_api_core.php
+++ b/lib/file_api_core.php
@@ -1,399 +1,401 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2014, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class file_api_core extends file_locale
{
const API_VERSION = 2;
const ERROR_CODE = 500;
const ERROR_INVALID = 501;
const OUTPUT_JSON = 'application/json';
const OUTPUT_HTML = 'text/html';
public $env = array(
'date_format' => 'Y-m-d H:i',
'language' => 'en_US',
);
protected $app_name = 'Kolab File API';
protected $drivers = array();
protected $icache = array();
protected $backend;
/**
* Returns API version
*/
public function client_version()
{
return self::API_VERSION;
}
/**
* Initialise authentication/configuration backend class
*
* @return file_storage Main storage driver
*/
public function get_backend()
{
if ($this->backend) {
return $this->backend;
}
$rcube = rcube::get_instance();
$driver = $rcube->config->get('fileapi_backend', 'kolab');
$this->backend = $this->load_driver_object($driver);
// configure api
$this->backend->configure($this->env);
return $this->backend;
}
/**
* Return supported/enabled external storage instances
*
* @param bool $as_objects Return drivers as objects not config data
*
* @return array List of storage drivers
*/
public function get_drivers($as_objects = false)
{
$rcube = rcube::get_instance();
$enabled = $rcube->config->get('fileapi_drivers');
$preconf = $rcube->config->get('fileapi_sources');
$result = array();
$all = array();
$iRony = defined('KOLAB_DAV_ROOT');
if (!empty($enabled)) {
$backend = $this->get_backend();
$drivers = $backend->driver_list();
foreach ($drivers as $item) {
// Disable webdav sources/drivers in iRony that point to the
// same host to prevent infinite recursion
if ($iRony && $item['driver'] == 'webdav') {
$self_url = parse_url($_SERVER['SCRIPT_URI']);
$item_url = parse_url($item['host']);
if ($self_url['host'] == $item_url['host']) {
continue;
}
}
$all[] = $item['title'];
if ($item['enabled'] && in_array($item['driver'], (array) $enabled)) {
$result[] = $as_objects ? $this->get_driver_object($item) : $item;
}
}
}
if (empty($result) && !empty($preconf)) {
foreach ((array) $preconf as $title => $item) {
if (!in_array($title, $all)) {
$item['title'] = $title;
$item['admin'] = true;
$result[] = $as_objects ? $this->get_driver_object($item) : $item;
}
}
}
return $result;
}
/**
* Return driver for specified file/folder path
*
* @param string $path Folder/file path
*
* @return array Storage driver object, modified path, driver config
*/
public function get_driver($path)
{
$drivers = $this->get_drivers();
foreach ($drivers as $item) {
$prefix = $item['title'] . file_storage::SEPARATOR;
if ($path == $item['title'] || strpos($path, $prefix) === 0) {
$selected = $item;
break;
}
}
if (empty($selected)) {
return array($this->get_backend(), $path);
}
$path = substr($path, strlen($selected['title']) + 1);
return array($this->get_driver_object($selected), $path, $selected);
}
/**
* Initialize driver instance
*
* @param array $config Driver config
*
* @return file_storage Storage driver instance
*/
public function get_driver_object($config)
{
$key = $config['title'];
if (empty($this->drivers[$key])) {
$this->drivers[$key] = $driver = $this->load_driver_object($config['driver']);
if ($config['username'] == '%u') {
$backend = $this->get_backend();
$auth_info = $backend->auth_info();
$config['username'] = $auth_info['username'];
$config['password'] = $auth_info['password'];
}
else if (!empty($config['password']) && empty($config['admin']) && !empty($key)) {
$config['password'] = $this->decrypt($config['password']);
}
// configure api
$driver->configure(array_merge($config, $this->env), $key);
}
return $this->drivers[$key];
}
/**
* Loads a driver
*/
public function load_driver_object($name)
{
$class = $name . '_file_storage';
if (!class_exists($class, false)) {
$include_path = __DIR__ . "/drivers/$name" . PATH_SEPARATOR;
$include_path .= ini_get('include_path');
set_include_path($include_path);
}
return new $class;
}
/**
* Returns storage(s) capabilities
*
* @param bool $full Return all drivers' capabilities
*
* @return array Capabilities
*/
public function capabilities($full = true)
{
$rcube = rcube::get_instance();
$backend = $this->get_backend();
$caps = array();
// check support for upload progress
if (($progress_sec = $rcube->config->get('upload_progress'))
&& ini_get('apc.rfc1867') && function_exists('apc_fetch')
) {
$caps[file_storage::CAPS_PROGRESS_NAME] = ini_get('apc.rfc1867_name');
$caps[file_storage::CAPS_PROGRESS_TIME] = $progress_sec;
}
// get capabilities of main storage module
foreach ($backend->capabilities() as $name => $value) {
// skip disabled capabilities
if ($value !== false) {
$caps[$name] = $value;
}
}
// Manticore support
if ($rcube->config->get('fileapi_manticore')) {
$caps['MANTICORE'] = true;
}
// WOPI support
if ($rcube->config->get('fileapi_wopi_office')) {
$caps['WOPI'] = true;
}
if (!$full) {
return $caps;
}
// get capabilities of other drivers
$drivers = $this->get_drivers(true);
foreach ($drivers as $driver) {
if ($driver != $backend) {
$title = $driver->title();
foreach ($driver->capabilities() as $name => $value) {
// skip disabled capabilities
if ($value !== false) {
$caps['MOUNTPOINTS'][$title][$name] = $value;
}
}
}
}
return $caps;
}
/**
* Get user name from user identifier (email address) using LDAP lookup
*
* @param string $email User identifier
*
* @return string User name
*/
public function resolve_user($email)
{
$key = "user:$email";
// make sure Kolab backend is initialized so kolab_storage can be found
$this->get_backend();
// @todo: Move this into drivers
if ($this->icache[$key] === null
&& class_exists('kolab_storage')
&& ($ldap = kolab_storage::ldap())
) {
$user = $ldap->get_user_record($email, $_SESSION['imap_host']);
$this->icache[$key] = $user ?: false;
}
if ($this->icache[$key]) {
return $this->icache[$key]['displayname'] ?: $this->icache[$key]['name'];
}
}
/**
* Return mimetypes list supported by built-in viewers
*
* @return array List of mimetypes
*/
protected function supported_mimetypes()
{
$rcube = rcube::get_instance();
$mimetypes = array();
$mimetypes_c = array();
$dir = __DIR__ . '/viewers';
if ($handle = opendir($dir)) {
while (false !== ($file = readdir($handle))) {
if (preg_match('/^([a-z0-9_]+)\.php$/i', $file, $matches)) {
include_once $dir . '/' . $file;
$class = 'file_viewer_' . $matches[1];
$viewer = new $class($this);
- $mimetypes = array_merge($mimetypes, $viewer->supported_mimetypes());
+ if ($supported = $viewer->supported_mimetypes()) {
+ $mimetypes = array_merge($mimetypes, $supported);
+ }
}
}
closedir($handle);
}
// Here we return mimetypes supported for editing and creation of files
// @TODO: maybe move this to viewers
if ($rcube->config->get('fileapi_wopi_office')) {
$mimetypes_c['application/vnd.oasis.opendocument.text'] = array('ext' => 'odt');
$mimetypes_c['application/vnd.oasis.opendocument.presentation'] = array('ext' => 'odp');
$mimetypes_c['application/vnd.oasis.opendocument.spreadsheet'] = array('ext' => 'ods');
}
else if ($rcube->config->get('fileapi_manticore')) {
$mimetypes_c['application/vnd.oasis.opendocument.text'] = array('ext' => 'odt');
}
$mimetypes_c['text/plain'] = array('ext' => 'txt');
$mimetypes_c['text/html'] = array('ext' => 'html');
foreach (array_keys($mimetypes_c) as $type) {
list ($app, $label) = explode('/', $type);
$label = preg_replace('/[^a-z]/', '', $label);
$mimetypes_c[$type]['label'] = $this->translate('type.' . $label);
}
return array(
'view' => $mimetypes,
'edit' => $mimetypes_c,
);
}
/**
* Encrypts data with current user password
*
* @param string $str A string to encrypt
*
* @return string Encrypted string (and base64-encoded)
*/
public function encrypt($str)
{
$rcube = rcube::get_instance();
$key = $this->get_crypto_key();
return $rcube->encrypt($str, $key, true);
}
/**
* Decrypts data encrypted with encrypt() method
*
* @param string $str Encrypted string (base64-encoded)
*
* @return string Decrypted string
*/
public function decrypt($str)
{
$rcube = rcube::get_instance();
$key = $this->get_crypto_key();
return $rcube->decrypt($str, $key, true);
}
/**
* Set encryption password
*/
protected function get_crypto_key()
{
$key = 'chwala_crypto_key';
$rcube = rcube::get_instance();
$backend = $this->get_backend();
$user = $backend->auth_info();
$password = $user['password'] . $user['username'];
// encryption password must be 24 characters, no less, no more
if (($len = strlen($password)) > 24) {
$password = substr($password, 0, 24);
}
else {
$password = $password . substr($rcube->config->get('des_key'), 0, 24 - $len);
}
$rcube->config->set($key, $password);
return $key;
}
}
diff --git a/lib/file_document.php b/lib/file_document.php
index 4229d04..dc9199f 100644
--- a/lib/file_document.php
+++ b/lib/file_document.php
@@ -1,774 +1,781 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2016, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Document editing sessions handling
*/
class file_document
{
protected $api;
protected $rc;
protected $user;
protected $sessions_table = 'chwala_sessions';
protected $invitations_table = 'chwala_invitations';
protected $icache = array();
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 &$mimetype File type
* @param string &$session_id Optional session ID to join to
+ * @param string $readonly Create readonly (one-time) session
*
* @return string An URI for specified file/session
* @throws Exception
*/
- public function session_start($file, &$mimetype, &$session_id = null)
+ public function session_start($file, &$mimetype, &$session_id = null, $readonly = false)
{
if ($file !== null) {
$uri = $this->path2uri($file, $driver);
}
$backend = $this->api->get_backend();
if ($session_id) {
$session = $this->session_info($session_id);
if (empty($session)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
// check session ownership
if ($session['owner'] != $this->user) {
// check if the user was invited
$invitations = $this->invitations_find(array('session_id' => $session_id, 'user' => $this->user));
$states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
if (empty($invitations) || !in_array($invitations[0]['status'], $states)) {
throw new Exception("No permission to join the editing session.", file_api_core::ERROR_CODE);
}
// automatically accept the invitation, if not done yet
if ($invitations[0]['status'] == self::STATUS_INVITED) {
$this->invitation_update($session_id, $this->user, self::STATUS_ACCEPTED);
}
}
$mimetype = $session['type'];
}
else if (!empty($uri)) {
// To prevent from creating new sessions for the same file+user
// (e.g. when user uses F5 to refresh the page), we check first
// if such a session exist and continue with it
$db = $this->rc->get_dbh();
- $res = $db->query("SELECT `id` FROM `{$this->sessions_table}`"
- . " WHERE `owner` = ? AND `uri` = ?", $this->user, $uri);
- if ($row = $db->fetch_assoc($res)) {
- $session_id = $row['id'];
- $res = true;
+ if (!$readonly) {
+ $res = $db->query("SELECT `id` FROM `{$this->sessions_table}`"
+ . " WHERE `owner` = ? AND `uri` = ? AND `readonly` = 0",
+ $this->user, $uri);
+
+ if ($row = $db->fetch_assoc($res)) {
+ $session_id = $row['id'];
+ $res = true;
+ }
}
- else if (!$db->is_error($res)) {
+
+ if (empty($res) && !$db->is_error($res)) {
$session_id = rcube_utils::bin2ascii(md5(time() . $uri, true));
$data = array('type' => $mimetype);
$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);
+ $res = $this->session_create($session_id, $uri, $owner, $data, $readonly);
}
if (!$res) {
throw new Exception("Failed creating document editing session", file_api_core::ERROR_CODE);
}
}
else {
throw new Exception("Failed creating document editing session (unknown file)", file_api_core::ERROR_CODE);
}
// Implementations should return real URI
return '';
}
/**
* Get file path (not URI) from session.
*
* @param string $id Session ID
* @param bool $join_mode Throw exception only if session does not exist
*
* @return 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
$uri = trim($this->path2uri($path), '/') . '/';
// get existing sessions
$sessions = array();
$filter = array('file', 'owner', 'owner_name', 'is_owner');
$db = $this->rc->get_dbh();
$result = $db->query("SELECT * FROM `{$this->sessions_table}`"
- . " WHERE `uri` LIKE '" . $db->escape($uri) . "%'");
+ . " WHERE `readonly` = 0 AND `uri` LIKE '" . $db->escape($uri) . "%'");
while ($row = $db->fetch_assoc($result)) {
if ($path = $this->uri2path($row['uri'])) {
$sessions[$row['id']] = $this->session_info_parse($row, $path, $filter);
}
}
// set 'is_invited' flag
if ($invitations && !empty($sessions)) {
$invitations = $this->invitations_find(array('user' => $this->user));
$states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
foreach ($invitations as $invitation) {
if (!empty($sessions[$invitation['session_id']]) && in_array($invitation['status'], $states)) {
$sessions[$invitation['session_id']]['is_invited'] = true;
}
}
}
return $sessions;
}
/**
* Delete editing session (only owner can do that)
*
* @param string $id Session identifier
*/
public function session_delete($id)
{
$db = $this->rc->get_dbh();
$result = $db->query("DELETE FROM `{$this->sessions_table}`"
. " WHERE `id` = ? AND `owner` = ?",
$id, $this->user);
return $db->affected_rows($result) > 0;
}
/**
* Create editing session
*/
- protected function session_create($id, $uri, $owner, $data)
+ protected function session_create($id, $uri, $owner, $data, $readonly = false)
{
// get user name
$owner_name = $this->api->resolve_user($owner) ?: '';
$db = $this->rc->get_dbh();
$result = $db->query("INSERT INTO `{$this->sessions_table}`"
- . " (`id`, `uri`, `owner`, `owner_name`, `data`)"
- . " VALUES (?, ?, ?, ?, ?)",
- $id, $uri, $owner, $owner_name, json_encode($data));
+ . " (`id`, `uri`, `owner`, `owner_name`, `data`, `readonly`)"
+ . " VALUES (?, ?, ?, ?, ?, ?)",
+ $id, $uri, $owner, $owner_name, json_encode($data), intval($readonly));
$success = $db->affected_rows($result) > 0;
if ($success) {
// @TODO
}
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
+ * Note: Readonly sessions are ignored here.
*
* @param array $param List parameters
*
* @return array Sessions list
*/
public function sessions_list($param = array())
{
$db = $this->rc->get_dbh();
$sessions = array();
// 1. Get sessions user has access to
$result = $db->query("SELECT * FROM `{$this->sessions_table}` s"
- . " WHERE s.`owner` = ? OR s.`id` IN ("
+ . " WHERE s.`readonly` = 0 AND (s.`owner` = ? OR s.`id` IN ("
. "SELECT i.`session_id` FROM `{$this->invitations_table}` i"
. " WHERE i.`user` = ?"
- . ")",
+ . "))",
$this->user, $this->user);
if ($db->is_error($result)) {
throw new Exception("Internal error.", file_api_core::ERROR_CODE);
}
while ($row = $db->fetch_assoc($result)) {
if ($path = $this->uri2path($row['uri'], true)) {
$sessions[$row['id']] = $this->session_info_parse($row, $path);
}
}
// 2. Get sessions user is eligible
// - get list of all folder URIs and find sessions for files in these locations
// @FIXME: in corner cases (user has many folders) this may produce a big query,
// maybe fetching all sessions and then comparing with list of locations would be faster?
$uris = $this->all_folder_locations();
$where = array_map(function($uri) use ($db) {
return 's.`uri` LIKE ' . $db->quote(str_replace('%', '_', $uri) . '/%');
}, $uris);
$result = $db->query("SELECT * FROM `{$this->sessions_table}` s"
- . " WHERE " . join(' OR ', $where));
+ . " WHERE s.`readonly` = 0 AND (" . join(' OR ', $where) . ")");
if ($db->is_error($result)) {
throw new Exception("Internal error.", file_api_core::ERROR_CODE);
}
while ($row = $db->fetch_assoc($result)) {
if (empty($sessions[$row['id']])) {
// remove filename (and anything after it) so we have the folder URI
// to check if it's on the folders list we have
$uri = substr($row['uri'], 0, strrpos($row['uri'], '/'));
if (in_array($uri, $uris) && ($path = $this->uri2path($row['uri'], true))) {
$sessions[$row['id']] = $this->session_info_parse($row, $path);
}
}
}
// set 'is_invited' flag
if (!empty($sessions)) {
$invitations = $this->invitations_find(array('user' => $this->user));
$states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
foreach ($invitations as $invitation) {
if (!empty($sessions[$invitation['session_id']]) && in_array($invitation['status'], $states)) {
$sessions[$invitation['session_id']]['is_invited'] = true;
}
}
}
// Sorting
$sort = !empty($params['sort']) ? $params['sort'] : 'name';
$index = array();
if (in_array($sort, array('name', 'file', 'owner'))) {
foreach ($sessions as $key => $val) {
if ($sort == 'name' || $sort == 'file') {
$path = explode(file_storage::SEPARATOR, $val['file']);
$index[$key] = $path[count($path) - 1];
continue;
}
$index[$key] = $val[$sort];
}
array_multisort($index, SORT_ASC, SORT_LOCALE_STRING, $sessions);
}
if ($params['reverse']) {
$sessions = array_reverse($sessions, true);
}
return $sessions;
}
/**
* Retern extra editor parameters to post the the viewer iframe
*
* @param array $info File info
*
* @return array POST parameters
*/
public function editor_post_params($info)
{
return array();
}
/**
* Find invitations for current user. This will return all
* invitations related to the user including his sessions.
*
* @param array $filter Search filter (see self::invitations_find())
*
* @return array Invitations list
*/
public function invitations_list($filter = array())
{
$filter['user'] = $this->user;
// list of invitations to the user or requested by him
$result = $this->invitations_find($filter, true);
unset($filter['user']);
$filter['owner'] = $this->user;
// other invitations that belong to the sessions owned by the user
if ($other = $this->invitations_find($filter, true)) {
$result = array_merge($result, $other);
}
return $result;
}
/**
* Find invitations for specified filter
*
* @param array $filter Search filter (see self::invitations_find())
* - session_id: session identifier
* - timestamp: "changed > ?" filter
* - user: Invitation user identifier
* - owner: Session owner identifier
* @param bool $extended Return session file names
*
* @return array Invitations list
*/
public function invitations_find($filter, $extended = false)
{
$db = $this->rc->get_dbh();
$query = '';
$select = "i.*";
foreach ($filter as $column => $value) {
if ($column == 'timestamp') {
$where[] = "i.`changed` > " . $db->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 i.`changed`");
if ($db->is_error($result)) {
throw new Exception("Internal error.", file_api_core::ERROR_CODE);
}
$invitations = array();
while ($row = $db->fetch_assoc($result)) {
if ($extended) {
try {
// add unix-timestamp of the `changed` date to the result
$dt = new DateTime($row['changed']);
$row['timestamp'] = $dt->format('U');
}
catch(Exception $e) { }
// add filename to the result
$filename = parse_url($row['uri'], PHP_URL_PATH);
$filename = pathinfo($filename, PATHINFO_BASENAME);
$filename = rawurldecode($filename);
$row['filename'] = $filename;
if ($path = $this->uri2path($row['uri'])) {
$row['file'] = $path;
}
unset($row['uri']);
}
$invitations[] = $row;
}
return $invitations;
}
/**
* Create an invitation
*
* @param string $session_id Document session identifier
* @param string $user User identifier (use null for current user)
* @param string $status Invitation status (invited, requested)
* @param string $comment Invitation description/comment
* @param string &$user_name Optional user name
*
* @throws Exception
*/
public function invitation_create($session_id, $user, $status = 'invited', $comment = '', &$user_name = '')
{
if (empty($user)) {
$user = $this->user;
}
if ($status != self::STATUS_INVITED && $status != self::STATUS_REQUESTED) {
throw new Exception("Invalid invitation status.", file_api_core::ERROR_CODE);
}
// get session information
$session = $this->session_info($session_id);
if (empty($session)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
// check session ownership, only owner can create 'new' invitations
if ($status == self::STATUS_INVITED && $session['owner'] != $this->user) {
throw new Exception("No permission to create an invitation.", file_api_core::ERROR_CODE);
}
if ($session['owner'] == $user) {
throw new Exception("Not possible to create an invitation for the session creator.", file_api_core::ERROR_CODE);
}
// get user name
if (empty($user_name)) {
$user_name = $this->api->resolve_user($user) ?: '';
}
// insert invitation
$db = $this->rc->get_dbh();
$result = $db->query("INSERT INTO `{$this->invitations_table}`"
. " (`session_id`, `user`, `user_name`, `status`, `comment`, `changed`)"
. " VALUES (?, ?, ?, ?, ?, " . $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
* @param bool $local Remove invitation only from local database
*
* @throws Exception
*/
public function invitation_delete($session_id, $user, $local = false)
{
$db = $this->rc->get_dbh();
$result = $db->query("DELETE FROM `{$this->invitations_table}`"
. " WHERE `session_id` = ? AND `user` = ?"
. " AND EXISTS (SELECT 1 FROM `{$this->sessions_table}` WHERE `id` = ? AND `owner` = ?)",
$session_id, $user, $session_id, $this->user);
if (!$db->affected_rows($result)) {
throw new Exception("Failed to delete an invitation.", file_api_core::ERROR_CODE);
}
}
/**
* Update an invitation status
*
* @param string $session_id Session identifier
* @param string $user User identifier (use null for current user)
* @param string $status Invitation status (accepted, declined)
* @param string $comment Invitation description/comment
*
* @throws Exception
*/
public function invitation_update($session_id, $user, $status, $comment = '')
{
if (empty($user)) {
$user = $this->user;
}
if ($status != self::STATUS_ACCEPTED && $status != self::STATUS_DECLINED) {
throw new Exception("Invalid invitation status.", file_api_core::ERROR_CODE);
}
// get session information
$session = $this->session_info($session_id);
if (empty($session)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
// check session ownership
if ($user != $this->user && $session['owner'] != $this->user) {
throw new Exception("No permission to update an invitation.", file_api_core::ERROR_CODE);
}
if ($session['owner'] == $this->user) {
$status = $status . '-owner';
}
$db = $this->rc->get_dbh();
$result = $db->query("UPDATE `{$this->invitations_table}`"
. " SET `status` = ?, `comment` = ?, `changed` = " . $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 a session URI (e.g. on file/folder move)
*
* @param string $from Source file/folder path
* @param string $to Destination file/folder path
* @param bool $is_folder True if the path is a folder
*/
public function session_uri_update($from, $to, $is_folder = false)
{
$db = $this->rc->get_dbh();
// Resolve paths
$from = $this->path2uri($from);
$to = $this->path2uri($to);
if ($is_folder) {
$set = "`uri` = REPLACE(`uri`, " . $db->quote($from . '/') . ", " . $db->quote($to .'/') . ")";
$where = "`uri` LIKE " . $db->quote(str_replace('%', '_', $from) . '/%');
}
else {
$set = "`uri` = " . $db->quote($to);
$where = "`uri` = " . $db->quote($from);
}
$db->query("UPDATE `{$this->sessions_table}` SET $set WHERE $where");
}
/**
* Parse session info data
*/
protected function session_info_parse($record, $path = null, $filter = array())
{
$session = array();
$fields = array('id', 'uri', 'owner', 'owner_name');
foreach ($fields as $field) {
if (isset($record[$field])) {
$session[$field] = $record[$field];
}
}
if ($path) {
$session['file'] = $path;
}
if (!empty($record['data']) && (empty($filter) || in_array('type', $filter))) {
$data = json_decode($record['data'], true);
$session['type'] = $data['type'];
}
// @TODO: is_invited?, last_modified?
if ($session['owner'] == $this->user) {
$session['is_owner'] = true;
}
if (!empty($filter)) {
$session = array_intersect_key($session, array_flip($filter));
}
return $session;
}
/**
* Get file URI from path
*/
protected function path2uri($path, &$driver = null)
{
list($driver, $path) = $this->api->get_driver($path);
return $driver->path2uri($path);
}
/**
* Get file path from the URI
*/
protected function uri2path($uri, $use_fallback = false)
{
$backend = $this->api->get_backend();
try {
return $backend->uri2path($uri);
}
catch (Exception $e) {
// do nothing
}
foreach ($this->api->get_drivers(true) as $driver) {
try {
$path = $driver->uri2path($uri);
$title = $driver->title();
if ($title) {
$path = $title . file_storage::SEPARATOR . $path;
}
return $path;
}
catch (Exception $e) {
// do nothing
}
}
// likely user has no access to the file, but has been invited,
// extract filename from the URI
if ($use_fallback && $uri) {
$path = parse_url($uri, PHP_URL_PATH);
$path = explode('/', $path);
$path = $path[count($path) - 1];
return $path;
}
}
/**
* Get URI of all user folders (with shared locations)
*/
protected function all_folder_locations()
{
$locations = array();
foreach (array_merge(array($this->api->get_backend()), $this->api->get_drivers(true)) as $driver) {
// Performance optimization: We're interested here in shared folders,
// Kolab is the only driver that currently supports them, ignore others
if (get_class($driver) != 'kolab_file_storage') {
continue;
}
try {
foreach ($driver->folder_list() as $folder) {
if ($uri = $driver->path2uri($folder)) {
$locations[] = $uri;
}
}
}
catch (Exception $e) {
// do nothing
}
}
return $locations;
}
}
diff --git a/lib/file_manticore.php b/lib/file_manticore.php
index a804ba7..55552a5 100644
--- a/lib/file_manticore.php
+++ b/lib/file_manticore.php
@@ -1,223 +1,224 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2016, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Document editing sessions handling (Manticore)
*/
class file_manticore extends file_document
{
protected $request;
/**
* Return viewer URI for specified file/session. This creates
* a new collaborative editing session when needed.
*
* @param string $file File path
* @param string &$mimetype File type
* @param string &$session_id Optional session ID to join to
+ * @param string $readonly Create readonly (one-time) session
*
* @return string Manticore URI
* @throws Exception
*/
- public function session_start($file, &$mimetype, &$session_id = null)
+ public function session_start($file, &$mimetype, &$session_id = null, $readonly = false)
{
- parent::session_start($file, $mimetype, $session_id);
+ parent::session_start($file, $mimetype, $session_id, $readonly);
// authenticate to Manticore, we need auth token for frame_uri
if (empty($_SESSION['manticore_token'])) {
$this->get_request();
}
// @TODO: make sure the session exists in Manticore?
return $this->frame_uri($session_id);
}
/**
* 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)
{
$success = parent::session_delete($id, $local);
// 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)
+ protected function session_create($id, $uri, $owner, $data, $readonly = false)
{
- $success = parent::session_create($id, $uri, $owner, $data);
+ $success = parent::session_create($id, $uri, $owner, $data, $readonly);
// 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;
}
/**
* 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 = '')
{
parent::invitation_create($session_id, $user, $status, $comment, $user_name);
// Update Manticore 'access' array
if ($status == file_document::STATUS_INVITED) {
$req = $this->get_request();
$res = $req->editor_add($session_id, $user, file_manticore_api::ACCESS_WRITE);
if (!$res) {
$this->invitation_delete($session_id, $user, true);
throw new Exception("Failed to create an invitation.", file_api_core::ERROR_CODE);
}
}
}
/**
* Delete an invitation (only session owner can do that)
*
* @param string $session_id Session identifier
* @param string $user User identifier
* @param bool $local Remove invitation only from local database
*
* @throws Exception
*/
public function invitation_delete($session_id, $user, $local = false)
{
parent::invitation_delete($session_id, $user, $local);
// Update Manticore 'access' array
if (!$local) {
$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 = '')
{
parent::invitation_update($session_id, $user, $status, $comment);
// Update Manticore 'access' array if an owner accepted an invitation request
if ($status == file_document::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);
}
}
}
/**
* 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'];
}
/**
* 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/lib/file_wopi.php b/lib/file_wopi.php
index 3d73e53..7ac6f3d 100644
--- a/lib/file_wopi.php
+++ b/lib/file_wopi.php
@@ -1,267 +1,272 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2016, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Document editing sessions handling (WOPI)
*/
class file_wopi extends file_document
{
protected $cache;
/**
* Return viewer URI for specified file/session. This creates
* a new collaborative editing session when needed.
*
* @param string $file File path
* @param string &$mimetype File type
* @param string &$session_id Optional session ID to join to
+ * @param string $readonly Create readonly (one-time) session
*
* @return string WOPI URI for specified document
* @throws Exception
*/
- public function session_start($file, &$mimetype, &$session_id = null)
+ public function session_start($file, &$mimetype, &$session_id = null, $readonly = false)
{
- parent::session_start($file, $mimetype, $session_id);
+ parent::session_start($file, $mimetype, $session_id, $readonly);
if ($session_id) {
// Create Chwala session for use as WOPI access_token
// This session will have access to this one document session only
$keys = array('language', 'user_id', 'user', 'username', 'password',
'storage_host', 'storage_port', 'storage_ssl');
$data = array_intersect_key($_SESSION, array_flip($keys));
$data['document_session'] = $session_id;
$this->token = $this->api->session->create($data);
}
return $this->frame_uri($session_id, $mimetype);
}
/**
* Generate URI of WOPI editing session (WOPIsrc)
*/
protected function frame_uri($id, $mimetype)
{
$capabilities = $this->capabilities();
if (empty($capabilities) || empty($mimetype)) {
return;
}
$metadata = $capabilities[strtolower($mimetype)];
if (empty($metadata)) {
return;
}
$office_url = rtrim($metadata['urlsrc'], ' /?'); // collabora
$service_url = rtrim($this->rc->config->get('fileapi_wopi_service'), ' /'); // kolab-wopi
$service_url .= '/wopi/files/' . $id;
// @TODO: Parsing and replacing placeholder values
// https://wopi.readthedocs.io/en/latest/discovery.html#action-urls
return $office_url . '?WOPISrc=' . urlencode($service_url);
}
/**
* Retern extra viewer parameters to post the the viewer iframe
*
* @param array $info File info
*
* @return array POST parameters
*/
public function editor_post_params($info)
{
// Access token TTL (number of milliseconds since January 1, 1970 UTC)
if ($ttl = $this->rc->config->get('session_lifetime', 0) * 60) {
$now = new DateTime('now', new DateTimeZone('UTC'));
$ttl = ($ttl + $now->format('U')) . '000';
}
$params = array(
'access_token' => $this->token,
'access_token_ttl' => $ttl ?: 0,
);
+ if (!empty($info['readonly'])) {
+ $params['permission'] = 'readonly';
+ }
+
// @TODO: we should/could also add:
- // lang, title, permission, timestamp, closebutton, revisionhistory
+ // lang, title, timestamp, closebutton, revisionhistory
return $params;
}
/**
* List supported mimetypes
*/
public function supported_filetypes()
{
$caps = $this->capabilities();
return array_keys($caps);
}
/**
* Uses WOPI discovery to get Office capabilities
* https://wopi.readthedocs.io/en/latest/discovery.html
*/
protected function capabilities()
{
$cache_key = 'wopi.capabilities';
if ($result = $this->get_from_cache($cache_key)) {
return $result;
}
$office_url = rtrim($this->rc->config->get('fileapi_wopi_office'), ' /');
$office_url .= '/hosting/discovery';
try {
$request = $this->http_request();
$request->setMethod(HTTP_Request2::METHOD_GET);
$request->setBody('');
$request->setUrl($office_url);
$response = $request->send();
$body = $response->getBody();
$code = $response->getStatus();
if (empty($body) || $code != 200) {
throw new Exception("Unexpected WOPI discovery response");
}
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
// parse XML output
// <wopi-discovery>
// <net-zone name="external-http">
// <app name="application/vnd.lotus-wordpro">
// <action ext="lwp" name="edit" urlsrc="https://office.example.org/loleaflet/1.8.3/loleaflet.html?"/>
// </app>
// ...
$node = new DOMDocument('1.0', 'UTF-8');
$node->loadXML($body);
$result = array();
foreach ($node->getElementsByTagName('app') as $app) {
if ($mimetype = $app->getAttribute('name')) {
if ($action = $app->getElementsByTagName('action')->item(0)) {
foreach ($action->attributes as $attr) {
$result[$mimetype][$attr->name] = $attr->value;
}
}
}
}
if (empty($result)) {
rcube::raise_error("Failed to parse WOPI discovery response: $body", true, true);
}
$this->save_in_cache($cache_key, $result);
return $result;
}
/**
* Initializes HTTP request object
*/
protected function http_request()
{
require_once 'HTTP/Request2.php';
$request = new HTTP_Request2();
// Configure connection options
$config = $this->rc->config;
$http_config = (array) $config->get('http_request', $config->get('kolab_http_request'));
// Deprecated config, all options are separated variables
if (empty($http_config)) {
$options = array(
'ssl_verify_peer',
'ssl_verify_host',
'ssl_cafile',
'ssl_capath',
'ssl_local_cert',
'ssl_passphrase',
'follow_redirects',
);
foreach ($options as $optname) {
if (($optvalue = $config->get($optname)) !== null
|| ($optvalue = $config->get('kolab_' . $optname)) !== null
) {
$http_config[$optname] = $optvalue;
}
}
}
if (!empty($http_config)) {
try {
$request->setConfig($http_config);
}
catch (Exception $e) {
rcube::log_error("HTTP: " . $e->getMessage());
}
}
// proxy User-Agent
$request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
// some HTTP server configurations require this header
$request->setHeader('accept', "application/json,text/javascript,*/*");
return $request;
}
protected function get_from_cache($key)
{
if ($cache = $this->get_cache) {
return $cache->get($key);
}
}
protected function save_in_cache($key, $value)
{
if ($cache = $this->get_cache) {
$cache->set($key, $value);
}
}
/**
* Getter for the shared cache engine object
*/
protected function get_cache()
{
if ($this->cache === null) {
$cache = $this->rc->get_cache_shared('chwala');
$this->cache = $cache ?: false;
}
return $this->cache;
}
}
diff --git a/lib/viewers/doc.php b/lib/viewers/doc.php
new file mode 100644
index 0000000..7acdbec
--- /dev/null
+++ b/lib/viewers/doc.php
@@ -0,0 +1,133 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API |
+ | |
+ | Copyright (C) 2011-2016, Kolab Systems AG |
+ | |
+ | This program is free software: you can redistribute it and/or modify |
+ | it under the terms of the GNU Affero General Public License as published |
+ | by the Free Software Foundation, either version 3 of the License, or |
+ | (at your option) any later version. |
+ | |
+ | This program is distributed in the hope that it will be useful, |
+ | but WITHOUT ANY WARRANTY; without even the implied warranty of |
+ | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
+ | GNU Affero General Public License for more details. |
+ | |
+ | You should have received a copy of the GNU Affero General Public License |
+ | along with this program. If not, see <http://www.gnu.org/licenses/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+/**
+ * Class integrating Collabora Online documents viewer
+ */
+class file_viewer_doc extends file_viewer
+{
+ /**
+ * Class constructor
+ *
+ * @param file_api File API object
+ */
+ public function __construct($api)
+ {
+ $this->api = $api;
+ }
+
+ /**
+ * Returns list of supported mimetype
+ *
+ * @return array List of mimetypes
+ */
+ public function supported_mimetypes()
+ {
+ $rcube = rcube::get_instance();
+
+ // Get list of supported types from Collabora
+ if ($rcube->config->get('fileapi_wopi_office')) {
+ $wopi = new file_wopi($this->api);
+ if ($types = $wopi->supported_filetypes()) {
+ return $types;
+ }
+ }
+
+ return array();
+ }
+
+ /**
+ * Check if mimetype is supported by the viewer
+ *
+ * @param string $mimetype File type
+ *
+ * @return bool True if mimetype is supported, False otherwise
+ */
+ public function supports($mimetype)
+ {
+ return in_array($mimetype, $this->supported_mimetypes());
+ }
+
+ /**
+ * Return file viewer URL
+ *
+ * @param string $file File name
+ * @param string $mimetype File type
+ */
+ public function href($file, $mimetype = null)
+ {
+ return file_utils::script_uri() . '?method=file_get'
+ . '&viewer=doc'
+ . '&file=' . urlencode($file)
+ . '&token=' . urlencode(session_id());
+ }
+
+ /**
+ * Print output and exit
+ *
+ * @param string $file File name
+ * @param string $mimetype File type
+ */
+ public function output($file, $mimetype = null)
+ {
+ // Create readonly session and get WOPI request parameters
+ $wopi = new file_wopi($this->api);
+ $url = $wopi->session_start($file, $mimetype, $session, true);
+
+ if (!$url) {
+ $this->api->output_error("Failed to open file", 404);
+ }
+
+ $info = array('readonly' => true);
+ $post = $wopi->editor_post_params($info);
+ $url = htmlentities($url);
+ $form = '';
+
+ foreach ($post as $name => $value) {
+ $form .= '<input type="hidden" name="' . $name . '" value="' . $value . '" />';
+ }
+
+ echo <<<EOT
+<html>
+ <head>
+ <script src="viewers/doc/file_editor.js" type="text/javascript" charset="utf-8"></script>
+ <style>
+ iframe, body { width: 100%; height: 100%; margin: 0; border: none; }
+ form { display: none; }
+ </style>
+ </head>
+ <body>
+ <iframe id="viewer" name="viewer" allowfullscreen></iframe>
+ <form target="viewer" method="post" action="$url">
+ $form
+ </form>
+ <script type="text/javascript">
+ var file_editor = new file_editor;
+ file_editor.init();
+ </script>
+ </body>
+</html>
+EOT;
+ }
+}
diff --git a/lib/viewers/doc/file_editor.js b/lib/viewers/doc/file_editor.js
new file mode 100644
index 0000000..fed909b
--- /dev/null
+++ b/lib/viewers/doc/file_editor.js
@@ -0,0 +1,34 @@
+
+function file_editor()
+{
+ this.editable = false;
+ this.printable = false;
+
+ this.init = function()
+ {
+ document.getElementsByTagName('form')[0].submit();
+ };
+
+ // switch editor into read-write mode
+ this.enable = function()
+ {
+ // @TODO
+ };
+
+ // switch editor into read-only mode
+ this.disable = function()
+ {
+ // @TODO
+ };
+
+ this.getContent = function()
+ {
+ // @TODO
+ };
+
+ // print file content
+ this.print = function()
+ {
+ // @TODO
+ };
+}
diff --git a/public_html/js/files_api.js b/public_html/js/files_api.js
index 577a4f1..4dd5296 100644
--- a/public_html/js/files_api.js
+++ b/public_html/js/files_api.js
@@ -1,1075 +1,1078 @@
/**
+--------------------------------------------------------------------------+
| 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> |
+--------------------------------------------------------------------------+
*/
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;
};
/********************************************************/
/********* 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) { if (typeof func == 'function') func(response); else ref[func](response); },
error: function(o, status, err) { ref.http_error(o, status, err, data); },
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) { if (typeof func == 'function') func(response); else ref[func](response); },
error: function(o, status, err) { ref.http_error(o, status, err, data); },
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, data)
{
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 = '&nbsp;&nbsp;&nbsp;' + 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, prefix, 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];
prefix = items.slice(0, items_len-1).join(this.env.directory_separator) + this.env.directory_separator;
for (n=0; n<tree.length; n++) {
folder = tree[n];
diff = folders[folder].depth - (items_len - 1);
if (diff >= 0 && folder.indexOf(prefix) === 0)
folders[folder].tree[diff] |= 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, '_');
+ var classes = [];
- return type;
+ classes.push(type.replace(/\/.*/, ''));
+ classes.push(type.replace(/[^a-z0-9]/g, '_'));
+
+ return classes.join(' ');
};
// 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 is editable (using File API viewer, Manticore or WOPI) (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,
wopi_doc = /^application\/vnd.oasis.opendocument./i;
// Manticore?
if (capabilities && capabilities.MANTICORE && doc.test(type))
res |= 4;
// WOPI (Collabora Online)?
// @TODO: this could use mimetypes from WOPI discovery
if (capabilities && capabilities.WOPI && wopi_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 | 4;
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 - 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
* invitationChange - method to handle invitation state updates
* invitationSave - method to handle invitation state update
*/
function manticore_api(conf)
{
var domain, manticore,
locks = {},
callbacks = {},
members = {},
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, is_txt, timeout)
{
if (conf.display_message)
return conf.display_message(label, type, is_txt, timeout);
if (type == 'error')
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;
};
// Handle messages from Manticore
this.message_handler = function(data)
{
var result;
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;
case 'sessionClosed':
this.display_message('sessionterminated', 'error');
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;
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) {
var msg = self.invitation_msg(invitation);
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, 30);
// update existing sessions info
if (conf.api && conf.api.sessions && invitation.file) {
var session, folder = conf.api.file_path(invitation.file),
is_invited = function() {
return !invitation.is_session_owner && /^(invited|accepted)/.test(invitation.status);
};
$.each(conf.api.sessions[folder] || {}, function(i, s) {
if (i == invitation.session_id || s.file == invitation.file) {
if (is_invited())
conf.api.sessions[folder][i].is_invited = true;
if (s.id == invitation.session_id)
session = conf.api.sessions[folder][i];
}
});
if (!session) {
if (!conf.api.sessions[folder])
conf.api.sessions[folder] = {};
conf.api.sessions[folder][invitation.session_id] = {
owner: invitation.owner,
owner_name: invitation.owner_name,
is_owner: invitation.is_session_owner,
is_invited: is_invited(),
file: invitation.file
};
}
}
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) {
if (invitation.status == 'invited' || invitation.status == 'declined-owner' || invitation.status == 'accepted-owner') {
invitation_change(invitation);
}
}
else {
if (invitation.status == 'accepted' || invitation.status == 'declined' || invitation.status == 'requested') {
invitation_change(invitation);
}
}
});
self.invitations_timestamp = response.result.timestamp;
};
this.invitation_msg = function(invitation)
{
return self.gettext(invitation.status.replace('-', '') + 'notice')
.replace('$user', invitation.user_name ? invitation.user_name : invitation.user)
.replace('$owner', invitation.owner_name ? invitation.owner_name : invitation.owner)
.replace('$file', invitation.filename);
};
// Request access to the editing session
this.invitation_request = function(invitation)
{
var params = {id: invitation.session_id, user: invitation.user || ''};
conf.api.req = this.set_busy(true, 'invitationrequesting');
conf.api.request('document_request', params, function(response) {
self.invitation_response(response, invitation, 'requested');
});
};
// Accept an invitations to the editing session
this.invitation_accept = function(invitation)
{
var params = {id: invitation.session_id, user: invitation.user || ''};
conf.api.req = this.set_busy(true, 'invitationaccepting');
conf.api.request('document_accept', params, function(response) {
self.invitation_response(response, invitation, 'accepted');
});
};
// Decline an invitations to the editing session
this.invitation_decline = function(invitation)
{
var params = {id: invitation.session_id, user: invitation.user || ''};
conf.api.req = this.set_busy(true, 'invitationdeclining');
conf.api.request('document_decline', params, function(response) {
self.invitation_response(response, invitation, 'declined');
});
};
// document_decline response handler
this.invitation_response = function(response, invitation, status)
{
if (!conf.api.response(response))
return;
invitation.status = status;
if (conf.invitationSaved)
conf.invitationSaved(invitation);
};
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, '&amp;')
.replace(/>/g, '&gt;')
.replace(/</g, '&lt;');
};
function object_is_empty(obj)
{
if (obj)
for (var i in obj)
if (i !== null)
return true;
return false;
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Jan 31, 10:58 PM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426531
Default Alt Text
(107 KB)

Event Timeline