Cleanup source code

Signed-off-by: Lukas Reschke <lukas@statuscode.ch>
pull/8/head
Lukas Reschke 7 years ago
parent f304fd4629
commit 75af90b4f1
No known key found for this signature in database
GPG Key ID: B9F6980CF6E759B1

@ -0,0 +1,46 @@
<?php
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
namespace OCA\Richdocuments\AppInfo;
use OC\AppFramework\Utility\TimeFactory;
use OCA\Richdocuments\WOPI\DiscoveryManager;
use OCP\AppFramework\App;
use OCP\AppFramework\IAppContainer;
class Application extends App {
public function __construct (array $urlParams = array()) {
parent::__construct('richdocuments', $urlParams);
$container = $this->getContainer();
$container->registerService(
DiscoveryManager::class,
function(IAppContainer $container) {
return new DiscoveryManager(
$container->getServer()->getHTTPClientService(),
$container->getServer()->getAppDataDir('richdocuments'),
$container->getServer()->getConfig(),
$container->getServer()->getL10N('richdocuments'),
new TimeFactory()
);
}
);
}
}

@ -1,94 +0,0 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2014 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This file is licensed under the Affero General Public License version 3 or
* later.
*/
namespace OCA\Richdocuments\AppInfo;
use \OCP\AppFramework\App;
use \OCA\Richdocuments\Controller\UserController;
use \OCA\Richdocuments\Controller\SessionController;
use \OCA\Richdocuments\Controller\DocumentController;
use \OCA\Richdocuments\Controller\SettingsController;
use \OCA\Richdocuments\AppConfig;
class Application extends App {
public function __construct (array $urlParams = array()) {
parent::__construct('richdocuments', $urlParams);
$container = $this->getContainer();
/**
* Controllers
*/
$container->registerService('UserController', function($c) {
return new UserController(
$c->query('AppName'),
$c->query('Request')
);
});
$container->registerService('SessionController', function($c) {
return new SessionController(
$c->query('AppName'),
$c->query('Request'),
$c->query('Logger'),
$c->query('UserId')
);
});
$container->registerService('DocumentController', function($c) {
return new DocumentController(
$c->query('AppName'),
$c->query('Request'),
$c->query('CoreConfig'),
$c->query('AppConfig'),
$c->query('L10N'),
$c->query('UserId'),
$c->query('ICacheFactory'),
$c->query('Logger')
);
});
$container->registerService('SettingsController', function($c) {
return new SettingsController(
$c->query('AppName'),
$c->query('Request'),
$c->query('L10N'),
$c->query('AppConfig'),
$c->query('UserId')
);
});
$container->registerService('AppConfig', function($c) {
return new AppConfig(
$c->query('CoreConfig')
);
});
/**
* Core
*/
$container->registerService('Logger', function($c) {
return $c->query('ServerContainer')->getLogger();
});
$container->registerService('CoreConfig', function($c) {
return $c->query('ServerContainer')->getConfig();
});
$container->registerService('L10N', function($c) {
return $c->query('ServerContainer')->getL10N($c->query('AppName'));
});
$container->registerService('UserId', function($c) {
$user = $c->query('ServerContainer')->getUserSession()->getUser();
$uid = is_null($user) ? '' : $user->getUID();
return $uid;
});
$container->registerService('ICacheFactory', function($c) {
return $c->query('ServerContainer')->getMemCacheFactory();
});
}
}

@ -4,57 +4,6 @@
<create>true</create>
<overwrite>false</overwrite>
<charset>utf8</charset>
<table>
<name>*dbprefix*richdocuments_session</name>
<declaration>
<field>
<name>es_id</name>
<type>text</type>
<notnull>true</notnull>
<length>64</length>
<comments>Editing session id</comments>
</field>
<field>
<name>genesis_url</name>
<type>text</type>
<length>512</length>
<comments>Relative to owner documents storage /welcome.odt</comments>
</field>
<field>
<name>genesis_hash</name>
<type>text</type>
<length>128</length>
<notnull>true</notnull>
<comments>To be sure the genesis did not change</comments>
</field>
<field>
<name>file_id</name>
<type>text</type>
<length>512</length>
<comments>Relative to storage e.g. /welcome.odt</comments>
</field>
<field>
<name>owner</name>
<type>text</type>
<notnull>true</notnull>
<length>64</length>
<comments>oC user who created the session</comments>
</field>
<index>
<name>richdocuments_session_ei_idx</name>
<primary>true</primary>
<unique>true</unique>
<field>
<name>es_id</name>
<sorting>ascending</sorting>
</field>
</index>
</declaration>
</table>
<table>
<name>*dbprefix*richdocuments_member</name>
<declaration>
@ -68,13 +17,6 @@
<length>4</length>
<comments>Unique per user and session</comments>
</field>
<field>
<name>es_id</name>
<type>text</type>
<notnull>true</notnull>
<length>64</length>
<comments>Related editing session id</comments>
</field>
<field>
<name>uid</name>
<type>text</type>
@ -118,162 +60,6 @@
</declaration>
</table>
<table>
<name>*dbprefix*richdocuments_op</name>
<declaration>
<field>
<name>seq</name>
<type>integer</type>
<notnull>true</notnull>
<autoincrement>1</autoincrement>
<unsigned>true</unsigned>
<length>4</length>
<comments>Sequence number</comments>
</field>
<field>
<name>es_id</name>
<type>text</type>
<notnull>true</notnull>
<length>64</length>
<comments>Editing session id</comments>
</field>
<field>
<name>member</name>
<type>integer</type>
<default>1</default>
<notnull>true</notnull>
<unsigned>true</unsigned>
<length>4</length>
<comments>User and time specific</comments>
</field>
<field>
<name>optype</name>
<type>text</type>
<notnull>false</notnull>
<length>64</length>
<comments>Operation type</comments>
</field>
<field>
<name>opspec</name>
<type>clob</type>
<notnull>false</notnull>
<comments>json-string</comments>
</field>
<index>
<name>richdocs_seq_pKey</name>
<primary>true</primary>
<field>
<name>seq</name>
<sorting>ascending</sorting>
</field>
</index>
<index>
<name>richdocuments_op_eis_idx</name>
<unique>true</unique>
<field>
<name>es_id</name>
<sorting>ascending</sorting>
</field>
<field>
<name>seq</name>
<sorting>ascending</sorting>
</field>
</index>
</declaration>
</table>
<table>
<name>*dbprefix*richdocuments_invite</name>
<declaration>
<field>
<name>es_id</name>
<type>text</type>
<notnull>true</notnull>
<length>64</length>
<comments>Related editing session id</comments>
</field>
<field>
<name>uid</name>
<type>text</type>
<length>64</length>
</field>
<field>
<name>status</name>
<type>integer</type>
<default>0</default>
<length>2</length>
</field>
<field>
<name>sent_on</name>
<type>integer</type>
<default></default>
<notnull>false</notnull>
<unsigned>true</unsigned>
<length>4</length>
</field>
</declaration>
</table>
<table>
<name>*dbprefix*richdocuments_revisions</name>
<declaration>
<field>
<name>es_id</name>
<type>text</type>
<notnull>true</notnull>
<length>64</length>
<comments>Related editing session id</comments>
</field>
<field>
<name>seq_head</name>
<type>integer</type>
<notnull>true</notnull>
<unsigned>true</unsigned>
<length>4</length>
<comments>Sequence head number</comments>
</field>
<field>
<name>member_id</name>
<type>integer</type>
<notnull>true</notnull>
<unsigned>true</unsigned>
<length>4</length>
<comments>the member that saved the revision</comments>
</field>
<field>
<name>file_id</name>
<type>text</type>
<length>512</length>
<comments>Relative to storage e.g. /welcome.odt</comments>
</field>
<field>
<name>save_hash</name>
<type>text</type>
<length>128</length>
<notnull>true</notnull>
<comments>used to lookup revision in documents folder of member, eg hash.odt</comments>
</field>
<index>
<name>richdocuments_rev_eis_idx</name>
<unique>true</unique>
<field>
<name>es_id</name>
<sorting>ascending</sorting>
</field>
<field>
<name>seq_head</name>
<sorting>ascending</sorting>
</field>
</index>
</declaration>
</table>
<table>
<name>*dbprefix*richdocuments_wopi</name>
<declaration>

@ -5,7 +5,7 @@
<description>Collabora Online allows you to to work with all kinds of office documents directly in your browser. This application requires Collabora Cloudsuite to be installed on one of your servers, please read the documentation to learn more about that.</description>
<summary>Edit office documents directly in your browser.</summary>
<licence>AGPL</licence>
<version>1.1.17</version>
<version>1.1.18</version>
<author>Collabora Productivity based on work of Frank Karlitschek, Victor Dubiniuk</author>
<bugs>https://github.com/nextcloud/richdocuments/issues</bugs>
<repository type="git">https://github.com/nextcloud/richdocuments.git</repository>

@ -9,38 +9,23 @@
* later.
*/
namespace OCA\Richdocuments;
namespace OCA\Richdocuments\AppInfo;
$application = new \OCA\Richdocuments\AppInfo\Application();
$application->registerRoutes($this, [
return [
'routes' => [
//users
['name' => 'user#rename', 'url' => 'ajax/user/rename', 'verb' => 'POST'],
['name' => 'user#disconnectUser', 'url' => 'ajax/user/disconnect', 'verb' => 'POST'],
['name' => 'user#disconnectGuest', 'url' => 'ajax/user/disconnectGuest', 'verb' => 'POST'],
//session
['name' => 'session#join', 'url' => 'session/user/join/{fileId}', 'verb' => 'POST'],
['name' => 'session#poll', 'url' => 'session/user/poll', 'verb' => 'POST'],
['name' => 'session#save', 'url' => 'session/user/save', 'verb' => 'POST'],
['name' => 'session#joinAsGuest', 'url' => 'session/guest/join/{token}', 'verb' => 'POST'],
['name' => 'session#pollAsGuest', 'url' => 'session/guest/poll/{token}', 'verb' => 'POST'],
['name' => 'session#saveAsGuest', 'url' => 'session/guest/save/{token}', 'verb' => 'POST'],
//documents
['name' => 'document#index', 'url' => 'index', 'verb' => 'GET'],
['name' => 'document#create', 'url' => 'ajax/documents/create', 'verb' => 'POST'],
['name' => 'document#serve', 'url' => 'ajax/genesis/{esId}', 'verb' => 'GET'],
['name' => 'document#rename', 'url' => 'ajax/documents/rename/{fileId}', 'verb' => 'POST'],
['name' => 'document#get', 'url' => 'ajax/documents/get/{fileId}', 'verb' => 'GET'],
['name' => 'document#listAll', 'url' => 'ajax/documents/list', 'verb' => 'GET'],
['name' => 'document#download', 'url' => 'ajax/download.php', 'verb' => 'GET'],
//documents - for WOPI access
['name' => 'document#wopiGetToken', 'url' => 'wopi/token/{fileId}', 'verb' => 'GET'],
['name' => 'document#wopiCheckFileInfo', 'url' => 'wopi/files/{fileId}', 'verb' => 'GET'],
['name' => 'document#wopiGetFile', 'url' => 'wopi/files/{fileId}/contents', 'verb' => 'GET'],
['name' => 'document#wopiPutFile', 'url' => 'wopi/files/{fileId}/contents', 'verb' => 'POST'],
// WOPI access
['name' => 'wopi#getToken', 'url' => 'wopi/token/{fileId}', 'verb' => 'GET'],
['name' => 'wopi#checkFileInfo', 'url' => 'wopi/files/{fileId}', 'verb' => 'GET'],
['name' => 'wopi#getFile', 'url' => 'wopi/files/{fileId}/contents', 'verb' => 'GET'],
['name' => 'wopi#putFile', 'url' => 'wopi/files/{fileId}/contents', 'verb' => 'POST'],
//settings
['name' => 'settings#setSettings', 'url' => 'ajax/admin.php', 'verb' => 'POST'],
['name' => 'settings#getSupportedMimes', 'url' => 'ajax/mimes.php', 'verb' => 'GET'],
['name' => 'settings#getSettings', 'url' => 'ajax/settings.php', 'verb' => 'GET'],
]
]);
];

