loolwsd: Introduce a readonly mode
Specified when websocket is initialized. Documents once opened in readonly mode cannot edit throughout the life of the session. This is very much like present view mode except the ability to change to edit mode. Change-Id: I176e3bbf210c3383268d1a5b50dc17f0cbfb26b8
This commit is contained in:
parent
162619be72
commit
62814d29cf
6 changed files with 76 additions and 23 deletions
|
@ -9,7 +9,10 @@ L.Socket = L.Class.extend({
|
|||
initialize: function (map) {
|
||||
this._map = map;
|
||||
try {
|
||||
this.socket = new WebSocket(map.options.server + '/lool/ws/' + map.options.doc);
|
||||
var params = {
|
||||
permission: map.options.permission
|
||||
};
|
||||
this.socket = new WebSocket(map.options.server + '/lool/ws/' + map.options.doc + '?' + $.param(params));
|
||||
} catch (e) {
|
||||
this.fire('error', {msg: _('Oops, there is a problem connecting to LibreOffice Online : ' + e), cmd: 'socket', kind: 'failed', id: 3});
|
||||
return null;
|
||||
|
|
|
@ -37,11 +37,13 @@ using Poco::StringTokenizer;
|
|||
ClientSession::ClientSession(const std::string& id,
|
||||
std::shared_ptr<Poco::Net::WebSocket> ws,
|
||||
std::shared_ptr<DocumentBroker> docBroker,
|
||||
std::shared_ptr<BasicTileQueue> queue) :
|
||||
std::shared_ptr<BasicTileQueue> queue,
|
||||
bool isReadOnly) :
|
||||
LOOLSession(id, Kind::ToClient, ws),
|
||||
_docBroker(docBroker),
|
||||
_queue(queue),
|
||||
_haveEditLock(std::getenv("LOK_VIEW_CALLBACK")),
|
||||
_isReadOnly(isReadOnly),
|
||||
_loadFailed(false),
|
||||
_loadPart(-1)
|
||||
{
|
||||
|
@ -92,7 +94,7 @@ bool ClientSession::_handleInput(const char *buffer, int length)
|
|||
return true;
|
||||
}
|
||||
|
||||
if (tokens[0] == "takeedit")
|
||||
if (!isReadOnly() && tokens[0] == "takeedit")
|
||||
{
|
||||
_docBroker->takeEditLock(getId());
|
||||
return true;
|
||||
|
@ -184,7 +186,7 @@ bool ClientSession::_handleInput(const char *buffer, int length)
|
|||
}
|
||||
|
||||
// Allow 'downloadas' for all kinds of views irrespective of editlock
|
||||
if (!isEditLocked() && tokens[0] != "downloadas" &&
|
||||
if ( (isReadOnly() || !isEditLocked()) && tokens[0] != "downloadas" &&
|
||||
tokens[0] != "userinactive" && tokens[0] != "useractive")
|
||||
{
|
||||
std::string dummyFrame = "dummymsg";
|
||||
|
|
|
@ -22,13 +22,15 @@ public:
|
|||
ClientSession(const std::string& id,
|
||||
std::shared_ptr<Poco::Net::WebSocket> ws,
|
||||
std::shared_ptr<DocumentBroker> docBroker,
|
||||
std::shared_ptr<BasicTileQueue> queue);
|
||||
std::shared_ptr<BasicTileQueue> queue,
|
||||
bool isReadOnly = false);
|
||||
|
||||
virtual ~ClientSession();
|
||||
|
||||
bool setEditLock(const bool value);
|
||||
void markEditLock(const bool value) { _haveEditLock = (value || std::getenv("LOK_VIEW_CALLBACK")); }
|
||||
bool isEditLocked() const { return _haveEditLock; }
|
||||
bool isReadOnly() const { return _isReadOnly; }
|
||||
|
||||
void setPeer(const std::shared_ptr<PrisonerSession>& peer) { _peer = peer; }
|
||||
bool shutdownPeer(Poco::UInt16 statusCode, const std::string& message);
|
||||
|
@ -88,6 +90,9 @@ private:
|
|||
// while other session opening the same document can only see
|
||||
bool _haveEditLock;
|
||||
|
||||
// Whether the session is opened as readonly
|
||||
bool _isReadOnly;
|
||||
|
||||
/// Our peer that connects us to the child.
|
||||
std::weak_ptr<PrisonerSession> _peer;
|
||||
|
||||
|
|
|
@ -108,6 +108,7 @@ DocumentBroker::DocumentBroker(const Poco::URI& uriPublic,
|
|||
_childProcess(childProcess),
|
||||
_lastSaveTime(std::chrono::steady_clock::now()),
|
||||
_markToDestroy(false),
|
||||
_lastEditableSession(false),
|
||||
_cursorPosX(0),
|
||||
_cursorPosY(0),
|
||||
_isLoaded(false),
|
||||
|
@ -192,10 +193,10 @@ bool DocumentBroker::save()
|
|||
|
||||
const auto uri = _uriPublic.toString();
|
||||
|
||||
// If we aren't potentially destroying just yet, and the file
|
||||
// If we aren't destroying the last editable session just yet, and the file
|
||||
// timestamp hasn't changed, skip saving.
|
||||
const auto newFileModifiedTime = Poco::File(_storage->getLocalRootPath()).getLastModified();
|
||||
if (!isMarkedToDestroy() && newFileModifiedTime == _lastFileModifiedTime)
|
||||
if (!isLastEditableSession() && newFileModifiedTime == _lastFileModifiedTime)
|
||||
{
|
||||
// Nothing to do.
|
||||
Log::debug() << "Skipping unnecessary saving to URI [" << uri
|
||||
|
@ -340,7 +341,13 @@ size_t DocumentBroker::addSession(std::shared_ptr<ClientSession>& session)
|
|||
Log::warn("DocumentBroker: Trying to add already existing session.");
|
||||
}
|
||||
|
||||
if (_sessions.size() == 1)
|
||||
if (session->isReadOnly())
|
||||
{
|
||||
Log::debug("Adding a readonly session [" + id + "]");
|
||||
}
|
||||
// TODO: Below is not always true. What if readonly session is already opened
|
||||
// In that case we still have to give edit lock to this *second* session.
|
||||
else if (_sessions.size() == 1)
|
||||
{
|
||||
Log::debug("Giving editing lock to the first session [" + id + "].");
|
||||
_sessions.begin()->second->markEditLock(true);
|
||||
|
@ -384,11 +391,14 @@ size_t DocumentBroker::removeSession(const std::string& id)
|
|||
|
||||
if (haveEditLock)
|
||||
{
|
||||
// pass the edit lock to first session in map
|
||||
it = _sessions.begin();
|
||||
if (it != _sessions.end())
|
||||
// pass the edit lock to first non-readonly session in map
|
||||
for (auto& session: _sessions)
|
||||
{
|
||||
it->second->setEditLock(true);
|
||||
if (!session.second->isReadOnly())
|
||||
{
|
||||
session.second->setEditLock(true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -624,14 +634,31 @@ void DocumentBroker::handleTileCombinedResponse(const std::vector<char>& payload
|
|||
}
|
||||
}
|
||||
|
||||
bool DocumentBroker::canDestroy()
|
||||
void DocumentBroker::startDestroy(const std::string& id)
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(_mutex);
|
||||
|
||||
auto currentSession = _sessions.find(id);
|
||||
assert(currentSession != _sessions.end());
|
||||
|
||||
// Check if session which is being destroyed is last non-readonly session
|
||||
bool isLastEditableSession = !currentSession->second->isReadOnly();
|
||||
for (auto& it: _sessions)
|
||||
{
|
||||
if (it.second->getId() == id)
|
||||
continue;
|
||||
|
||||
if (!it.second->isReadOnly())
|
||||
{
|
||||
isLastEditableSession = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Last editable session going away
|
||||
_lastEditableSession = isLastEditableSession;
|
||||
|
||||
// Last view going away, can destroy.
|
||||
_markToDestroy = (_sessions.size() <= 1);
|
||||
|
||||
return _markToDestroy;
|
||||
}
|
||||
|
||||
void DocumentBroker::setModified(const bool value)
|
||||
|
|
|
@ -217,9 +217,12 @@ public:
|
|||
void handleTileResponse(const std::vector<char>& payload);
|
||||
void handleTileCombinedResponse(const std::vector<char>& payload);
|
||||
|
||||
// Called when the last view is going out.
|
||||
bool canDestroy();
|
||||
// Called before destroying any session
|
||||
// This method calculates and sets important states of
|
||||
// session being destroyed.
|
||||
void startDestroy(const std::string& id);
|
||||
bool isMarkedToDestroy() const { return _markToDestroy; }
|
||||
bool isLastEditableSession() const { return _lastEditableSession; }
|
||||
|
||||
bool handleInput(const std::vector<char>& payload);
|
||||
|
||||
|
@ -246,6 +249,7 @@ private:
|
|||
std::unique_ptr<StorageBase> _storage;
|
||||
std::unique_ptr<TileCache> _tileCache;
|
||||
std::atomic<bool> _markToDestroy;
|
||||
std::atomic<bool> _lastEditableSession;
|
||||
int _cursorPosX;
|
||||
int _cursorPosY;
|
||||
bool _isLoaded;
|
||||
|
|
|
@ -624,6 +624,15 @@ private:
|
|||
docBrokers.emplace(docKey, docBroker);
|
||||
}
|
||||
|
||||
// Check if readonly session is required
|
||||
bool isReadOnly = false;
|
||||
for (auto& param: uriPublic.getQueryParameters())
|
||||
{
|
||||
Log::debug("Query param: " + param.first + ", value: " + param.second);
|
||||
if (param.first == "permission")
|
||||
isReadOnly = param.second == "readonly";
|
||||
}
|
||||
|
||||
// Above this point exceptions are safe and will auto-cleanup.
|
||||
// Below this, we need to cleanup internal references.
|
||||
std::shared_ptr<ClientSession> session;
|
||||
|
@ -632,7 +641,7 @@ private:
|
|||
// For ToClient sessions, we store incoming messages in a queue and have a separate
|
||||
// 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<ClientSession>(id, ws, docBroker, queue);
|
||||
session = std::make_shared<ClientSession>(id, ws, docBroker, queue, isReadOnly);
|
||||
|
||||
// Request the child to connect to us and add this session.
|
||||
auto sessionsCount = docBroker->addSession(session);
|
||||
|
@ -671,7 +680,10 @@ private:
|
|||
// We cannot destroy it, before save, if this is the last session
|
||||
// Otherwise, we may end up removing the one and only session.
|
||||
bool removedSession = false;
|
||||
auto canDestroy = docBroker->canDestroy();
|
||||
docBroker->startDestroy(id);
|
||||
|
||||
// We issue a force-save when last editable (non-readonly) session is going away
|
||||
bool forceSave = docBroker->isLastEditableSession();
|
||||
sessionsCount = docBroker->getSessionsCount();
|
||||
if (sessionsCount > 1)
|
||||
{
|
||||
|
@ -681,14 +693,14 @@ private:
|
|||
}
|
||||
|
||||
// If we are the last, we must wait for the save to complete.
|
||||
if (canDestroy)
|
||||
if (forceSave)
|
||||
{
|
||||
Log::info("Shutdown of the last session, saving the document before tearing down.");
|
||||
Log::info("Shutdown of the last editable (non-readonly) session, saving the document before tearing down.");
|
||||
}
|
||||
|
||||
// We need to wait until the save notification reaches us
|
||||
// and Storage persists the document.
|
||||
if (!docBroker->autoSave(canDestroy, COMMAND_TIMEOUT_MS))
|
||||
if (!docBroker->autoSave(forceSave, COMMAND_TIMEOUT_MS))
|
||||
{
|
||||
Log::error("Auto-save before closing failed.");
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue