[Libreoffice-commits] online.git: Branch 'distro/collabora/collabora-online-1-0' - loleaflet/dist loleaflet/src loolwsd/ChildProcessSession.cpp loolwsd/ChildProcessSession.hpp loolwsd/DocumentBroker.cpp loolwsd/DocumentBroker.hpp loolwsd/LOKitClient.cpp loolwsd/LOKitHelper.hpp loolwsd/LOOLKit.cpp loolwsd/LOOLSession.cpp loolwsd/LOOLSession.hpp loolwsd/LOOLWSD.cpp loolwsd/MasterProcessSession.cpp loolwsd/MasterProcessSession.hpp loolwsd/protocol.txt loolwsd/Storage.cpp loolwsd/Storage.hpp loolwsd/test

Ashod Nakashian ashod.nakashian at collabora.co.uk
Tue Oct 4 14:48:21 UTC 2016


 loleaflet/dist/toolbar.css                   |    9 +
 loleaflet/dist/toolbar/toolbar.js            |   80 +++++++++++++++
 loleaflet/src/control/Toolbar.js             |    8 +
 loleaflet/src/core/Socket.js                 |    3 
 loleaflet/src/layer/tile/CalcTileLayer.js    |    1 
 loleaflet/src/layer/tile/ImpressTileLayer.js |    1 
 loleaflet/src/layer/tile/TileLayer.js        |   32 ++++++
 loleaflet/src/layer/tile/WriterTileLayer.js  |    1 
 loleaflet/src/map/Map.js                     |   18 +++
 loleaflet/src/map/handler/Map.Keyboard.js    |    4 
 loolwsd/ChildProcessSession.cpp              |   33 ++++--
 loolwsd/ChildProcessSession.hpp              |   33 +++++-
 loolwsd/DocumentBroker.cpp                   |    8 +
 loolwsd/DocumentBroker.hpp                   |    4 
 loolwsd/LOKitClient.cpp                      |    2 
 loolwsd/LOKitHelper.hpp                      |    6 -
 loolwsd/LOOLKit.cpp                          |  138 +++++++++++++++++++++------
 loolwsd/LOOLSession.cpp                      |    7 +
 loolwsd/LOOLSession.hpp                      |    3 
 loolwsd/LOOLWSD.cpp                          |    9 +
 loolwsd/MasterProcessSession.cpp             |    7 +
 loolwsd/MasterProcessSession.hpp             |    2 
 loolwsd/Storage.cpp                          |   14 +-
 loolwsd/Storage.hpp                          |    2 
 loolwsd/protocol.txt                         |    6 +
 loolwsd/test/httpwstest.cpp                  |    1 
 26 files changed, 370 insertions(+), 62 deletions(-)

New commits:
commit 63d1d67ff129cafd41a406e9da05694e47f087c5
Author: Ashod Nakashian <ashod.nakashian at collabora.co.uk>
Date:   Sun Aug 14 18:02:51 2016 -0400

    Correct comment authorship; and bring userlist in UI
    
    This includes cherry-picking many commits as listed
    below plus some hackery left and right to make it work with our
    old code here.
    
    To bring userlists here, we treat sessionIds like viewIds and
    provide it to the UI. This helped in easy integration and less
    hackery.
    
    For comments, we pass the name of the author in the uno command
    everytime, otherwise in this non-multiview case, there is no way
    for core to know who issued the command.
    
    List of cherry-picked commits:
    
    loolwsd: convert function-object callbacks to abstract interface
    (cherry picked from commit 5af1f18e364068e4b0dc1cac5a71b6dc5afd3bb4)
    loolwsd: Receive WOPI userid, username
    (cherry picked from commit f8ebb54af0948dbd4b9cb7bdceb44d90b190f4f9)
    Don't check for nullptr after dereferencing.
    (cherry picked from commit 2090f121b61c34a2285430f495aee88c24ebc2e4)
    loolwsd: correct multiview creation
    (cherry picked from commit 6536ba2fe218d26940af12baff2f61b41ccc7300)
    loolwsd: Pass sessions' usernames to core
    (cherry picked from commit 67444eeb80ad521edcab905217c4777cdeacb8c2)
    loolwsd: Handle user names with spaces; encode/decode properly
    (cherry picked from commit db5efae2b81449186552ae6838806304e1d61a48)
    loolwsd: Echo back view information to clients
    (cherry picked from commit 7d48cd6f175e57ce701be307bfbf782b308ddee9)
    loolwsd: Always send the updated view info to clients
    (cherry picked from commit 46107dd0c8973f48117d50111fe7a397320412c8)
    loleaflet: Infrastructure for user list
    (cherry picked from commit 3d8b73da8be0e5727c0613db131e06509ea1a051)
    loleaflet: Show a user list in bottom toolbar
    (cherry picked from commit 3d29df6f998df339896b1355b6fa9186dcc60892)
    loleaflet: Don't use input for userlist; use menu list
    (cherry picked from commit aca73d2ac52e0ab4c7b64f7575447cb2b65a21c8)
    loleaflet: Include username in removeview signal
    (cherry picked from commit 394054ecbf5a89a1a69a00344f1bb63c22a4b2cc)
    loleaflet: Notifications when new user joins, leaves
    (cherry picked from commit d223f2a0e41d8166321044d151de452870f09f3c)
    loleaflet: Better algorithm for adding/removing users from list
    (cherry picked from commit 958c0e35341c73f6636ba141f8fe82ea1ceeee21)
    loleaflet: User count in user list
    (cherry picked from commit 04064b2e23c00af284fd671a73af980bda4236cc)
    loleaflet: userlist l10n fix
    (cherry picked from commit d3eda7d19857c17d98e4000f712cde61066684fe)
    loleaflet: Handle new message, 'viewinfo:'
    (cherry picked from commit 626eab255a23b985507c08c74558de7a67ce40c8)
    loleaflet: Show usercolor in userlist
    (cherry picked from commit 5891371dcacf04182827317482df6b92700aabbd)
    
    Change-Id: I88c6ae53e280c054729c84497b6a0e416c0c13cc