@ -1,753 +0,0 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2014 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This file is licensed under the Affero General Public License version 3 or
* later.
*/
namespace OCA\Richdocuments\Controller;
use \OCP\AppFramework\Controller;
use \OCP\IRequest;
use \OCP\IConfig;
use \OCP\IL10N;
use \OCP\AppFramework\Http\ContentSecurityPolicy;
use \OCP\AppFramework\Http\JSONResponse;
use \OCP\AppFramework\Http\TemplateResponse;
use \OCA\Richdocuments\AppConfig;
use \OCA\Richdocuments\Db;
use \OCA\Richdocuments\Helper;
use \OCA\Richdocuments\Storage;
use \OCA\Richdocuments\Download;
use \OCA\Richdocuments\DownloadResponse;
use \OCA\Richdocuments\File;
use \OCA\Richdocuments\Genesis;
use \OC\Files\View;
use \OCP\ICacheFactory;
use \OCP\ILogger;
class ResponseException extends \Exception {
private $hint;
public function __construct($description, $hint = '') {
parent::__construct($description);
$this->hint = $hint;
}
public function getHint() {
return $this->hint;
}
}
class DocumentController extends Controller {
private $uid;
private $l10n;
private $settings;
private $appConfig;
private $cache;
private $logger;
const ODT_TEMPLATE_PATH = '/assets/odttemplate.odt';
public function __construct($appName, IRequest $request, IConfig $settings, AppConfig $appConfig, IL10N $l10n, $uid, ICacheFactory $cache, ILogger $logger){
parent::__construct($appName, $request);
$this->uid = $uid;
$this->l10n = $l10n;
$this->settings = $settings;
$this->appConfig = $appConfig;
$this->cache = $cache->create($appName);
$this->logger = $logger;
}
/**
* @param \SimpleXMLElement $discovery
* @param string $mimetype
*/
private function getWopiSrcUrl($discovery_parsed, $mimetype) {
if(is_null($discovery_parsed) || $discovery_parsed == false) {
return null;
}
$result = $discovery_parsed->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype));
if ($result && count($result) > 0) {
return array(
'urlsrc' => (string)$result[0]['urlsrc'],
'action' => (string)$result[0]['name']
);
}
return null;
}
/**
* Log the user with given $userid.
* This function should only be used from public controller methods where no
* existing session exists, for example, when loolwsd is directly calling a
* public method with its own access token. After validating the access
* token, and retrieving the correct user with help of access token, it can
* be set as current user with help of this method.
*
* @param string $userid
*/
private function loginUser($userid) {
\OC_Util::tearDownFS();
$users = \OC::$server->getUserManager()->search($userid, 1, 0);
if (count($users) > 0) {
$user = array_shift($users);
if (strcasecmp($user->getUID(), $userid) === 0) {
// clear the existing sessions, if any
\OC::$server->getSession()->close();
// initialize a dummy memory session
$session = new \OC\Session\Memory('');
// wrap it
$cryptoWrapper = \OC::$server->getSessionCryptoWrapper();
$session = $cryptoWrapper->wrapSession($session);
// set our session
\OC::$server->setSession($session);
\OC::$server->getUserSession()->setUser($user);
}
}
\OC_Util::setupFS();
}
/**
* Log out the current user
* This is helpful when we are artifically logged in as someone
*/
private function logoutUser() {
\OC_Util::tearDownFS();
\OC::$server->getSession()->close();
}
private function responseError($message, $hint = ''){
$errors = array('errors' => array(array('error' => $message, 'hint' => $hint)));
$response = new TemplateResponse('', 'error', $errors, 'error');
return $response;
}
/**
* Return the original wopi url or test wopi url
*/
private function getWopiUrl($tester) {
$wopiurl = '';
if ($tester) {
$wopiurl = $this->appConfig->getAppValue('test_wopi_url');
} else {
$wopiurl = $this->appConfig->getAppValue('wopi_url');
}
return $wopiurl;
}
/**
* Return true if the currently logged in user is a tester.
* This depends on whether current user is the member of one of the groups
* mentioned in settings (test_server_groups)
*/
private function isTester() {
$tester = false;
$user = \OC::$server->getUserSession()->getUser()->getUID();
$testgroups = array_filter(explode('|', $this->appConfig->getAppValue('test_server_groups')));
\OC::$server->getLogger()->debug('Testgroups are {testgroups}', [
'app' => $this->appName,
'testgroups' => $testgroups
]);
foreach ($testgroups as $testgroup) {
$test = \OC::$server->getGroupManager()->get($testgroup);
if ($test !== null && sizeof($test->searchUsers($user)) > 0) {
\OC::$server->getLogger()->debug('User {user} found in {group}', [
'app' => $this->appName,
'user' => $user,
'group' => $testgroup
]);
$tester = true;
break;
}
}
return $tester;
}
/** Return the content of discovery.xml - either from cache, or download it.
*/
private function getDiscovery(){
\OC::$server->getLogger()->debug('getDiscovery(): Getting discovery.xml from the cache.');
$tester = $this->isTester();
$wopiRemote = $this->getWopiUrl($tester);
$discoveryKey = 'discovery.xml';
if ($tester) {
$discoveryKey = 'discovery.xml_test';
}
// Provides access to information about the capabilities of a WOPI client
// and the mechanisms for invoking those abilities through URIs.
$wopiDiscovery = $wopiRemote . '/hosting/discovery';
// Read the memcached value (if the memcache is installed)
$discovery = $this->cache->get($discoveryKey);
if (is_null($discovery)) {
$contact_admin = $this->l10n->t('Please contact the "%s" administrator.', array($wopiRemote));
try {
$wopiClient = \OC::$server->getHTTPClientService()->newClient();
$discovery = $wopiClient->get($wopiDiscovery)->getBody();
}
catch (\Exception $e) {
$error_message = $e->getMessage();
if (preg_match('/^cURL error ([0-9]*):/', $error_message, $matches)) {
$admin_check = $this->l10n->t('Please ask your administrator to check the Collabora Online server setting. The exact error message was: ') . $error_message;
$curl_error = $matches[1];
switch ($curl_error) {
case '1':
throw new ResponseException($this->l10n->t('Collabora Online: The protocol specified in "%s" is not allowed.', array($wopiRemote)), $admin_check);
case '3':
throw new ResponseException($this->l10n->t('Collabora Online: Malformed URL "%s".', array($wopiRemote)), $admin_check);
case '6':
throw new ResponseException($this->l10n->t('Collabora Online: Cannot resolve the host "%s".', array($wopiRemote)), $admin_check);
case '7':
throw new ResponseException($this->l10n->t('Collabora Online: Cannot connect to the host "%s".', array($wopiRemote)), $admin_check);
case '60':
throw new ResponseException($this->l10n->t('Collabora Online: SSL certificate is not installed.'), $this->l10n->t('Please ask your administrator to add ca-chain.cert.pem to the ca-bundle.crt, for example "cat /etc/loolwsd/ca-chain.cert.pem >> <server-installation>/resources/config/ca-bundle.crt" . The exact error message was: ') . $error_message);
}
}
throw new ResponseException($this->l10n->t('Collabora Online unknown error: ') . $error_message, $contact_admin);
}
if (!$discovery) {
throw new ResponseException($this->l10n->t('Collabora Online: Unable to read discovery.xml from "%s".', array($wopiRemote)), $contact_admin);
}
\OC::$server->getLogger()->debug('Storing the discovery.xml under key ' . $discoveryKey . ' to the cache.');
$this->cache->set($discoveryKey, $discovery, 3600);
}
return $discovery;
}
/** Prepare document(s) structure
*/
private function prepareDocuments($rawDocuments){
$discovery_parsed = null;
try {
$discovery = $this->getDiscovery();
$loadEntities = libxml_disable_entity_loader(true);
$discovery_parsed = simplexml_load_string($discovery);
libxml_disable_entity_loader($loadEntities);
if ($discovery_parsed === false) {
$this->cache->remove('discovery.xml');
$wopiRemote = $this->getWopiUrl($this->isTester());
return array(
'status' => 'error',
'message' => $this->l10n->t('Collabora Online: discovery.xml from "%s" is not a well-formed XML string.', array($wopiRemote)),
'hint' => $this->l10n->t('Please contact the "%s" administrator.', array($wopiRemote))
);
}
}
catch (ResponseException $e) {
return array(
'status' => 'error',
'message' => $e->getMessage(),
'hint' => $e->getHint()
);
}
$fileIds = array();
$documents = array();
$lolang = strtolower(str_replace('_', '-', $this->settings->getUserValue($this->uid, 'core', 'lang', 'en')));
foreach ($rawDocuments as $key=>$document) {
if (is_object($document)){
$documents[] = $document->getData();
} else {
$documents[$key] = $document;
}
$documents[$key]['icon'] = preg_replace('/\.png$/', '.svg', \OCP\Template::mimetype_icon($document['mimetype']));
$documents[$key]['hasPreview'] = \OC::$server->getPreviewManager()->isMimeSupported($document['mimetype']);
$ret = $this->getWopiSrcUrl($discovery_parsed, $document['mimetype']);
$documents[$key]['urlsrc'] = $ret['urlsrc'];
$documents[$key]['action'] = $ret['action'];
$documents[$key]['lolang'] = $lolang;
$fileIds[] = $document['fileid'];
}
usort($documents, function($a, $b){
return @$b['mtime']-@$a['mtime'];
});
$session = new Db\Session();
$sessions = $session->getCollectionBy('file_id', $fileIds);
$members = array();
$member = new Db\Member();
foreach ($sessions as $session) {
$members[$session['es_id']] = $member->getActiveCollection($session['es_id']);
}
return array(
'status' => 'success', 'documents' => $documents,'sessions' => $sessions,'members' => $members
);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*/
public function index(){
$wopiRemote = $this->getWopiUrl($this->isTester());
if (($parts = parse_url($wopiRemote)) && isset($parts['scheme']) && isset($parts['host'])) {
$webSocketProtocol = "ws://";
if ($parts['scheme'] == "https") {
$webSocketProtocol = "wss://";
}
$webSocket = sprintf(
"%s%s%s",
$webSocketProtocol,
$parts['host'],
isset($parts['port']) ? ":" . $parts['port'] : "");
}
else {
return $this->responseError($this->l10n->t('Collabora Online: Invalid URL "%s".', array($wopiRemote)), $this->l10n->t('Please ask your administrator to check the Collabora Online server setting.'));
}
$user = \OC::$server->getUserSession()->getUser();
$usergroups = array_filter(\OC::$server->getGroupManager()->getUserGroupIds($user));
$usergroups = join('|', $usergroups);
\OC::$server->getLogger()->debug('User is in groups: {groups}', [ 'app' => $this->appName, 'groups' => $usergroups ]);
\OC::$server->getNavigationManager()->setActiveEntry( 'richdocuments_index' );
$maxUploadFilesize = \OCP\Util::maxUploadFilesize("/");
$response = new TemplateResponse('richdocuments', 'documents', [
'enable_previews' => $this->settings->getSystemValue('enable_previews', true),
'uploadMaxFilesize' => $maxUploadFilesize,
'uploadMaxHumanFilesize' => \OCP\Util::humanFileSize($maxUploadFilesize),
'allowShareWithLink' => $this->settings->getAppValue('core', 'shareapi_allow_links', 'yes'),
'wopi_url' => $webSocket,
'doc_format' => $this->appConfig->getAppValue('doc_format')
]);
$policy = new ContentSecurityPolicy();
$policy->addAllowedScriptDomain('\'self\' http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js http://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.12/jquery.mousewheel.min.js \'unsafe-eval\' ' . $wopiRemote);
/* frame-src is deprecated on Firefox, but Safari wants it! */
$policy->addAllowedFrameDomain('\'self\' http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js http://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.12/jquery.mousewheel.min.js \'unsafe-eval\' ' . $wopiRemote . ' blob:');
$policy->addAllowedChildSrcDomain('\'self\' http://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js http://cdnjs.cloudflare.com/ajax/libs/jquery-mousewheel/3.1.12/jquery.mousewheel.min.js \'unsafe-eval\' ' . $wopiRemote);
$policy->addAllowedConnectDomain($webSocket);
$policy->addAllowedImageDomain('*');
$policy->allowInlineScript(true);
$policy->addAllowedFontDomain('data:');
$response->setContentSecurityPolicy($policy);
return $response;
}
/**
* @NoAdminRequired
*/
public function create(){
$mimetype = $this->request->post['mimetype'];
$filename = $this->request->post['filename'];
$dir = $this->request->post['dir'];
$view = new View('/' . $this->uid . '/files');
if (!$dir){
$dir = '/';
}
$basename = $this->l10n->t('New Document.odt');
switch ($mimetype) {
case 'application/vnd.oasis.opendocument.spreadsheet':
$basename = $this->l10n->t('New Spreadsheet.ods');
break;
case 'application/vnd.oasis.opendocument.presentation':
$basename = $this->l10n->t('New Presentation.odp');
break;
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
$basename = $this->l10n->t('New Document.docx');
break;
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
$basename = $this->l10n->t('New Spreadsheet.xlsx');
break;
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
$basename = $this->l10n->t('New Presentation.pptx');
break;
default:
// to be safe
$mimetype = 'application/vnd.oasis.opendocument.text';
break;
}
if (!$filename){
$path = Helper::getNewFileName($view, $dir . '/' . $basename);
} else {
$path = $dir . '/' . $filename;
}
$content = '';
if (class_exists('\OC\Files\Type\TemplateManager')){
$manager = \OC_Helper::getFileTemplateManager();
$content = $manager->getTemplate($mimetype);
}
if (!$content){
$content = file_get_contents(dirname(__DIR__) . self::ODT_TEMPLATE_PATH);
}
$discovery_parsed = null;
try {
$discovery = $this->getDiscovery();
$loadEntities = libxml_disable_entity_loader(true);
$discovery_parsed = simplexml_load_string($discovery);
libxml_disable_entity_loader($loadEntities);
if ($discovery_parsed === false) {
$this->cache->remove('discovery.xml');
$wopiRemote = $this->getWopiUrl($this->isTester());
return array(
'status' => 'error',
'message' => $this->l10n->t('Collabora Online: discovery.xml from "%s" is not a well-formed XML string.', array($wopiRemote)),
'hint' => $this->l10n->t('Please contact the "%s" administrator.', array($wopiRemote))
);
}
}
catch (ResponseException $e) {
return array(
'status' => 'error',
'message' => $e->getMessage(),
'hint' => $e->getHint()
);
}
if ($content && $view->file_put_contents($path, $content)){
$info = $view->getFileInfo($path);
$ret = $this->getWopiSrcUrl($discovery_parsed, $mimetype);
$response = array(
'status' => 'success',
'fileid' => $info['fileid'],
'urlsrc' => $ret['urlsrc'],
'action' => $ret['action'],
'lolang' => $this->settings->getUserValue($this->uid, 'core', 'lang', 'en'),
'data' => \OCA\Files\Helper::formatFileInfo($info)
);
} else {
$response = array(
'status' => 'error',
'message' => (string) $this->l10n->t('Can\'t create document')
);
}
return $response;
}
/**
* @NoAdminRequired
* Generates and returns an access token for a given fileId.
* Only for authenticated users!
*/
public function wopiGetToken($fileId){
$arr = explode('_', $fileId, 2);
$version = '0';
if (count($arr) == 2) {
$fileId = $arr[0];
$version = $arr[1];
}
\OC::$server->getLogger()->debug('Generating WOPI Token for file {fileId}, version {version}.', [ 'app' => $this->appName, 'fileId' => $fileId, 'version' => $version ]);
$view = \OC\Files\Filesystem::getView();
$path = $view->getPath($fileId);
$updatable = (bool)$view->isUpdatable($path);
// Check if the editor (user who is accessing) is in editable group
// UserCanWrite only if
// 1. No edit groups are set or
// 2. if they are set, it is in one of the edit groups
$editorUid = \OC::$server->getUserSession()->getUser()->getUID();
$editGroups = array_filter(explode('|', $this->appConfig->getAppValue('edit_groups')));
if ($updatable && count($editGroups) > 0) {
$updatable = false;
foreach($editGroups as $editGroup) {
$editorGroup = \OC::$server->getGroupManager()->get($editGroup);
if ($editorGroup !== null && sizeof($editorGroup->searchUsers($editorUid)) > 0) {
\OC::$server->getLogger()->debug("Editor {editor} is in edit group {group}", [
'app' => $this->appName,
'editor' => $editorUid,
'group' => $editGroup
]);
$updatable = true;
break;
}
}
}
// If token is for some versioned file
if ($version !== '0') {
\OC::$server->getLogger()->debug('setting updatable to false');
$updatable = false;
}
\OC::$server->getLogger()->debug('File with {fileid} has updatable set to {updatable}', [ 'app' => $this->appName, 'fileid' => $fileId, 'updatable' => $updatable ]);
$row = new Db\Wopi();
$serverHost = $this->request->getServerProtocol() . '://' . $this->request->getServerHost();
$token = $row->generateFileToken($fileId, $version, $updatable, $serverHost);
// Return the token.
return array(
'status' => 'success',
'token' => $token
);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* Returns general info about a file.
*/
public function wopiCheckFileInfo($fileId){
$token = $this->request->getParam('access_token');
$arr = explode('_', $fileId, 2);
$version = '0';
if (count($arr) == 2) {
$fileId = $arr[0];
$version = $arr[1];
}
\OC::$server->getLogger()->debug('Getting info about file {fileId}, version {version} by token {token}.', [ 'app' => $this->appName, 'fileId' => $fileId, 'version' => $version, 'token' => $token ]);
$row = new Db\Wopi();
$row->loadBy('token', $token);
$res = $row->getPathForToken($fileId, $version, $token);
if ($res == false || http_response_code() != 200)
{
return false;
}
// Login the user to see his mount locations
$this->loginUser($res['owner']);
$view = new \OC\Files\View('/' . $res['owner'] . '/files');
$info = $view->getFileInfo($res['path']);
$this->logoutUser();
if (!$info) {
http_response_code(404);
return false;
}
$editorName = \OC::$server->getUserManager()->get($res['editor'])->getDisplayName();
return array(
'BaseFileName' => $info['name'],
'Size' => $info['size'],
'Version' => $version,
'UserId' => $res['editor'],
'UserFriendlyName' => $editorName,
'UserCanWrite' => $res['canwrite'] ? true : false,
'PostMessageOrigin' => $res['server_host']
);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* Given an access token and a fileId, returns the contents of the file.
* Expects a valid token in access_token parameter.
*/
public function wopiGetFile($fileId){
$token = $this->request->getParam('access_token');
$arr = explode('_', $fileId, 2);
$version = '0';
if (count($arr) == 2) {
$fileId = $arr[0];
$version = $arr[1];
}
\OC::$server->getLogger()->debug('Getting contents of file {fileId}, version {version} by token {token}.', [ 'app' => $this->appName, 'fileId' => $fileId, 'version' => $version, 'token' => $token ]);
$row = new Db\Wopi();
$row->loadBy('token', $token);
//TODO: Support X-WOPIMaxExpectedSize header.
$res = $row->getPathForToken($fileId, $version, $token);
$ownerid = $res['owner'];
// Login the user to see his mount locations
$this->loginUser($ownerid);
$view = new \OC\Files\View('/' . $res['owner'] . '/files');
$info = $view->getFileInfo($res['path']);
if (!$info) {
http_response_code(404);
return false;
}
$filename = '';
// If some previous version is requested, fetch it from Files_Version app
if ($version !== '0') {
\OCP\JSON::checkAppEnabled('files_versions');
$filename = '/files_versions/' . $info['name'] . '.v' . $version;
} else {
$filename = '/files' . $res['path'];
}
$this->logoutUser();
return new DownloadResponse($this->request, $ownerid, $filename);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
* Given an access token and a fileId, replaces the files with the request body.
* Expects a valid token in access_token parameter.
*/
public function wopiPutFile($fileId){
$token = $this->request->getParam('access_token');
$arr = explode('_', $fileId, 2);
$version = '0';
if (count($arr) == 2) {
$fileId = $arr[0];
$version = $arr[1];
}
\OC::$server->getLogger()->debug('Putting contents of file {fileId}, version {version} by token {token}.', [ 'app' => $this->appName, 'fileId' => $fileId, 'version' => $version, 'token' => $token ]);
$row = new Db\Wopi();
$row->loadBy('token', $token);
$res = $row->getPathForToken($fileId, $version, $token);
if (!$res['canwrite']) {
return array(
'status' => 'error',
'message' => 'Permission denied'
);
}
// Log-in as the user to regiser the change under her name.
$editorid = $res['editor'];
// This call is made from loolwsd, so we need to initialize the
// session before we can make the user who opened the document
// login. This is necessary to make activity app register the
// change made to this file under this user's (editorid) name.
$this->loginUser($editorid);
// Set up the filesystem view for the owner (where the file actually is).
$userid = $res['owner'];
$root = '/' . $userid . '/files';
$view = new \OC\Files\View($root);
// Read the contents of the file from the POST body and store.
$content = fopen('php://input', 'r');
\OC::$server->getLogger()->debug('Storing file {fileId} by {editor} owned by {owner}.', [ 'app' => $this->appName, 'fileId' => $fileId, 'editor' => $editorid, 'owner' => $userid ]);
// Setup the FS which is needed to emit hooks (versioning).
\OC_Util::tearDownFS();
\OC_Util::setupFS($userid);
$view->file_put_contents($res['path'], $content);
$this->logoutUser();
return array(
'status' => 'success'
);
}
/**
* @NoAdminRequired
* @PublicPage
* Process partial/complete file download
*/
public function serve($esId){
$session = new Db\Session();
$session->load($esId);
$filename = $session->getGenesisUrl() ? $session->getGenesisUrl() : '';
return new DownloadResponse($this->request, $session->getOwner(), $filename);
}
/**
* @NoAdminRequired
*/
public function download($path){
if (!$path){
$response = new JSONResponse();
$response->setStatus(Http::STATUS_BAD_REQUEST);
return $response;
}
$fullPath = '/files' . $path;
$fileInfo = \OC\Files\Filesystem::getFileInfo($path);
if ($fileInfo){
$file = new File($fileInfo->getId());
$genesis = new Genesis($file);
$fullPath = $genesis->getPath();
}
return new DownloadResponse($this->request, $this->uid, $fullPath);
}
/**
* @NoAdminRequired
*/
public function rename($fileId){
$name = $this->request->post['name'];
$view = \OC\Files\Filesystem::getView();
$path = $view->getPath($fileId);
if ($name && $view->is_file($path) && $view->isUpdatable($path)) {
$newPath = dirname($path) . '/' . $name;
if ($view->rename($path, $newPath)) {
return array('status' => 'success');
}
}
return array(
'status' => 'error',
'message' => (string) $this->l10n->t('You don\'t have permission to rename this document')
);
}
/**
* @NoAdminRequired
* Get file information about single document with fileId
*/
public function get($fileId){
$documents = array();
$documents[0] = Storage::getDocumentById($fileId);
return $this->prepareDocuments($documents);
}
/**
* @NoAdminRequired
* lists the documents the user has access to (including shared files, once the code in core has been fixed)
* also adds session and member info for these files
*/
public function listAll(){
return $this->prepareDocuments(Storage::getDocuments());
}
}

@ -1,314 +0,0 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2014 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This file is licensed under the Affero General Public License version 3 or
* later.
*/
namespace OCA\Richdocuments\Controller;
use \OCP\AppFramework\Controller;
use \OCP\IRequest;
use \OCP\AppFramework\Http;
use \OCP\AppFramework\Http\JSONResponse;
use \OCA\Richdocuments\Db;
use \OCA\Richdocuments\File;
use \OCA\Richdocuments\Helper;
use OCA\Richdocuments\Filter;
use \OC\Files\View;
class BadRequestException extends \Exception {
protected $body = "";
public function setBody($body){
$this->body = $body;
}
public function getBody(){
return $this->body;
}
}
class SessionController extends Controller{
protected $uid;
protected $logger;
protected $shareToken;
public function __construct($appName, IRequest $request, $logger, $uid){
parent::__construct($appName, $request);
$this->uid = $uid;
$this->logger = $logger;
}
/**
* @NoAdminRequired
* @PublicPage
*/
public function joinAsGuest($token, $name){
$uid = substr($name, 0, 16);
try {
$file = File::getByShareToken($token);
if ($file->isPasswordProtected() && !$file->checkPassword('')){
throw new \Exception('Not authorized');
}
$response = array_merge(
Db\Session::start($uid, $file),
[ 'status'=>'success' ]
);
} catch (\Exception $e){
$this->logger->warning('Starting a session failed. Reason: ' . $e->getMessage(), ['app' => $this->appName]);
$response = [ 'status'=>'error' ];
}
return $response;
}
/**
* @NoAdminRequired
* @PublicPage
*/
public function pollAsGuest($command, $args){
$this->shareToken = $this->request->getParam('token');
return $this->poll($command, $args);
}
/**
* Store the document content to its origin
* @NoAdminRequired
* @PublicPage
*/
public function saveAsGuest(){
$this->shareToken = $this->request->getParam('token');
return $this->save();
}
/**
* @NoAdminRequired
*/
public function join($fileId){
try {
$view = \OC\Files\Filesystem::getView();
$path = $view->getPath($fileId);
$file = new File($fileId);
$response = Db\Session::start($this->uid, $file);
$response = array_merge(
$response,
[ 'status'=>'success' ]
);
} catch (\Exception $e){
$this->logger->warning('Starting a session failed. Reason: ' . $e->getMessage(), [ 'app' => $this->appName ]);
$response = [ 'status'=>'error' ];
}
return $response;
}
/**
* @NoAdminRequired
*/
public function poll($command, $args){
$response = new JSONResponse();
try{
$esId = isset($args['es_id']) ? $args['es_id'] : null;
$session = $this->loadSession($esId);
$memberId = isset($args['member_id']) ? $args['member_id'] : null;
$member = $this->loadMember($memberId);
$this->validateSession($session);
switch ($command){
case 'sync_ops':
$seqHead = (string) isset($args['seq_head']) ? $args['seq_head'] : null;
if (!is_null($seqHead)){
$ops = isset($args['client_ops']) ? $args['client_ops'] : [];
$op = new Db\Op();
$currentHead = $op->getHeadSeq($esId);
try {
$member->updateActivity($memberId);
} catch (\Exception $e){
//Db error. Not critical
}
$response->setData(
$session->syncOps($memberId, $currentHead, $seqHead, $ops)
);
$inactiveMembers = $member->updateByTimeout($esId);
foreach ($inactiveMembers as $inactive){
$op->removeCursor($esId, $inactive);
$op->removeMember($esId, $inactive);
}
} else {
// Error - no seq_head passed
throw new BadRequestException();
}
break;
default:
$ex = new BadRequestException();
$ex->setBody(
implode(',', $this->request->getParams())
);
throw $ex;
}
} catch (BadRequestException $e){
$response->setStatus(Http::STATUS_BAD_REQUEST);
$response->setData(
[ 'err' => 'bad request:[' . $e->getBody() . ']' ]
);
}
return $response;
}
/**
* Store the document content to its origin
* @NoAdminRequired
*/
public function save(){
$response = new JSONResponse();
try {
$esId = $this->request->server['HTTP_WEBODF_SESSION_ID'];
$session = $this->loadSession($esId);
$memberId = $this->request->server['HTTP_WEBODF_MEMBER_ID'];
$currentMember = $this->loadMember($memberId, $esId);
// Extra info for future usage
// $sessionRevision = $this->request->server['HTTP_WEBODF_SESSION_REVISION'];
//NB ouch! New document content is passed as an input stream content
$stream = fopen('php://input','r');
if (!$stream){
throw new \Exception('New content missing');
}
$content = stream_get_contents($stream);
try {
if ($currentMember->getIsGuest()){
$file = File::getByShareToken($currentMember->getToken());
} else {
$file = new File($session->getFileId());
}
$view = $file->getOwnerView(true);
$path = $file->getPath(true);
} catch (\Exception $e){
//File was deleted or unshared. We need to save content as new file anyway
//Sorry, but for guests it would be lost :(
if ($this->uid){
$view = new View('/' . $this->uid . '/files');
$dir = '/';
$path = Helper::getNewFileName($view, $dir . 'New Document.odt');
} else {
throw $e;
}
}
$member = new Db\Member();
$members = $member->getActiveCollection($esId);
$memberIds = array_map(
function($x){
return ($x['member_id']);
},
$members
);
// Active users except current user
$memberCount = count($memberIds) - 1;
if ($view->file_exists($path)){
$currentHash = $view->hash('sha1', $path, false);
if (!Helper::isVersionsEnabled() && $currentHash !== $session->getGenesisHash()){
// Original file was modified externally. Save to a new one
$path = Helper::getNewFileName($view, $path, '-conflict');
}
$mimetype = $view->getMimeType($path);
} else {
$mimetype = Storage::MIMETYPE_LIBREOFFICE_WORDPROCESSOR;
}
$data = Filter::write($content, $mimetype);
if ($view->file_put_contents($path, $data['content'])){
// Not a last user
if ($memberCount>0){
// Update genesis hash to prevent conflicts
$this->logger->debug('Update hash', [ 'app' => $this->appName ]);
$session->updateGenesisHash($esId, sha1($data['content']));
} else {
// Last user. Kill session data
Db\Session::cleanUp($esId);
}
$view->touch($path);
}
$response->setData(['status'=>'success']);
} catch (\Exception $e){
$response->setStatus(Http::STATUS_INTERNAL_SERVER_ERROR);
$response->setData([]);
$this->logger->warning('Saving failed. Reason:' . $e->getMessage(), [ 'app' => $this->appName ]);
}
return $response;
}
protected function validateSession($session){
try {
if (is_null($this->shareToken)) {
new File($session->getFileId());
} else {
File::getByShareToken($this->shareToken);
}
} catch (\Exception $e){
$this->logger->warning('Error. Session no longer exists. ' . $e->getMessage(), [ 'app' => $this->appName ]);
$ex = new BadRequestException();
$ex->setBody(
implode(',', $this->request->getParams())
);
throw $ex;
}
}
protected function loadSession($esId){
if (!$esId){
throw new \Exception('Session id can not be empty');
}
$session = new Db\Session();
$session->load($esId);
if (!$session->getEsId()){
throw new \Exception('Session does not exist');
}
return $session;
}
protected function loadMember($memberId, $expectedEsId = null){
if (!$memberId){
throw new \Exception('Member id can not be empty');
}
$member = new Db\Member();
$member->load($memberId);
//check if member belongs to the session
if (!is_null($expectedEsId) && $expectedEsId !== $member->getEsId()){
throw new \Exception($memberId . ' does not belong to session ' . $expectedEsId);
}
return $member;
}
}

@ -1,81 +0,0 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2014 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This file is licensed under the Affero General Public License version 3 or
* later.
*/
namespace OCA\Richdocuments\Controller;
use \OCP\AppFramework\Controller;
use \OCP\IRequest;
use \OCP\AppFramework\Http\JSONResponse;
use \OCA\Richdocuments\Db;
class UserController extends Controller {
public function __construct($appName, IRequest $request){
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
*/
public function disconnectUser($memberId, $esId){
return $this->disconnect($memberId, $esId);
}
/**
* @NoAdminRequired
* @PublicPage
*/
public function disconnectGuest($memberId, $esId){
return $this->disconnect($memberId, $esId);
}
private function disconnect($memberId, $esId){
$member = new Db\Member();
$member->loadBy('member_id', $memberId);
if ($esId && $member->hasData()){
if ($member->getEsId() === $esId && $member->getStatus() == Db\Member::MEMBER_STATUS_ACTIVE){
$member->deactivate(array($memberId));
$op = new Db\Op();
$op->removeMember($esId, $memberId);
}
}
return array('status'=>'success');
}
/**
* @NoAdminRequired
* @PublicPage
* @param int $memberId
* @param string $name
*/
public function rename($memberId, $name){
$member = new Db\Member();
$member->load($memberId);
if ($member->getEsId()
&& $member->getStatus() == Db\Member::MEMBER_STATUS_ACTIVE
&& $member->getIsGuest()
){
$guestMark = Db\Member::getGuestPostfix();
if (substr($name, -strlen($guestMark)) !== $guestMark){
$name = $name . ' ' . $guestMark;
}
$op = new Db\Op();
$op->changeNick($member->getEsId(), $memberId, $name);
}
return array('status'=>'success');
}
}

@ -1,170 +0,0 @@
/* Copyright (c) 2011, Jan-Christoph Borchardt, http://jancborchardt.net
This file is licensed under the Affero General Public License version 3 or later.
See the COPYING-README file. */
#dropdown {
background: #eee;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
box-shadow: 0 2px 3px rgba(50, 50, 50, .4);
display: block;
margin-right: 0;
position: absolute;
right: 0;
width: 420px;
z-index: 500;
padding: 16px;
}
@media only screen and (min-width: 768px) and (max-width: 990px) {
#dropdown {
/* this limits the dropdown to float below the sidebar for mid narrow screens */
left: 20px;
}
}
#dropdown.shareDropDown .unshare.icon-loading-small {
margin-top: 1px;
}
#dropdown.shareDropDown .shareWithLoading,
#dropdown.shareDropDown .linkShare .icon-loading-small {
display: inline-block !important;
padding-left: 10px;
}
#dropdown.shareDropDown .shareWithLoading {
position: relative;
right: 70px;
top: 2px;
}
#dropdown.shareDropDown .icon-loading-small.hidden {
display: none !important;
}
#dropdown .shareWithRemoteInfo {
padding: 11px 20px;
}
#dropdown .avatar {
margin-right: 8px;
display: inline-block;
overflow: hidden;
vertical-align: middle;
width: 32px;
height: 32px;
}
#shareWithList {
list-style-type:none;
padding:8px;
}
#shareWithList li {
padding-top: 10px;
padding-bottom: 10px;
font-weight: bold;
line-height: 21px;
white-space: normal;
}
#shareWithList .unshare img, #shareWithList .showCruds img {
vertical-align:text-bottom; /* properly align icons */
}
#shareWithList label input[type=checkbox]{
margin-left: 0;
position: relative;
}
#shareWithList .username{
padding-right: 8px;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 254px;
display: inline-block;
overflow: hidden;
vertical-align: middle;
}
#shareWithList li label{
margin-right: 8px;
}
#dropdown label {
font-weight:400;
white-space: nowrap;
}
#dropdown input[type="checkbox"] {
margin:0 3px 0 8px;
vertical-align: middle;
}
a.showCruds {
display:inline;
opacity:.5;
}
a.unshare {
display:inline;
float:right;
opacity:.5;
padding:5px 0 0 5px !important;
margin-top:-5px;
}
#link {
border-top:1px solid #ddd;
padding-top:8px;
}
#dropdown input[type="text"],#dropdown input[type="password"] {
width: 86%;
margin-left: 7px;
}
#dropdown form {
font-size: 100%;
margin-left: 0;
margin-right: 0;
}
#linkText,#linkPass,#expiration {
display:none;
}
#link #showPassword img {
padding-left:5px;
width:12px;
}
.reshare,#link label,
#expiration label {
display: inline-block;
padding: 6px 4px;
}
a.showCruds:hover,a.unshare:hover {
opacity:1;
}
#defaultExpireMessage, /* fix expire message going out of box */
.reshare { /* fix shared by text going out of box */
white-space:normal;
}
#defaultExpireMessage { /* show message on new line */
display: block;
padding-left: 4px;
/* TODO: style the dropdown in a proper way - border-box, etc. */
width: 90%;
}
.ui-autocomplete { /* limit dropdown height to 4 1/2 entries */
max-height:103px;
overflow-y:auto;
overflow-x:hidden;
}
.notCreatable {
padding-left: 12px;
padding-top: 12px;
color: #999;
}

