From 99b8c5773fd1471a5bffaae79c2778c4e7d8d87c Mon Sep 17 00:00:00 2001 From: Hadi Nategh Date: Fri, 12 Aug 2016 16:29:55 +0200 Subject: [PATCH] W.I.P. collab editor: First implementation for syncing operation --- .../inc/class.filemanager_collab.inc.php | 144 ++++++++++++++++++ .../inc/class.filemanager_collab_bo.inc.php | 101 +++++++++--- .../inc/class.filemanager_hooks.inc.php | 27 ++++ filemanager/js/app.js | 15 +- 4 files changed, 258 insertions(+), 29 deletions(-) create mode 100644 filemanager/inc/class.filemanager_collab.inc.php diff --git a/filemanager/inc/class.filemanager_collab.inc.php b/filemanager/inc/class.filemanager_collab.inc.php new file mode 100644 index 0000000000..9384df3b3b --- /dev/null +++ b/filemanager/inc/class.filemanager_collab.inc.php @@ -0,0 +1,144 @@ + + * @copyright (c) 2016 by Stylite AG + * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License + * @version $Id$ + */ + +use EGroupware\Api; + +class filemanager_collab extends filemanager_collab_bo { + + /** + * Methods callable via menuaction + * + * @var array + */ + var $public_functions = array( + 'poll' => true + ); + + /** + * Constructor + * + */ + function __construct() + { + parent::__construct(); + } + + /** + * Join session, initialises edit session for opened file by user + * + * @param type $es_id session id + * @return array returns an array consists of session data + */ + function join_session ($es_id) + { + $response = $this->initSession($es_id); + $response += array ( + 'id' => $GLOBALS['egw_info']['user']['account_id'], + 'full_name' => $GLOBALS['egw_info']['user']['account_fullname'], + 'success' => true + ); + + return $response; + } + + /** + * Polling mechanisim to sysncronise data + * + * @throws Exception + */ + function poll () + { + // Get POST request payload + $payload = file_get_contents('php://input'); + $params = $payload? json_decode ($payload, true): null; + $response = array(); + if (is_array($params)) + { + switch ($params['command']) + { + case 'join_session': + $response = $this->join_session($params['args']['es_id'],$params['args']['user_id']); + break; + case 'sync_ops': + try + { + $memberid = $params['args']['member_id']? $params['args']['member_id']: ''; + $es_id = $params['args']['es_id']; + $seq_head = (string) isset($params['args']['seq_head'])? $params['args']['seq_head']: null; + if(!is_null($seq_head)) + { + $client_ops = $params['args']['client_ops']? $params['args']['client_ops']: []; + $current_seq_head = $this->OP_getHeadSeq($es_id); + if ($seq_head == $current_seq_head) { + + if (count($client_ops)>0) + { + $newHead = $this->get_newHead($es_id, $memberid, $client_ops); + $response = array( + 'result' => 'added', + 'head_seq' => $newHead ? $newHead : $current_seq_head + ); + } + else + { + $response = array( + 'result' => 'new_ops', + 'ops' => array(), + 'head_seq' => $current_seq_head + ); + } + } + else + { + $response = array( + 'result' => count($client_ops)>0 ? 'conflict' : 'new_ops', + 'ops' => $this->OP_getOPSECS($es_id, $seq_head), + 'head_seq' => $current_seq_head, + ); + } + + } + else + { + throw new Exception('Invalid seq head!'); + } + } catch (Exception $ex) { + error_log($ex->getMessage()); + } + + break; + default: + // + } + } + header('content-type: application/json; charset=utf-8'); + echo json_encode($response); + exit(); + } + + /** + * Function to get a new head sequence + * + * @param string $es_id session id + * @param string $member_id member id + * @param array $client_ops opspec from client side + * + * @return string return a seq head number + */ + function get_newHead ($es_id, $member_id, $client_ops) + { + $this->OP_addOPS($es_id, $member_id, $client_ops); + + return $this->OP_getHeadSeq($es_id); + } + +} \ No newline at end of file diff --git a/filemanager/inc/class.filemanager_collab_bo.inc.php b/filemanager/inc/class.filemanager_collab_bo.inc.php index 6ef3d17e52..0d0c7e859b 100644 --- a/filemanager/inc/class.filemanager_collab_bo.inc.php +++ b/filemanager/inc/class.filemanager_collab_bo.inc.php @@ -58,22 +58,28 @@ class filemanager_collab_bo */ protected function initSession ($es_id) { - $session = $this->db->select(self::SESSION_TABLE,'*', array('collab_es_id' => $es_id)); + $query = $this->db->select(self::SESSION_TABLE,'*', array('collab_es_id' => $es_id),__LINE__,__FILE__); $full_name = $GLOBALS['egw_info']['user']['account_fullname']; - $color = $GLOBALS['egw_info']['user']['prefs']['collab_user_color']; + $color = $GLOBALS['egw_info']['user']['preferences']['filemanager']['collab_user_color']; $user_id = $GLOBALS['egw_info']['user']['account_id']; + $imageUrl = $GLOBALS['egw_info']['server']['webserver_url'].'/index.php?menuaction=addressbook.addressbook_ui.photo&account_id='.$user_id; - if (!($result = self::db2id($session->fields)) && $es_id) + if (!($result = self::db2id($query->fetchRow())) && $es_id) { $result = self::db2id($this->SESSION_add2Db($es_id)); } - $this->MEMBER_add2Db($es_id, $user_id, $color); + if (is_null($result['member_id'] = $this->MEMBER_getUserMemberId($es_id, $user_id))) + { + $this->MEMBER_add2Db($es_id, $user_id, $color); - $member_id = $this->MEMBER_getLastMember(); + $result['member_id'] = $this->MEMBER_getLastMember(); - $this->OP_addMember($es_id, $member_id, $full_name, $user_id, $color); + $this->OP_addMember($es_id, $result['member_id'], $full_name, $user_id, $color, $imageUrl); + } + + return $result; } /** @@ -99,7 +105,6 @@ class filemanager_collab_bo return $data; } - /** * OP addMember function is backend equivalent to addMember from frontend * @@ -110,12 +115,12 @@ class filemanager_collab_bo * @param string $color * @param string $imageUrl */ - protected function OP_addMember($es_id, $member_id, $full_name, $user_id, $color, $imageUrl) + protected function OP_addMember($es_id, $member_id, $full_name, $user_id, $color='', $imageUrl='') { $date = new DateTime(); $op = array( 'optype' => 'AddMember', - 'member' => (string) $member_id, + 'memberid' => (string) $member_id, 'timestamp' => $date->getTimestamp(), 'setProperties' => array( 'fullName' => $full_name, @@ -139,7 +144,7 @@ class filemanager_collab_bo { if (!$es_id) throw new Exception ('Session id must be given, none given!'); - $headSeq = $this->db->select( + $query = $this->db->select( self::OP_TABLE, 'collab_seq', array('collab_es_id' => $es_id), @@ -149,8 +154,58 @@ class filemanager_collab_bo 'ORDER BY collab_seq DESC LIMIT 1', 'filemanager' ); + $head_seq = $query->fetchRow(); + return is_array($head_seq)? $head_seq['collab_seq']: ''; + } - return $headSeq->fields? $headSeq->fields['collab_seq']: ''; + /** + * Function to get opsepcs of session base on seq head + * + * @param string $es_id session id + * @param string $seq_head sequence head + * + * @return array returns array of opspecs + */ + protected function OP_getOPSECS($es_id, $seq_head) + { + if ($seq_head == "") + { + $seq_head = -1; + } + $ops = array(); + $query = $this->db->select( + self::OP_TABLE, + 'collab_opspec', + 'collab_es_id ="'. $es_id.'" AND collab_seq >'.$seq_head, + __LINE__, + __FILE__, + false, 'ORDER BY collab_seq ASC','filemanager'); + + foreach ($query as $spec) + { + $op = json_decode($spec['collab_opspec'], true); + $op['memberid'] = strval($op['memberid']); + $ops [] = $op; + } + return $ops; + } + + /** + * Function to ops data array into op table + * + * @param string $es_id session id + * @param string $member_id member id + * @param array $client_ops array of ops + * @throws Exception if no array of ops given + */ + protected function OP_addOPS ($es_id, $member_id, $client_ops) + { + if (count($client_ops)<= 0) throw new Exception ('ops need to be an array of op data, none array given!'); + + foreach ($client_ops as $op) + { + $this->OP_add2Db($op, $es_id); + } } /** @@ -163,7 +218,7 @@ class filemanager_collab_bo { $data = array ( 'collab_es_id' => $es_id, - 'collab_member' => $op['member'], + 'collab_member' => $op['memberid'], 'collab_optype' => $op['optype'], 'collab_opspec' => json_encode($op) ); @@ -192,21 +247,21 @@ class filemanager_collab_bo * * @param string $member_id member id * - * @return array returns an array consist of member record + * @return string member id or null * @throws Exception throws exception if no member id is given */ - protected function MEMBER_getMember ($member_id) + protected function MEMBER_getUserMemberId ($es_id, $user_id) { - if (!$member_id) throw new Exception ('Member id must be given, none given!'); - $member = $this->db->select( + if (!$es_id || !$user_id) throw new Exception ('Member id must be given, none given!'); + $query = $this->db->select( self::MEMBER_TABLE, - '*', - array('collab_member_id' => $member_id), + 'collab_member_id', + array('collab_es_id' => $es_id, 'collab_uid' => $user_id), __LINE__, __FILE__ ); - - return $member->fields? self::db2id($member->fileds): []; + $member = $query->fetchRow(); + return is_array($member)? $member['collab_member_id']: null; } /** @@ -230,7 +285,7 @@ class filemanager_collab_bo */ protected function MEMBER_getLastMember() { - $last_row = $this->db->select( + $query = $this->db->select( self::MEMBER_TABLE, 'collab_member_id', '', @@ -239,7 +294,7 @@ class filemanager_collab_bo false, "ORDER BY `collab_member_id` DESC LIMIT 1;" ); - - return $last_row->fields? $last_row->fields['collab_member_id']: 0; + $last_row = $query->fetchRow(); + return is_array($last_row)? $last_row['collab_member_id']: 0; } } \ No newline at end of file diff --git a/filemanager/inc/class.filemanager_hooks.inc.php b/filemanager/inc/class.filemanager_hooks.inc.php index 26063b10e7..5985489433 100644 --- a/filemanager/inc/class.filemanager_hooks.inc.php +++ b/filemanager/inc/class.filemanager_hooks.inc.php @@ -124,6 +124,13 @@ class filemanager_hooks ); $settings = array( + 'sections.1' => array( + 'type' => 'section', + 'title' => lang('General settings'), + 'no_lang'=> true, + 'xmlrpc' => False, + 'admin' => False + ), 'startfolder' => array( 'type' => 'input', 'name' => 'startfolder', @@ -227,6 +234,26 @@ class filemanager_hooks 'admin' => False, ); } + + $settings += array ( + 'sections.2' => array( + 'type' => 'section', + 'title' => lang('Collab Editor settings'), + 'no_lang'=> true, + 'xmlrpc' => False, + 'admin' => False + ), + 'collab_user_color' => array( + 'type' => 'color', + 'label' => lang('User color indicator'), + 'name' => 'collab_user_color', + 'help' => lang('Use eg. %1 or %2','#FF0000','orange'), + 'no_lang'=> true, + 'xmlrpc' => True, + 'admin' => False, + ) + ); + return $settings; } diff --git a/filemanager/js/app.js b/filemanager/js/app.js index 2e1eb1fc74..998aa55fa2 100644 --- a/filemanager/js/app.js +++ b/filemanager/js/app.js @@ -10,8 +10,9 @@ */ /*egw:uses - /api/js/webodf/wodotexteditor/wodotexteditor/wodotexteditor.js; - /api/js/webodf/wodotexteditor/wodotexteditor/webodf.js; + /api/js/webodf/collab/webodf.js; + /api/js/webodf/collab/wodocollabtexteditor.js; + /api/js/webodf/collab/dojo-amalgamation.js; */ /** @@ -119,7 +120,7 @@ app.classes.filemanager = AppJS.extend( { // need to make body rock solid to avoid extra scrollbars jQuery('body').css({overflow:'hidden'}); - this._init_odf_editor (); + this._init_odf_collab_editor (); } }, @@ -1082,10 +1083,12 @@ app.classes.filemanager = AppJS.extend( var serverOptions = { "serverParams": { - url:egw.webserverUrl+'/api/js/webodf/poll.php?action=poll', + url:egw.link('/index.php?', { + menuaction: 'filemanager.filemanager_collab.poll' + }), genesisUrl:egw.webserverUrl+file_path }, - "sessionId": base64.toBase64(egw.webserverUrl+file_path), + "sessionId": egw.webserverUrl+file_path, editorOptions: { allFeaturesEnabled: true, userData: { @@ -1332,7 +1335,7 @@ app.classes.filemanager = AppJS.extend( serverParams = _args.serverParams, sessionId = _args.sessionId, editorOptions = jQuery.extend(_args.editorOptions,{networkSecurityToken:'', closeCallback:this.editor_close}), - userId = egw.user('account_lid'), + userId = egw.user('account_id'), memberId, self = this;