Page MenuHomePhorge

No OneTemporary

Size
134 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/api/document.php b/lib/api/document.php
index 9ca0a94..7bc9df2 100644
--- a/lib/api/document.php
+++ b/lib/api/document.php
@@ -1,319 +1,341 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2015, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class file_api_document extends file_api_common
{
/**
* Request handler
*/
public function handle()
{
$method = $_SERVER['REQUEST_METHOD'];
$this->args = $_GET;
if ($method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) {
$method = $_SERVER['HTTP_X_HTTP_METHOD'];
}
// Invitation notifications
if ($this->args['method'] == 'invitations') {
return $this->invitations();
}
// Sessions list
if ($this->args['method'] == 'sessions') {
return $this->sessions();
}
// Session and invitations management
if (strpos($this->args['method'], 'document_') === 0) {
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
$post = file_get_contents('php://input');
$this->args += (array) json_decode($post, true);
unset($post);
}
if (empty($this->args['id'])) {
throw new Exception("Missing document ID.", file_api_core::ERROR_CODE);
}
switch ($this->args['method']) {
case 'document_delete':
case 'document_invite':
case 'document_request':
case 'document_decline':
case 'document_accept':
case 'document_cancel':
case 'document_info':
return $this->{$this->args['method']}($this->args['id']);
}
}
// Document content actions for Manticore
else if ($method == 'PUT' || $method == 'GET') {
if (empty($this->args['id'])) {
throw new Exception("Missing document ID.", file_api_core::ERROR_CODE);
}
$file = $this->get_file_path($this->args['id']);
return $this->{'document_' . strtolower($method)}($file);
}
throw new Exception("Unknown method", file_api_core::ERROR_INVALID);
}
/**
* Get file path from manticore session identifier
*/
protected function get_file_path($id)
{
$document = new file_document($this->api);
- return $document->session_file($id);
+ $file = $document->session_file($id);
+
+ return $file['file'];
}
/**
* Get invitations list
*/
protected function invitations()
{
$timestamp = time();
// Initial tracking request, return just the current timestamp
if ($this->args['timestamp'] == -1) {
return array('timestamp' => $timestamp);
// @TODO: in this mode we should likely return all invitations
// that require user action, otherwise we may skip some unintentionally
}
- $manticore = new file_manticore($this->api);
- $filter = array();
+ $document = new file_document($this->api);
+ $filter = array();
if ($this->args['timestamp']) {
$filter['timestamp'] = $this->args['timestamp'];
}
- $list = $manticore->invitations_list($filter);
+ $list = $document->invitations_list($filter);
return array(
'list' => $list,
'timestamp' => $timestamp,
);
}
/**
* Get sessions list
*/
protected function sessions()
{
- $manticore = new file_manticore($this->api);
+ $document = new file_document($this->api);
$params = array(
'reverse' => rcube_utils::get_boolean((string) $this->args['reverse']),
);
if (!empty($this->args['sort'])) {
$params['sort'] = strtolower($this->args['sort']);
}
- return $manticore->sessions_list($params);
+ return $document->sessions_list($params);
}
/**
* Close (delete) manticore session
*/
protected function document_delete($id)
{
- $manticore = new file_manticore($this->api);
+ $document = file_document::get_handler($this->api, $id);
- if (!$manticore->session_delete($id)) {
+ if (!$document->session_delete($id)) {
throw new Exception("Failed deleting the document session.", file_api_core::ERROR_CODE);
}
}
/**
* Invite/add a session participant(s)
*/
protected function document_invite($id)
{
- $manticore = new file_manticore($this->api);
- $users = $this->args['users'];
- $comment = $this->args['comment'];
+ $document = file_document::get_handler($this->api, $id);
+ $users = $this->args['users'];
+ $comment = $this->args['comment'];
if (empty($users)) {
throw new Exception("Invalid arguments.", file_api_core::ERROR_CODE);
}
foreach ((array) $users as $user) {
if (!empty($user['user'])) {
- $manticore->invitation_create($id, $user['user'], file_manticore::STATUS_INVITED, $comment, $user['name']);
+ $document->invitation_create($id, $user['user'], file_document::STATUS_INVITED, $comment, $user['name']);
$result[] = array(
'session_id' => $id,
'user' => $user['user'],
'user_name' => $user['name'],
- 'status' => file_manticore::STATUS_INVITED,
+ 'status' => file_document::STATUS_INVITED,
);
}
}
return array(
'list' => $result,
);
}
/**
* Request an invitation to a session
*/
protected function document_request($id)
{
- $manticore = new file_manticore($this->api);
- $manticore->invitation_create($id, null, file_manticore::STATUS_REQUESTED, $this->args['comment']);
+ $document = file_document::get_handler($this->api, $id);
+ $document->invitation_create($id, null, file_document::STATUS_REQUESTED, $this->args['comment']);
}
/**
* Decline an invitation to a session
*/
protected function document_decline($id)
{
- $manticore = new file_manticore($this->api);
- $manticore->invitation_update($id, $this->args['user'], file_manticore::STATUS_DECLINED, $this->args['comment']);
+ $document = file_document::get_handler($this->api, $id);
+ $document->invitation_update($id, $this->args['user'], file_document::STATUS_DECLINED, $this->args['comment']);
}
/**
* Accept an invitation to a session
*/
protected function document_accept($id)
{
- $manticore = new file_manticore($this->api);
- $manticore->invitation_update($id, $this->args['user'], file_manticore::STATUS_ACCEPTED, $this->args['comment']);
+ $document = file_document::get_handler($this->api, $id);
+ $document->invitation_update($id, $this->args['user'], file_document::STATUS_ACCEPTED, $this->args['comment']);
}
/**
* Remove a session participant(s) - cancel invitations
*/
protected function document_cancel($id)
{
- $manticore = new file_manticore($this->api);
- $users = $this->args['users'];
+ $document = file_document::get_handler($this->api, $id);
+ $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);
+ $document->invitation_delete($id, $user);
$result[] = $user;
}
return array(
'list' => $result,
);
}
/**
* Return document informations
*/
protected function document_info($id)
{
- list($driver, $path) = $this->api->get_driver($this->get_file_path($id));
-
- $document = new file_document($this->api);
+ $document = file_document::get_handler($this->api, $id);
+ $file = $document->session_file($id);
$session = $document->session_info($id);
- $result = $driver->file_info($path);
$rcube = rcube::get_instance();
+ try {
+ list($driver, $path) = $this->api->get_driver($file['file']);
+ $result = $driver->file_info($path);
+ }
+ catch (Exception $e) {
+ // invited users may have no permission,
+ // use file data from the session
+ $result = array(
+ 'size' => $file['size'],
+ 'name' => $file['name'],
+ 'modified' => $file['modified'],
+ 'type' => $file['type'],
+ );
+ }
+
$result['owner'] = $session['owner'];
$result['owner_name'] = $session['owner_name'];
$result['user'] = $rcube->user->get_username();
+ $result['readonly'] = !empty($session['readonly']);
+ $result['origin'] = $session['origin'];
if ($result['owner'] == $result['user']) {
$result['user_name'] = $result['owner_name'];
}
else {
$result['user_name'] = $this->api->resolve_user($result['user']) ?: '';
}
return $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(
+ $file_data = array(
'path' => $tmp_path,
'type' => rcube_mime::file_content_type($tmp_path, $file),
);
- $driver->file_update($path, $file);
+ $driver->file_update($path, $file_data);
// remove the temp file
unlink($tmp_path);
+
+ // Update the file metadata in session
+ $file_data = $driver->file_info($file);
+ $document = file_document::get_handler($this->api, $this->args['id']);
+ $document->session_update($this->args['id'], $file_data);
}
/**
* 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/api/file_get.php b/lib/api/file_get.php
index 8116b54..451d1d6 100644
--- a/lib/api/file_get.php
+++ b/lib/api/file_get.php
@@ -1,103 +1,103 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2014, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
class file_api_file_get extends file_api_common
{
protected $driver;
/**
* Request handler
*/
public function handle()
{
parent::handle();
$this->api->output_type = file_api_core::OUTPUT_HTML;
if (!isset($this->args['file']) || $this->args['file'] === '') {
header("HTTP/1.0 ".file_api_core::ERROR_CODE." Missing file name");
}
$method = $_SERVER['REQUEST_METHOD'];
if ($method == 'POST' && !empty($_SERVER['HTTP_X_HTTP_METHOD'])) {
$method = $_SERVER['HTTP_X_HTTP_METHOD'];
}
$params = array(
'force-download' => rcube_utils::get_boolean((string) $this->args['force-download']),
'force-type' => $this->args['force-type'],
'head' => $this->args['head'] ?: $method == 'HEAD',
);
list($this->driver, $path) = $this->api->get_driver($this->args['file']);
if (!empty($this->args['viewer'])) {
$this->file_view($path, $this->args, $params);
}
try {
$this->driver->file_get($path, $params);
}
catch (Exception $e) {
header("HTTP/1.0 " . file_api_core::ERROR_CODE . " " . $e->getMessage());
}
exit;
}
/**
* File vieweing request handler
*/
protected function file_view($file, $args, $params)
{
$viewer = $args['viewer'];
$path = __DIR__ . "/../viewers/$viewer.php";
$class = "file_viewer_$viewer";
if (!file_exists($path)) {
return;
}
// get file info
try {
$info = $this->driver->file_info($file);
}
catch (Exception $e) {
header("HTTP/1.0 " . file_api_core::ERROR_CODE . " " . $e->getMessage());
exit;
}
include_once $path;
$viewer = new $class($this->api);
// check if specified viewer supports file type
// otherwise return (fallback to file_get action)
if (!$viewer->supports($info['type'])) {
return;
}
- $viewer->output($args['file'], $info['type']);
+ $viewer->output($args['file'], $info);
exit;
}
}
diff --git a/lib/api/file_info.php b/lib/api/file_info.php
index f5ca11d..6627daa 100644
--- a/lib/api/file_info.php
+++ b/lib/api/file_info.php
@@ -1,169 +1,180 @@
<?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_file_info extends file_api_common
{
/**
* Request handler
*/
public function handle()
{
parent::handle();
// 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);
$manticore = $capabilities['MANTICORE'];
$wopi = $capabilities['WOPI'];
// support file_info by session ID
if (!isset($this->args['file']) || $this->args['file'] === '') {
if (($manticore || $wopi) && !empty($this->args['session'])) {
- $this->args['file'] = $this->file_document_file($this->args['session']);
+ if ($info = $this->file_document_file($this->args['session'])) {
+ $this->args['file'] = $info['file'];
+ }
}
else {
throw new Exception("Missing file name", file_api_core::ERROR_CODE);
}
}
if ($this->args['file'] !== null) {
- list($driver, $path) = $this->api->get_driver($this->args['file']);
+ try {
+ list($driver, $path) = $this->api->get_driver($this->args['file']);
- $info = $driver->file_info($path);
- $info['file'] = $this->args['file'];
+ $info = $driver->file_info($path);
+ $info['file'] = $this->args['file'];
+ }
+ catch (Exception $e) {
+ // Invited user may have no access to the file,
+ // ignore errors if session exists
+ if (!$this->args['viewer'] || !$this->args['session']) {
+ throw $e;
+ }
+ }
}
// Possible 'viewer' types are defined in files_api.js:file_type_supported()
// 1 - Native browser support
// 2 - Chwala viewer exists
// 4 - Editor exists (manticore/wopi)
if (rcube_utils::get_boolean((string) $this->args['viewer'])) {
if ($this->args['file'] !== null) {
$this->file_viewer_info($info);
}
if ((intval($this->args['viewer']) & 4)) {
// @TODO: Chwala client should have a possibility to select
// between wopi and manticore?
if (!$wopi || !$this->file_wopi_handler($info)) {
if ($manticore) {
$this->file_manticore_handler($info);
}
}
}
}
// check writable flag
if ($this->args['file'] !== null) {
$path = explode(file_storage::SEPARATOR, $path);
array_pop($path);
$path = implode(file_storage::SEPARATOR, $path);
$acl = $driver->folder_rights($path);
$info['writable'] = ($acl & file_storage::ACL_WRITE) != 0;
}
return $info;
}
/**
* Merge file viewer data into file info
*/
protected function file_viewer_info(&$info)
{
$file = $this->args['file'];
$viewer = $this->find_viewer($info['type']);
if ($viewer) {
$info['viewer'] = array();
if ($frame = $viewer->frame($file, $info['type'])) {
$info['viewer']['frame'] = $frame;
}
else if ($href = $viewer->href($file, $info['type'])) {
$info['viewer']['href'] = $href;
}
}
}
/**
* Get file from manticore/wopi session
*/
protected function file_document_file($session_id)
{
- $document = new file_document($this->api);
+ $document = file_document::get_handler($this->api, $session_id);
return $document->session_file($session_id, true);
}
/**
* Merge manticore session data into file info
*/
protected function file_manticore_handler(&$info)
{
$manticore = new file_manticore($this->api);
$file = $this->args['file'];
$session = $this->args['session'];
if (in_array_nocase($info['type'], $manticore->supported_filetypes(true))) {
$info['viewer']['manticore'] = true;
}
else {
return false;
}
- if ($uri = $manticore->session_start($file, $info['type'], $session)) {
+ if ($uri = $manticore->session_start($file, $info, $session)) {
$info['viewer']['href'] = $uri;
$info['viewer']['post'] = $manticore->editor_post_params($info);
$info['session'] = $manticore->session_info($session, true);
}
return true;
}
/**
* Merge WOPI session data into file info
*/
protected function file_wopi_handler(&$info)
{
$wopi = new file_wopi($this->api);
$file = $this->args['file'];
$session = $this->args['session'];
if (in_array_nocase($info['type'], $wopi->supported_filetypes(true))) {
$info['viewer']['wopi'] = true;
}
else {
return false;
}
- if ($uri = $wopi->session_start($file, $info['type'], $session)) {
+ if ($uri = $wopi->session_start($file, $info, $session)) {
$info['viewer']['href'] = $uri;
$info['viewer']['post'] = $wopi->editor_post_params($info);
$info['session'] = $wopi->session_info($session, true);
}
return true;
}
}
diff --git a/lib/file_document.php b/lib/file_document.php
index 6e7ab98..57a688a 100644
--- a/lib/file_document.php
+++ b/lib/file_document.php
@@ -1,778 +1,864 @@
<?php
/**
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2012-2016, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as published |
| by the Free Software Foundation, either version 3 of the License, or |
| (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License |
| along with this program. If not, see <http://www.gnu.org/licenses/> |
+--------------------------------------------------------------------------+
| Author: Aleksander Machniak <machniak@kolabsys.com> |
+--------------------------------------------------------------------------+
*/
/**
* Document editing sessions handling
*/
class file_document
{
protected $api;
protected $rc;
protected $user;
protected $sessions_table = 'chwala_sessions';
protected $invitations_table = 'chwala_invitations';
protected $icache = array();
+ protected $file_meta_items = array('type', 'name', 'size', 'modified');
const STATUS_INVITED = 'invited';
const STATUS_REQUESTED = 'requested';
const STATUS_ACCEPTED = 'accepted';
const STATUS_DECLINED = 'declined';
const STATUS_DECLINED_OWNER = 'declined-owner'; // same as 'declined' but done by the session owner
const STATUS_ACCEPTED_OWNER = 'accepted-owner'; // same as 'accepted' but done by the session owner
/**
* Class constructor
*
- * @param file_api Chwala API app instance
+ * @param file_api $api Chwala API app instance
*/
public function __construct($api)
{
$this->rc = rcube::get_instance();
$this->api = $api;
$this->user = $_SESSION['user'];
$db = $this->rc->get_dbh();
$this->sessions_table = $db->table_name($this->sessions_table);
$this->invitations_table = $db->table_name($this->invitations_table);
}
+ /**
+ * Detect type of file_document class to use for specified session
+ *
+ * @param file_api $api Chwala API app instance
+ * @param string $session_id Document session ID
+ *
+ * @return file_document Document object
+ */
+ public static function get_handler($api, $session_id)
+ {
+ // we add "w-" prefix to wopi session identifiers,
+ // so we can distinguish it from manticore sessions
+ if (strpos($session_id, 'w-') === 0) {
+ return new file_wopi($api);
+ }
+
+ return new file_manticore($api);
+ }
+
/**
* Return viewer URI for specified file/session. This creates
* a new collaborative editing session when needed.
*
* @param string $file File path
- * @param string &$mimetype File type
+ * @param array &$file_info File metadata (e.g. type)
* @param string &$session_id Optional session ID to join to
* @param string $readonly Create readonly (one-time) session
*
* @return string An URI for specified file/session
* @throws Exception
*/
- public function session_start($file, &$mimetype, &$session_id = null, $readonly = false)
+ public function session_start($file, &$file_info, &$session_id = null, $readonly = false)
{
if ($file !== null) {
$uri = $this->path2uri($file, $driver);
}
$backend = $this->api->get_backend();
if ($session_id) {
$session = $this->session_info($session_id);
if (empty($session)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
// check session ownership
if ($session['owner'] != $this->user) {
// check if the user was invited
$invitations = $this->invitations_find(array('session_id' => $session_id, 'user' => $this->user));
$states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
if (empty($invitations) || !in_array($invitations[0]['status'], $states)) {
throw new Exception("No permission to join the editing session.", file_api_core::ERROR_CODE);
}
// automatically accept the invitation, if not done yet
if ($invitations[0]['status'] == self::STATUS_INVITED) {
$this->invitation_update($session_id, $this->user, self::STATUS_ACCEPTED);
}
}
- $mimetype = $session['type'];
+ $file_info['type'] = $session['type'];
}
else if (!empty($uri)) {
// To prevent from creating new sessions for the same file+user
// (e.g. when user uses F5 to refresh the page), we check first
// if such a session exist and continue with it
$db = $this->rc->get_dbh();
$res = $db->query("SELECT `id` FROM `{$this->sessions_table}`"
. " WHERE `owner` = ? AND `uri` = ? AND `readonly` = ?",
$this->user, $uri, intval($readonly));
if ($row = $db->fetch_assoc($res)) {
$session_id = $row['id'];
$res = true;
}
else if (!$db->is_error($res)) {
$session_id = rcube_utils::bin2ascii(md5(time() . $uri, true));
- $data = array('type' => $mimetype);
$owner = $this->user;
+ $data = array('origin' => $this->get_origin());
+
+ // store some file data, they will be used
+ // by invited users that has no access to the storage
+ foreach ($this->file_meta_items as $item) {
+ if (isset($file_info[$item])) {
+ $data[$item] = $file_info[$item];
+ }
+ }
+
+ // bind the session ID with editor type (see file_document::get_handler())
+ if ($this instanceof file_wopi) {
+ $session_id = 'w-' . $session_id;
+ }
// we'll store user credentials if the file comes from
// an external source that requires authentication
if ($backend != $driver) {
$auth = $driver->auth_info();
$auth['password'] = $this->rc->encrypt($auth['password']);
$data['auth_info'] = $auth;
}
$res = $this->session_create($session_id, $uri, $owner, $data, $readonly);
}
if (!$res) {
throw new Exception("Failed creating document editing session", file_api_core::ERROR_CODE);
}
}
else {
throw new Exception("Failed creating document editing session (unknown file)", file_api_core::ERROR_CODE);
}
// Implementations should return real URI
return '';
}
/**
* Get file path (not URI) from session.
*
* @param string $id Session ID
* @param bool $join_mode Throw exception only if session does not exist
*
- * @return string File path
+ * @return array File info (file, type, size)
* @throws Exception
*/
public function session_file($id, $join_mode = false)
{
$session = $this->session_info($id);
if (empty($session)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
$path = $this->uri2path($session['uri']);
if (empty($path) && (!$join_mode || $session['owner'] == $this->user)) {
throw new Exception("Document session not found.", file_api_core::ERROR_CODE);
}
// check permissions to the session
if ($session['owner'] != $this->user) {
$invitations = $this->invitations_find(array('session_id' => $id, 'user' => $this->user));
$states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
if (empty($invitations) || !in_array($invitations[0]['status'], $states)) {
throw new Exception("No permission to join the editing session.", file_api_core::ERROR_CODE);
}
}
- return $path;
+ $result = array('file' => $path);
+
+ foreach ($this->file_meta_items as $item) {
+ if (isset($session[$item])) {
+ $result[$item] = $session[$item];
+ }
+ }
+
+ return $result;
}
/**
* Get editing session info
*
* @param string $id Session identifier
* @param bool $with_invitations Return invitations list
*
* @return array Session data
*/
public function session_info($id, $with_invitations = false)
{
$session = $this->icache["session:$id"];
if (!$session) {
$db = $this->rc->get_dbh();
$result = $db->query("SELECT * FROM `{$this->sessions_table}`"
. " WHERE `id` = ?", $id);
if ($row = $db->fetch_assoc($result)) {
$session = $this->session_info_parse($row);
$this->icache["session:$id"] = $session;
}
}
if ($session) {
if ($session['owner'] == $this->user) {
$session['is_owner'] = true;
}
if ($with_invitations && $session['is_owner']) {
$session['invitations'] = $this->invitations_find(array('session_id' => $id));
}
}
return $session;
}
/**
* Find editing sessions for specified path
*/
public function session_find($path, $invitations = true)
{
// create an URI for specified path
$uri = trim($this->path2uri($path), '/') . '/';
// get existing sessions
$sessions = array();
$filter = array('file', 'owner', 'owner_name', 'is_owner');
$db = $this->rc->get_dbh();
$result = $db->query("SELECT * FROM `{$this->sessions_table}`"
. " WHERE `readonly` = 0 AND `uri` LIKE '" . $db->escape($uri) . "%'");
while ($row = $db->fetch_assoc($result)) {
if ($path = $this->uri2path($row['uri'])) {
$sessions[$row['id']] = $this->session_info_parse($row, $path, $filter);
}
}
// set 'is_invited' flag
if ($invitations && !empty($sessions)) {
$invitations = $this->invitations_find(array('user' => $this->user));
$states = array(self::STATUS_INVITED, self::STATUS_ACCEPTED, self::STATUS_ACCEPTED_OWNER);
foreach ($invitations as $invitation) {
if (!empty($sessions[$invitation['session_id']]) && in_array($invitation['status'], $states)) {
$sessions[$invitation['session_id']]['is_invited'] = true;
}
}
}
return $sessions;
}
/**
* Delete editing session (only owner can do that)
*
* @param string $id Session identifier
*/
public function session_delete($id)
{
$db = $this->rc->get_dbh();
$result = $db->query("DELETE FROM `{$this->sessions_table}`"
. " WHERE `id` = ? AND `owner` = ?",
$id, $this->user);
return $db->affected_rows($result) > 0;
}
+ /**
+ * Update editing session
+ *
+ * @param string $id Session ID
+ * @param array $data Session metadata
+ */
+ public function session_update($id, $data)
+ {
+ $db = $this->rc->get_dbh();
+ $result = $db->query("SELECT `data` FROM `{$this->sessions_table}`"
+ . " WHERE `id` = ?", $id);
+
+ if ($row = $db->fetch_assoc($result)) {
+ // merge only relevant information
+ $data = array_intersect_key($data, array_flip($this->file_meta_items));
+ if (empty($data)) {
+ return true;
+ }
+
+ $sess_data = json_decode($row['data'], true);
+ $sess_data = array_merge($sess_data, $data);
+
+ $result = $db->query("UPDATE `{$this->sessions_table}`"
+ . " SET `data` = ? WHERE `id` = ?",
+ json_encode($sess_data), $id);
+
+ return $db->affected_rows($result) > 0;
+ }
+
+ return false;
+ }
+
/**
* Create editing session
*/
protected function session_create($id, $uri, $owner, $data, $readonly = false)
{
// get user name
$owner_name = $this->api->resolve_user($owner) ?: '';
$db = $this->rc->get_dbh();
$result = $db->query("INSERT INTO `{$this->sessions_table}`"
. " (`id`, `uri`, `owner`, `owner_name`, `data`, `readonly`)"
. " VALUES (?, ?, ?, ?, ?, ?)",
$id, $uri, $owner, $owner_name, json_encode($data), intval($readonly));
- $success = $db->affected_rows($result) > 0;
-
- if ($success) {
-
- // @TODO
- }
-
- return $success;
+ return $db->affected_rows($result) > 0;
}
/**
* Find sessions, including:
* 1. to which the user has access (is a creator or has been invited)
* 2. to which the user is considered eligible to request authorization
* to participate in the session by already having access to the file
* Note: Readonly sessions are ignored here.
*
* @param array $param List parameters
*
* @return array Sessions list
*/
public function sessions_list($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.`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 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');
+ $fields = array('id', 'uri', 'owner', 'owner_name', 'readonly');
foreach ($fields as $field) {
if (isset($record[$field])) {
$session[$field] = $record[$field];
}
}
if ($path) {
$session['file'] = $path;
}
- if (!empty($record['data']) && (empty($filter) || in_array('type', $filter))) {
- $data = json_decode($record['data'], true);
- $session['type'] = $data['type'];
+ if (!empty($record['data'])) {
+ $data = json_decode($record['data'], true);
+ $fields = array_merge($this->file_meta_items, array('origin'));
+
+ foreach ($fields as $field) {
+ if (empty($filter) || in_array($field, $filter)) {
+ $session[$field] = $data[$field];
+ }
+ }
}
// @TODO: is_invited?, last_modified?
if ($session['owner'] == $this->user) {
$session['is_owner'] = true;
}
if (!empty($filter)) {
$session = array_intersect_key($session, array_flip($filter));
}
return $session;
}
/**
* Get file URI from path
*/
protected function path2uri($path, &$driver = null)
{
list($driver, $path) = $this->api->get_driver($path);
return $driver->path2uri($path);
}
/**
* Get file path from the URI
*/
protected function uri2path($uri, $use_fallback = false)
{
$backend = $this->api->get_backend();
try {
return $backend->uri2path($uri);
}
catch (Exception $e) {
// do nothing
}
foreach ($this->api->get_drivers(true) as $driver) {
try {
$path = $driver->uri2path($uri);
$title = $driver->title();
if ($title) {
$path = $title . file_storage::SEPARATOR . $path;
}
return $path;
}
catch (Exception $e) {
// do nothing
}
}
// likely user has no access to the file, but has been invited,
// extract filename from the URI
if ($use_fallback && $uri) {
$path = parse_url($uri, PHP_URL_PATH);
$path = explode('/', $path);
$path = $path[count($path) - 1];
return $path;
}
}
/**
* Get URI of all user folders (with shared locations)
*/
protected function all_folder_locations()
{
$locations = array();
foreach (array_merge(array($this->api->get_backend()), $this->api->get_drivers(true)) as $driver) {
// Performance optimization: We're interested here in shared folders,
// Kolab is the only driver that currently supports them, ignore others
if (get_class($driver) != 'kolab_file_storage') {
continue;
}
try {
foreach ($driver->folder_list() as $folder) {
if ($uri = $driver->path2uri($folder)) {
$locations[] = $uri;
}
}
}
catch (Exception $e) {
// do nothing
}
}
return $locations;
}
+
+ /**
+ * Get request origin, use Referer header if specified
+ */
+ protected function get_origin()
+ {
+ if (!empty($_SERVER['HTTP_REFERER'])) {
+ $url = parse_url($_SERVER['HTTP_REFERER']);
+
+ return $url['scheme'] . '://' . $url['host'] . ($url['port'] ?: '');
+ }
+
+ return $_SERVER['REQUEST_SCHEME'] . '://' . $_SERVER['HTTP_HOST'];
+ }
}
diff --git a/lib/file_manticore.php b/lib/file_manticore.php
index 278d6f9..39c9771 100644
--- a/lib/file_manticore.php
+++ b/lib/file_manticore.php
@@ -1,238 +1,238 @@
<?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 array &$file_info File metadata (e.g. type)
* @param string &$session_id Optional session ID to join to
* @param string $readonly Create readonly (one-time) session
*
* @return string Manticore URI
* @throws Exception
*/
- public function session_start($file, &$mimetype, &$session_id = null, $readonly = false)
+ public function session_start($file, &$file_info, &$session_id = null, $readonly = false)
{
- parent::session_start($file, $mimetype, $session_id, $readonly);
+ parent::session_start($file, $file_info, $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, $readonly = false)
{
$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);
}
}
}
/**
* List supported mimetypes
*
* @param bool $editable Return only editable mimetypes
*
* @return array List of supported mimetypes
*/
public function supported_filetypes($editable = false)
{
return array(
'application/vnd.oasis.opendocument.text',
);
}
/**
* 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_viewer.php b/lib/file_viewer.php
index 48c49d9..d8ca18f 100644
--- a/lib/file_viewer.php
+++ b/lib/file_viewer.php
@@ -1,95 +1,95 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-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> |
+--------------------------------------------------------------------------+
*/
/**
* Abstract viewer class
*/
abstract class file_viewer
{
protected $mimetypes = array();
protected $api;
/**
* 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()
{
return $this->mimetypes;
}
/**
* Check if mimetype is supported by the viewer
*
* @param string $mimetype File type
*
* @return bool
*/
public function supports($mimetype)
{
return in_array($mimetype, $this->mimetypes);
}
/**
* Print output and exit
*
- * @param string $file File name
- * @param string $mimetype File type
+ * @param string $file File name
+ * @param array $file_info File metadata (e.g. type)
*/
- public function output($file, $mimetype = null)
+ public function output($file, $file_info = array())
{
}
/**
* Return output of file content area
*
* @param string $file File name
* @param string $mimetype File type
*/
public function frame($file, $mimetype = null)
{
}
/**
* Return file URL of file content area
*
* @param string $file File name
* @param string $mimetype File type
*/
public function href($file, $mimetype = null)
{
}
}
diff --git a/lib/file_wopi.php b/lib/file_wopi.php
index a416d5a..7382aed 100644
--- a/lib/file_wopi.php
+++ b/lib/file_wopi.php
@@ -1,311 +1,307 @@
<?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;
// Mimetypes supported by CODE, but not advertised by all possible names
protected $aliases = array(
'application/vnd.corel-draw' => 'image/x-coreldraw',
);
/**
* 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 array &$file_info File metadata (e.g. type)
* @param string &$session_id Optional session ID to join to
* @param string $readonly Create readonly (one-time) session
*
* @return string WOPI URI for specified document
* @throws Exception
*/
- public function session_start($file, &$mimetype, &$session_id = null, $readonly = false)
+ public function session_start($file, &$file_info, &$session_id = null, $readonly = false)
{
- parent::session_start($file, $mimetype, $session_id, $readonly);
+ parent::session_start($file, $file_info, $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);
+ return $this->frame_uri($session_id, $file_info['type']);
}
/**
* 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
+ * Retern extra viewer parameters to post to 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, timestamp, closebutton, revisionhistory
return $params;
}
/**
* List supported mimetypes
*
* @param bool $editable Return only editable mimetypes
*
* @return array List of supported mimetypes
*/
public function supported_filetypes($editable = false)
{
$caps = $this->capabilities();
if ($editable) {
$editable = array();
foreach ($caps as $mimetype => $c) {
if ($c['name'] == 'edit') {
$editable[] = $mimetype;
}
}
return $editable;
}
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 $this->apply_aliases($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 $this->apply_aliases($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;
}
/**
* Get cached data
*/
protected function get_from_cache($key)
{
if ($cache = $this->get_cache) {
return $cache->get($key);
}
}
/**
* Store data in cache
*/
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;
}
/**
* Support more mimetypes in CODE capabilities
*/
protected function apply_aliases($caps)
{
foreach ($this->aliases as $type => $alias) {
if (isset($caps[$type]) && !isset($caps[$alias])) {
$caps[$alias] = $caps[$type];
}
}
return $caps;
}
}
diff --git a/lib/viewers/doc.php b/lib/viewers/doc.php
index 7acdbec..0a80e7c 100644
--- a/lib/viewers/doc.php
+++ b/lib/viewers/doc.php
@@ -1,133 +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
+ * @param string $file File name
+ * @param array $file_info File metadata (e.g. type)
*/
- public function output($file, $mimetype = null)
+ public function output($file, $file_info = array())
{
// Create readonly session and get WOPI request parameters
$wopi = new file_wopi($this->api);
- $url = $wopi->session_start($file, $mimetype, $session, true);
+ $url = $wopi->session_start($file, $file_info, $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/image.php b/lib/viewers/image.php
index 8b288a4..96ced54 100644
--- a/lib/viewers/image.php
+++ b/lib/viewers/image.php
@@ -1,111 +1,111 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-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 implementing image viewer (with format converter)
*
* NOTE: some formats are supported by browser, don't use viewer when not needed.
*/
class file_viewer_image extends file_viewer
{
protected $mimetypes = array(
'image/bmp',
'image/png',
'image/jpeg',
'image/jpg',
'image/pjpeg',
'image/gif',
'image/tiff',
'image/x-tiff',
);
/**
* Class constructor
*
* @param file_api File API object
*/
public function __construct($api)
{
// @TODO: disable types not supported by some browsers
$this->api = $api;
}
/**
* Return file viewer URL
*
* @param string $file File name
* @param string $mimetype File type
*/
public function href($file, $mimetype = null)
{
$href = file_utils::script_uri() . '?method=file_get'
. '&file=' . urlencode($file)
. '&token=' . urlencode(session_id());
// we redirect to self only images with types unsupported
// by browser
if (in_array($mimetype, $this->mimetypes)) {
$href .= '&viewer=image';
}
return $href;
}
/**
* Print output and exit
*
- * @param string $file File name
- * @param string $mimetype File type
+ * @param string $file File name
+ * @param array $file_info File metadata (e.g. type)
*/
- public function output($file, $mimetype = null)
+ public function output($file, $file_info = array())
{
/*
// conversion not needed
- if (preg_match('/^image/p?jpe?g$/i', $mimetype)) {
+ if (preg_match('/^image/p?jpe?g$/i', $file_info['type'])) {
$this->api->api->file_get($file);
return;
}
*/
$rcube = rcube::get_instance();
$temp_dir = unslashify($rcube->config->get('temp_dir'));
$file_path = tempnam($temp_dir, 'rcmImage');
list($driver, $file) = $this->api->get_driver($file);
// write content to temp file
$fd = fopen($file_path, 'w');
$driver->file_get($file, array(), $fd);
fclose($fd);
// convert image to jpeg and send it to the browser
$image = new rcube_image($file_path);
if ($image->convert(rcube_image::TYPE_JPG, $file_path)) {
header("Content-Type: image/jpeg");
header("Content-Length: " . filesize($file_path));
readfile($file_path);
}
unlink($file_path);
}
}
diff --git a/lib/viewers/odf.php b/lib/viewers/odf.php
index c9d1b63..c285908 100644
--- a/lib/viewers/odf.php
+++ b/lib/viewers/odf.php
@@ -1,136 +1,136 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-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 integrating ODF documents viewer from http://webodf.org
*/
class file_viewer_odf extends file_viewer
{
protected $mimetypes = array(
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.graphics',
'application/vnd.oasis.opendocument.chart',
// 'application/vnd.oasis.opendocument.formula',
'application/vnd.oasis.opendocument.image',
'application/vnd.oasis.opendocument.text-master',
// 'application/vnd.sun.xml.base',
// 'application/vnd.oasis.opendocument.base',
// 'application/vnd.oasis.opendocument.database',
'application/vnd.oasis.opendocument.text-template',
'application/vnd.oasis.opendocument.spreadsheet-template',
'application/vnd.oasis.opendocument.presentation-template',
'application/vnd.oasis.opendocument.graphics-template',
'application/vnd.oasis.opendocument.chart-template',
// 'application/vnd.oasis.opendocument.formula-template',
'application/vnd.oasis.opendocument.image-template',
);
/**
* Class constructor
*
* @param file_api File API object
*/
public function __construct($api)
{
$this->api = $api;
$browser = $api->get_browser();
// disable viewer in unsupported browsers
if ($browser->ie && $browser->ver < 9) {
$this->mimetypes = array();
}
}
/**
* Returns list of supported mimetype
*
* @return array List of mimetypes
*/
public function supported_mimetypes()
{
// @TODO: check supported browsers
return $this->mimetypes;
}
/**
* Check if mimetype is supported by the viewer
*
* @param string $mimetype File type
*
* @return bool
*/
public function supports($mimetype)
{
return in_array($mimetype, $this->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=odf'
. '&file=' . urlencode($file)
. '&token=' . urlencode(session_id());
}
/**
* Print output and exit
*
- * @param string $file File name
- * @param string $mimetype File type
+ * @param string $file File name
+ * @param array $file_info File metadata (e.g. type)
*/
- public function output($file, $mimetype = null)
+ public function output($file, $file_info = array())
{
$file_uri = $this->api->file_url($file);
echo <<<EOT
<html>
<head>
<link rel="stylesheet" type="text/css" href="viewers/odf/webodf.css" />
<script type="text/javascript" src="viewers/odf/webodf.js" charset="utf-8"></script>
<script type="text/javascript" charset="utf-8">
function init() {
var odfelement = document.getElementById("odf"),
odfcanvas = new odf.OdfCanvas(odfelement);
odfcanvas.load("$file_uri");
}
window.setTimeout(init, 0);
</script>
</head>
<body>
<div id="odf"></div>
</body>
</html>
EOT;
}
}
diff --git a/lib/viewers/text.php b/lib/viewers/text.php
index 8708f77..3e9b4b8 100644
--- a/lib/viewers/text.php
+++ b/lib/viewers/text.php
@@ -1,216 +1,216 @@
<?php
/*
+--------------------------------------------------------------------------+
| This file is part of the Kolab File API |
| |
| Copyright (C) 2011-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 integrating text editor http://ajaxorg.github.io/ace
*/
class file_viewer_text extends file_viewer
{
/**
* Mimetype to tokenizer map
*
* @var array
*/
protected $mimetypes = array(
'text/plain' => 'text',
'text/html' => 'html',
'text/javascript' => 'javascript',
'text/ecmascript' => 'javascript',
'text/x-c' => 'c_cpp',
'text/css' => 'css',
'text/x-java-source' => 'java',
'text/x-php' => 'php',
'text/x-sh' => 'sh',
'text/xml' => 'xml',
'application/xml' => 'xml',
'application/x-vbscript' => 'vbscript',
'message/rfc822' => 'text',
'application/x-empty' => 'text',
);
/**
* File extension to highligter mode mapping
*
* @var array
*/
protected $extensions = array(
'php' => '/^(php|phpt|inc)$/',
'html' => '/^html?$/',
'css' => '/^css$/',
'xml' => '/^xml$/',
'javascript' => '/^js$/',
'sh' => '/^sh$/',
);
/**
* Returns list of supported mimetype
*
* @return array List of mimetypes
*/
public function supported_mimetypes()
{
// we return only mimetypes not starting with text/
$mimetypes = array();
foreach (array_keys($this->mimetypes) as $type) {
if (strpos($type, 'text/') !== 0) {
$mimetypes[] = $type;
}
}
return $mimetypes;
}
/**
* Check if mimetype is supported by the viewer
*
* @param string $mimetype File type
*
* @return bool
*/
public function supports($mimetype)
{
return $this->mimetypes[$mimetype] || preg_match('/^text\/(?!(pdf|x-pdf))/', $mimetype);
}
/**
* Print file content
*/
protected function print_file($file)
{
$stdout = fopen('php://output', 'w');
stream_filter_register('file_viewer_text', 'file_viewer_content_filter');
stream_filter_append($stdout, 'file_viewer_text');
list($driver, $file) = $this->api->get_driver($file);
$driver->file_get($file, array(), $stdout);
}
/**
* Return file viewer URL
*
* @param string $file File name
* @param string $mimetype File type
*/
public function href($file, $mimetype = null)
{
return $this->api->file_url($file) . '&viewer=text';
}
/**
* Print output and exit
*
- * @param string $file File name
- * @param string $mimetype File type
+ * @param string $file File name
+ * @param array $file_info File metadata (e.g. type)
*/
- public function output($file, $mimetype = null)
+ public function output($file, $file_info = array())
{
- $mode = $this->get_mode($mimetype, $file);
+ $mode = $this->get_mode($file_info['type'], $file);
$href = addcslashes($this->api->file_url($file), "'");
echo '<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Editor</title>
<script src="viewers/text/ace.js" type="text/javascript" charset="utf-8"></script>
<script src="viewers/text/file_editor.js" type="text/javascript" charset="utf-8"></script>
<style>
#editor { top: 0; right: 0; bottom: 0; left: 0; position: absolute; font-size: 14px; padding: 0; margin: 0; }
.ace_search_options { float: right; }
</style>
</head>
<body>
<pre id="editor">';
$this->print_file($file);
echo "</pre>
<script>
var file_editor = new file_editor;
file_editor.init('editor', '$mode', '$href');
</script>
</body>
</html>";
}
protected function get_mode($mimetype, $filename)
{
$mimetype = strtolower($mimetype);
if ($this->mimetypes[$mimetype]) {
return $this->mimetypes[$mimetype];
}
$filename = explode('.', $filename);
$extension = count($filename) > 1 ? array_pop($filename) : null;
if ($extension) {
foreach ($this->extensions as $mode => $regexp) {
if (preg_match($regexp, $extension)) {
return $mode;
}
}
}
return 'text';
}
}
/**
* PHP stream filter to detect escape html special chars in a file
*/
class file_viewer_content_filter extends php_user_filter
{
private $buffer = '';
private $cutoff = 2048;
function onCreate()
{
$this->cutoff = rand(2048, 3027);
return true;
}
function filter($in, $out, &$consumed, $closing)
{
while ($bucket = stream_bucket_make_writeable($in)) {
$bucket->data = htmlspecialchars($bucket->data, ENT_COMPAT | ENT_HTML401 | ENT_IGNORE);
$this->buffer .= $bucket->data;
// keep buffer small enough
if (strlen($this->buffer) > 4096) {
$this->buffer = substr($this->buffer, $this->cutoff);
}
$consumed += $bucket->datalen; // or strlen($bucket->data)?
stream_bucket_append($out, $bucket);
}
return PSFS_PASS_ON;
}
}
diff --git a/public_html/js/files_api.js b/public_html/js/files_api.js
index 56509b6..4fbeef1 100644
--- a/public_html/js/files_api.js
+++ b/public_html/js/files_api.js
@@ -1,1091 +1,1195 @@
/**
+--------------------------------------------------------------------------+
| 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 '';
var classes = [];
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;
type = String(type).toLowerCase();
if (capabilities) {
// Manticore?
$.each(capabilities.MANTICORE_EDITABLE || [], function() {
if (type == this) {
res |= 4;
return false;
}
});
// old version of the check
if (capabilities && capabilities.MANTICORE && doc.test(type))
res |= 4;
// WOPI (Collabora Online)?
$.each(capabilities.WOPI_EDITABLE || [], function() {
if (type == this) {
res |= 4;
return false;
}
});
}
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
+ * Class implementing Document Editor Host API
+ * supporting Manticore and Collabora Online
*
* Configuration:
- * iframe - manticore iframe element
+ * iframe - editor 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
+ * domain - iframe origin
*
* 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)
+function document_editor_api(conf)
{
- var domain, manticore,
+ var domain, editor,
+ is_wopi = false,
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
+ // Handle messages from the editor
this.message_handler = function(data)
{
var result;
- if (callbacks[data.id])
+ data = this.message_convert(data);
+
+ if (data.id && callbacks[data.id])
result = callbacks[data.id](data);
+ else if (typeof data.callback == 'function')
+ result = data.callback(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) {
+ if (conf.members_list && members[data.memberId]) {
$('#' + members[data.memberId].id, conf.members_list).remove();
delete members[data.memberId];
}
break;
case 'sessionClosed':
this.display_message('sessionterminated', 'error');
break;
}
};
+ // Convert WOPI postMessage into internal (Manticore) format
+ this.message_convert = function(data)
+ {
+ // In WOPI data is JSON-stringified
+ if ($.type(data) == 'string')
+ data = JSON.parse(data);
+
+ // non-WOPI format
+ if (!data.MessageId)
+ return data;
+
+ var value = data.Values,
+ result = {name: data.MessageId, wopi: true, id: data.MessageId},
+ member_fn = function(value) {
+ var color = value.Color;
+
+ // make sure color is in css hex format
+ if (color) {
+ if ($.type(color) == 'string' && color.charAt(0) != '#')
+ color = '#' + color;
+ else if ($.type(color) == 'number')
+ color = ((color)>>>0).toString(16).slice(-6);
+ color = '#' + ("000000").substring(0, 6 - color.length) + color;
+ }
+
+ return {
+ memberId: value.ViewId,
+ email: value.UserId,
+ fullName: value.UserName,
+ color: color
+ };
+ };
+
+ switch (result.name) {
+ // WOPI editor is ready
+ case 'App_LoadingStatus':
+ is_wopi = true;
+ result.name = 'ready';
+ break;
+
+ // WOPI session member exited
+ case 'View_Removed':
+ result.name = 'memberRemoved';
+ result.memberId = value.ViewId;
+ break;
+
+ // WOPI session member entered
+ case 'View_Added':
+ result.name = 'memberAdded';
+ $.extend(result, member_fn(value));
+ break;
+
+ // Listing WOPI session members
+ case 'Get_Views_Resp':
+ result.list = $.map(value || [], member_fn);
+ result.callback = function(data) { self.members_list(data.list); return false; };
+ break;
+ }
+
+ return result;
+ };
+
+ // Sends Manticore postMessage
this.post = function(action, data, callback, lock_label)
{
+ if (is_wopi) {
+ // replace Manticore messages with WOPI messages
+ // ignore unsupported functionality
+ switch (action) {
+ case 'getMembers':
+ // ignore, Collabora Online sends View_Added for current user
+ break;
+ }
+
+ return;
+ }
+
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);
+ editor.postMessage(data, domain);
+ };
+
+ // Sends WOPI postMessage
+ this.wopi_post = function(action, data)
+ {
+ var msg = {
+ MessageId: action,
+ SendTime: Date.now(),
+ Values: data || {}
+ };
+
+ editor.postMessage(JSON.stringify(msg), domain);
};
+ // Callback for 'ready' message
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);
- });
+ this.get_members(function(data) { self.members_list(data.value); });
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; });
+ img.on('error', function() { this.src = conf.photo_default_url; });
}
return img;
};
+ this.members_list = function(list)
+ {
+ var images = [], id = (new Date).getTime();
+
+ $.each(list || [], function() {
+ images.push(self.member_item(this, id++));
+ });
+
+ $(conf.members_list).html('').append(images);
+ };
+
// 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
+ // Got editor iframe, use editor's API
if (conf.iframe) {
- manticore = conf.iframe.contentWindow;
+ editor = conf.iframe.contentWindow;
+ domain = conf.domain;
- if (/^(https?:\/\/[^/]+)/i.test(conf.iframe.src))
+ if (!domain && /^(https?:\/\/[^/]+)/i.test(conf.iframe.src))
domain = RegExp.$1;
- // Register 'message' event to receive messages from Manticore iframe
+ // Register 'message' event to receive messages from the editor iframe
window.addEventListener('message', function(event) {
- if (event.source == manticore && event.origin == domain) {
+ if (event.source == editor && 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');
+
+ this.wopi_post('Host_PostmessageReady');
}
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
Sun, Feb 1, 12:28 AM (1 d, 20 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
426560
Default Alt Text
(134 KB)

Event Timeline