@ -15,17 +15,6 @@ var documentsSettings = {
);
},
saveGroups: function(groups) {
var data = {
'edit_groups': groups
};
$.post(
OC.filePath('richdocuments', 'ajax', 'admin.php'),
data
);
},
saveDocFormat: function(format) {
$.post(
OC.filePath('richdocuments', 'ajax', 'admin.php'),
@ -33,112 +22,19 @@ var documentsSettings = {
);
},
saveTestWopi: function(groups, server) {
var data = {
'test_wopi_url': server,
'test_server_groups': groups
};
OC.msg.startAction('#test-documents-admin-msg', t('richdocuments', 'Saving...'));
$.post(
OC.filePath('richdocuments', 'ajax', 'admin.php'),
data,
documentsSettings.afterSaveTestWopi
);
},
afterSaveTestWopi: function(response) {
$('#test_wopi_apply').attr('disabled', false);
OC.msg.finishedAction('#test-documents-admin-msg', response);
},
afterSave : function(response){
$('#wopi_apply').attr('disabled', false);
OC.msg.finishedAction('#documents-admin-msg', response);
},
initEditGroups: function() {
var groups = $('#edit_group_select').val();
if (groups !== '') {
OC.Settings.setupGroupsSelect($('#edit_group_select'));
$('.edit-groups-enable').attr('checked', 'checked');
} else {
$('.edit-groups-enable').attr('checked', null);
}
},
initTestWopiServer: function() {
var groups = $(document).find('#test_server_group_select').val();
var testserver = $(document).find('#test_wopi_url').val();
if (groups === '' || testserver === '') {
$('.test-server-enable').attr('checked', null);
} else {
OC.Settings.setupGroupsSelect($('#test_server_group_select'));
$('.test-server-enable').attr('checked', 'checked');
}
},
initialize: function() {
documentsSettings.initEditGroups();
documentsSettings.initTestWopiServer();
$('#wopi_apply').on('click', documentsSettings.save);
$(document).on('change', '.test-server-enable', function() {
var page = $(this).parent();
var $select = page.find('#test_server_group_select');
$select.val('');
page.find('#test-server-section').toggleClass('hidden', !this.checked);
if (this.checked) {
OC.Settings.setupGroupsSelect($select, {
placeholder: t('core', 'None')
});
} else {
$select.select2('destroy');
page.find('#test_wopi_url').val('');
documentsSettings.saveTestWopi('', '');
}
});
$(document).on('click', '#test_wopi_apply', function() {
var groups = $(this).parent().find('#test_server_group_select').val();
var testserver = $(this).parent().find('#test_wopi_url').val();
if (groups !== '' && testserver !== '') {
documentsSettings.saveTestWopi(groups, testserver);
} else {
OC.msg.finishedError('#test-documents-admin-msg', 'Both fields required');
}
});
$(document).on('change', '.doc-format-ooxml', function() {
var ooxml = this.checked;
documentsSettings.saveDocFormat(ooxml ? 'ooxml' : 'odf');
});
$(document).on('change', '#edit_group_select', function() {
var element = $(this).parent().find('input.edit-groups-enable');
var groups = $(this).val();
documentsSettings.saveGroups(groups);
});
$(document).on('change', '.edit-groups-enable', function() {
var $select = $(this).parent().find('#edit_group_select');
$select.val('');
if (this.checked) {
OC.Settings.setupGroupsSelect($select, {
placeholder: t('core', 'All')
});
} else {
$select.select2('destroy');
}
$select.change();
});
}
};