diff --git a/loleaflet/dist/toolbar.css b/loleaflet/dist/toolbar.css
index 7fc6660..35badbb 100644
--- a/loleaflet/dist/toolbar.css
+++ b/loleaflet/dist/toolbar.css
@@ -304,3 +304,12 @@ button.leaflet-control-search-next
     border: 0px;
     background: rgba(66, 151, 215, 1);
 }
+
+tr.useritem:hover {
+    cursor: default;
+    background-color: rgba(67, 172, 232, 0.25);
+}
+
+tr.useritem > td {
+    padding: 7px;
+}
diff --git a/loleaflet/dist/toolbar/toolbar.js b/loleaflet/dist/toolbar/toolbar.js
index 5ec34ea..83334ff 100644
--- a/loleaflet/dist/toolbar/toolbar.js
+++ b/loleaflet/dist/toolbar/toolbar.js
@@ -430,6 +430,7 @@ $(function () {
 			onClick(e.target);
 		}
 	});
+
 	$('#toolbar-down').w2toolbar({
 		name: 'toolbar-down',
 		items: [
@@ -449,6 +450,8 @@ $(function () {
 			{type: 'break'},
 			{type: 'button',  id: 'takeedit', img: 'edit', hint: _('Take edit lock (others can only view)'), caption: _('VIEWING')},
 			{type: 'break'},
+			{type: 'drop', id: 'userlist', text: _('No users'), html: '<div id="userlist_container"><table id="userlist_table"><tbody></tbody></table></div>' },
+			{type: 'break'},
 			{type: 'button',  id: 'prev', img: 'prev', hint: _('Previous page')},
 			{type: 'button',  id: 'next', img: 'next', hint: _('Next page')},
 			{type: 'break', id: 'prevnextbreak'},
@@ -458,6 +461,9 @@ $(function () {
 			{type: 'button',  id: 'zoomin', img: 'zoomin', hint: _('Zoom in')}
 		],
 		onClick: function (e) {
+			if (e.item.id === 'userlist') {
+				return;
+			}
 			onClick(e.target, e.item, e.subItem);
 		}
 	});
@@ -475,7 +481,9 @@ var formatButtons = {
 
 var takeEditPopupMessage = '<div>' + _('You are viewing now.') + '<br/>' + _('Click here to take edit.') + '</div>';
 var takeEditPopupTimeout = null;
-
+var userJoinedPopupMessage = '<div>' + _('%user has joined') + '</div>';
+var userLeftPopupMessage = '<div>' + _('%user has left') + '</div>';
+var userPopupTimeout = null;
 
 function toggleButton(toolbar, state, command)
 {
@@ -1327,6 +1335,76 @@ map.on('statusindicator', function (e) {
 	}
 });
 
+function getUserItem(viewId, userName) {
+	var html = '<tr class="useritem" id="user-' + viewId + '">' +
+	             '<td class="username">' + userName + '</td>' +
+	           '</tr>';
+	return html;
+}
+var nUsers = _('%n users');
+function updateUserListCount() {
+	var userlistItem = w2ui['toolbar-down'].get('userlist');
+	var count = $(userlistItem.html).find('#userlist_table tbody tr').length;
+	if (count > 1) {
+		userlistItem.text = nUsers.replace('%n', count);
+	} else if (count === 1) {
+		userlistItem.text = _('1 user');
+	} else {
+		userlistItem.text = _('No users');
+	}
+
+	w2ui['toolbar-down'].refresh();
+}
+
+map.on('addview', function(e) {
+	if (!e.viewId || !e.username)
+		return;
+
+	$('#tb_toolbar-down_item_userlist')
+		.w2overlay({
+			class: 'loleaflet-font',
+			html: userJoinedPopupMessage.replace('%user', e.username),
+			style: 'padding: 5px'
+		});
+	clearTimeout(userPopupTimeout);
+	userPopupTimeout = setTimeout(function() {
+		$('#tb_toolbar-down_item_userlist').w2overlay('');
+		clearTimeout(userPopupTimeout);
+		userPopupTimeout = null;
+	}, 3000);
+
+	var username = e.username;
+	if (e.viewId === map._docLayer._viewId) {
+		username = _('You');
+	}
+	var userlistItem = w2ui['toolbar-down'].get('userlist');
+	var newhtml = $(userlistItem.html).find('#userlist_table tbody').append(getUserItem(e.viewId, username)).parent().parent()[0].outerHTML;
+	userlistItem.html = newhtml;
+	updateUserListCount();
+});
+
+map.on('removeview', function(e) {
+	if (!e.viewId || !e.username)
+		return;
+
+	$('#tb_toolbar-down_item_userlist')
+		.w2overlay({
+			class: 'loleaflet-font',
+			html: userLeftPopupMessage.replace('%user', e.username),
+			style: 'padding: 5px'
+		});
+	clearTimeout(userPopupTimeout);
+	userPopupTimeout = setTimeout(function() {
+		$('#tb_toolbar-down_item_userlist').w2overlay('');
+		clearTimeout(userPopupTimeout);
+		userPopupTimeout = null;
+	}, 3000);
+
+	var userlistItem = w2ui['toolbar-down'].get('userlist');
+	userlistItem.html = $(userlistItem.html).find('#user-' + e.viewId).remove().end()[0].outerHTML;
+	updateUserListCount();
+});
+
 $(window).resize(function() {
 	resizeToolbar();
 });
diff --git a/loleaflet/src/control/Toolbar.js b/loleaflet/src/control/Toolbar.js
index 19eeaa9..e088b1d 100644
--- a/loleaflet/src/control/Toolbar.js
+++ b/loleaflet/src/control/Toolbar.js
@@ -121,6 +121,14 @@ L.Map.include({
 
 	sendUnoCommand: function (command, json) {
 		if (this._permission === 'edit') {
+			if (typeof json === 'undefined' && command === '.uno:InsertAnnotation') {
+				json = {
+					Author: {
+						type: 'string',
+						value: this._viewInfo[this._docLayer._viewId]
+					}
+				};
+			}
 			this._socket.sendMessage('uno ' + command + (json ? ' ' + JSON.stringify(json) : ''));
 		}
 	},
diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js
index 59ceb10..1b2d03f 100644
--- a/loleaflet/src/core/Socket.js
+++ b/loleaflet/src/core/Socket.js
@@ -388,6 +388,9 @@ L.Socket = L.Class.extend({
 			else if (tokens[i].substring(0, 5) === 'font=') {
 				command.font = window.decodeURIComponent(tokens[i].substring(5));
 			}
+			else if (tokens[i].substring(0, 7) === 'viewid=') {
+				command.viewid = tokens[i].substring(7);
+			}
 			else if (tokens[i].substring(0, 7) === 'params=') {
 				command.params = tokens[i].substring(7).split(',');
 			}
diff --git a/loleaflet/src/layer/tile/CalcTileLayer.js b/loleaflet/src/layer/tile/CalcTileLayer.js
index 83fe155..b3ac37f 100644
--- a/loleaflet/src/layer/tile/CalcTileLayer.js
+++ b/loleaflet/src/layer/tile/CalcTileLayer.js
@@ -154,6 +154,7 @@ L.CalcTileLayer = L.TileLayer.extend({
 			this._docType = command.type;
 			this._parts = command.parts;
 			this._selectedPart = command.selectedPart;
+			this._viewId = parseInt(command.viewid);
 			var mapSize = this._map.getSize();
 			var width = this._docWidthTwips / this._tileWidthTwips * this._tileSize;
 			var height = this._docHeightTwips / this._tileHeightTwips * this._tileSize;
diff --git a/loleaflet/src/layer/tile/ImpressTileLayer.js b/loleaflet/src/layer/tile/ImpressTileLayer.js
index 2d8e8c0..613ffc1 100644
--- a/loleaflet/src/layer/tile/ImpressTileLayer.js
+++ b/loleaflet/src/layer/tile/ImpressTileLayer.js
@@ -119,6 +119,7 @@ L.ImpressTileLayer = L.TileLayer.extend({
 			this._updateMaxBounds(true);
 			this._documentInfo = textMsg;
 			this._parts = command.parts;
+			this._viewId = parseInt(command.viewid);
 			this._selectedPart = command.selectedPart;
 			this._resetPreFetching(true);
 			this._update();
diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index 68fb0ff..c53d73b 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -347,6 +347,9 @@ L.TileLayer = L.GridLayer.extend({
 		else if (textMsg.startsWith('contextmenu:')) {
 			this._onContextMenuMsg(textMsg);
 		}
+		else if (textMsg.startsWith('viewinfo:')) {
+			this._onViewInfoMsg(textMsg);
+		}
 	},
 
 	_onCommandValuesMsg: function (textMsg) {
@@ -510,6 +513,35 @@ L.TileLayer = L.GridLayer.extend({
 		this._onUpdateCursor();
 	},
 
+	_addView: function(viewId, username) {
+		this._map.addView(viewId, username);
+	},
+
+	_removeView: function(viewId) {
+		this._map.removeView(viewId);
+	},
+
+	_onViewInfoMsg: function(textMsg) {
+		textMsg = textMsg.substring('viewinfo: '.length);
+		var viewInfo = JSON.parse(textMsg);
+
+		// A new view
+		var viewIds = [];
+		for (var viewInfoIdx in viewInfo) {
+			if (!(parseInt(viewInfo[viewInfoIdx].id) in this._map._viewInfo)) {
+				this._addView(viewInfo[viewInfoIdx].id, viewInfo[viewInfoIdx].username);
+			}
+			viewIds.push(viewInfo[viewInfoIdx].id);
+		}
+
+		// Check if any view is deleted
+		for (viewInfoIdx in this._map._viewInfo) {
+			if (viewIds.indexOf(parseInt(viewInfoIdx)) === -1) {
+				this._removeView(parseInt(viewInfoIdx));
+			}
+		}
+	},
+
 	_onPartPageRectanglesMsg: function (textMsg) {
 		textMsg = textMsg.substring(19);
 		var pages = textMsg.split(';');
diff --git a/loleaflet/src/layer/tile/WriterTileLayer.js b/loleaflet/src/layer/tile/WriterTileLayer.js
index a42b67d..ab604aa 100644
--- a/loleaflet/src/layer/tile/WriterTileLayer.js
+++ b/loleaflet/src/layer/tile/WriterTileLayer.js
@@ -122,6 +122,7 @@ L.WriterTileLayer = L.TileLayer.extend({
 			this._parts = 1;
 			this._currentPage = command.selectedPart;
 			this._pages = command.parts;
+			this._viewId = parseInt(command.viewid);
 			this._map.fire('pagenumberchanged', {
 				currentPage: this._currentPage,
 				pages: this._pages,
diff --git a/loleaflet/src/map/Map.js b/loleaflet/src/map/Map.js
index df45177..ba72f67 100644
--- a/loleaflet/src/map/Map.js
+++ b/loleaflet/src/map/Map.js
@@ -117,11 +117,29 @@ L.Map = L.Evented.extend({
 
 		this.showBusy(_('Initializing...'), false);
 		this.on('statusindicator', this._onUpdateProgress, this);
+
+		// View info (user names and view ids)
+		this._viewInfo = {};
 	},
 
 
 	// public methods that modify map state
 
+	addView: function(viewid, username) {
+		this._viewInfo[viewid] = username;
+		this.fire('addview', {viewId: viewid, username: username});
+	},
+
+	removeView: function(viewid) {
+		var username = this._viewInfo[viewid];
+		delete this._viewInfo[viewid];
+		this.fire('removeview', {viewId: viewid, username: username});
+	},
+
+	getViewName: function(viewid) {
+		return this._viewInfo[viewid];
+	},
+
 	// replaced by animation-powered implementation in Map.PanAnimation.js
 	setView: function (center, zoom) {
 		zoom = zoom === undefined ? this.getZoom() : zoom;
diff --git a/loleaflet/src/map/handler/Map.Keyboard.js b/loleaflet/src/map/handler/Map.Keyboard.js
index e75ffed..7b8671e 100644
--- a/loleaflet/src/map/handler/Map.Keyboard.js
+++ b/loleaflet/src/map/handler/Map.Keyboard.js
@@ -365,7 +365,7 @@ L.Map.Keyboard = L.Handler.extend({
 
 			// need to handle Ctrl + Alt + C separately for Firefox
 			if (e.originalEvent.key === 'c' && e.originalEvent.altKey) {
-				this._map._socket.sendMessage('uno .uno:InsertAnnotation');
+				this._map.sendUnoCommand('.uno:InsertAnnotation');
 				return true;
 			}
 
@@ -380,7 +380,7 @@ L.Map.Keyboard = L.Handler.extend({
 					return true;
 				case 67: // c
 				case 77: // m
-					this._map._socket.sendMessage('uno .uno:InsertAnnotation');
+					this._map.sendUnoCommand('.uno:InsertAnnotation');
 					return true;
 				case 68: // d
 					this._map._socket.sendMessage('uno .uno:InsertEndnote');
diff --git a/loolwsd/ChildProcessSession.cpp b/loolwsd/ChildProcessSession.cpp
index 3fe0ea0..e4d34e3 100644
--- a/loolwsd/ChildProcessSession.cpp
+++ b/loolwsd/ChildProcessSession.cpp
@@ -10,6 +10,7 @@
 #include "config.h"
 
 #include <iostream>
+#include <sstream>
 #include <thread>
 
 #include <Poco/Exception.h>
@@ -290,15 +291,13 @@ ChildProcessSession::ChildProcessSession(const std::string& id,
                                          std::shared_ptr<WebSocket> ws,
                                          LibreOfficeKitDocument * loKitDocument,
                                          const std::string& jailId,
-                                         OnLoadCallback onLoad,
-                                         OnUnloadCallback onUnload) :
+                                         IDocumentManager& docManager) :
     LOOLSession(id, Kind::ToMaster, ws),
     _loKitDocument(loKitDocument),
     _multiView(std::getenv("LOK_VIEW_CALLBACK")),
     _jailId(jailId),
     _viewId(0),
-    _onLoad(onLoad),
-    _onUnload(onUnload),
+    _docManager(docManager),
     _callbackWorker(new CallbackWorker(_callbackQueue, *this))
 {
     Log::info("ChildProcessSession ctor [" + getName() + "].");
@@ -326,7 +325,8 @@ void ChildProcessSession::disconnect()
         if (_multiView)
             _loKitDocument->pClass->setView(_loKitDocument, _viewId);
 
-        _onUnload(getId());
+        _docManager.onUnload(getId());
+        _docManager.notifyViewInfo();
 
         LOOLSession::disconnect();
     }
@@ -348,6 +348,9 @@ bool ChildProcessSession::_handleInput(const char *buffer, int length)
         if (_multiView)
             _loKitDocument->pClass->setView(_loKitDocument, _viewId);
 
+        // Notify all views about updated view info
+        _docManager.notifyViewInfo();
+
         const int curPart = _loKitDocument->pClass->getPart(_loKitDocument);
         sendTextFrame("curpart: part=" + std::to_string(curPart));
         sendTextFrame("setpart: part=" + std::to_string(curPart));
@@ -596,7 +599,7 @@ bool ChildProcessSession::loadDocument(const char * /*buffer*/, int /*length*/,
 
     std::unique_lock<std::recursive_mutex> lock(Mutex);
 
-    _loKitDocument = _onLoad(getId(), _jailedFilePath, _docPassword, renderOpts, _haveDocPassword);
+    _loKitDocument = _docManager.onLoad(getId(), _jailedFilePath, _userName, _docPassword, renderOpts, _haveDocPassword);
     if (!_loKitDocument)
     {
         Log::error("Failed to get LoKitDocument instance.");
@@ -605,8 +608,17 @@ bool ChildProcessSession::loadDocument(const char * /*buffer*/, int /*length*/,
 
     if (_multiView)
     {
+        std::ostringstream ossViewInfo;
         _viewId = _loKitDocument->pClass->getView(_loKitDocument);
-        _loKitDocument->pClass->initializeForRendering(_loKitDocument, (renderOpts.empty() ? nullptr : renderOpts.c_str()));
+        const auto viewId = std::to_string(_viewId);
+
+        // Create a message object
+        Object::Ptr viewInfoObj = new Object();
+        viewInfoObj->set("id", viewId);
+        viewInfoObj->set("username", _userName);
+        viewInfoObj->stringify(ossViewInfo);
+
+        Log::info("Created new view with viewid: [" + viewId + "] for username: [" + _userName + "].");
     }
 
     _docType = LOKitHelper::getDocumentTypeAsString(_loKitDocument);
@@ -616,10 +628,13 @@ bool ChildProcessSession::loadDocument(const char * /*buffer*/, int /*length*/,
     }
 
     // Respond by the document status, which has no arguments.
-    Log::debug("Sending status after load.");
+    Log::debug("Sending status after loading view " + std::to_string(_viewId) + ".");
     if (!getStatus(nullptr, 0))
         return false;
 
+    // Inform everyone (including this one) about updated view info
+    _docManager.notifyViewInfo();
+
     Log::info("Loaded session " + getId());
     return true;
 }
@@ -673,7 +688,7 @@ bool ChildProcessSession::getStatus(const char* /*buffer*/, int /*length*/)
     if (_multiView)
         _loKitDocument->pClass->setView(_loKitDocument, _viewId);
 
-    const auto status = LOKitHelper::documentStatus(_loKitDocument);
+    const auto status = LOKitHelper::documentStatus(_loKitDocument, Util::decodeId(getId()));
     if (status.empty())
     {
         Log::error("Failed to get document status.");
diff --git a/loolwsd/ChildProcessSession.hpp b/loolwsd/ChildProcessSession.hpp
index 8a760cb..b1bacd0 100644
--- a/loolwsd/ChildProcessSession.hpp
+++ b/loolwsd/ChildProcessSession.hpp
@@ -23,8 +23,28 @@
 #include "LOOLSession.hpp"
 
 class CallbackWorker;
-typedef std::function<LibreOfficeKitDocument*(const std::string&, const std::string&, const std::string&, const std::string&, bool)> OnLoadCallback;
-typedef std::function<void(const std::string&)> OnUnloadCallback;
+/// An abstract interface that defines the
+/// DocumentManager interface and functionality.
+class IDocumentManager
+{
+public:
+    /// Reqest loading a document, or a new view, if one exists.
+    virtual
+    LibreOfficeKitDocument* onLoad(const std::string& sessionId,
+                                   const std::string& jailedFilePath,
+                                   const std::string& userName,
+                                   const std::string& docPassword,
+                                   const std::string& renderOpts,
+                                   const bool haveDocPassword) = 0;
+    /// Unload a client session, which unloads the document
+    /// if it is the last and only.
+    virtual
+    void onUnload(const std::string& sessionId) = 0;
+
+    /// Send updated view info to all active sessions
+    virtual
+    void notifyViewInfo() = 0;
+};
 
 class ChildProcessSession final : public LOOLSession
 {
@@ -40,8 +60,7 @@ public:
                         std::shared_ptr<Poco::Net::WebSocket> ws,
                         LibreOfficeKitDocument * loKitDocument,
                         const std::string& jailId,
-                        OnLoadCallback onLoad,
-                        OnUnloadCallback onUnload);
+                        IDocumentManager& docManager);
     virtual ~ChildProcessSession();
 
     virtual bool getStatus(const char *buffer, int length) override;
@@ -52,7 +71,8 @@ public:
 
     virtual void disconnect() override;
 
-    int getViewId() const  { return _viewId; }
+    int getViewId() const { return _viewId; }
+    const std::string getViewUserName() const { return _userName; }
 
     const std::string& getDocType() const { return _docType; }
 
@@ -98,8 +118,7 @@ private:
     /// View ID, returned by createView() or 0 by default.
     int _viewId;
     std::map<int, std::string> _lastDocStates;
-    OnLoadCallback _onLoad;
-    OnUnloadCallback _onUnload;
+    IDocumentManager& _docManager;
 
     std::unique_ptr<CallbackWorker> _callbackWorker;
     Poco::Thread _callbackThread;
diff --git a/loolwsd/DocumentBroker.cpp b/loolwsd/DocumentBroker.cpp
index 2d9cecf..818c32b 100644
--- a/loolwsd/DocumentBroker.cpp
+++ b/loolwsd/DocumentBroker.cpp
@@ -105,16 +105,20 @@ DocumentBroker::DocumentBroker(const Poco::URI& uriPublic,
     Log::info("DocumentBroker [" + _uriPublic.toString() + "] created. DocKey: [" + _docKey + "]");
 }
 
-void DocumentBroker::validate(const Poco::URI& uri)
+const StorageBase::FileInfo DocumentBroker::validate(const Poco::URI& uri)
 {
     Log::info("Validating: " + uri.toString());
     try
     {
         auto storage = StorageBase::create("", "", uri);
-        if (storage == nullptr || !storage->getFileInfo(uri).isValid())
+        auto fileinfo = storage->getFileInfo(uri);
+        Log::info("After checkfileinfo: " + fileinfo._filename);
+        if (!fileinfo.isValid())
         {
             throw BadRequestException("Invalid URI or access denied.");
         }
+
+        return fileinfo;
     }
     catch (const std::exception&)
     {
diff --git a/loolwsd/DocumentBroker.hpp b/loolwsd/DocumentBroker.hpp
index 53f6ec7..5fd2edd 100644
--- a/loolwsd/DocumentBroker.hpp
+++ b/loolwsd/DocumentBroker.hpp
@@ -25,10 +25,10 @@
 
 #include "IoUtil.hpp"
 #include "MasterProcessSession.hpp"
+#include "Storage.hpp"
 #include "Util.hpp"
 
 // Forwards.
-class StorageBase;
 class TileCache;
 
 /// Represents a new LOK child that is read
@@ -143,7 +143,7 @@ public:
                     << " sessions left." << Log::end;
     }
 
-    void validate(const Poco::URI& uri);
+    const StorageBase::FileInfo validate(const Poco::URI& uri);
 
     /// Loads a document from the public URI into the jail.
     bool load(const std::string& jailId);
diff --git a/loolwsd/LOKitClient.cpp b/loolwsd/LOKitClient.cpp
index 5cb96d5..dcbd28d 100644
--- a/loolwsd/LOKitClient.cpp
+++ b/loolwsd/LOKitClient.cpp
@@ -143,7 +143,7 @@ protected:
                     std::cout << "? syntax" << std::endl;
                     continue;
                 }
-                std::cout << LOKitHelper::documentStatus(loKitDocument) << std::endl;
+                std::cout << LOKitHelper::documentStatus(loKitDocument, 0) << std::endl;
                 for (int i = 0; i < loKitDocument->pClass->getParts(loKitDocument); i++)
                 {
                     std::cout << "  " << i << ": '" << loKitDocument->pClass->getPartName(loKitDocument, i) << "'" << std::endl;
diff --git a/loolwsd/LOKitHelper.hpp b/loolwsd/LOKitHelper.hpp
index 00e13b9..0db0973 100644
--- a/loolwsd/LOKitHelper.hpp
+++ b/loolwsd/LOKitHelper.hpp
@@ -105,7 +105,7 @@ namespace LOKitHelper
     }
 
     inline
-    std::string documentStatus(LibreOfficeKitDocument *loKitDocument)
+    std::string documentStatus(LibreOfficeKitDocument *loKitDocument, unsigned sessionId)
     {
         char* ptrValue;
         assert(loKitDocument && "null loKitDocument");
@@ -119,8 +119,10 @@ namespace LOKitHelper
 
         long width, height;
         loKitDocument->pClass->getDocumentSize(loKitDocument, &width, &height);
+        // Lets use sessionId in non-multiview case like viewid in multiview case
         oss << " width=" << width
-            << " height=" << height;
+            << " height=" << height
+            << " viewid=" << sessionId;
 
         if (type == LOK_DOCTYPE_SPREADSHEET || type == LOK_DOCTYPE_PRESENTATION)
         {
diff --git a/loolwsd/LOOLKit.cpp b/loolwsd/LOOLKit.cpp
index f0e16d7..67d6d24 100644
--- a/loolwsd/LOOLKit.cpp
+++ b/loolwsd/LOOLKit.cpp
@@ -26,12 +26,15 @@
 #include <cstdlib>
 #include <iostream>
 #include <memory>
+#include <sstream>
 
 #define LOK_USE_UNSTABLE_API
 #include <LibreOfficeKit/LibreOfficeKitInit.h>
 #include <LibreOfficeKit/LibreOfficeKitEnums.h>
 
 #include <Poco/Exception.h>
+#include <Poco/JSON/Object.h>
+#include <Poco/JSON/Parser.h>
 #include <Poco/Net/HTTPClientSession.h>
 #include <Poco/Net/HTTPRequest.h>
 #include <Poco/Net/HTTPResponse.h>
@@ -63,6 +66,10 @@ using namespace LOOLProtocol;
 
 using Poco::Exception;
 using Poco::File;
+using Poco::JSON::Array;
+using Poco::JSON::Object;
+using Poco::JSON::Parser;
+
 using Poco::Net::NetException;
 using Poco::Net::HTTPClientSession;
 using Poco::Net::HTTPResponse;
@@ -341,7 +348,7 @@ private:
 /// per process. But for security reasons don't.
 /// However, we could have a loolkit instance
 /// per user or group of users (a trusted circle).
-class Document
+class Document : public IDocumentManager
 {
 public:
     /// We have two types of password protected documents
@@ -463,10 +470,7 @@ public:
             auto ws = std::make_shared<WebSocket>(cs, request, response);
             ws->setReceiveTimeout(0);
 
-            auto session = std::make_shared<ChildProcessSession>(sessionId, ws, _loKitDocument, _jailId,
-                           [this](const std::string& id, const std::string& uri, const std::string& docPassword,
-                                  const std::string& renderOpts, bool haveDocPassword) { return onLoad(id, uri, docPassword, renderOpts, haveDocPassword); },
-                           [this](const std::string& id) { onUnload(id); });
+            auto session = std::make_shared<ChildProcessSession>(sessionId, ws, _loKitDocument, _jailId, *this);
 
             auto thread = std::make_shared<Connection>(session, ws);
             const auto aInserted = _connections.emplace(intSessionId, thread);
@@ -878,9 +882,10 @@ private:
     /// Load a document (or view) and register callbacks.
     LibreOfficeKitDocument* onLoad(const std::string& sessionId,
                                    const std::string& uri,
+                                   const std::string& userName,
                                    const std::string& docPassword,
                                    const std::string& renderOpts,
-                                   bool haveDocPassword)
+                                   const bool haveDocPassword) override
     {
         Log::info("Session " + sessionId + " is loading. " + std::to_string(_clientViews) + " views loaded.");
 
@@ -896,7 +901,11 @@ private:
 
         try
         {
-            load(sessionId, uri, docPassword, renderOpts, haveDocPassword);
+            load(sessionId, uri, userName, docPassword, renderOpts, haveDocPassword);
+            if (!_loKitDocument)
+            {
+                return nullptr;
+            }
         }
         catch (const std::exception& exc)
         {
@@ -913,10 +922,11 @@ private:
         return _loKitDocument;
     }
 
-    void onUnload(const std::string& sessionId)
+    void onUnload(const std::string& sessionId) override
     {
         const unsigned intSessionId = Util::decodeId(sessionId);
         const auto it = _connections.find(intSessionId);
+        Log::info("Unloading [" + sessionId + "].");
         if (it == _connections.end() || !it->second || !_loKitDocument)
         {
             // Nothing to do.
@@ -944,13 +954,53 @@ private:
         }
     }
 
+    /// Notify all views of viewId and their associated usernames
+    void notifyViewInfo() override
+    {
+        std::unique_lock<std::mutex> lock(_mutex);
+
+        // Store the list of viewid, username mapping in a map
+        Array::Ptr viewInfoArray = new Array();
+        int arrayIndex = 0;
+        for (auto& connectionIt : _connections)
+        {
+            Object::Ptr viewInfoObj = new Object();
+
+            if (connectionIt.second->isRunning())
+            {
+                const auto session = connectionIt.second->getSession();
+                viewInfoObj->set("id", Util::decodeId(session->getId()));
+                viewInfoObj->set("username", session->getViewUserName());
+
+                viewInfoArray->set(arrayIndex++, viewInfoObj);
+            }
+        }
+
+        std::ostringstream ossViewInfo;
+        viewInfoArray->stringify(ossViewInfo);
+
+        // Broadcast updated viewinfo to all _active_ connections
+        for (auto& connectionIt: _connections)
+        {
+            if (connectionIt.second->isRunning())
+            {
+                auto session = connectionIt.second->getSession();
+                if (session->isActive())
+                {
+                    session->sendTextFrame("viewinfo: " + ossViewInfo.str());
+                }
+            }
+        }
+    }
+
 private:
 
     LibreOfficeKitDocument* load(const std::string& sessionId,
                                  const std::string& uri,
+                                 const std::string& userName,
                                  const std::string& docPassword,
                                  const std::string& renderOpts,
-                                 bool haveDocPassword)
+                                 const bool haveDocPassword)
     {
         const unsigned intSessionId = Util::decodeId(sessionId);
         const auto it = _connections.find(intSessionId);
@@ -1013,27 +1063,9 @@ private:
                 return nullptr;
             }
 
-            // initializeForRendering() should be called before
-            // registerCallback(), as the previous creates a new view in
-            // Impress.
-            _loKitDocument->pClass->initializeForRendering(_loKitDocument, (renderOpts.empty() ? nullptr : renderOpts.c_str()));
-
-            if (_multiView)
-            {
-                Log::info("Loading view to document from URI: [" + uri + "] for session [" + sessionId + "].");
-                const auto viewId = _loKitDocument->pClass->createView(_loKitDocument);
-
-                _loKitDocument->pClass->registerCallback(_loKitDocument, ViewCallback, reinterpret_cast<void*>(intSessionId));
-
-                Log::info() << "Document [" << _url << "] view ["
-                            << viewId << "] loaded, leaving "
-                            << (_clientViews + 1) << " views." << Log::end;
-            }
-            else
-            {
-                _loKitDocument->pClass->registerCallback(_loKitDocument, DocumentCallback, this);
-            }
-
+            // Only save the options on opening the document.
+            // No support for changing them after opening a document.
+            _renderOpts = renderOpts;
         }
         else
         {
@@ -1058,6 +1090,51 @@ private:
             }
         }
 
+        Object::Ptr renderOptsObj = new Object();
+
+        // Fill the object with renderoptions, if any
+        if (!_renderOpts.empty()) {
+            Parser parser;
+            Poco::Dynamic::Var var = parser.parse(_renderOpts);
+            renderOptsObj = var.extract<Object::Ptr>();
+        }
+
+        // Append name of the user, if any, who opened the document to rendering options
+        if (!userName.empty())
+        {
+            Object::Ptr authorContainer = new Object();
+            Object::Ptr authorObj = new Object();
+            authorObj->set("type", "string");
+            std::string decodedUserName;
+            URI::decode(userName, decodedUserName);
+            authorObj->set("value", decodedUserName);
+
+            renderOptsObj->set(".uno:Author", authorObj);
+        }
+
+        std::ostringstream ossRenderOpts;
+        renderOptsObj->stringify(ossRenderOpts);
+
+        // initializeForRendering() should be called before
+        // registerCallback(), as the previous creates a new view in Impress.
+        _loKitDocument->pClass->initializeForRendering(_loKitDocument, ossRenderOpts.str().c_str());
+
+        if (_multiView)
+        {
+            Log::info("Loading view to document from URI: [" + uri + "] for session [" + sessionId + "].");
+            const auto viewId = _loKitDocument->pClass->createView(_loKitDocument);
+
+            Log::info() << "Document [" << _url << "] view ["
+                        << viewId << "] loaded, leaving "
+                        << (_clientViews + 1) << " views." << Log::end;
+        }
+
+        // initializeForRendering() should be called before
+        // registerCallback(), as the previous creates a new view in Impress.
+        _loKitDocument->pClass->initializeForRendering(_loKitDocument, _renderOpts.c_str());
+
+        _loKitDocument->pClass->registerCallback(_loKitDocument, DocumentCallback, this);
+
         return _loKitDocument;
     }
 
@@ -1069,6 +1146,7 @@ private:
     const std::string _docKey;
     const std::string _url;
     std::string _jailedUrl;
+    std::string _renderOpts;
 
     LibreOfficeKitDocument *_loKitDocument;
 
diff --git a/loolwsd/LOOLSession.cpp b/loolwsd/LOOLSession.cpp
index 3eda87d..4e1f064 100644
--- a/loolwsd/LOOLSession.cpp
+++ b/loolwsd/LOOLSession.cpp
@@ -29,6 +29,7 @@
 #include <Poco/Path.h>
 #include <Poco/String.h>
 #include <Poco/StringTokenizer.h>
+#include <Poco/URI.h>
 
 #include "Common.hpp"
 #include "LOOLProtocol.hpp"
@@ -154,6 +155,12 @@ void LOOLSession::parseDocOptions(const StringTokenizer& tokens, int& part, std:
             _jailedFilePath = tokens[i].substr(strlen("jail="));
             ++offset;
         }
+        else if (tokens[i].find("author=") == 0)
+        {
+            std::string userName = tokens[i].substr(strlen("author="));
+            Poco::URI::decode(userName, _userName);
+            ++offset;
+        }
         else if (tokens[i].find("timestamp=") == 0)
         {
             timestamp = tokens[i].substr(strlen("timestamp="));
diff --git a/loolwsd/LOOLSession.hpp b/loolwsd/LOOLSession.hpp
index 13fa109..22b883b 100644
--- a/loolwsd/LOOLSession.hpp
+++ b/loolwsd/LOOLSession.hpp
@@ -130,6 +130,9 @@ protected:
     // Whether websocket received close frame.  Closing Handshake
     std::atomic<bool> _isCloseFrame;
 
+    /// Name of the user to whom the session belongs to
+    std::string _userName;
+
 private:
 
     virtual bool _handleInput(const char *buffer, int length) = 0;
diff --git a/loolwsd/LOOLWSD.cpp b/loolwsd/LOOLWSD.cpp
index 94ae192..81c26bd 100644
--- a/loolwsd/LOOLWSD.cpp
+++ b/loolwsd/LOOLWSD.cpp
@@ -691,8 +691,8 @@ private:
         }
 
         // Validate the URI and Storage before moving on.
-        docBroker->validate(uriPublic);
-        Log::debug("Validated [" + uriPublic.toString() + "].");
+        const auto fileinfo = docBroker->validate(uriPublic);
+        Log::debug("Validated [" + uriPublic.toString() + "]");
 
         if (newDoc)
         {
@@ -709,6 +709,11 @@ private:
             // thread to pump them. This is to empty the queue when we get a "canceltiles" message.
             auto queue = std::make_shared<BasicTileQueue>();
             session = std::make_shared<MasterProcessSession>(id, LOOLSession::Kind::ToClient, ws, docBroker, queue);
+            if (!fileinfo._userName.empty())
+            {
+                Log::debug(uriPublic.toString() + " requested with username [" + fileinfo._userName + "]");
+                session->setUserName(fileinfo._userName);
+            }
 
             // Request the child to connect to us and add this session.
             auto sessionsCount = docBroker->addSession(session);
diff --git a/loolwsd/MasterProcessSession.cpp b/loolwsd/MasterProcessSession.cpp
index 8a84c8a..97e73a5 100644
--- a/loolwsd/MasterProcessSession.cpp
+++ b/loolwsd/MasterProcessSession.cpp
@@ -645,6 +645,13 @@ void MasterProcessSession::dispatchChild()
     oss << " url=" << _docBroker->getPublicUri().toString();
     oss << " jail=" << _docBroker->getJailedUri().toString();
 
+    if (!_userName.empty())
+    {
+        std::string encodedUserName;
+        Poco::URI::encode(_userName, "", encodedUserName);
+        oss << " author=" + encodedUserName;
+    }
+
     if (_loadPart >= 0)
         oss << " part=" + std::to_string(_loadPart);
 
diff --git a/loolwsd/MasterProcessSession.hpp b/loolwsd/MasterProcessSession.hpp
index 19a4788..eeed160 100644
--- a/loolwsd/MasterProcessSession.hpp
+++ b/loolwsd/MasterProcessSession.hpp
@@ -53,6 +53,8 @@ class MasterProcessSession final : public LOOLSession, public std::enable_shared
 
     bool shutdownPeer(Poco::UInt16 statusCode, const std::string& message);
 
+    void setUserName(const std::string& userName) { _userName = userName; }
+
 public:
     // Raise this flag on ToClient from ToPrisoner to let ToClient know of load failures
     bool _bLoadError = false;
diff --git a/loolwsd/Storage.cpp b/loolwsd/Storage.cpp
index 9f1694e..79c76e7 100644
--- a/loolwsd/Storage.cpp
+++ b/loolwsd/Storage.cpp
@@ -137,6 +137,8 @@ std::unique_ptr<StorageBase> StorageBase::create(const std::string& jailRoot, co
     if (UnitWSD::get().createStorage(jailRoot, jailPath, uri, storage))
     {
         Log::info("Storage load hooked.");
+        if (storage)
+            return storage;
     }
     else if (uri.isRelative() || uri.getScheme() == "file")
     {
@@ -174,7 +176,7 @@ StorageBase::FileInfo LocalStorage::getFileInfo(const Poco::URI& uri)
     const auto file = Poco::File(path);
     const auto lastModified = file.getLastModified();
     const auto size = file.getSize();
-    return FileInfo({filename, lastModified, size});
+    return FileInfo({filename, lastModified, size, "localhost", "Local Host"});
 }
 
 std::string LocalStorage::loadStorageFileToLocal()
@@ -275,6 +277,8 @@ StorageBase::FileInfo WopiStorage::getFileInfo(const Poco::URI& uri)
     // Parse the response.
     std::string filename;
     size_t size = 0;
+    std::string userId;
+    std::string userName;
     std::string resMsg;
     Poco::StreamCopier::copyToString(rs, resMsg);
     Log::debug("WOPI::CheckFileInfo returned: " + resMsg);
@@ -287,10 +291,12 @@ StorageBase::FileInfo WopiStorage::getFileInfo(const Poco::URI& uri)
         const auto& object = result.extract<Poco::JSON::Object::Ptr>();
         filename = object->get("BaseFileName").toString();
         size = std::stoul (object->get("Size").toString(), nullptr, 0);
+        userId = object->get("UserId").toString();
+        userName = object->get("UserFriendlyName").toString();
     }
 
     // WOPI doesn't support file last modified time.
-    return FileInfo({filename, Poco::Timestamp(), size});
+    return FileInfo({filename, Poco::Timestamp(), size, userId, userName});
 }
 
 /// uri format: http://server/<...>/wopi*/files/<id>/content
@@ -385,8 +391,8 @@ StorageBase::FileInfo WebDAVStorage::getFileInfo(const Poco::URI& uri)
 {
     Log::debug("Getting info for webdav uri [" + uri.toString() + "].");
     (void)uri;
-    assert(!"Not Implemented!");
-    return FileInfo({"bazinga", Poco::Timestamp(), 0});
+    assert(false && "Not Implemented!");
+    return FileInfo({"bazinga", Poco::Timestamp(), 0, "admin", "admin"});
 }
 
 std::string WebDAVStorage::loadStorageFileToLocal()
diff --git a/loolwsd/Storage.hpp b/loolwsd/Storage.hpp
index 84aa8a2..4c0df32 100644
--- a/loolwsd/Storage.hpp
+++ b/loolwsd/Storage.hpp
@@ -36,6 +36,8 @@ public:
         std::string _filename;
         Poco::Timestamp _modifiedTime;
         size_t _size;
+        std::string _userId;
+        std::string _userName;
     };
 
     /// localStorePath the absolute root path of the chroot.
diff --git a/loolwsd/protocol.txt b/loolwsd/protocol.txt
index a3b1c82..d880f31 100644
--- a/loolwsd/protocol.txt
+++ b/loolwsd/protocol.txt
@@ -302,6 +302,12 @@ unocommandresult: <payload>
 Callback that an UNO command has finished.
 See LOK_CALLBACK_UNO_COMMAND_RESULT for details.
 
+viewinfo: <payload>
+
+    Message is sent everytime there is any change in view information.
+    <payload> consists of an array of JSON objects. Structure of JSON
+    objects is like : {"id": <viewid>, "username": <Full Name of the user>}
+
 pong
 
     See above.
diff --git a/loolwsd/test/httpwstest.cpp b/loolwsd/test/httpwstest.cpp
index bf87306..725edf6 100644
--- a/loolwsd/test/httpwstest.cpp
+++ b/loolwsd/test/httpwstest.cpp
@@ -1233,6 +1233,7 @@ void HTTPWSTest::testInactiveClient()
                                             token == "invalidatetiles:" ||
                                             token == "invalidatecursor:" ||
                                             token == "statechanged:" ||
+                                            token == "viewinfo:" ||
                                             token == "editlock:");
 
                     // End when we get state changed.


More information about the Libreoffice-commits mailing list