@ -18,40 +18,11 @@ $.widget('oc.documentGrid', {
},
_load : function (fileId){
var that = this;
var url = 'apps/richdocuments/ajax/documents/list';
var dataObj = {};
if (fileId){
url = 'apps/richdocuments/ajax/documents/get/{fileId}';
dataObj = { fileId: fileId };
}
return $.getJSON(OC.generateUrl(url, dataObj))
.done(function (result) {
if (!result || result.status === 'error') {
documentsMain.loadError = true;
if (result && result.message) {
documentsMain.loadErrorMessage = result.message;
}
else {
documentsMain.loadErrorMessage = t('richdocuments', 'Failed to load the document, please contact your administrator.');
}
if (result && result.hint) {
documentsMain.loadErrorHint = result.hint;
}
}
else {
that.options.documents = result.documents;
that.options.sessions = result.sessions;
that.options.members = result.members;
documentsMain.urlsrc = result.documents[0].urlsrc;
documentsMain.fullPath = result.documents[0].path;
}
})
.fail(function(data){
console.log(t('richdocuments','Failed to load documents.'));
});
var url = OC.generateUrl('apps/richdocuments/wopi/token/{file_id}', {file_id: fileId});
$.get(
url,
documentsMain.initSession
);
},
_render : function (data){
@ -152,39 +123,36 @@ var documentsMain = {
$('#revViewerContainer').prepend($('<div id="revViewer">'));
}
$.get(OC.generateUrl('apps/richdocuments/wopi/token/{fileId}', { fileId: fileId }),
function (result) {
// WOPISrc - URL that loolwsd will access (ie. pointing to ownCloud)
var wopiurl = window.location.protocol + '//' + window.location.host + OC.generateUrl('apps/richdocuments/wopi/files/{file_id}', {file_id: fileId});
var wopisrc = encodeURIComponent(wopiurl);
// urlsrc - the URL from discovery xml that we access for the particular
// document; we add various parameters to that.
// The discovery is available at
// https://<loolwsd-server>:9980/hosting/discovery
var urlsrc = documentsMain.urlsrc +
"WOPISrc=" + wopisrc +
"&title=" + encodeURIComponent(title) +
"&lang=" + OC.getLocale() +
"&permission=readonly";
// access_token - must be passed via a form post
var access_token = encodeURIComponent(result.token);
// form to post the access token for WOPISrc
var form = '<form id="loleafletform_viewer" name="loleafletform_viewer" target="loleafletframe_viewer" action="' + urlsrc + '" method="post">' +
'<input name="access_token" value="' + access_token + '" type="hidden"/></form>';
// iframe that contains the Collabora Online Viewer
var frame = '<iframe id="loleafletframe_viewer" name= "loleafletframe_viewer" style="width:100%;height:100%;position:absolute;"/>';
$('#revViewer').append(form);
$('#revViewer').append(frame);
// submit that
$('#loleafletform_viewer').submit();
documentsMain.isViewerMode = true;
});
// WOPISrc - URL that loolwsd will access (ie. pointing to ownCloud)
var wopiurl = window.location.protocol + '//' + window.location.host + OC.generateUrl('apps/richdocuments/wopi/files/{file_id}', {file_id: fileId});
var wopisrc = encodeURIComponent(wopiurl);
// urlsrc - the URL from discovery xml that we access for the particular
// document; we add various parameters to that.
// The discovery is available at
// https://<loolwsd-server>:9980/hosting/discovery
var urlsrc = documentsMain.urlsrc +
"WOPISrc=" + wopisrc +
"&title=" + encodeURIComponent(title) +
"&lang=" + OC.getLocale() +
"&permission=readonly";
// access_token - must be passed via a form post
var access_token = encodeURIComponent(documentsMain.token);
// form to post the access token for WOPISrc
var form = '<form id="loleafletform_viewer" name="loleafletform_viewer" target="loleafletframe_viewer" action="' + urlsrc + '" method="post">' +
'<input name="access_token" value="' + access_token + '" type="hidden"/></form>';
// iframe that contains the Collabora Online Viewer
var frame = '<iframe id="loleafletframe_viewer" name= "loleafletframe_viewer" style="width:100%;height:100%;position:absolute;"/>';
$('#revViewer').append(form);
$('#revViewer').append(frame);
// submit that
$('#loleafletform_viewer').submit();
documentsMain.isViewerMode = true;
// for closing revision mode
$('#revPanelHeader .closeButton').click(function(e) {
@ -344,91 +312,81 @@ var documentsMain = {
$('title').text(title + ' - ' + documentsMain.UI.mainTitle);
$.get(OC.generateUrl('apps/richdocuments/wopi/token/{fileId}', { fileId: fileId }),
function (result) {
if (!result || result.status === 'error') {
if (result && result.message){
documentsMain.UI.notify(result.message);
}
documentsMain.onEditorShutdown(t('richdocuments', 'Failed to aquire access token. Please re-login and try again.'));
return;
}
// WOPISrc - URL that loolwsd will access (ie. pointing to ownCloud)
var wopiurl = window.location.protocol + '//' + window.location.host + OC.generateUrl('apps/richdocuments/wopi/files/{file_id}', {file_id: documentsMain.fileId});
var wopisrc = encodeURIComponent(wopiurl);
// urlsrc - the URL from discovery xml that we access for the particular
// document; we add various parameters to that.
// The discovery is available at
// https://<loolwsd-server>:9980/hosting/discovery
var urlsrc = documentsMain.urlsrc +
"WOPISrc=" + wopisrc +
"&title=" + encodeURIComponent(title) +
"&lang=" + OC.getLocale() +
"&closebutton=1" +
"&revisionhistory=1";
if (!documentsMain.canEdit || action === "view") {
urlsrc += "&permission=readonly";
}
// WOPISrc - URL that loolwsd will access (ie. pointing to ownCloud)
var wopiurl = window.location.protocol + '//' + window.location.host + OC.generateUrl('apps/richdocuments/wopi/files/{file_id}', {file_id: documentsMain.fileId});
var wopisrc = encodeURIComponent(wopiurl);
// urlsrc - the URL from discovery xml that we access for the particular
// document; we add various parameters to that.
// The discovery is available at
// https://<loolwsd-server>:9980/hosting/discovery
var urlsrc = documentsMain.urlsrc +
"WOPISrc=" + wopisrc +
"&title=" + encodeURIComponent(title) +
"&lang=" + OC.getLocale() +
"&closebutton=1" +
"&revisionhistory=1";
if (!documentsMain.canEdit || action === "view") {
urlsrc += "&permission=readonly";
}
// access_token - must be passed via a form post
var access_token = encodeURIComponent(result.token);
// access_token - must be passed via a form post
var access_token = encodeURIComponent(documentsMain.token);
// form to post the access token for WOPISrc
var form = '<form id="loleafletform" name="loleafletform" target="loleafletframe" action="' + urlsrc + '" method="post">' +
'<input name="access_token" value="' + access_token + '" type="hidden"/></form>';
// form to post the access token for WOPISrc
var form = '<form id="loleafletform" name="loleafletform" target="loleafletframe" action="' + urlsrc + '" method="post">' +
'<input name="access_token" value="' + access_token + '" type="hidden"/></form>';
// iframe that contains the Collabora Online
var frame = '<iframe id="loleafletframe" name= "loleafletframe" allowfullscreen style="width:100%;height:100%;position:absolute;" />';
// iframe that contains the Collabora Online
var frame = '<iframe id="loleafletframe" name= "loleafletframe" allowfullscreen style="width:100%;height:100%;position:absolute;" />';
$('#mainContainer').append(form);
$('#mainContainer').append(frame);
$('#mainContainer').append(form);
$('#mainContainer').append(frame);
// Listen for App_LoadingStatus as soon as possible
$('#loleafletframe').ready(function() {
var editorInitListener = function(e) {
var msg = JSON.parse(e.data);
if (msg.MessageId === 'App_LoadingStatus') {
window.removeEventListener('message', editorInitListener, false);
}
};
window.addEventListener('message', editorInitListener, false);
});
// Listen for App_LoadingStatus as soon as possible
$('#loleafletframe').ready(function() {
var editorInitListener = function(e) {
var msg = JSON.parse(e.data);
if (msg.MessageId === 'App_LoadingStatus') {
window.removeEventListener('message', editorInitListener, false);
}
};
window.addEventListener('message', editorInitListener, false);
});
$('#loleafletframe').load(function(){
// And start listening to incoming post messages
window.addEventListener('message', function(e){
if (documentsMain.isViewerMode) {
return;
}
$('#loleafletframe').load(function(){
// And start listening to incoming post messages
window.addEventListener('message', function(e){
if (documentsMain.isViewerMode) {
return;
}
try {
var msg = JSON.parse(e.data).MessageId;
} catch(exc) {
msg = e.data;
}
if (msg === 'UI_Close' || msg === 'close') {
documentsMain.onClose();
} else if (msg === 'rev-history') {
documentsMain.UI.showRevHistory(documentsMain.fullPath);
}
});
try {
var msg = JSON.parse(e.data).MessageId;
} catch(exc) {
msg = e.data;
}
if (msg === 'UI_Close' || msg === 'close') {
documentsMain.onClose();
} else if (msg === 'rev-history') {
documentsMain.UI.showRevHistory(documentsMain.fullPath);
}
});
// Tell the LOOL iframe that we are ready now
documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Host_PostmessageReady', {});
// Tell the LOOL iframe that we are ready now
documentsMain.WOPIPostMessage($('#loleafletframe')[0], 'Host_PostmessageReady', {});
// LOOL Iframe is ready, turn off our overlay
// This should ideally be taken off when we receive App_LoadingStatus, but
// for backward compatibility with older lool, lets keep it here till we decide
// to break older lools
documentsMain.overlay.documentOverlay('hide');
});
// LOOL Iframe is ready, turn off our overlay
// This should ideally be taken off when we receive App_LoadingStatus, but
// for backward compatibility with older lool, lets keep it here till we decide
// to break older lools
documentsMain.overlay.documentOverlay('hide');
});
// submit that
$('#loleafletform').submit();
// submit that
$('#loleafletform').submit();
});
},
hideEditor : function(){
@ -447,16 +405,6 @@ var documentsMain = {
});
},
showSave : function (){
$('#odf-close').hide();
$('#saving-document').show();
},
hideSave : function(){
$('#saving-document').hide();
$('#odf-close').show();
},
showProgress : function(message){
if (!message){
message = '&nbsp;';
@ -479,25 +427,12 @@ var documentsMain = {
var fileId;
documentsMain.UI.init();
if (!OC.currentUser){
documentsMain.isGuest = true;
if ($("[name='document']").val()){
$(documentsMain.toolbar).appendTo('#header');
documentsMain.prepareSession();
documentsMain.joinSession(
$("[name='document']").val()
);
}
} else {
// Does anything indicate that we need to autostart a session?
fileId = parent.location.hash.replace(/^\W*/, '');
// Does anything indicate that we need to autostart a session?
fileId = parent.location.hash.replace(/^\W*/, '');
if (fileId.indexOf('_') >= 0) {
documentsMain.returnToDir = unescape(fileId.replace(/^[^_]*_/, ''));
fileId = fileId.replace(/_.*/, '');
}
if (fileId.indexOf('_') >= 0) {
documentsMain.returnToDir = unescape(fileId.replace(/^[^_]*_/, ''));
fileId = fileId.replace(/_.*/, '');
}
documentsMain.show(fileId);
@ -505,7 +440,6 @@ var documentsMain = {
if (fileId) {
documentsMain.overlay.documentOverlay('show');
documentsMain.prepareSession();
documentsMain.joinSession(fileId);
}
documentsMain.ready = true;
@ -530,14 +464,14 @@ var documentsMain = {
},
initSession: function(response) {
if(response && (response.id && !response.es_id)){
return documentsMain.view(response.id);
}
documentsMain.urlsrc = response.documents[0].urlsrc;
documentsMain.fullPath = response.documents[0].path;
documentsMain.token = response.documents[0].token;
$('footer,nav').hide();
$(documentsMain.toolbar).appendTo('#header');
if (!response || !response.status || response.status==='error'){
if (!response) {
documentsMain.onEditorShutdown(t('richdocuments', 'Failed to load this document. Please check if it can be opened with an external editor. This might also mean it has been unshared or deleted recently.'));
return;
}
@ -558,9 +492,8 @@ var documentsMain = {
documentsMain.fileId = response.file_id;
documentsMain.fileName = response.title;
documentsMain.esId = response.es_id;
documentsMain.memberId = response.member_id;
documentsMain.canEdit = response.permissions & OC.PERMISSION_UPDATE;
documentsMain.canEdit = Boolean(response.permissions & OC.PERMISSION_UPDATE);
documentsMain.loadDocument(response);
@ -571,22 +504,6 @@ var documentsMain = {
});
},
joinSession: function(fileId) {
console.log('joining session '+fileId);
var url;
if (documentsMain.isGuest){
url = OC.generateUrl('apps/richdocuments/session/guest/join/{token}', {token: fileId});
} else {
url = OC.generateUrl('apps/richdocuments/session/user/join/{file_id}', {file_id: fileId});
}
$.post(
url,
{ name : $("[name='memberName']").val() },
documentsMain.initSession
);
},
view : function(id){
OC.addScript('richdocuments', 'viewer/viewer', function() {
$(window).off('beforeunload');
@ -680,77 +597,6 @@ var documentsMain = {
}
};
//init
var Files = Files || {
// FIXME: copy/pasted from Files.isFileNameValid, needs refactor into core
isFileNameValid:function (name) {
if (name === '.') {
throw t('files', '\'.\' is an invalid file name.');
} else if (name.length === 0) {
throw t('files', 'File name cannot be empty.');
}
// check for invalid characters
var invalid_characters = ['\\', '/', '<', '>', ':', '"', '|', '?', '*'];
for (var i = 0; i < invalid_characters.length; i++) {
if (name.indexOf(invalid_characters[i]) !== -1) {
throw t('files', "Invalid name, '\\', '/', '<', '>', ':', '\"', '|', '?' and '*' are not allowed.");
}
}
return true;
},
updateStorageStatistics: function(){}
},
FileList = FileList || {};
FileList.getCurrentDirectory = function(){
return $('#dir').val() || '/';
};
FileList.highlightFiles = function(files, highlightFunction) {
};
FileList.findFile = function(filename) {
var documents = documentsMain.docs.documentGrid('option').documents;
return _.find(documents, function(aFile) {
return (aFile.name === filename);
}) || false;
};
FileList.generatePreviewUrl = function(urlSpec) {
urlSpec = urlSpec || {};
if (!urlSpec.x) {
urlSpec.x = 32;
}
if (!urlSpec.y) {
urlSpec.y = 32;
}
urlSpec.x *= window.devicePixelRatio;
urlSpec.y *= window.devicePixelRatio;
urlSpec.x = Math.ceil(urlSpec.x);
urlSpec.y = Math.ceil(urlSpec.y);
urlSpec.forceIcon = 0;
return OC.generateUrl('/core/preview.png?') + $.param(urlSpec);
};
FileList.isFileNameValid = function (name) {
var trimmedName = name.trim();
if (trimmedName === '.' || trimmedName === '..') {
throw t('files', '"{name}" is an invalid file name.', {name: name});
} else if (trimmedName.length === 0) {
throw t('files', 'File name cannot be empty.');
}
return true;
};
FileList.setViewerMode = function(){
};
FileList.findFile = function(fileName){
fullPath = escapeHTML(FileList.getCurrentDirectory + '/' + fileName);
return !!$('.documentslist .document:not(.template,.progress) a[original-title="' + fullPath + '"]').length;
};
$(document).ready(function() {
if (!OCA.Files) {

File diff suppressed because it is too large Load Diff

@ -0,0 +1,166 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2014 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This file is licensed under the Affero General Public License version 3 or
* later.
*/
namespace OCA\Richdocuments\Controller;
use OCA\Richdocuments\WOPI\DiscoveryManager;
use \OCP\AppFramework\Controller;
use \OCP\IRequest;
use \OCP\IConfig;
use \OCP\IL10N;
use \OCP\AppFramework\Http\ContentSecurityPolicy;
use \OCP\AppFramework\Http\TemplateResponse;
use \OCA\Richdocuments\AppConfig;
use \OCA\Richdocuments\Helper;
use \OC\Files\View;
use \OCP\ICacheFactory;
class DocumentController extends Controller {
private $uid;
private $l10n;
private $settings;
private $appConfig;
private $cache;
/** @var DiscoveryManager */
private $discoveryManager;
const ODT_TEMPLATE_PATH = '/assets/odttemplate.odt';
public function __construct($appName,
$UserId,
IRequest $request,
IConfig $settings,
AppConfig $appConfig,
IL10N $l10n,
ICacheFactory $cache,
DiscoveryManager $discoveryManager) {
parent::__construct($appName, $request);
$this->uid = $UserId;
$this->l10n = $l10n;
$this->settings = $settings;
$this->appConfig = $appConfig;
$this->cache = $cache->create($appName);
$this->discoveryManager = $discoveryManager;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* @return TemplateResponse
*/
public function index(){
$response = new TemplateResponse('richdocuments', 'documents');
$policy = new ContentSecurityPolicy();
$policy->addAllowedFrameDomain($this->appConfig->getAppValue('wopi_url'));
$response->setContentSecurityPolicy($policy);
return $response;
}
/**
* @NoAdminRequired
*/
public function create(){
$mimetype = $this->request->post['mimetype'];
$filename = $this->request->post['filename'];
$dir = $this->request->post['dir'];
$view = new View('/' . $this->uid . '/files');
if (!$dir){
$dir = '/';
}
$basename = $this->l10n->t('New Document.odt');
switch ($mimetype) {
case 'application/vnd.oasis.opendocument.spreadsheet':
$basename = $this->l10n->t('New Spreadsheet.ods');
break;
case 'application/vnd.oasis.opendocument.presentation':
$basename = $this->l10n->t('New Presentation.odp');
break;
case 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
$basename = $this->l10n->t('New Document.docx');
break;
case 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet':
$basename = $this->l10n->t('New Spreadsheet.xlsx');
break;
case 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
$basename = $this->l10n->t('New Presentation.pptx');
break;
default:
// to be safe
$mimetype = 'application/vnd.oasis.opendocument.text';
break;
}
if (!$filename){
$path = Helper::getNewFileName($view, $dir . '/' . $basename);
} else {
$path = $dir . '/' . $filename;
}
$content = '';
if (class_exists('\OC\Files\Type\TemplateManager')){
$manager = \OC_Helper::getFileTemplateManager();
$content = $manager->getTemplate($mimetype);
}
if (!$content){
$content = file_get_contents(dirname(__DIR__) . self::ODT_TEMPLATE_PATH);
}
$discovery_parsed = null;
try {
$discovery = $this->discoveryManager->get();
$loadEntities = libxml_disable_entity_loader(true);
$discovery_parsed = simplexml_load_string($discovery);
libxml_disable_entity_loader($loadEntities);
if ($discovery_parsed === false) {
$this->cache->remove('discovery.xml');
$wopiRemote = $this->getWopiUrl(false);
return array(
'status' => 'error',
'message' => $this->l10n->t('Collabora Online: discovery.xml from "%s" is not a well-formed XML string.', array($wopiRemote)),
'hint' => $this->l10n->t('Please contact the "%s" administrator.', array($wopiRemote))
);
}
}
catch (ResponseException $e) {
return array(
'status' => 'error',
'message' => $e->getMessage(),
'hint' => $e->getHint()
);
}
if ($content && $view->file_put_contents($path, $content)){
$info = $view->getFileInfo($path);
$ret = $this->getWopiSrcUrl($discovery_parsed, $mimetype);
$response = array(
'status' => 'success',
'fileid' => $info['fileid'],
'urlsrc' => $ret['urlsrc'],
'action' => $ret['action'],
'lolang' => $this->settings->getUserValue($this->uid, 'core', 'lang', 'en'),
'data' => \OCA\Files\Helper::formatFileInfo($info)
);
} else {
$response = array(
'status' => 'error',
'message' => (string) $this->l10n->t('Can\'t create document')
);
}
return $response;
}
}

@ -12,23 +12,32 @@
namespace OCA\Richdocuments\Controller;
use \OCP\AppFramework\Controller;
use OCP\AppFramework\Http\JSONResponse;
use \OCP\IRequest;
use \OCP\IL10N;
use \OCP\AppFramework\Http\JSONResponse;
use OCP\AppFramework\Http\TemplateResponse;
use OCA\Richdocuments\AppConfig;
use OCA\Richdocuments\Filter;
class SettingsController extends Controller{
private $userId;
/** @var IL10N */
private $l10n;
/** @var AppConfig */
private $appConfig;
public function __construct($appName, IRequest $request, IL10N $l10n, AppConfig $appConfig, $userId){
/**
* @param string $appName
* @param IRequest $request
* @param IL10N $l10n
* @param AppConfig $appConfig
* @param string $userId
*/
public function __construct($appName,
IRequest $request,
IL10N $l10n,
AppConfig $appConfig,
$userId) {
parent::__construct($appName, $request);
$this->userId = $userId;
$this->l10n = $l10n;
$this->appConfig = $appConfig;
}
@ -45,20 +54,26 @@ class SettingsController extends Controller{
/**
* @NoAdminRequired
*
* @return JSONResponse
*/
public function getSettings() {
return array(
return new JSONResponse([
'doc_format' => $this->appConfig->getAppValue('doc_format'),
'wopi_url' => $this->appConfig->getAppValue('wopi_url'),
'test_wopi_url' => $this->appConfig->getAppValue('test_wopi_url'),
'test_server_groups' => $this->appConfig->getAppValue('test_server_groups')
);
]);
}
public function setSettings($wopi_url, $edit_groups, $doc_format, $test_wopi_url, $test_server_groups){
/**
* @param string $wopi_url
* @param string $doc_format
* @return JSONResponse
*/
public function setSettings($wopi_url,
$doc_format){
$message = $this->l10n->t('Saved');
if (!is_null($wopi_url)){
if ($wopi_url !== null){
$this->appConfig->setAppValue('wopi_url', $wopi_url);
$colon = strpos($wopi_url, ':', 0);
@ -67,22 +82,10 @@ class SettingsController extends Controller{
}
}
if (!is_null($edit_groups)){
$this->appConfig->setAppValue('edit_groups', $edit_groups);
}
if (!is_null($doc_format)){
if ($doc_format !== null) {
$this->appConfig->setAppValue('doc_format', $doc_format);
}
if (!is_null($test_wopi_url)){
$this->appConfig->setAppValue('test_wopi_url', $test_wopi_url);
}
if (!is_null($test_server_groups)){
$this->appConfig->setAppValue('test_server_groups', $test_server_groups);
}
$richMemCache = \OC::$server->getMemCacheFactory()->create('richdocuments');
$richMemCache->clear('discovery.xml');
@ -91,6 +94,6 @@ class SettingsController extends Controller{
'data' => array('message' => (string) $message)
);
return $response;
return new JSONResponse($response);
}
}

@ -0,0 +1,245 @@
<?php
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
namespace OCA\Richdocuments\Controller;
use OCA\Richdocuments\Db\Wopi;
use OCA\Richdocuments\WOPI\Parser;
use OCP\AppFramework\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\JSONResponse;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\IRequest;
use OCP\IUserManager;
use OCP\AppFramework\Http\StreamResponse;
class WopiController extends Controller {
/** @var IRootFolder */
private $rootFolder;
/** @var string */
private $userId;
/** @var IUserManager */
private $userManager;
/** @var Parser */
private $wopiParser;
/**
* @param string $appName
* @param IRequest $request
* @param IRootFolder $rootFolder
* @param string $UserId
* @param IUserManager $userManager
* @param Parser $wopiParser
*/
public function __construct($appName,
$UserId,
IRequest $request,
IRootFolder $rootFolder,
IUserManager $userManager,
Parser $wopiParser) {
parent::__construct($appName, $request);
$this->rootFolder = $rootFolder;
$this->userId = $UserId;
$this->userManager = $userManager;
$this->wopiParser = $wopiParser;
}
/**
* Generates and returns an access token for a given fileId
*
* @NoAdminRequired
*
* @param string $fileId
* @return JSONResponse
*/
public function getToken($fileId) {
$arr = explode('_', $fileId, 2);
$version = '0';
if (count($arr) === 2) {
list($fileId, $version) = $arr;
}
try {
/** @var File $file */
$file = $this->rootFolder->getUserFolder($this->userId)->getById($fileId)[0];
$updatable = $file->isUpdateable();
} catch (\Exception $e) {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
// If token is for some versioned file
if ($version !== '0') {
$updatable = false;
}
$row = new Wopi();
$serverHost = $this->request->getServerProtocol() . '://' . $this->request->getServerHost();
$token = $row->generateFileToken($fileId, $version, $updatable, $serverHost);
try {
$userFolder = $this->rootFolder->getUserFolder($this->userId);
/** @var File $file */
$file = $userFolder->getById($fileId)[0];
$sessionData['title'] = basename($file->getPath());
$sessionData['permissions'] = $file->getPermissions();
$sessionData['file_id'] = $file->getId();
$sessionData['documents'] = [
0 => [
'urlsrc' => $this->wopiParser->getUrlSrc($file->getMimeType())['urlsrc'],
'path' => $file->getPath(),
'token' => $token,
],
];
return new JSONResponse($sessionData);
} catch (\Exception $e){
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
}
/**
* Returns general info about a file.
*
* @NoAdminRequired
* @NoCSRFRequired
* @PublicPage
*
* @param string $fileId
* @return JSONResponse
*/
public function checkFileInfo($fileId) {
$token = $this->request->getParam('access_token');
$arr = explode('_', $fileId, 2);
$version = '0';
if (count($arr) === 2) {
list($fileId, $version) = $arr;
}
$row = new Wopi();
$row->loadBy('token', $token);
$res = $row->getPathForToken($fileId, $version, $token);
if ($res === false) {
return new JSONResponse();
}
// Login the user to see his mount locations
try {
/** @var File $file */
$userFolder = $this->rootFolder->getUserFolder($res['editor']);
$file = $userFolder->getById($fileId)[0];
} catch (\Exception $e) {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
return new JSONResponse(
[
'BaseFileName' => $file->getName(),
'Size' => $file->getSize(),
'Version' => $version,
'UserId' => $res['editor'],
'UserFriendlyName' => $this->userManager->get($res['editor'])->getDisplayName(),
'UserCanWrite' => $res['canwrite'] ? true : false,
'PostMessageOrigin' => $res['server_host'],
]
);
}
/**
* Given an access token and a fileId, returns the contents of the file.
* Expects a valid token in access_token parameter.
*
* @PublicPage
* @NoCSRFRequired
*
* @param string $fileId
* @param string $access_token
* @return Http\Response
*/
public function getFile($fileId,
$access_token) {
$arr = explode('_', $fileId, 2);
$version = '0';
if (count($arr) === 2) {
list($fileId, $version) = $arr;
}
$row = new Wopi();
$row->loadBy('token', $access_token);
$res = $row->getPathForToken($fileId, $version, $access_token);
try {
/** @var File $file */
$userFolder = $this->rootFolder->getUserFolder($res['editor']);
$file = $userFolder->getById($fileId)[0];
$response = new StreamResponse($file->fopen('rb'));
$response->addHeader('Content-Disposition', 'attachment');
$response->addHeader('Content-Type', 'application/octet-stream');
return $response;
} catch (\Exception $e) {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
}
/**
* Given an access token and a fileId, replaces the files with the request body.
* Expects a valid token in access_token parameter.
*
* @PublicPage
* @NoCSRFRequired
*
* @param string $fileId
* @param string $access_token
* @return JSONResponse
*/
public function putFile($fileId, $access_token) {
$arr = explode('_', $fileId, 2);
$version = '0';
if (count($arr) === 2) {
list($fileId, $version) = $arr;
}
$row = new Wopi();
$row->loadBy('token', $access_token);
$res = $row->getPathForToken($fileId, $version, $access_token);
if (!$res['canwrite']) {
return new JSONResponse([], Http::STATUS_FORBIDDEN);
}
try {
/** @var File $file */
$userFolder = $this->rootFolder->getUserFolder($res['editor']);
$file = $userFolder->getById($fileId)[0];
$content = fopen('php://input', 'rb');
$file->putContent($content);
return new JSONResponse();
} catch (\Exception $e) {
return new JSONResponse([], Http::STATUS_INTERNAL_SERVER_ERROR);
}
}
}

@ -0,0 +1,117 @@
<?php
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
namespace OCA\Richdocuments\WOPI;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Files\IAppData;
use OCP\Files\NotFoundException;
use OCP\Files\SimpleFS\ISimpleFolder;
use OCP\Http\Client\IClientService;
use OCP\IConfig;
use OCP\IL10N;
use OCP\Notification\IApp;
class DiscoveryManager {
/** @var IClientService */
private $clientService;
/** @var ISimpleFolder */
private $appData;
/** @var IConfig */
private $config;
/** @var IL10N */
private $l10n;
/** @var ITimeFactory */
private $timeFactory;
/**
* @param IClientService $clientService
* @param IAppData $appData
* @param IConfig $config
* @param IL10N $l10n
* @param ITimeFactory $timeFactory
*/
public function __construct(IClientService $clientService,
IAppData $appData,
IConfig $config,
IL10N $l10n,
ITimeFactory $timeFactory) {
$this->clientService = $clientService;
try {
$this->appData = $appData->getFolder('richdocuments');
} catch (NotFoundException $e) {
$this->appData = $appData->newFolder('richdocuments');
}
$this->config = $config;
$this->l10n = $l10n;
$this->timeFactory = $timeFactory;
}
public function get() {
// First check if there is a local valid discovery file
try {
$file = $this->appData->getFile('discovery.xml');
$decodedFile = json_decode($file->getContent(), true);
if($decodedFile['timestamp'] + 3600 > $this->timeFactory->getTime()) {
return $decodedFile['data'];
}
} catch (NotFoundException $e) {
$file = $this->appData->newFile('discovery.xml');
}
$remoteHost = $this->config->getAppValue('richdocuments', 'wopi_url');
$wopiDiscovery = $remoteHost . '/hosting/discovery';
$client = $this->clientService->newClient();
try {
$response = $client->get($wopiDiscovery);
} catch (\Exception $e) {
$error_message = $e->getMessage();
if (preg_match('/^cURL error ([0-9]*):/', $error_message, $matches)) {
$admin_check = $this->l10n->t('Please ask your administrator to check the Collabora Online server setting. The exact error message was: ') . $error_message;
$curl_error = $matches[1];
switch ($curl_error) {
case '1':
throw new ResponseException($this->l10n->t('Collabora Online: The protocol specified in "%s" is not allowed.', array($wopiRemote)), $admin_check);
case '3':
throw new ResponseException($this->l10n->t('Collabora Online: Malformed URL "%s".', array($wopiRemote)), $admin_check);
case '6':
throw new ResponseException($this->l10n->t('Collabora Online: Cannot resolve the host "%s".', array($wopiRemote)), $admin_check);
case '7':
throw new ResponseException($this->l10n->t('Collabora Online: Cannot connect to the host "%s".', array($wopiRemote)), $admin_check);
case '60':
throw new ResponseException($this->l10n->t('Collabora Online: SSL certificate is not installed.'), $this->l10n->t('Please ask your administrator to add ca-chain.cert.pem to the ca-bundle.crt, for example "cat /etc/loolwsd/ca-chain.cert.pem >> <server-installation>/resources/config/ca-bundle.crt" . The exact error message was: ') . $error_message);
}
}
throw new ResponseException($this->l10n->t('Collabora Online unknown error: ') . $error_message, $contact_admin);
}
$responseBody = $response->getBody();
$file->putContent(
json_encode([
'data' => $responseBody,
'timestamp' => $this->timeFactory->getTime(),
])
);
return $responseBody;
}
}

@ -0,0 +1,58 @@
<?php
/**
* @copyright Copyright (c) 2016 Lukas Reschke <lukas@statuscode.ch>
*
* @license GNU AGPL version 3 or any later version
*
* 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/>.
*
*/
namespace OCA\Richdocuments\WOPI;
class Parser {
/** @var DiscoveryManager */
private $discoveryManager;
/**
* @param DiscoveryManager $discoveryManager
*/
public function __construct(DiscoveryManager $discoveryManager) {
$this->discoveryManager = $discoveryManager;
}
/**
* @param $mimetype
* @return array
* @throws \Exception
*/
public function getUrlSrc($mimetype) {
$discovery = $this->discoveryManager->get();
$loadEntities = libxml_disable_entity_loader(true);
$discoveryParsed = simplexml_load_string($discovery);
libxml_disable_entity_loader($loadEntities);
$result = $discoveryParsed->xpath(sprintf('/wopi-discovery/net-zone/app[@name=\'%s\']/action', $mimetype));
if ($result && count($result) > 0) {
return [
'urlsrc' => (string)$result[0]['urlsrc'],
'action' => (string)$result[0]['name'],
];
}
throw new \Exception('Could not find urlsrc in WOPI');
}
}

@ -1,130 +0,0 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2013 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This file is licensed under the Affero General Public License version 3 or
* later.
*/
namespace OCA\Richdocuments\Db;
/**
* @method boolean getIsGuest()
* @method string getEsId()
* @method string getToken()
* @method int getStatus()
*/
class Member extends \OCA\Richdocuments\Db{
const DB_TABLE = '`*PREFIX*richdocuments_member`';
const ACTIVITY_THRESHOLD = 90; // 1.5 Minutes
const MEMBER_STATUS_ACTIVE = 1;
const MEMBER_STATUS_INACTIVE = 2;
protected $tableName = '`*PREFIX*richdocuments_member`';
protected $insertStatement = 'INSERT INTO `*PREFIX*richdocuments_member` (`es_id`, `uid`, `color`, `last_activity`, `is_guest`, `token`)
VALUES (?, ?, ?, ?, ?, ?)';
protected $loadStatement = 'SELECT * FROM `*PREFIX*richdocuments_member` WHERE `member_id`= ?';
public static function getGuestPostfix(){
return '(' . \OC::$server->getL10n('richdocuments')->t('guest') . ')';
}
public function updateActivity($memberId){
return $this->execute(
'UPDATE ' . $this->tableName . ' SET `last_activity`=?, `status`=? WHERE `member_id`=?',
array(
time(),
self::MEMBER_STATUS_ACTIVE,
$memberId
)
);
}
public function getActiveCollection($esId){
$result = $this->execute('
SELECT `es_id`, `member_id`
FROM ' . self::DB_TABLE . '
WHERE `es_id`= ?
AND `status`=?
',
array(
$esId,
self::MEMBER_STATUS_ACTIVE
)
);
$members = $result->fetchAll();
if (!is_array($members)){
$members = array();
}
return $members;
}
/**
* Mark members as inactive
* @param string $esId - session Id
* @return array - list of memberId that were marked as inactive
*/
public function updateByTimeout($esId){
$time = $this->getInactivityPeriod();
$result = $this->execute('
SELECT `member_id`
FROM ' . self::DB_TABLE . '
WHERE `es_id`= ?
AND `last_activity`<?
AND `status`=?
',
array(
$esId,
$time,
self::MEMBER_STATUS_ACTIVE
)
);
$deactivated = $result->fetchAll();
if (is_array($deactivated) && count($deactivated)){
$deactivated = array_map(
function($x){
return ($x['member_id']);
}, $deactivated
);
$this->deactivate($deactivated);
} else {
$deactivated = array();
}
return $deactivated;
}
/**
* Update members to inactive state
* @param array $memberIds
*/
public function deactivate($memberIds){
$stmt = $this->buildInQuery('member_id', $memberIds);
array_unshift($memberIds, self::MEMBER_STATUS_INACTIVE);
$this->execute('
UPDATE ' . $this->tableName . '
SET `status`=?
WHERE ' . $stmt,
$memberIds
);
}
protected function getInactivityPeriod(){
return time() - self::ACTIVITY_THRESHOLD;
}
}

@ -1,190 +0,0 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2013 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This file is licensed under the Affero General Public License version 3 or
* later.
*/
namespace OCA\Richdocuments\Db;
class Op extends \OCA\Richdocuments\Db {
const DB_TABLE = '`*PREFIX*richdocuments_op`';
protected $tableName = '`*PREFIX*richdocuments_op`';
protected $insertStatement = 'INSERT INTO `*PREFIX*richdocuments_op` (`es_id`, `optype`, `member`, `opspec`) VALUES (?, ?, ?, ?)';
public static function addOpsArray($esId, $memberId, $ops){
$opObj = new Op();
foreach ($ops as $op) {
if (!$opObj->canInsertOp($esId, $memberId, $op)){
continue;
}
$opObj->setData(array(
$esId,
$op['optype'],
$memberId,
json_encode($op)
));
$opObj->insert();
}
return $opObj->getHeadSeq($esId);
}
/**
* @returns "" when there are no Ops, or the seq of the last Op
*/
public function getHeadSeq($esId){
$query = \OC::$server->getDatabaseConnection()->prepare('
SELECT `seq`
FROM ' . $this->tableName . '
WHERE `es_id`=?
ORDER BY `seq` DESC
', 1);
$result = $query->execute([$esId]);
return !$result ? "" : $query->fetchColumn();
}
public function getOpsAfterJson($esId, $seq){
$ops = $this->getOpsAfter($esId, $seq);
if (!is_array($ops)){
$ops = array();
}
$ops = array_map(
function($x){
$decoded = json_decode($x['opspec'], true);
$decoded['memberid'] = strval($decoded['memberid']);
return $decoded;
},
$ops
);
return $ops;
}
public function getOpsAfter($esId, $seq){
if ($seq == ""){
$seq = -1;
}
$query = \OC::$server->getDatabaseConnection()->prepare('
SELECT `opspec`
FROM ' . self::DB_TABLE . '
WHERE `es_id`=?
AND `seq`>?
ORDER BY `seq` ASC
');
$query->execute(array($esId, $seq));
return $query->fetchAll();
}
public function addMember($esId, $memberId, $fullName, $userId, $color, $imageUrl){
$op = array(
'optype' => 'AddMember',
'memberid' => (string) $memberId,
'timestamp' => $this->getMillisecondsAsString(),
'setProperties' => array(
'fullName' => $fullName,
'color' => $color,
'imageUrl' => $imageUrl,
'uid' => $userId,
)
);
$this->insertOp($esId, $memberId, $op);
}
public function removeCursor($esId, $memberId){
$op = array(
'optype' => 'RemoveCursor',
'memberid' => (string) $memberId,
'reason' => 'server-idle',
'timestamp' => $this->getMillisecondsAsString()
);
$this->insertOp($esId, $memberId, $op);
}
public function removeMember($esId, $memberId){
$op = array(
'optype' => 'RemoveMember',
'memberid' => (string) $memberId,
'timestamp' => $this->getMillisecondsAsString()
);
$this->insertOp($esId, $memberId, $op);
}
//TODO: Implement https://github.com/kogmbh/WebODF/blob/master/webodf/lib/ops/OpUpdateMember.js#L95
public function changeNick($esId, $memberId, $fullName){
$op = array(
'optype' => 'UpdateMember',
'memberid' => (string) $memberId,
'timestamp' => $this->getMillisecondsAsString(),
'setProperties' => array(
'fullName' => $fullName,
)
);
$this->insertOp($esId, $memberId, $op);
}
protected function insertOp($esId, $memberId, $op){
if ($this->canInsertOp($esId, $memberId, $op)){
$op = new Op(array(
$esId,
$op['optype'],
$memberId,
json_encode($op)
));
$op->insert();
}
}
protected function canInsertOp($esId, $memberId, $op){
$cursorOps = array('AddCursor', 'RemoveCursor');
$memberOps = array('AddMember', 'RemoveMember');
$result = true;
switch ($op['optype']){
case 'AddCursor':
$ops = $this->getFilteredMemberOps($esId, $memberId, $cursorOps);
$result = !count($ops) || $ops[0]['optype'] === 'RemoveCursor';
break;
case 'RemoveCursor':
$ops = $this->getFilteredMemberOps($esId, $memberId, $cursorOps);
$result = count($ops) && $ops[0]['optype'] === 'AddCursor';
break;
case 'AddMember':
$ops = $this->getFilteredMemberOps($esId, $memberId, $memberOps);
$result = !count($ops) || $ops[0]['optype'] === 'RemoveMember';
break;
case 'RemoveMember':
$ops = $this->getFilteredMemberOps($esId, $memberId, $memberOps);
$result = count($ops) && $ops[0]['optype'] === 'AddMember';
break;
}
return $result;
}
protected function getFilteredMemberOps($esId, $memberId, $targetOps){
$stmt = $this->buildInQuery('optype', $targetOps);
$result = $this->execute('
SELECT `optype` FROM ' . $this->tableName . '
WHERE es_id=? AND member=? AND ' . $stmt . 'ORDER BY `seq` DESC',
array_merge(array($esId, $memberId), $targetOps)
);
$ops = $result->fetchAll();
if (!is_array($ops)){
$ops = array();
}
return $ops;
}
protected function getMillisecondsAsString(){
$microtime = microtime();
list($usec, $sec) = explode(" ", $microtime);
$milliseconds = $sec.substr($usec, 2, 3);
return $milliseconds;
}
}

@ -1,212 +0,0 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2013 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This file is licensed under the Affero General Public License version 3 or
* later.
*/
namespace OCA\Richdocuments\Db;
use OCP\Security\ISecureRandom;
use OCA\Richdocuments\Filter;
/**
* Session management
*
* @method string getEsId()
* @method int getFileId()
* @method string getGenesisUrl()
* @method string getOwner()
* @method string getGenesisHash()
*
*/
class Session extends \OCA\Richdocuments\Db {
/**
* DB table
*/
const DB_TABLE = '`*PREFIX*richdocuments_session`';
protected $tableName = '`*PREFIX*richdocuments_session`';
protected $insertStatement = 'INSERT INTO `*PREFIX*richdocuments_session` (`es_id`, `genesis_url`, `genesis_hash`, `owner`, `file_id`)
VALUES (?, ?, ?, ?, ?)';
protected $loadStatement = 'SELECT * FROM `*PREFIX*richdocuments_session` WHERE `es_id`= ?';
/**
* Start a editing session or return an existing one
* @param string $uid of the user starting a session
* @param \OCA\Richdocuments\File $file - file object
* @return array
* @throws \Exception
*/
public static function start($uid, $file){
// Create a directory to store genesis
$genesis = new \OCA\Richdocuments\Genesis($file);
$oldSession = new Session();
$oldSession->loadBy('file_id', $file->getFileId());
//If there is no existing session we need to start a new one
if (!$oldSession->hasData()){
$newSession = new Session(array(
$genesis->getPath(),
$genesis->getHash(),
$file->getOwner(),
$file->getFileId()
));
if (!$newSession->insert()){
throw new \Exception('Failed to add session into database');
}
}
$sessionData = $oldSession
->loadBy('file_id', $file->getFileId())
->getData()
;
$memberColor = \OCA\Richdocuments\Helper::getMemberColor($uid);
$member = new \OCA\Richdocuments\Db\Member([
$sessionData['es_id'],
$uid,
$memberColor,
time(),
intval($file->isPublicShare()),
$file->getToken()
]);
if (!$member->insert()){
throw new \Exception('Failed to add member into database');
}
$sessionData['member_id'] = (string) $member->getLastInsertId();
// Do we have OC_Avatar in out disposal?
if (\OC::$server->getConfig()->getSystemValue('enable_avatars', true) !== true){
$imageUrl = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAAAAACH5BAAAAAAALAAAAAABAAEAAAICTAEAOw==';
} else {
$imageUrl = $uid;
}
$displayName = $file->isPublicShare()
? $uid . ' ' . \OCA\Richdocuments\Db\Member::getGuestPostfix()
: \OC::$server->getUserSession()->getUser()->getDisplayName($uid)
;
$userId = $file->isPublicShare() ? $displayName : \OC::$server->getUserSession()->getUser()->getUID();
$op = new \OCA\Richdocuments\Db\Op();
$op->addMember(
$sessionData['es_id'],
$sessionData['member_id'],
$displayName,
$userId,
$memberColor,
$imageUrl
);
$sessionData['title'] = basename($file->getPath());
$sessionData['permissions'] = $file->getPermissions();
return $sessionData;
}
public static function cleanUp($esId){
$session = new Session();
$session->deleteBy('es_id', $esId);
$member = new \OCA\Richdocuments\Db\Member();
$member->deleteBy('es_id', $esId);
$op= new \OCA\Richdocuments\Db\Op();
$op->deleteBy('es_id', $esId);
}
public function syncOps($memberId, $currentHead, $clientHead, $clientOps){
$hasOps = count($clientOps)>0;
$op = new \OCA\Richdocuments\Db\Op();
// TODO handle the case ($currentHead == "") && ($seqHead != "")
if ($clientHead == $currentHead) {
// matching heads
if ($hasOps) {
// incoming ops without conflict
// Add incoming ops, respond with a new head
$newHead = \OCA\Richdocuments\Db\Op::addOpsArray($this->getEsId(), $memberId, $clientOps);
$result = array(
'result' => 'added',
'head_seq' => $newHead ? $newHead : $currentHead
);
} else {
// no incoming ops (just checking for new ops...)
$result = array(
'result' => 'new_ops',
'ops' => array(),
'head_seq' => $currentHead
);
}
} else { // HEADs do not match
$result = array(
'result' => $hasOps ? 'conflict' : 'new_ops',
'ops' => $op->getOpsAfterJson($this->getEsId(), $clientHead),
'head_seq' => $currentHead,
);
}
return $result;
}
public function insert(){
$esId = $this->getUniqueSessionId();
array_unshift($this->data, $esId);
return parent::insert();
}
public function updateGenesisHash($esId, $genesisHash){
return $this->execute(
'UPDATE `*PREFIX*richdocuments_session` SET `genesis_hash`=? WHERE `es_id`=?',
array(
$genesisHash, $esId
)
);
}
public function getInfo($esId){
$result = $this->execute('
SELECT `s`.*, COUNT(`m`.`member_id`) AS `users`
FROM ' . $this->tableName . ' AS `s`
LEFT JOIN `*PREFIX*richdocuments_member` AS `m` ON `s`.`es_id`=`m`.`es_id`
AND `m`.`status`=' . Db\Member::MEMBER_STATUS_ACTIVE . '
AND `m`.`uid` != ?
WHERE `s`.`es_id` = ?
GROUP BY `m`.`es_id`
',
[
\OC::$server->getUserSession()->getUser()->getUID(),
$esId
]
);
$info = $result->fetch();
if (!is_array($info)){
$info = array();
}
return $info;
}
protected function getUniqueSessionId(){
$testSession = new Session();
do{
$id = \OC::$server->getSecureRandom()
->getMediumStrengthGenerator()
->generate(30, ISecureRandom::CHAR_LOWER . ISecureRandom::CHAR_DIGITS);
} while ($testSession->load($id)->hasData());
return $id;
}
}

@ -1,5 +1,4 @@
<?php
/**
* ownCloud - Richdocuments App
*
@ -15,11 +14,6 @@ namespace OCA\Richdocuments\Db;
use \OCA\Richdocuments\Download;
use \OCA\Richdocuments\DownloadResponse;
/**
* @method string generateFileToken()
* @method string getPathForToken()
*/
class Wopi extends \OCA\Richdocuments\Db{
const DB_TABLE = '`*PREFIX*richdocuments_wopi`';

@ -1,123 +0,0 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2014 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This file is licensed under the Affero General Public License version 3 or
* later.
*/
namespace OCA\Richdocuments;
use \OCP\AppFramework\Http;
use \OCP\IRequest;
use \OC\Files\View;
class DownloadResponse extends \OCP\AppFramework\Http\Response {
private $request;
private $view;
private $path;
/**
* @param IRequest $request
* @param string $user
* @param string $path
*/
public function __construct(IRequest $request, $user, $path) {
$this->request = $request;
$this->user = $user;
$this->path = $path;
$this->view = new View('/' . $user);
if (!$this->view->file_exists($path)){
$this->setStatus(Http::STATUS_NOT_FOUND);
}
}
public function render(){
if ($this->getStatus() === Http::STATUS_NOT_FOUND){
return '';
}
$info = $this->view->getFileInfo($this->path);
$this->ETag = $info['etag'];
$content = $this->view->file_get_contents($this->path);
$data = \OCA\Richdocuments\Filter::read($content, $info['mimetype']);
$size = strlen($data['content']);
if (isset($this->request->server['HTTP_RANGE']) && !is_null($this->request->server['HTTP_RANGE'])){
$isValidRange = preg_match('/^bytes=\d*-\d*(,\d*-\d*)*$/', $this->request->server['HTTP_RANGE']);
if (!$isValidRange){
return $this->sendRangeNotSatisfiable($size);
}
$ranges = explode(',', substr($this->request->server['HTTP_RANGE'], 6));
foreach ($ranges as $range){
$parts = explode('-', $range);
if ($parts[0]==='' && $parts[1]=='') {
$this->sendNotSatisfiable($size);
}
if ($parts[0]==='') {
$start = $size - $parts[1];
$end = $size - 1;
} else {
$start = $parts[0];
$end = ($parts[1]==='') ? $size - 1 : $parts[1];
}
if ($start > $end){
$this->sendNotSatisfiable($size);
}
$buffer = substr($data['content'], $start, $end - $start);
$md5Sum = md5($buffer);
// send the headers and data
$this->addHeader('Content-Length', $end - $start);
$this->addHeader('Content-md5', $md5Sum);
$this->addHeader('Accept-Ranges', 'bytes');
$this->addHeader('Content-Range', 'bytes ' . $start . '-' . ($end) . '/' . $size);
$this->addHeader('Connection', 'close');
$this->addHeader('Content-Type', $data['mimetype']);
$this->addContentDispositionHeader();
return $buffer;
}
}
$this->addHeader('Content-Type', $data['mimetype']);
$this->addContentDispositionHeader();
$this->addHeader('Content-Length', $size);
return $data['content'];
}
/**
* Send 416 if we can't satisfy the requested ranges
* @param integer $filesize
*/
protected function sendRangeNotSatisfiable($filesize){
$this->setStatus(Http::STATUS_REQUEST_RANGE_NOT_SATISFIABLE);
$this->addHeader('Content-Range', 'bytes */' . $filesize); // Required in 416.
return '';
}
protected function addContentDispositionHeader(){
$encodedName = rawurlencode(basename($this->path));
$isIE = preg_match("/MSIE/", $this->request->server["HTTP_USER_AGENT"]);
if ($isIE){
$this->addHeader(
'Content-Disposition',
'attachment; filename="' . $encodedName . '"'
);
} else {
$this->addHeader(
'Content-Disposition',
'attachment; filename*=UTF-8\'\'' . $encodedName . '; filepath="' . $encodedName . '"'
);
}
}
}

@ -1,200 +0,0 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2013 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Richdocuments;
use \OC\Files\View;
class File {
protected $fileId;
protected $owner;
protected $sharing;
protected $token;
protected $passwordProtected = false;
protected $ownerView;
protected $ownerViewFiles;
protected $path;
protected $pathFiles;
public function __construct($fileId, $shareOps = null, $token = ''){
if (!$fileId){
throw new \Exception('No valid file has been passed');
}
$this->fileId = $fileId;
$this->sharing = $shareOps;
$this->token = $token;
if ($this->isPublicShare()) {
if (isset($this->sharing['uid_owner'])){
$this->owner = $this->sharing['uid_owner'];
if (!\OC::$server->getUserManager()->userExists($this->sharing['uid_owner'])) {
throw new \Exception('Share owner' . $this->sharing['uid_owner'] . ' does not exist ');
}
\OC_Util::tearDownFS();
\OC_Util::setupFS($this->sharing['uid_owner']);
} else {
throw new \Exception($this->fileId . ' is a broken share');
}
} else {
$this->owner = \OC::$server->getUserSession()->getUser()->getUID();
}
$this->initViews();
}
public static function getByShareToken($token){
$linkItem = \OCP\Share::getShareByToken($token, false);
if (is_array($linkItem) && isset($linkItem['uid_owner'])) {
// seems to be a valid share
$rootLinkItem = \OCP\Share::resolveReShare($linkItem);
} else {
throw new \Exception('This file was probably unshared');
}
$file = new File($rootLinkItem['file_source'], $rootLinkItem, $token);
if (isset($linkItem['share_with']) && !empty($linkItem['share_with'])){
$file->setPasswordProtected(true);
}
return $file;
}
public function getToken(){
return $this->token;
}
public function getFileId(){
return $this->fileId;
}
public function setToken($token){
$this->token = $token;
}
public function isPublicShare(){
return !empty($this->token);
}
public function isPasswordProtected(){
return $this->passwordProtected;
}
/**
* @param string $password
* @return boolean
*/
public function checkPassword($password){
$shareId = $this->sharing['id'];
if (!$this->isPasswordProtected()
|| (\OC::$server->getSession()->exists('public_link_authenticated')
&& \OC::$server->getSession()->get('public_link_authenticated') === $shareId
)
){
return true;
}
// Check Password
$newHash = '';
if(\OC::$server->getHasher()->verify($password, $this->getPassword(), $newHash)) {
\OC::$server->getSession()->set('public_link_authenticated', $shareId);
/**
* FIXME: Migrate old hashes to new hash format
* Due to the fact that there is no reasonable functionality to update the password
* of an existing share no migration is yet performed there.
* The only possibility is to update the existing share which will result in a new
* share ID and is a major hack.
*
* In the future the migration should be performed once there is a proper method
* to update the share's password. (for example `$share->updatePassword($password)`
*
* @link https://github.com/owncloud/core/issues/10671
*/
if(!empty($newHash)) {
}
return true;
}
return false;
}
/**
* @param boolean $value
*/
public function setPasswordProtected($value){
$this->passwordProtected = $value;
}
public function getOwner(){
return $this->owner;
}
public function getOwnerView($relativeToFiles = false){
return $relativeToFiles ? $this->ownerViewFiles : $this->ownerView;
}
public function getPath($relativeToFiles = false){
return $relativeToFiles ? $this->pathFiles : $this->path;
}
public function getPermissions(){
$fileInfo = $this->ownerView->getFileInfo($this->path);
return $fileInfo->getPermissions();
}
protected function initViews(){
$this->ownerView = new View('/' . $this->owner);
$this->ownerViewFiles = new View('/' . $this->owner . '/files');
$this->path = $this->ownerView->getPath($this->fileId);
$this->pathFiles = $this->ownerViewFiles->getPath($this->fileId);
if (!$this->path || !$this->pathFiles) {
throw new \Exception($this->fileId . ' can not be resolved');
}
if (!$this->ownerView->file_exists($this->path)) {
throw new \Exception($this->path . ' doesn\'t exist');
}
if (!$this->ownerViewFiles->file_exists($this->pathFiles)) {
throw new \Exception($this->pathFiles . ' doesn\'t exist');
}
if (!$this->ownerView->is_file($this->path)){
throw new \Exception('Object ' . $this->path . ' is not a file.');
}
//TODO check if it is a valid odt
$mimetype = $this->ownerView->getMimeType($this->path);
if (!Filter::isSupportedMimetype($mimetype)){
throw new \Exception( $this->path . ' is ' . $mimetype . ' and is not supported by RichDocuments app');
}
}
protected function getPassword(){
return $this->sharing['share_with'];
}
}

@ -1,97 +0,0 @@
<?php
/**
* ownCloud - Richdocuments App
*
* @author Victor Dubiniuk
* @copyright 2013 Victor Dubiniuk victor.dubiniuk@gmail.com
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Richdocuments;
use \OC\Files\View;
class Genesis {
const DOCUMENTS_DIRNAME='/documents';
protected $view;
protected $path;
protected $hash;
/**
* Create new genesis document
* @param File $file
* */
public function __construct(File $file){
$view = $file->getOwnerView();
$path = $file->getPath();
$owner = $file->getOwner();
$this->view = new View('/' . $owner);
if (!$this->view->file_exists(self::DOCUMENTS_DIRNAME)){
$this->view->mkdir(self::DOCUMENTS_DIRNAME );
}
$this->validate($view, $path);
$this->hash = $view->hash('sha1', $path, false);
$this->path = self::DOCUMENTS_DIRNAME . '/' . $this->hash . '.odt';
if (!$this->view->file_exists($this->path)){
//copy new genesis to /user/documents/{hash}.odt
// get decrypted content
$content = $view->file_get_contents($path);
$mimetype = $view->getMimeType($path);
$data = Filter::read($content, $mimetype);
$this->view->file_put_contents($this->path, $data['content']);
}
try {
$this->validate($this->view, $this->path);
} catch (\Exception $e){
throw new \Exception('Failed to copy genesis');
}
}
public function getPath(){
return $this->path;
}
public function getHash(){
return $this->hash;
}
/**
* Check if genesis is valid
* @param \OC\Files\View $view
* @param string $path relative to the view
* @throws \Exception
*/
protected function validate($view, $path){
if (!$view->file_exists($path)){
throw new \Exception('Document not found ' . $path);
}
if (!$view->is_file($path)){
throw new \Exception('Object ' . $path . ' is not a file.');
}
//TODO check if it is a valid odt
}
}

@ -1,5 +1,4 @@
<?php
/**
* ownCloud - Richdocuments App
*
@ -37,109 +36,4 @@ class Helper {
public static function isVersionsEnabled(){
return \OCP\App::isEnabled('files_versions');
}
public static function getRandomColor(){
$str = dechex(floor(rand(0, 16777215)));
return '#' . str_pad($str, 6, "0", STR_PAD_LEFT);
}
/**
* @param string $name
* @return string
*/
public static function getMemberColor($name){
$hash = md5($name);
$maxRange = hexdec('ffffffffffffffffffffffffffffffff');
$hue = hexdec($hash) / $maxRange * 256;
return '#' . self::convertHSLToRGB($hue, 90, 60);
}
/**
* @param integer $iH
* @param integer $iS
* @param integer $iV
* @return string
*/
protected static function convertHSLToRGB($iH, $iS, $iV){
if ($iH < 0){
$iH = 0; // Hue:
}
if ($iH > 360){
$iH = 360; // 0-360
}
if ($iS < 0){
$iS = 0; // Saturation:
}
if ($iS > 100){
$iS = 100; // 0-100
}
if ($iV < 0){
$iV = 0; // Lightness:
}
if ($iV > 100){
$iV = 100; // 0-100
}
$dS = $iS / 100.0; // Saturation: 0.0-1.0
$dV = $iV / 100.0; // Lightness: 0.0-1.0
$dC = $dV * $dS; // Chroma: 0.0-1.0
$dH = $iH / 60.0; // H-Prime: 0.0-6.0
$dT = $dH; // Temp variable
while ($dT >= 2.0)
$dT -= 2.0; // php modulus does not work with float
$dX = $dC * (1 - abs($dT - 1)); // as used in the Wikipedia link
switch ($dH){
case($dH >= 0.0 && $dH < 1.0):
$dR = $dC;
$dG = $dX;
$dB = 0.0;
break;
case($dH >= 1.0 && $dH < 2.0):
$dR = $dX;
$dG = $dC;
$dB = 0.0;
break;
case($dH >= 2.0 && $dH < 3.0):
$dR = 0.0;
$dG = $dC;
$dB = $dX;
break;
case($dH >= 3.0 && $dH < 4.0):
$dR = 0.0;
$dG = $dX;
$dB = $dC;
break;
case($dH >= 4.0 && $dH < 5.0):
$dR = $dX;
$dG = 0.0;
$dB = $dC;
break;
case($dH >= 5.0 && $dH < 6.0):
$dR = $dC;
$dG = 0.0;
$dB = $dX;
break;
default:
$dR = 0.0;
$dG = 0.0;
$dB = 0.0;
break;
}
$dM = $dV - $dC;
$dR += $dM;
$dG += $dM;
$dB += $dM;
$dR *= 255;
$dG *= 255;
$dB *= 255;
$dR = str_pad(dechex(round($dR)), 2, "0", STR_PAD_LEFT);
$dG = str_pad(dechex(round($dG)), 2, "0", STR_PAD_LEFT);
$dB = str_pad(dechex(round($dB)), 2, "0", STR_PAD_LEFT);
return $dR.$dG.$dB;
}
}

@ -136,7 +136,6 @@ class Storage {
}
}
Db\Session::cleanUp($session->getEsId());
}
private static function processDocuments($rawDocuments){

@ -10,23 +10,6 @@ script('richdocuments', 'admin');
<span id="documents-admin-msg" class="msg"></span>
<br/>
<input type="checkbox" class="test-server-enable" id="test_server_enable-richdocuments" />
<label for="test-server-enable"><?php p($l->t('Enable test server for specific groups')) ?></label><br/>
<p id="test-server-section" class="indent <?php if ($_['test_server_groups'] === '' || $_['test_wopi_url'] === '') p('hidden') ?>">
<label for="test_server_group_select"><?php p($l->t('Groups')) ?></label>
<input type="hidden" id="test_server_group_select" value="<?php p($_['test_server_groups'])?>" title="<?php p($l->t('None')); ?>" style="width: 200px" /><br/>
<label for="test_wopi_url"><?php p($l->t('Test server')) ?></label>
<input type="text" name="test_wopi_url" id="test_wopi_url" value="<?php p($_['test_wopi_url'])?>" style="width:300px;" /><br/>
<em><?php p($l->t('URL (and port) of the Collabora Online test server.')) ?></em><br/>
<button type="button" id="test_wopi_apply"><?php p($l->t('Apply')) ?></button>
<span id="test-documents-admin-msg" class="msg"></span>
</p>
<input type="checkbox" class="edit-groups-enable" id="edit_groups_enable-richdocuments" />
<label for="edit_groups_enable-richdocuments"><?php p($l->t('Enable edit for specific groups')) ?></label>
<input type="hidden" id="edit_group_select" value="<?php p($_['edit_groups'])?>" title="<?php p($l->t('All')); ?>" style="width: 200px">
<br/>
<input type="checkbox" class="doc-format-ooxml" id="doc_format_ooxml_enable-richdocuments" <?php p($_['doc_format'] === 'ooxml' ? 'checked' : '') ?> />
<label for="doc_format_ooxml_enable-richdocuments"><?php p($l->t('Use OOXML by default for new files')) ?></label>
</div>

@ -1,10 +1,6 @@
<?php
style( 'richdocuments', 'share' );
style( 'richdocuments', 'style' );
script('richdocuments', 'share');
script('richdocuments', 'documents');
script('files', 'file-upload');
script('files', 'jquery.fileupload');
?>
<div id="documents-content">
<ul class="documentslist">

Loading…
Cancel
Save