[Libreoffice-commits] online.git: Branch 'feature/proxyhack' - 107 commits - android/app android/lib common/FileUtil.cpp common/LOOLWebSocket.hpp common/Session.hpp common/Util.cpp common/Util.hpp configure.ac cypress_test/data cypress_test/integration_tests cypress_test/Makefile.am cypress_test/package.json docker/l10n-docker-nightly.sh ios/Mobile ios/Mobile.xcodeproj kit/ChildSession.cpp kit/Kit.cpp loleaflet/css loleaflet/html loleaflet/images loleaflet/js loleaflet/Makefile.am loleaflet/po loleaflet/spec loleaflet/src loolkitconfig-mobile.xcu loolwsd.xml.in Makefile.am net/Socket.cpp net/Socket.hpp test/Makefile.am wsd/ClientSession.cpp wsd/ClientSession.hpp wsd/DocumentBroker.cpp wsd/DocumentBroker.hpp wsd/FileServer.cpp wsd/LOOLWSD.cpp wsd/LOOLWSD.hpp wsd/ProxyProtocol.cpp wsd/ProxyProtocol.hpp

Michael Meeks (via logerrit) logerrit at kemper.freedesktop.org
Tue Apr 7 20:44:12 UTC 2020


Rebased ref, commits from common ancestor:
commit 023dc01cb8aa60efa99a86400d56b7ff14e2dda5
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Mar 21 20:03:37 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    Proxy: don't leave out sockets lingering around for the !flush case.
    
    Change-Id: I13ad123a6c3a068a676eae5e509367e727e9ac06

diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 6af0a33bf..6b0db2261 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -181,11 +181,14 @@ void ProxyProtocolHandler::handleIncomingMessage(SocketDisposition &disposition)
 int ProxyProtocolHandler::sendMessage(const char *msg, const size_t len, bool text, bool flush)
 {
     _writeQueue.push_back(std::make_shared<Message>(msg, len, text));
-    auto sock = popOutSocket();
-    if (sock && flush)
+    if (flush)
     {
-        flushQueueTo(sock);
-        sock->shutdown();
+        auto sock = popOutSocket();
+        if (sock)
+        {
+            flushQueueTo(sock);
+            sock->shutdown();
+        }
     }
 
     return len;
commit 3237cef6f5b50b3b9e92f78a3b618853a387b3de
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Mar 21 15:07:10 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    Proxy: improve debugging & naming.
    
    Change-Id: Ifba669a33855a67c9a4e968db42ef1a2cb301d63

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index eb2a0d88b..adcda3bde 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -216,7 +216,7 @@
 		};
 		this.parseIncomingArray = function(arr) {
 			var decoder = new TextDecoder();
-			console.debug('Parse incoming array of length ' + arr.length);
+			console.debug('proxy: parse incoming array of length ' + arr.length);
 			for (var i = 0; i < arr.length; ++i)
 			{
 				var left = arr.length - i;
@@ -274,7 +274,7 @@
 					if (this.status == 200)
 						that.parseIncomingArray(new Uint8Array(this.response));
 					else
-						console.debug('Error on incoming response');
+						console.debug('proxy: error on incoming response');
 				});
 			}
 			req.send(that.sendQueue);
@@ -300,21 +300,24 @@
 				this.sendTimeout = setTimeout(this.doSend, 2 /* ms */);
 		};
 		this.close = function() {
-			console.debug('close socket');
+			console.debug('proxy: close socket');
 			this.readyState = 3;
 			this.onclose();
+			clearInterval(this.waitInterval);
+			this.waitInterval = undefined;
 		};
 		this.getEndPoint = function(type) {
 			var base = this.uri;
 			return base.replace(/^ws/, 'http') + '/' + type;
 		};
-		console.debug('New proxy socket ' + this.id + ' ' + this.uri);
+		console.debug('proxy: new socket ' + this.id + ' ' + this.uri);
 
 		// queue fetch of session id.
 		this.getSessionId();
 
 		// horrors ...
-		this.readInterval = setInterval(function() {
+		this.waitConnect = function() {
+			console.debug('proxy: waiting - ' + that.readWaiting + ' on session ' + that.sessionId);
 			if (that.readWaiting > 4) // max 4 waiting connections concurrently.
 				return;
 			if (that.sessionId == 'fetchsession')
@@ -329,13 +332,16 @@
 			});
 			req.addEventListener('loadend', function() {
 				that.readWaiting--;
+				console.debug('proxy: wait ended, re-issue');
+				that.waitConnect();
 			});
-			req.open('GET', that.getEndPoint('read'));
+			req.open('GET', that.getEndPoint('wait'));
 			req.setRequestHeader('SessionId', that.sessionId);
 			req.responseType = 'arraybuffer';
 			req.send('');
 			that.readWaiting++;
-		}, 250);
+		};
+		this.waitInterval = setInterval(this.waitConnect, 250);
 	};
 
 	if (global.socketProxy)
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index c1e745a6e..a71bce1b3 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2855,7 +2855,7 @@ private:
             none, url, docKey, _id, uriPublic);
 
         std::string fullURL = request.getURI();
-        std::string ending = "/ws/read";
+        std::string ending = "/ws/wait";
         bool isWaiting = (fullURL.size() > ending.size() &&
                           std::equal(ending.rbegin(), ending.rend(), fullURL.rbegin()));
         if (docBroker)
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 0928b4541..6af0a33bf 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -31,7 +31,7 @@ void DocumentBroker::handleProxyRequest(
     std::shared_ptr<ClientSession> clientSession;
     if (sessionId == "fetchsession")
     {
-        LOG_TRC("Create session for " << _docKey);
+        LOG_TRC("proxy: Create session for " << _docKey);
         clientSession = createNewClientSession(
                 std::make_shared<ProxyProtocolHandler>(),
                 id, uriPublic, isReadOnly, hostNoTrust);
@@ -39,7 +39,7 @@ void DocumentBroker::handleProxyRequest(
         LOOLWSD::checkDiskSpaceAndWarnClients(true);
         LOOLWSD::checkSessionLimitsAndWarnClients();
 
-        LOG_TRC("Returning id " << clientSession->getId());
+        LOG_TRC("proxy: Returning sessionId " << clientSession->getId());
 
         std::ostringstream oss;
         oss << "HTTP/1.1 200 OK\r\n"
@@ -57,7 +57,7 @@ void DocumentBroker::handleProxyRequest(
     }
     else
     {
-        LOG_TRC("Find session for " << _docKey << " with id " << sessionId);
+        LOG_TRC("proxy: find session for " << _docKey << " with id " << sessionId);
         for (const auto &it : _sessions)
         {
             if (it.second->getId() == sessionId)
@@ -133,28 +133,29 @@ void ProxyProtocolHandler::handleRequest(bool isWaiting, const std::shared_ptr<S
 {
     auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
 
-    LOG_INF("Proxy handle request type: " << (isWaiting ? "wait" : "respond"));
+    LOG_INF("proxy: handle request type: " << (isWaiting ? "wait" : "respond") <<
+            " on socket #" << socket->getFD());
 
     if (!isWaiting)
     {
         if (!_msgHandler)
-            LOG_WRN("unusual - incoming message with no-one to handle it");
+            LOG_WRN("proxy: unusual - incoming message with no-one to handle it");
         else if (!parseEmitIncoming(streamSocket))
         {
             std::stringstream oss;
             streamSocket->dumpState(oss);
-            LOG_ERR("bad socket structure " << oss.str());
+            LOG_ERR("proxy: bad socket structure " << oss.str());
         }
     }
 
     if (!flushQueueTo(streamSocket) && isWaiting)
     {
-        LOG_TRC("Queue a waiting socket");
+        LOG_TRC("proxy: queue a waiting out socket #" << streamSocket->getFD());
         // longer running 'write socket' (marked 'read' by the client)
         _outSockets.push_back(streamSocket);
         if (_outSockets.size() > 16)
         {
-            LOG_ERR("Unexpected - client opening many concurrent waiting connections " << _outSockets.size());
+            LOG_ERR("proxy: Unexpected - client opening many concurrent waiting connections " << _outSockets.size());
             // cleanup older waiting sockets.
             auto sockWeak = _outSockets.front();
             _outSockets.erase(_outSockets.begin());
@@ -180,7 +181,7 @@ void ProxyProtocolHandler::handleIncomingMessage(SocketDisposition &disposition)
 int ProxyProtocolHandler::sendMessage(const char *msg, const size_t len, bool text, bool flush)
 {
     _writeQueue.push_back(std::make_shared<Message>(msg, len, text));
-    auto sock = popWriteSocket();
+    auto sock = popOutSocket();
     if (sock && flush)
     {
         flushQueueTo(sock);
@@ -230,16 +231,24 @@ int ProxyProtocolHandler::getPollEvents(std::chrono::steady_clock::time_point /*
     return events;
 }
 
-void ProxyProtocolHandler::performWrites()
+/// slurp from the core to us, @returns true if there are messages to send
+bool ProxyProtocolHandler::slurpHasMessages()
 {
-    if (_msgHandler)
+    if (_msgHandler && _msgHandler->hasQueuedMessages())
         _msgHandler->writeQueuedMessages();
-    if (_writeQueue.size() <= 0)
+
+    return _writeQueue.size() > 0;
+}
+
+void ProxyProtocolHandler::performWrites()
+{
+    if (!slurpHasMessages())
         return;
 
-    auto sock = popWriteSocket();
+    auto sock = popOutSocket();
     if (sock)
     {
+        LOG_TRC("proxy: performWrites");
         flushQueueTo(sock);
         sock->shutdown();
     }
@@ -247,9 +256,8 @@ void ProxyProtocolHandler::performWrites()
 
 bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &socket)
 {
-    // slurp from the core to us.
-    if (_msgHandler && _msgHandler->hasQueuedMessages())
-        _msgHandler->writeQueuedMessages();
+    if (!slurpHasMessages())
+        return false;
 
     size_t totalSize = 0;
     for (auto it : _writeQueue)
@@ -258,6 +266,8 @@ bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &soc
     if (!totalSize)
         return false;
 
+    LOG_TRC("proxy: flushQueue of size " << totalSize << " to socket #" << socket->getFD() << " & close");
+
     std::ostringstream oss;
     oss << "HTTP/1.1 200 OK\r\n"
         "Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
@@ -276,7 +286,7 @@ bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &soc
 }
 
 // LRU-ness ...
-std::shared_ptr<StreamSocket> ProxyProtocolHandler::popWriteSocket()
+std::shared_ptr<StreamSocket> ProxyProtocolHandler::popOutSocket()
 {
     std::weak_ptr<StreamSocket> sock;
     while (!_outSockets.empty())
@@ -285,8 +295,12 @@ std::shared_ptr<StreamSocket> ProxyProtocolHandler::popWriteSocket()
         _outSockets.erase(_outSockets.begin());
         auto realSock = sock.lock();
         if (realSock)
+        {
+            LOG_TRC("proxy: popped an out socket #" << realSock->getFD() << " leaving: " << _outSockets.size());
             return realSock;
+        }
     }
+    LOG_TRC("proxy: no out sockets to pop.");
     return std::shared_ptr<StreamSocket>();
 }
 
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
index 61f0f32be..22cd48fb2 100644
--- a/wsd/ProxyProtocol.hpp
+++ b/wsd/ProxyProtocol.hpp
@@ -61,7 +61,8 @@ public:
     void handleRequest(bool isWaiting, const std::shared_ptr<Socket> &socket);
 
 private:
-    std::shared_ptr<StreamSocket> popWriteSocket();
+    std::shared_ptr<StreamSocket> popOutSocket();
+    bool slurpHasMessages();
     int sendMessage(const char *msg, const size_t len, bool text, bool flush);
     bool flushQueueTo(const std::shared_ptr<StreamSocket> &socket);
 
commit 2b5422aa5f6b4ec91eee971baec5b54f009c6c0b
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Mar 21 14:27:15 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    Proxy: ensure dumpState dumps via the MessageHandlerInterface too.
    
    Change-Id: If514e2359188d56bbf7ddef6e04f9d8bf5c50910

diff --git a/net/Socket.cpp b/net/Socket.cpp
index cb19c99cd..80ae0a1b1 100644
--- a/net/Socket.cpp
+++ b/net/Socket.cpp
@@ -399,6 +399,8 @@ void WebSocketHandler::dumpState(std::ostream& os)
     if (_wsPayload.size() > 0)
         Util::dumpHex(os, "\t\tws queued payload:\n", "\t\t", _wsPayload);
     os << "\n";
+    if (_msgHandler)
+        _msgHandler->dumpState(os);
 }
 
 void StreamSocket::dumpState(std::ostream& os)
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 7db033c99..0928b4541 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -217,6 +217,8 @@ void ProxyProtocolHandler::dumpState(std::ostream& os)
     os << "proxy protocol sockets: " << _outSockets.size() << " writeQueue: " << _writeQueue.size() << ":\n";
     for (auto it : _writeQueue)
         Util::dumpHex(os, "\twrite queue entry:", "\t\t", *it);
+    if (_msgHandler)
+        _msgHandler->dumpState(os);
 }
 
 int ProxyProtocolHandler::getPollEvents(std::chrono::steady_clock::time_point /* now */,
commit d7a05473ac12bb8a088e5eec4a59a9b797306c94
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Mar 21 14:19:49 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    Proxy: make eslint happier.
    
    Change-Id: I9ecec787a9a69633a015459eaf39d4b8bd5bb61d

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index f0451384b..eb2a0d88b 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -1,4 +1,5 @@
 /* -*- js-indent-level: 8 -*- */
+/* global Uint8Array */
 (function (global) {
 
 	var ua = navigator.userAgent.toLowerCase(),
@@ -312,8 +313,6 @@
 		// queue fetch of session id.
 		this.getSessionId();
 
-		var that = this;
-
 		// horrors ...
 		this.readInterval = setInterval(function() {
 			if (that.readWaiting > 4) // max 4 waiting connections concurrently.
commit 5b324a74763e49328b13257f34e697875b1d67d8
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Mar 20 20:45:38 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    Proxy: poll for output space if we need waking.
    
    Change-Id: I18a5e71bd3342eea7992672d9be1f5518ea008e3

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 79ca29fcd..f0451384b 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -215,6 +215,7 @@
 		};
 		this.parseIncomingArray = function(arr) {
 			var decoder = new TextDecoder();
+			console.debug('Parse incoming array of length ' + arr.length);
 			for (var i = 0; i < arr.length; ++i)
 			{
 				var left = arr.length - i;
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 25602f146..7db033c99 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -219,6 +219,15 @@ void ProxyProtocolHandler::dumpState(std::ostream& os)
         Util::dumpHex(os, "\twrite queue entry:", "\t\t", *it);
 }
 
+int ProxyProtocolHandler::getPollEvents(std::chrono::steady_clock::time_point /* now */,
+                                        int &/* timeoutMaxMs */)
+{
+    int events = POLLIN;
+    if (_msgHandler && _msgHandler->hasQueuedMessages())
+        events |= POLLOUT;
+    return events;
+}
+
 void ProxyProtocolHandler::performWrites()
 {
     if (_msgHandler)
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
index ca7070b27..61f0f32be 100644
--- a/wsd/ProxyProtocol.hpp
+++ b/wsd/ProxyProtocol.hpp
@@ -35,11 +35,7 @@ public:
     void handleIncomingMessage(SocketDisposition &/* disposition */) override;
 
     int getPollEvents(std::chrono::steady_clock::time_point /* now */,
-                      int &/* timeoutMaxMs */) override
-    {
-        // underlying buffer based polling is fine.
-        return POLLIN;
-    }
+                      int &/* timeoutMaxMs */) override;
 
     void checkTimeout(std::chrono::steady_clock::time_point /* now */) override
     {
commit 6da95e72d4e09f2d09fc23803850042a85352256
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Mar 20 20:15:08 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    Proxy: open four wait sockets concurrently.
    
    Change-Id: I08b85677be528b7aa77272a8527c9bacf3f7c336

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index c203087de..79ca29fcd 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -206,7 +206,7 @@
 		this.sessionId = 'fetchsession';
 		this.id = window.proxySocketCounter++;
 		this.sendCounter = 0;
-		this.readWaiting = false;
+		this.readWaiting = 0;
 		this.onclose = function() {
 		};
 		this.onerror = function() {
@@ -315,9 +315,9 @@
 
 		// horrors ...
 		this.readInterval = setInterval(function() {
-			if (this.readWaiting) // one at a time for now
+			if (that.readWaiting > 4) // max 4 waiting connections concurrently.
 				return;
-			if (this.sessionId == 'fetchsession')
+			if (that.sessionId == 'fetchsession')
 				return; // waiting for our session id.
 			var req = new XMLHttpRequest();
 			// fetch session id:
@@ -326,13 +326,15 @@
 					that.parseIncomingArray(new Uint8Array(this.response));
 				else
 					console.debug('Handle error ' + this.status);
-				that.readWaiting = false;
+			});
+			req.addEventListener('loadend', function() {
+				that.readWaiting--;
 			});
 			req.open('GET', that.getEndPoint('read'));
 			req.setRequestHeader('SessionId', that.sessionId);
 			req.responseType = 'arraybuffer';
 			req.send('');
-			that.readWaiting = true;
+			that.readWaiting++;
 		}, 250);
 	};
 
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 28f378e7c..e436cba31 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -258,7 +258,8 @@ public:
         const Poco::URI& uriPublic,
         const bool isReadOnly,
         const std::string& hostNoTrust,
-        const std::shared_ptr<StreamSocket> &socket);
+        const std::shared_ptr<StreamSocket> &socket,
+        bool isWaiting);
 
     /// Thread safe termination of this broker if it has a lingering thread
     void joinThread();
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 4d7a71221..c1e745a6e 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2853,11 +2853,16 @@ private:
         // Request a kit process for this doc.
         std::shared_ptr<DocumentBroker> docBroker = findOrCreateDocBroker(
             none, url, docKey, _id, uriPublic);
+
+        std::string fullURL = request.getURI();
+        std::string ending = "/ws/read";
+        bool isWaiting = (fullURL.size() > ending.size() &&
+                          std::equal(ending.rbegin(), ending.rend(), fullURL.rbegin()));
         if (docBroker)
         {
             // need to move into the DocumentBroker context before doing session lookup / creation etc.
             std::string id = _id;
-            disposition.setMove([docBroker, id, uriPublic, isReadOnly, hostNoTrust, sessionId]
+            disposition.setMove([docBroker, id, uriPublic, isReadOnly, hostNoTrust, sessionId, isWaiting]
                                 (const std::shared_ptr<Socket> &moveSocket)
                 {
                     LOG_TRC("Setting up docbroker thread for " << docBroker->getDocKey());
@@ -2867,7 +2872,8 @@ private:
                     // We no longer own this socket.
                     moveSocket->setThreadOwner(std::thread::id());
 
-                    docBroker->addCallback([docBroker, id, uriPublic, isReadOnly, hostNoTrust, sessionId, moveSocket]()
+                    docBroker->addCallback([docBroker, id, uriPublic, isReadOnly, hostNoTrust,
+                                            sessionId, moveSocket, isWaiting]()
                         {
                             // Now inside the document broker thread ...
                             LOG_TRC("In the docbroker thread for " << docBroker->getDocKey());
@@ -2877,7 +2883,7 @@ private:
                             {
                                 docBroker->handleProxyRequest(
                                     sessionId, id, uriPublic, isReadOnly,
-                                    hostNoTrust, streamSocket);
+                                    hostNoTrust, streamSocket, isWaiting);
                                 return;
                             }
                             catch (const UnauthorizedRequestException& exc)
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 8aaff0131..25602f146 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -25,7 +25,8 @@ void DocumentBroker::handleProxyRequest(
     const Poco::URI& uriPublic,
     const bool isReadOnly,
     const std::string& hostNoTrust,
-    const std::shared_ptr<StreamSocket> &socket)
+    const std::shared_ptr<StreamSocket> &socket,
+    bool isWaiting)
 {
     std::shared_ptr<ClientSession> clientSession;
     if (sessionId == "fetchsession")
@@ -82,7 +83,7 @@ void DocumentBroker::handleProxyRequest(
     auto proxy = std::static_pointer_cast<ProxyProtocolHandler>(
         protocol);
 
-    proxy->handleRequest(uriPublic.toString(), socket);
+    proxy->handleRequest(isWaiting, socket);
 }
 
 bool ProxyProtocolHandler::parseEmitIncoming(
@@ -128,16 +129,13 @@ bool ProxyProtocolHandler::parseEmitIncoming(
     return true;
 }
 
-void ProxyProtocolHandler::handleRequest(const std::string &uriPublic,
-                                         const std::shared_ptr<Socket> &socket)
+void ProxyProtocolHandler::handleRequest(bool isWaiting, const std::shared_ptr<Socket> &socket)
 {
     auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
 
-    bool bRead = uriPublic.find("/write") == std::string::npos;
-    LOG_INF("Proxy handle request " << uriPublic << " type: " <<
-            (bRead ? "read" : "write"));
+    LOG_INF("Proxy handle request type: " << (isWaiting ? "wait" : "respond"));
 
-    if (bRead)
+    if (!isWaiting)
     {
         if (!_msgHandler)
             LOG_WRN("unusual - incoming message with no-one to handle it");
@@ -149,13 +147,27 @@ void ProxyProtocolHandler::handleRequest(const std::string &uriPublic,
         }
     }
 
-    if (!flushQueueTo(streamSocket) && !bRead)
+    if (!flushQueueTo(streamSocket) && isWaiting)
     {
-        // longer running 'write socket'
-        _writeSockets.push_back(streamSocket);
+        LOG_TRC("Queue a waiting socket");
+        // longer running 'write socket' (marked 'read' by the client)
+        _outSockets.push_back(streamSocket);
+        if (_outSockets.size() > 16)
+        {
+            LOG_ERR("Unexpected - client opening many concurrent waiting connections " << _outSockets.size());
+            // cleanup older waiting sockets.
+            auto sockWeak = _outSockets.front();
+            _outSockets.erase(_outSockets.begin());
+            auto sock = sockWeak.lock();
+            if (sock)
+                sock->shutdown();
+        }
     }
     else
+    {
+        LOG_TRC("Return a reply immediately");
         socket->shutdown();
+    }
 }
 
 void ProxyProtocolHandler::handleIncomingMessage(SocketDisposition &disposition)
@@ -202,7 +214,7 @@ void ProxyProtocolHandler::getIOStats(uint64_t &sent, uint64_t &recv)
 
 void ProxyProtocolHandler::dumpState(std::ostream& os)
 {
-    os << "proxy protocol sockets: " << _writeSockets.size() << " writeQueue: " << _writeQueue.size() << ":\n";
+    os << "proxy protocol sockets: " << _outSockets.size() << " writeQueue: " << _writeQueue.size() << ":\n";
     for (auto it : _writeQueue)
         Util::dumpHex(os, "\twrite queue entry:", "\t\t", *it);
 }
@@ -256,10 +268,10 @@ bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &soc
 std::shared_ptr<StreamSocket> ProxyProtocolHandler::popWriteSocket()
 {
     std::weak_ptr<StreamSocket> sock;
-    while (!_writeSockets.empty())
+    while (!_outSockets.empty())
     {
-        sock = _writeSockets.front();
-        _writeSockets.erase(_writeSockets.begin());
+        sock = _outSockets.front();
+        _outSockets.erase(_outSockets.begin());
         auto realSock = sock.lock();
         if (realSock)
             return realSock;
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
index 091ac3295..ca7070b27 100644
--- a/wsd/ProxyProtocol.hpp
+++ b/wsd/ProxyProtocol.hpp
@@ -61,11 +61,8 @@ public:
     void shutdown(bool goingAway = false, const std::string &statusMessage = "") override;
     void getIOStats(uint64_t &sent, uint64_t &recv) override;
     void dumpState(std::ostream& os);
-
     bool parseEmitIncoming(const std::shared_ptr<StreamSocket> &socket);
-
-    void handleRequest(const std::string &uriPublic,
-                       const std::shared_ptr<Socket> &socket);
+    void handleRequest(bool isWaiting, const std::shared_ptr<Socket> &socket);
 
 private:
     std::shared_ptr<StreamSocket> popWriteSocket();
@@ -89,7 +86,7 @@ private:
     };
     /// queue things when we have no socket to hand.
     std::vector<std::shared_ptr<Message>> _writeQueue;
-    std::vector<std::weak_ptr<StreamSocket>> _writeSockets;
+    std::vector<std::weak_ptr<StreamSocket>> _outSockets;
 };
 
 #endif
commit 4bac787da762cf09bd0f744c191db2bce0f96e99
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Mar 20 19:05:48 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    Proxy: re-write css image URLs to handle the proxy.
    
    Change-Id: I09f3dea2f5e3a51869d5b0aa3f473d8f3ba75f44

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 6ac45e5eb..c203087de 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -336,6 +336,33 @@
 		}, 250);
 	};
 
+	if (global.socketProxy)
+	{
+		// re-write relative URLs in CSS - somewhat grim.
+		window.addEventListener('load', function() {
+			var sheets = document.styleSheets;
+			for (var i = 0; i < sheets.length; ++i) {
+				var relBases = sheets[i].href.split('/');
+				relBases.pop(); // bin last - css name.
+				var replaceBase = 'url("' + relBases.join('/') + '/images/';
+
+				var rules = sheets[i].cssRules || sheets[i].rules;
+				for (var r = 0; r < rules.length; ++r) {
+					if (!rules[r] || !rules[r].style)
+						continue;
+					var img = rules[r].style.backgroundImage;
+					if (img === '' || img === undefined)
+						continue;
+					if (img.startsWith('url("images/'))
+					{
+						rules[r].style.backgroundImage =
+							img.replace('url("images/', replaceBase);
+					}
+				}
+			}
+		}, false);
+	}
+
 	global.createWebSocket = function(uri) {
 		if (global.socketProxy) {
 			return new global.ProxySocket(uri);
commit 0e961e21879d1d55c64c5e0cec9e6fc6dcafe0a8
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Mar 20 16:38:14 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    Proxy: send multiple messages in a single request.
    
    Change-Id: Ic0a303979478801bd23941e8893ce5721cf3e732

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 6f058d9da..6ac45e5eb 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -195,6 +195,7 @@
 
 	global.proxySocketCounter = 0;
 	global.ProxySocket = function (uri) {
+		var that = this;
 		this.uri = uri;
 		this.binaryType = 'arraybuffer';
 		this.bufferedAmount = 0;
@@ -254,56 +255,48 @@
 				i += size; // skip trailing '\n' in loop-increment
 			}
 		};
-		this.parseIncoming = function(type, msg) {
-			if (type === 'blob')
-			{
-				var fileReader = new FileReader();
-				var that = this;
-				fileReader.onload = function(event) {
-					that.parseIncomingArray(event.target.result);
-				};
-				fileReader.readAsArrayBuffer(msg);
-			}
-			else if (type === 'arraybuffer')
-			{
-				this.parseIncomingArray(new Uint8Array(msg));
-			}
-			else if (type === 'text' || type === '')
-			{
-				const encoder = new TextEncoder()
-				const arr = encoder.encode(msg);
-				this.parseIncomingArray(arr);
-			}
-			else
-				console.debug('Unknown encoding type: ' + type);
-		};
-		this.send = function(msg) {
-			console.debug('send msg "' + msg + '"');
+		this.sendQueue = '';
+		this.sendTimeout = undefined;
+		this.doSend = function () {
+			that.sendTimeout = undefined;
+			console.debug('send msg "' + that.sendQueue + '"');
 			var req = new XMLHttpRequest();
-			req.open('POST', this.getEndPoint('write'));
-			req.setRequestHeader('SessionId', this.sessionId);
-			if (this.sessionId === 'fetchsession')
-			{
-				req.responseType = 'text';
-				req.addEventListener('load', function() {
-					console.debug('got session: ' + this.responseText);
-					that.sessionId = this.responseText;
-					that.readyState = 1;
-					that.onopen();
-				});
-			}
+			req.open('POST', that.getEndPoint('write'));
+			req.setRequestHeader('SessionId', that.sessionId);
+			if (that.sessionId === 'fetchsession')
+				console.debug('session fetch not completed');
 			else
 			{
 				req.responseType = 'arraybuffer';
 				req.addEventListener('load', function() {
 					if (this.status == 200)
-						that.parseIncoming(this.responseType, this.response);
+						that.parseIncomingArray(new Uint8Array(this.response));
 					else
 						console.debug('Error on incoming response');
 				});
 			}
-			req.send('B0x' + msg.length.toString(16) + '\n' + msg + '\n');
-		},
+			req.send(that.sendQueue);
+			that.sendQueue = '';
+		};
+		this.getSessionId = function() {
+			var req = new XMLHttpRequest();
+			req.open('POST', that.getEndPoint('write'));
+			req.setRequestHeader('SessionId', that.sessionId);
+			req.responseType = 'text';
+			req.addEventListener('load', function() {
+				console.debug('got session: ' + this.responseText);
+				that.sessionId = this.responseText;
+				that.readyState = 1;
+				that.onopen();
+			});
+			req.send('');
+		};
+		this.send = function(msg) {
+			this.sendQueue = this.sendQueue.concat(
+				'B0x' + msg.length.toString(16) + '\n' + msg + '\n');
+			if (this.sessionId !== 'fetchsession' && this.sendTimeout === undefined)
+				this.sendTimeout = setTimeout(this.doSend, 2 /* ms */);
+		};
 		this.close = function() {
 			console.debug('close socket');
 			this.readyState = 3;
@@ -315,7 +308,9 @@
 		};
 		console.debug('New proxy socket ' + this.id + ' ' + this.uri);
 
-		this.send('fetchsession');
+		// queue fetch of session id.
+		this.getSessionId();
+
 		var that = this;
 
 		// horrors ...
@@ -328,7 +323,7 @@
 			// fetch session id:
 			req.addEventListener('load', function() {
 				if (this.status == 200)
-					that.parseIncoming(this.responseType, this.response);
+					that.parseIncomingArray(new Uint8Array(this.response));
 				else
 					console.debug('Handle error ' + this.status);
 				that.readWaiting = false;
commit ed5a55513746f4677dee3c33b56845b3ca788520
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Thu Mar 19 15:54:28 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    Proxy protocol bits.
    
    For now very silly: [T|B] + hex length + \n + content + \n
    
    Change-Id: I256b834a23cca975a705da2c569887665ac6be02

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 947649197..6f058d9da 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -212,19 +212,97 @@
 		};
 		this.onmessage = function() {
 		};
+		this.parseIncomingArray = function(arr) {
+			var decoder = new TextDecoder();
+			for (var i = 0; i < arr.length; ++i)
+			{
+				var left = arr.length - i;
+				if (left < 4)
+				{
+					console.debug('no data left');
+					break;
+				}
+				var type = String.fromCharCode(arr[i+0]);
+				if (type != 'T' && type != 'B')
+				{
+					console.debug('wrong data type: ' + type);
+					break;
+				}
+				if (arr[i+1] !== 48 && arr[i+2] !== 120) // '0x'
+				{
+					console.debug('missing hex preamble');
+					break;
+				}
+				i += 3;
+				var numStr = '';
+				var start = i;
+				while (arr[i] != 10) // '\n'
+					i++;
+				numStr = decoder.decode(arr.slice(start, i)); // FIXME: IE11
+				var size = parseInt(numStr, 16);
+
+				i++; // skip \n
+
+				var data;
+				if (type == 'T') // FIXME: IE11
+					data = decoder.decode(arr.slice(i, i + size));
+				else
+					data = arr.slice(i, i + size);
+
+				this.onmessage({ data: data });
+
+				i += size; // skip trailing '\n' in loop-increment
+			}
+		};
+		this.parseIncoming = function(type, msg) {
+			if (type === 'blob')
+			{
+				var fileReader = new FileReader();
+				var that = this;
+				fileReader.onload = function(event) {
+					that.parseIncomingArray(event.target.result);
+				};
+				fileReader.readAsArrayBuffer(msg);
+			}
+			else if (type === 'arraybuffer')
+			{
+				this.parseIncomingArray(new Uint8Array(msg));
+			}
+			else if (type === 'text' || type === '')
+			{
+				const encoder = new TextEncoder()
+				const arr = encoder.encode(msg);
+				this.parseIncomingArray(arr);
+			}
+			else
+				console.debug('Unknown encoding type: ' + type);
+		};
 		this.send = function(msg) {
 			console.debug('send msg "' + msg + '"');
 			var req = new XMLHttpRequest();
 			req.open('POST', this.getEndPoint('write'));
 			req.setRequestHeader('SessionId', this.sessionId);
 			if (this.sessionId === 'fetchsession')
+			{
+				req.responseType = 'text';
 				req.addEventListener('load', function() {
 					console.debug('got session: ' + this.responseText);
 					that.sessionId = this.responseText;
 					that.readyState = 1;
 					that.onopen();
 				});
-			req.send(msg);
+			}
+			else
+			{
+				req.responseType = 'arraybuffer';
+				req.addEventListener('load', function() {
+					if (this.status == 200)
+						that.parseIncoming(this.responseType, this.response);
+					else
+						console.debug('Error on incoming response');
+				});
+			}
+			req.send('B0x' + msg.length.toString(16) + '\n' + msg + '\n');
 		},
 		this.close = function() {
 			console.debug('close socket');
@@ -237,7 +315,6 @@
 		};
 		console.debug('New proxy socket ' + this.id + ' ' + this.uri);
 
-		// FIXME: perhaps a little risky.
 		this.send('fetchsession');
 		var that = this;
 
@@ -250,20 +327,16 @@
 			var req = new XMLHttpRequest();
 			// fetch session id:
 			req.addEventListener('load', function() {
-				console.debug('read: ' + this.responseText);
 				if (this.status == 200)
-				{
-					that.onmessage({ data: this.response });
-				}
+					that.parseIncoming(this.responseType, this.response);
 				else
-				{
 					console.debug('Handle error ' + this.status);
-				}
 				that.readWaiting = false;
 			});
 			req.open('GET', that.getEndPoint('read'));
-			req.setRequestHeader('SessionId', this.sessionId);
-			req.send(that.sessionId);
+			req.setRequestHeader('SessionId', that.sessionId);
+			req.responseType = 'arraybuffer';
+			req.send('');
 			that.readWaiting = true;
 		}, 250);
 	};
diff --git a/net/Socket.hpp b/net/Socket.hpp
index 9c5db2e7a..dd8e9effd 100644
--- a/net/Socket.hpp
+++ b/net/Socket.hpp
@@ -86,6 +86,10 @@ public:
     {
         _disposition = Type::CLOSED;
     }
+    std::shared_ptr<Socket> getSocket() const
+    {
+        return _socket;
+    }
     bool isMove() { return _disposition == Type::MOVE; }
     bool isClosed() { return _disposition == Type::CLOSED; }
 
@@ -1064,6 +1068,12 @@ public:
         std::vector<std::pair<size_t, size_t>> _spans;
     };
 
+    /// remove all queued input bytes
+    void clearInput()
+    {
+        _inBuffer.clear();
+    }
+
     /// Remove the first @count bytes from input buffer
     void eraseFirstInputBytes(const MessageMap &map)
     {
@@ -1227,6 +1237,8 @@ public:
     /// Does it look like we have some TLS / SSL where we don't expect it ?
     bool sniffSSL() const;
 
+    void dumpState(std::ostream& os) override;
+
 protected:
     /// Override to handle reading of socket data differently.
     virtual int readData(char* buf, int len)
@@ -1250,8 +1262,6 @@ protected:
 #endif
     }
 
-    void dumpState(std::ostream& os) override;
-
     void setShutdownSignalled(bool shutdownSignalled)
     {
         _shutdownSignalled = shutdownSignalled;
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index cb8a080d2..28f378e7c 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -258,7 +258,7 @@ public:
         const Poco::URI& uriPublic,
         const bool isReadOnly,
         const std::string& hostNoTrust,
-        const std::shared_ptr<Socket> &moveSocket);
+        const std::shared_ptr<StreamSocket> &socket);
 
     /// Thread safe termination of this broker if it has a lingering thread
     void joinThread();
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 81bb22fa2..4d7a71221 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2877,7 +2877,7 @@ private:
                             {
                                 docBroker->handleProxyRequest(
                                     sessionId, id, uriPublic, isReadOnly,
-                                    hostNoTrust, moveSocket);
+                                    hostNoTrust, streamSocket);
                                 return;
                             }
                             catch (const UnauthorizedRequestException& exc)
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
index 41043a57a..8aaff0131 100644
--- a/wsd/ProxyProtocol.cpp
+++ b/wsd/ProxyProtocol.cpp
@@ -25,7 +25,7 @@ void DocumentBroker::handleProxyRequest(
     const Poco::URI& uriPublic,
     const bool isReadOnly,
     const std::string& hostNoTrust,
-    const std::shared_ptr<Socket> &socket)
+    const std::shared_ptr<StreamSocket> &socket)
 {
     std::shared_ptr<ClientSession> clientSession;
     if (sessionId == "fetchsession")
@@ -37,6 +37,22 @@ void DocumentBroker::handleProxyRequest(
         addSession(clientSession);
         LOOLWSD::checkDiskSpaceAndWarnClients(true);
         LOOLWSD::checkSessionLimitsAndWarnClients();
+
+        LOG_TRC("Returning id " << clientSession->getId());
+
+        std::ostringstream oss;
+        oss << "HTTP/1.1 200 OK\r\n"
+            "Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
+            "User-Agent: " WOPI_AGENT_STRING "\r\n"
+            "Content-Length: " << clientSession->getId().size() << "\r\n"
+            "Content-Type: application/json\r\n"
+            "X-Content-Type-Options: nosniff\r\n"
+            "\r\n"
+            << clientSession->getId();
+
+        socket->send(oss.str());
+        socket->shutdown();
+        return;
     }
     else
     {
@@ -69,13 +85,186 @@ void DocumentBroker::handleProxyRequest(
     proxy->handleRequest(uriPublic.toString(), socket);
 }
 
+bool ProxyProtocolHandler::parseEmitIncoming(
+    const std::shared_ptr<StreamSocket> &socket)
+{
+    std::vector<char> &in = socket->getInBuffer();
+
+    std::stringstream oss;
+    socket->dumpState(oss);
+    LOG_TRC("Parse message:\n" << oss.str());
+
+    while (in.size() > 0)
+    {
+        if (in[0] != 'T' && in[0] != 'B')
+        {
+            LOG_ERR("Invalid message type " << in[0]);
+            return false;
+        }
+        auto it = in.begin() + 1;
+        for (; it != in.end() && *it != '\n'; ++it);
+        *it = '\0';
+        uint64_t len = strtoll( &in[1], nullptr, 16 );
+        in.erase(in.begin(), it + 1);
+        if (len > in.size())
+        {
+            LOG_ERR("Invalid message length " << len << " vs " << in.size());
+            return false;
+        }
+        // far from efficient:
+        std::vector<char> data;
+        data.insert(data.begin(), in.begin(), in.begin() + len + 1);
+        in.erase(in.begin(), in.begin() + len);
+
+        if (in.size() < 1 || in[0] != '\n')
+        {
+            LOG_ERR("Missing final newline");
+            return false;
+        }
+        in.erase(in.begin(), in.begin() + 1);
+
+        _msgHandler->handleMessage(data);
+    }
+    return true;
+}
+
 void ProxyProtocolHandler::handleRequest(const std::string &uriPublic,
                                          const std::shared_ptr<Socket> &socket)
 {
+    auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
+
     bool bRead = uriPublic.find("/write") == std::string::npos;
     LOG_INF("Proxy handle request " << uriPublic << " type: " <<
             (bRead ? "read" : "write"));
-    (void)socket;
+
+    if (bRead)
+    {
+        if (!_msgHandler)
+            LOG_WRN("unusual - incoming message with no-one to handle it");
+        else if (!parseEmitIncoming(streamSocket))
+        {
+            std::stringstream oss;
+            streamSocket->dumpState(oss);
+            LOG_ERR("bad socket structure " << oss.str());
+        }
+    }
+
+    if (!flushQueueTo(streamSocket) && !bRead)
+    {
+        // longer running 'write socket'
+        _writeSockets.push_back(streamSocket);
+    }
+    else
+        socket->shutdown();
+}
+
+void ProxyProtocolHandler::handleIncomingMessage(SocketDisposition &disposition)
+{
+    std::stringstream oss;
+    disposition.getSocket()->dumpState(oss);
+    LOG_ERR("If you got here, it means we failed to parse this properly in handleRequest: " << oss.str());
+}
+
+int ProxyProtocolHandler::sendMessage(const char *msg, const size_t len, bool text, bool flush)
+{
+    _writeQueue.push_back(std::make_shared<Message>(msg, len, text));
+    auto sock = popWriteSocket();
+    if (sock && flush)
+    {
+        flushQueueTo(sock);
+        sock->shutdown();
+    }
+
+    return len;
+}
+
+int ProxyProtocolHandler::sendTextMessage(const char *msg, const size_t len, bool flush) const
+{
+    LOG_TRC("ProxyHack - send text msg " + std::string(msg, len));
+    return const_cast<ProxyProtocolHandler *>(this)->sendMessage(msg, len, true, flush);
+}
+
+int ProxyProtocolHandler::sendBinaryMessage(const char *data, const size_t len, bool flush) const
+{
+    LOG_TRC("ProxyHack - send binary msg len " << len);
+    return const_cast<ProxyProtocolHandler *>(this)->sendMessage(data, len, false, flush);
+}
+
+void ProxyProtocolHandler::shutdown(bool goingAway, const std::string &statusMessage)
+{
+    LOG_TRC("ProxyHack - shutdown " << goingAway << ": " << statusMessage);
+}
+
+void ProxyProtocolHandler::getIOStats(uint64_t &sent, uint64_t &recv)
+{
+    sent = recv = 0;
+}
+
+void ProxyProtocolHandler::dumpState(std::ostream& os)
+{
+    os << "proxy protocol sockets: " << _writeSockets.size() << " writeQueue: " << _writeQueue.size() << ":\n";
+    for (auto it : _writeQueue)
+        Util::dumpHex(os, "\twrite queue entry:", "\t\t", *it);
+}
+
+void ProxyProtocolHandler::performWrites()
+{
+    if (_msgHandler)
+        _msgHandler->writeQueuedMessages();
+    if (_writeQueue.size() <= 0)
+        return;
+
+    auto sock = popWriteSocket();
+    if (sock)
+    {
+        flushQueueTo(sock);
+        sock->shutdown();
+    }
+}
+
+bool ProxyProtocolHandler::flushQueueTo(const std::shared_ptr<StreamSocket> &socket)
+{
+    // slurp from the core to us.
+    if (_msgHandler && _msgHandler->hasQueuedMessages())
+        _msgHandler->writeQueuedMessages();
+
+    size_t totalSize = 0;
+    for (auto it : _writeQueue)
+        totalSize += it->size();
+
+    if (!totalSize)
+        return false;
+
+    std::ostringstream oss;
+    oss << "HTTP/1.1 200 OK\r\n"
+        "Last-Modified: " << Util::getHttpTimeNow() << "\r\n"
+        "User-Agent: " WOPI_AGENT_STRING "\r\n"
+        "Content-Length: " << totalSize << "\r\n"
+        "Content-Type: application/json\r\n"
+        "X-Content-Type-Options: nosniff\r\n"
+        "\r\n";
+    socket->send(oss.str());
+
+    for (auto it : _writeQueue)
+        socket->send(it->data(), it->size(), false);
+    _writeQueue.clear();
+
+    return true;
+}
+
+// LRU-ness ...
+std::shared_ptr<StreamSocket> ProxyProtocolHandler::popWriteSocket()
+{
+    std::weak_ptr<StreamSocket> sock;
+    while (!_writeSockets.empty())
+    {
+        sock = _writeSockets.front();
+        _writeSockets.erase(_writeSockets.begin());
+        auto realSock = sock.lock();
+        if (realSock)
+            return realSock;
+    }
+    return std::shared_ptr<StreamSocket>();
 }
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
index 1f88e1fa7..091ac3295 100644
--- a/wsd/ProxyProtocol.hpp
+++ b/wsd/ProxyProtocol.hpp
@@ -10,19 +10,21 @@
 #ifndef INCLUDED_PROXY_PROTOCOL_HPP
 #define INCLUDED_PROXY_PROTOCOL_HPP
 
+#include <memory>
 #include <net/Socket.hpp>
 
-/// Interface for building a websocket from this ...
+/**
+ * Implementation that builds a websocket like protocol from many
+ * individual proxied HTTP requests back to back.
+ *
+ * we use a trivial framing: <hex-length>\r\n<content>\r\n
+ */
 class ProxyProtocolHandler : public ProtocolHandlerInterface
 {
 public:
-    ProxyProtocolHandler()
-    {
-    }
+    ProxyProtocolHandler() { }
 
-    virtual ~ProxyProtocolHandler()
-    {
-    }
+    virtual ~ProxyProtocolHandler() { }
 
     /// Will be called exactly once by setHandler
     void onConnect(const std::shared_ptr<StreamSocket>& /* socket */) override
@@ -30,10 +32,7 @@ public:
     }
 
     /// Called after successful socket reads.
-    void handleIncomingMessage(SocketDisposition &/* disposition */) override
-    {
-        assert("we get our data a different way" && false);
-    }
+    void handleIncomingMessage(SocketDisposition &/* disposition */) override;
 
     int getPollEvents(std::chrono::steady_clock::time_point /* now */,
                       int &/* timeoutMaxMs */) override
@@ -46,9 +45,7 @@ public:
     {
     }
 
-    void performWrites() override
-    {
-    }
+    void performWrites() override;
 
     void onDisconnect() override
     {
@@ -59,40 +56,40 @@ public:
     /// Clear all external references
     virtual void dispose() { _msgHandler.reset(); }
 
-    int sendTextMessage(const char *msg, const size_t len, bool flush = false) const override
-    {
-        LOG_TRC("ProxyHack - send text msg " + std::string(msg, len));
-        (void) flush;
-        return len;
-    }
+    int sendTextMessage(const char *msg, const size_t len, bool flush = false) const override;
+    int sendBinaryMessage(const char *data, const size_t len, bool flush = false) const override;
+    void shutdown(bool goingAway = false, const std::string &statusMessage = "") override;
+    void getIOStats(uint64_t &sent, uint64_t &recv) override;
+    void dumpState(std::ostream& os);
 
-    int sendBinaryMessage(const char *data, const size_t len, bool flush = false) const override
-    {
-        (void) data; (void) flush;
-        LOG_TRC("ProxyHack - send binary msg len " << len);
-        return len;
-    }
-
-    void shutdown(bool goingAway = false, const std::string &statusMessage = "") override
-    {
-        LOG_TRC("ProxyHack - shutdown " << goingAway << ": " << statusMessage);
-    }
-
-    void getIOStats(uint64_t &sent, uint64_t &recv) override
-    {
-        sent = recv = 0;
-    }
-
-    void dumpState(std::ostream& os)
-    {
-        os << "proxy protocol\n";
-    }
+    bool parseEmitIncoming(const std::shared_ptr<StreamSocket> &socket);
 
     void handleRequest(const std::string &uriPublic,
                        const std::shared_ptr<Socket> &socket);
 
 private:
-    std::vector<std::weak_ptr<StreamSocket>> _sockets;
+    std::shared_ptr<StreamSocket> popWriteSocket();
+    int sendMessage(const char *msg, const size_t len, bool text, bool flush);
+    bool flushQueueTo(const std::shared_ptr<StreamSocket> &socket);
+
+    struct Message : public std::vector<char>
+    {
+        Message(const char *msg, const size_t len, bool text)
+        {
+            const char *type = text ? "T" : "B";
+            insert(end(), type, type + 1);
+            std::ostringstream os;
+            os << std::hex << "0x" << len << "\n";
+            std::string str = os.str();
+            insert(end(), str.c_str(), str.c_str() + str.size());
+            insert(end(), msg, msg + len);
+            const char *terminator = "\n";
+            insert(end(), terminator, terminator + 1);
+        }
+    };
+    /// queue things when we have no socket to hand.
+    std::vector<std::shared_ptr<Message>> _writeQueue;
+    std::vector<std::weak_ptr<StreamSocket>> _writeSockets;
 };
 
 #endif
commit f8a535f576c6b51caf6a9b12a49fd596ea725878
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Mar 4 13:54:04 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    Proxy websocket prototype.
    
    Try to read/write avoiding a websocket.
    
    Change-Id: I382039fa88f1030a63df1e47f687df2ee5a6055b

diff --git a/Makefile.am b/Makefile.am
index e4e6ed5db..92d87fa50 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -112,6 +112,7 @@ loolwsd_sources = common/Crypto.cpp \
                   wsd/AdminModel.cpp \
                   wsd/Auth.cpp \
                   wsd/DocumentBroker.cpp \
+                  wsd/ProxyProtocol.cpp \
                   wsd/LOOLWSD.cpp \
                   wsd/ClientSession.cpp \
                   wsd/FileServer.cpp \
@@ -203,6 +204,7 @@ wsd_headers = wsd/Admin.hpp \
               wsd/Auth.hpp \
               wsd/ClientSession.hpp \
               wsd/DocumentBroker.hpp \
+              wsd/ProxyProtocol.hpp \
               wsd/Exceptions.hpp \
               wsd/FileServer.hpp \
               wsd/LOOLWSD.hpp \
diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index a4fd77bab..947649197 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -185,16 +185,97 @@
 		};
 		this.onopen = function() {
 		};
+		this.close = function() {
+		};
 	};
-
-	global.FakeWebSocket.prototype.close = function() {
-	};
-
 	global.FakeWebSocket.prototype.send = function(data) {
 		this.sendCounter++;
 		window.postMobileMessage(data);
 	};
 
+	global.proxySocketCounter = 0;
+	global.ProxySocket = function (uri) {
+		this.uri = uri;
+		this.binaryType = 'arraybuffer';
+		this.bufferedAmount = 0;
+		this.extensions = '';
+		this.protocol = '';
+		this.connected = true;
+		this.readyState = 0; // connecting
+		this.sessionId = 'fetchsession';
+		this.id = window.proxySocketCounter++;
+		this.sendCounter = 0;
+		this.readWaiting = false;
+		this.onclose = function() {
+		};
+		this.onerror = function() {
+		};
+		this.onmessage = function() {
+		};
+		this.send = function(msg) {
+			console.debug('send msg "' + msg + '"');
+			var req = new XMLHttpRequest();
+			req.open('POST', this.getEndPoint('write'));
+			req.setRequestHeader('SessionId', this.sessionId);
+			if (this.sessionId === 'fetchsession')
+				req.addEventListener('load', function() {
+					console.debug('got session: ' + this.responseText);
+					that.sessionId = this.responseText;
+					that.readyState = 1;
+					that.onopen();
+				});
+			req.send(msg);
+		},
+		this.close = function() {
+			console.debug('close socket');
+			this.readyState = 3;
+			this.onclose();
+		};
+		this.getEndPoint = function(type) {
+			var base = this.uri;
+			return base.replace(/^ws/, 'http') + '/' + type;
+		};
+		console.debug('New proxy socket ' + this.id + ' ' + this.uri);
+
+		// FIXME: perhaps a little risky.
+		this.send('fetchsession');
+		var that = this;
+
+		// horrors ...
+		this.readInterval = setInterval(function() {
+			if (this.readWaiting) // one at a time for now
+				return;
+			if (this.sessionId == 'fetchsession')
+				return; // waiting for our session id.
+			var req = new XMLHttpRequest();
+			// fetch session id:
+			req.addEventListener('load', function() {
+				console.debug('read: ' + this.responseText);
+				if (this.status == 200)
+				{
+					that.onmessage({ data: this.response });
+				}
+				else
+				{
+					console.debug('Handle error ' + this.status);
+				}
+				that.readWaiting = false;
+			});
+			req.open('GET', that.getEndPoint('read'));
+			req.setRequestHeader('SessionId', this.sessionId);
+			req.send(that.sessionId);
+			that.readWaiting = true;
+		}, 250);
+	};
+
+	global.createWebSocket = function(uri) {
+		if (global.socketProxy) {
+			return new global.ProxySocket(uri);
+		} else {
+			return new WebSocket(uri);
+		}
+	};
+
 	// If not debug, don't print anything on the console
 	// except in tile debug mode (Ctrl-Shift-Alt-d)
 	console.log2 = console.log;
@@ -219,7 +300,8 @@
 				window.postMobileError(log);
 			} else if (global.socket && (global.socket instanceof WebSocket) && global.socket.readyState === 1) {
 				global.socket.send(log);
-			} else if (global.socket && (global.socket instanceof global.L.Socket) && global.socket.connected()) {
+			} else if (global.socket && global.L && global.L.Socket &&
+				   (global.socket instanceof global.L.Socket) && global.socket.connected()) {
 				global.socket.sendMessage(log);
 			} else {
 				var req = new XMLHttpRequest();
@@ -294,7 +376,7 @@
 		var websocketURI = global.host + global.serviceRoot + '/lool/' + encodeURIComponent(global.docURL + (docParams ? '?' + docParams : '')) + '/ws' + wopiSrc;
 
 		try {
-			global.socket = new WebSocket(websocketURI);
+			global.socket = global.createWebSocket(websocketURI);
 		} catch (err) {
 			console.log(err);
 		}
diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js
index d4e76d4e4..13f3601ca 100644
--- a/loleaflet/src/core/Socket.js
+++ b/loleaflet/src/core/Socket.js
@@ -49,7 +49,7 @@ L.Socket = L.Class.extend({
 			}
 
 			try {
-				this.socket = new WebSocket(this.getWebSocketBaseURI(map) + wopiSrc);
+				this.socket = window.createWebSocket(this.getWebSocketBaseURI(map) + wopiSrc);
 			} catch (e) {
 				// On IE 11 there is a limitation on the number of WebSockets open to a single domain (6 by default and can go to 128).
 				// Detect this and hint the user.
diff --git a/net/Socket.hpp b/net/Socket.hpp
index d09c39334..9c5db2e7a 100644
--- a/net/Socket.hpp
+++ b/net/Socket.hpp
@@ -452,6 +452,11 @@ public:
         }
     }
 
+    std::shared_ptr<ProtocolHandlerInterface> getProtocol() const
+    {
+        return _protocol;
+    }
+
     /// Do we have something to send ?
     virtual bool hasQueuedMessages() const = 0;
     /// Please send them to me then.
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index b47285dd0..e347dbd74 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -37,9 +37,6 @@ public:
     void construct();
     virtual ~ClientSession();
 
-    /// Lookup any session by id.
-    static std::shared_ptr<ClientSession> getById(const std::string &id);
-
     void setReadOnly() override;
 
     enum SessionState {
@@ -288,7 +285,6 @@ private:
     std::chrono::steady_clock::time_point _viewLoadStart;
 };
 
-
 #endif
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index e304cd60c..cb8a080d2 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -251,6 +251,15 @@ public:
         const bool isReadOnly,
         const std::string& hostNoTrust);
 
+    /// Find or create a new client session for the PHP proxy
+    void handleProxyRequest(
+        const std::string& sessionId,
+        const std::string& id,
+        const Poco::URI& uriPublic,
+        const bool isReadOnly,
+        const std::string& hostNoTrust,
+        const std::shared_ptr<Socket> &moveSocket);
+
     /// Thread safe termination of this broker if it has a lingering thread
     void joinThread();
 
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 54ba8e2b9..81bb22fa2 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -238,6 +238,9 @@ namespace
 #if ENABLE_SUPPORT_KEY
 inline void shutdownLimitReached(const std::shared_ptr<ProtocolHandlerInterface>& proto)
 {
+    if (!proto)
+        return;
+
     const std::string error = Poco::format(PAYLOAD_UNAVAILABLE_LIMIT_REACHED, LOOLWSD::MaxDocuments, LOOLWSD::MaxConnections);
     LOG_INF("Sending client 'hardlimitreached' message: " << error);
 
@@ -1786,9 +1789,12 @@ static std::shared_ptr<DocumentBroker>
         if (docBroker->isMarkedToDestroy())
         {
             LOG_WRN("DocBroker with docKey [" << docKey << "] that is marked to be destroyed. Rejecting client request.");
-            std::string msg("error: cmd=load kind=docunloading");
-            proto->sendTextMessage(msg.data(), msg.size());
-            proto->shutdown(true, "error: cmd=load kind=docunloading");
+            if (proto)
+            {
+                std::string msg("error: cmd=load kind=docunloading");
+                proto->sendTextMessage(msg.data(), msg.size());
+                proto->shutdown(true, "error: cmd=load kind=docunloading");
+            }
             return nullptr;
         }
     }
@@ -1804,9 +1810,12 @@ static std::shared_ptr<DocumentBroker>
     }
 
     // Indicate to the client that we're connecting to the docbroker.
-    const std::string statusConnect = "statusindicator: connect";
-    LOG_TRC("Sending to Client [" << statusConnect << "].");
-    proto->sendTextMessage(statusConnect.data(), statusConnect.size());
+    if (proto)
+    {
+        const std::string statusConnect = "statusindicator: connect";
+        LOG_TRC("Sending to Client [" << statusConnect << "].");
+        proto->sendTextMessage(statusConnect.data(), statusConnect.size());
+    }
 
     if (!docBroker)
     {
@@ -2264,6 +2273,13 @@ private:
 //                    Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ...
                     handleClipboardRequest(request, message, disposition);
                 }
+                else if (request.has("ProxyPrefix") && reqPathTokens.count() > 2 &&
+                         (reqPathTokens[reqPathTokens.count()-2] == "ws"))
+                {
+                    std::string decodedUri; // WOPISrc
+                    Poco::URI::decode(reqPathTokens[1], decodedUri);
+                    handleClientProxyRequest(request, decodedUri, message, disposition);
+                }
                 else if (!(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) &&
                     reqPathTokens.count() > 0 && reqPathTokens[0] == "lool")
                 {
@@ -2797,6 +2813,99 @@ private:
     }
 #endif
 
+    void handleClientProxyRequest(const Poco::Net::HTTPRequest& request,
+                                  std::string url,
+                                  Poco::MemoryInputStream& message,
+                                  SocketDisposition &disposition)
+    {
+        if (!request.has("SessionId"))
+            throw BadRequestException("No session id header on proxied request");
+
+        std::string sessionId = request.get("SessionId");
+
+        LOG_INF("URL [" << url << "].");
+        const auto uriPublic = DocumentBroker::sanitizeURI(url);
+        LOG_INF("URI [" << uriPublic.getPath() << "].");
+        const auto docKey = DocumentBroker::getDocKey(uriPublic);
+        LOG_INF("DocKey [" << docKey << "].");
+        const std::string fileId = Util::getFilenameFromURL(docKey);
+        Util::mapAnonymized(fileId, fileId); // Identity mapping, since fileId is already obfuscated
+
+        LOG_INF("Starting Proxy request handler for session [" << _id << "] on url [" << LOOLWSD::anonymizeUrl(url) << "].");
+
+        // Check if readonly session is required
+        bool isReadOnly = false;
+        for (const auto& param : uriPublic.getQueryParameters())
+        {
+            LOG_DBG("Query param: " << param.first << ", value: " << param.second);
+            if (param.first == "permission" && param.second == "readonly")
+            {
+                isReadOnly = true;
+            }
+        }
+
+        const std::string hostNoTrust = (LOOLWSD::ServerName.empty() ? request.getHost() : LOOLWSD::ServerName);
+
+        LOG_INF("URL [" << LOOLWSD::anonymizeUrl(url) << "] is " << (isReadOnly ? "readonly" : "writable") << ".");
+        (void)request; (void)message; (void)disposition;
+
+        std::shared_ptr<ProtocolHandlerInterface> none;
+        // Request a kit process for this doc.
+        std::shared_ptr<DocumentBroker> docBroker = findOrCreateDocBroker(
+            none, url, docKey, _id, uriPublic);
+        if (docBroker)
+        {
+            // need to move into the DocumentBroker context before doing session lookup / creation etc.
+            std::string id = _id;
+            disposition.setMove([docBroker, id, uriPublic, isReadOnly, hostNoTrust, sessionId]
+                                (const std::shared_ptr<Socket> &moveSocket)
+                {
+                    LOG_TRC("Setting up docbroker thread for " << docBroker->getDocKey());
+                    // Make sure the thread is running before adding callback.
+                    docBroker->startThread();
+
+                    // We no longer own this socket.
+                    moveSocket->setThreadOwner(std::thread::id());
+
+                    docBroker->addCallback([docBroker, id, uriPublic, isReadOnly, hostNoTrust, sessionId, moveSocket]()
+                        {
+                            // Now inside the document broker thread ...
+                            LOG_TRC("In the docbroker thread for " << docBroker->getDocKey());
+
+                            auto streamSocket = std::static_pointer_cast<StreamSocket>(moveSocket);
+                            try
+                            {
+                                docBroker->handleProxyRequest(
+                                    sessionId, id, uriPublic, isReadOnly,
+                                    hostNoTrust, moveSocket);
+                                return;
+                            }
+                            catch (const UnauthorizedRequestException& exc)
+                            {
+                                LOG_ERR("Unauthorized Request while loading session for " << docBroker->getDocKey() << ": " << exc.what());
+                            }
+                            catch (const StorageConnectionException& exc)
+                            {
+                                LOG_ERR("Error while loading : " << exc.what());
+                            }
+                            catch (const std::exception& exc)
+                            {
+                                LOG_ERR("Error while loading : " << exc.what());
+                            }
+                            // badness occured:
+                            std::ostringstream oss;
+                            oss << "HTTP/1.1 400\r\n"
+                                << "Date: " << Util::getHttpTimeNow() << "\r\n"
+                                << "User-Agent: LOOLWSD WOPI Agent\r\n"
+                                << "Content-Length: 0\r\n"
+                                << "\r\n";
+                            streamSocket->send(oss.str());
+                            streamSocket->shutdown();
+                        });
+                });
+        }
+    }
+
     void handleClientWsUpgrade(const Poco::Net::HTTPRequest& request, const std::string& url,
                                SocketDisposition &disposition)
     {
@@ -2859,7 +2968,7 @@ private:
             // Request a kit process for this doc.
             std::shared_ptr<DocumentBroker> docBroker = findOrCreateDocBroker(
                 std::static_pointer_cast<ProtocolHandlerInterface>(ws), url, docKey, _id, uriPublic);
-             if (docBroker)
+            if (docBroker)
             {
 #if MOBILEAPP
                 const std::string hostNoTrust;
diff --git a/wsd/ProxyProtocol.cpp b/wsd/ProxyProtocol.cpp
new file mode 100644
index 000000000..41043a57a
--- /dev/null
+++ b/wsd/ProxyProtocol.cpp
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include <config.h>
+
+#include "DocumentBroker.hpp"
+#include "ClientSession.hpp"
+#include "ProxyProtocol.hpp"
+#include "Exceptions.hpp"
+#include "LOOLWSD.hpp"
+#include <Socket.hpp>
+
+#include <atomic>
+#include <cassert>
+
+void DocumentBroker::handleProxyRequest(
+    const std::string& sessionId,
+    const std::string& id,
+    const Poco::URI& uriPublic,
+    const bool isReadOnly,
+    const std::string& hostNoTrust,
+    const std::shared_ptr<Socket> &socket)
+{
+    std::shared_ptr<ClientSession> clientSession;
+    if (sessionId == "fetchsession")
+    {
+        LOG_TRC("Create session for " << _docKey);
+        clientSession = createNewClientSession(
+                std::make_shared<ProxyProtocolHandler>(),
+                id, uriPublic, isReadOnly, hostNoTrust);
+        addSession(clientSession);
+        LOOLWSD::checkDiskSpaceAndWarnClients(true);
+        LOOLWSD::checkSessionLimitsAndWarnClients();
+    }
+    else
+    {
+        LOG_TRC("Find session for " << _docKey << " with id " << sessionId);
+        for (const auto &it : _sessions)
+        {
+            if (it.second->getId() == sessionId)
+            {
+                clientSession = it.second;
+                break;
+            }
+        }
+        if (!clientSession)
+        {
+            LOG_ERR("Invalid session id used " << sessionId);
+            throw BadRequestException("invalid session id");
+        }
+    }
+
+    auto protocol = clientSession->getProtocol();
+    auto streamSocket = std::static_pointer_cast<StreamSocket>(socket);
+    streamSocket->setHandler(protocol);
+
+    // this DocumentBroker's poll handles reading & writing
+    addSocketToPoll(socket);
+
+    auto proxy = std::static_pointer_cast<ProxyProtocolHandler>(
+        protocol);
+
+    proxy->handleRequest(uriPublic.toString(), socket);
+}
+
+void ProxyProtocolHandler::handleRequest(const std::string &uriPublic,
+                                         const std::shared_ptr<Socket> &socket)
+{
+    bool bRead = uriPublic.find("/write") == std::string::npos;
+    LOG_INF("Proxy handle request " << uriPublic << " type: " <<
+            (bRead ? "read" : "write"));
+    (void)socket;
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/ProxyProtocol.hpp b/wsd/ProxyProtocol.hpp
new file mode 100644
index 000000000..1f88e1fa7
--- /dev/null
+++ b/wsd/ProxyProtocol.hpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#ifndef INCLUDED_PROXY_PROTOCOL_HPP
+#define INCLUDED_PROXY_PROTOCOL_HPP
+
+#include <net/Socket.hpp>
+
+/// Interface for building a websocket from this ...
+class ProxyProtocolHandler : public ProtocolHandlerInterface
+{
+public:
+    ProxyProtocolHandler()
+    {
+    }
+
+    virtual ~ProxyProtocolHandler()
+    {
+    }
+
+    /// Will be called exactly once by setHandler
+    void onConnect(const std::shared_ptr<StreamSocket>& /* socket */) override
+    {
+    }
+
+    /// Called after successful socket reads.
+    void handleIncomingMessage(SocketDisposition &/* disposition */) override
+    {
+        assert("we get our data a different way" && false);
+    }
+
+    int getPollEvents(std::chrono::steady_clock::time_point /* now */,
+                      int &/* timeoutMaxMs */) override
+    {
+        // underlying buffer based polling is fine.
+        return POLLIN;
+    }
+
+    void checkTimeout(std::chrono::steady_clock::time_point /* now */) override
+    {
+    }
+
+    void performWrites() override
+    {
+    }
+
+    void onDisconnect() override
+    {
+        // connections & sockets come and go a lot.
+    }
+
+public:
+    /// Clear all external references
+    virtual void dispose() { _msgHandler.reset(); }
+
+    int sendTextMessage(const char *msg, const size_t len, bool flush = false) const override
+    {
+        LOG_TRC("ProxyHack - send text msg " + std::string(msg, len));
+        (void) flush;
+        return len;
+    }
+
+    int sendBinaryMessage(const char *data, const size_t len, bool flush = false) const override
+    {
+        (void) data; (void) flush;
+        LOG_TRC("ProxyHack - send binary msg len " << len);
+        return len;
+    }
+
+    void shutdown(bool goingAway = false, const std::string &statusMessage = "") override
+    {
+        LOG_TRC("ProxyHack - shutdown " << goingAway << ": " << statusMessage);
+    }
+
+    void getIOStats(uint64_t &sent, uint64_t &recv) override
+    {
+        sent = recv = 0;
+    }
+
+    void dumpState(std::ostream& os)
+    {
+        os << "proxy protocol\n";
+    }
+
+    void handleRequest(const std::string &uriPublic,
+                       const std::shared_ptr<Socket> &socket);
+
+private:
+    std::vector<std::weak_ptr<StreamSocket>> _sockets;
+};
+
+#endif
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
commit 24714a9038b29ae1bfdcfc2102c54331b4077eb7
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Mar 4 13:52:51 2020 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Apr 7 18:12:03 2020 +0100

    ProxyPrefix: allow the user to specify a custom prefix.
    
    This allows us to re-direct web traffic via a proxy quite simply
    during fetch, instead of changing the service root.
    
    Change-Id: I28d348467e48394d581fca4da4c199348a2ca8e0

diff --git a/loleaflet/html/loleaflet.html.m4 b/loleaflet/html/loleaflet.html.m4
index a2c1a45cf..2f932ad0c 100644
--- a/loleaflet/html/loleaflet.html.m4
+++ b/loleaflet/html/loleaflet.html.m4
@@ -237,6 +237,7 @@ m4_ifelse(MOBILEAPP,[true],
       window.reuseCookies = '';
       window.protocolDebug = false;
       window.frameAncestors = '';
+      window.socketProxy = false;
       window.tileSize = 256;],
      [window.host = '%HOST%';
       window.serviceRoot = '%SERVICE_ROOT%';
@@ -249,6 +250,7 @@ m4_ifelse(MOBILEAPP,[true],
       window.reuseCookies = '%REUSE_COOKIES%';
       window.protocolDebug = %PROTOCOL_DEBUG%;
       window.frameAncestors = '%FRAME_ANCESTORS%';
+      window.socketProxy = %SOCKET_PROXY%;
       window.tileSize = 256;])
 m4_syscmd([cat ]GLOBAL_JS)m4_dnl
 
diff --git a/wsd/FileServer.cpp b/wsd/FileServer.cpp
index 9d833c510..9007a463a 100644
--- a/wsd/FileServer.cpp
+++ b/wsd/FileServer.cpp
@@ -595,6 +595,17 @@ constexpr char BRANDING[] = "branding";
 constexpr char BRANDING_UNSUPPORTED[] = "branding-unsupported";
 #endif
 
+namespace {
+    // The user can override the ServerRoot with a new prefix.
+    std::string getResponseRoot(const HTTPRequest &request)
+    {
+        if (!request.has("ProxyPrefix"))
+            return LOOLWSD::ServiceRoot;
+        std::string proxyPrefix = request.get("ProxyPrefix", "");
+        return proxyPrefix;
+    }
+}
+
 void FileServerRequestHandler::preprocessFile(const HTTPRequest& request, Poco::MemoryInputStream& message,
                                               const std::shared_ptr<StreamSocket>& socket)
 {
@@ -641,16 +652,22 @@ void FileServerRequestHandler::preprocessFile(const HTTPRequest& request, Poco::
         }
     }
 
-    const auto& config = Application::instance().config();
+    std::string socketProxy = "false";
+    if (request.has("ProxyPrefix"))
+        socketProxy = "true";
+    Poco::replaceInPlace(preprocess, std::string("%SOCKET_PROXY%"), socketProxy);
+
+    std::string responseRoot = getResponseRoot(request);
 
     Poco::replaceInPlace(preprocess, std::string("%ACCESS_TOKEN%"), escapedAccessToken);
     Poco::replaceInPlace(preprocess, std::string("%ACCESS_TOKEN_TTL%"), std::to_string(tokenTtl));
     Poco::replaceInPlace(preprocess, std::string("%ACCESS_HEADER%"), escapedAccessHeader);
     Poco::replaceInPlace(preprocess, std::string("%HOST%"), host);
     Poco::replaceInPlace(preprocess, std::string("%VERSION%"), std::string(LOOLWSD_VERSION_HASH));
-    Poco::replaceInPlace(preprocess, std::string("%SERVICE_ROOT%"), LOOLWSD::ServiceRoot);
+    Poco::replaceInPlace(preprocess, std::string("%SERVICE_ROOT%"), responseRoot);
     Poco::replaceInPlace(preprocess, std::string("%OS_INFO%"), LOOLWSD::OSInfo);
 
+    const auto& config = Application::instance().config();
     std::string protocolDebug = "false";
     if (config.getBool("logging.protocol"))
         protocolDebug = "true";
@@ -659,16 +676,16 @@ void FileServerRequestHandler::preprocessFile(const HTTPRequest& request, Poco::
     static const std::string linkCSS("<link rel=\"stylesheet\" href=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.css\">");
     static const std::string scriptJS("<script src=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.js\"></script>");
 
-    std::string brandCSS(Poco::format(linkCSS, LOOLWSD::ServiceRoot, std::string(BRANDING)));
-    std::string brandJS(Poco::format(scriptJS, LOOLWSD::ServiceRoot, std::string(BRANDING)));
+    std::string brandCSS(Poco::format(linkCSS, responseRoot, std::string(BRANDING)));
+    std::string brandJS(Poco::format(scriptJS, responseRoot, std::string(BRANDING)));
 
 #if ENABLE_SUPPORT_KEY
     const std::string keyString = config.getString("support_key", "");
     SupportKey key(keyString);
     if (!key.verify() || key.validDaysRemaining() <= 0)
     {
-        brandCSS = Poco::format(linkCSS, LOOLWSD::ServiceRoot, std::string(BRANDING_UNSUPPORTED));
-        brandJS = Poco::format(scriptJS, LOOLWSD::ServiceRoot, std::string(BRANDING_UNSUPPORTED));
+        brandCSS = Poco::format(linkCSS, responseRoot, std::string(BRANDING_UNSUPPORTED));
+        brandJS = Poco::format(scriptJS, responseRoot, std::string(BRANDING_UNSUPPORTED));
     }
 #endif
 
@@ -851,13 +868,15 @@ void FileServerRequestHandler::preprocessAdminFile(const HTTPRequest& request,co
     if (!FileServerRequestHandler::isAdminLoggedIn(request, response))
         throw Poco::Net::NotAuthenticatedException("Invalid admin login");
 
+    std::string responseRoot = getResponseRoot(request);
+
     static const std::string scriptJS("<script src=\"%s/loleaflet/" LOOLWSD_VERSION_HASH "/%s.js\"></script>");
     static const std::string footerPage("<div class=\"footer navbar-fixed-bottom text-info text-center\"><strong>Key:</strong> %s   <strong>Expiry Date:</strong> %s</div>");
 
     const std::string relPath = getRequestPathname(request);
     LOG_DBG("Preprocessing file: " << relPath);
     std::string adminFile = *getUncompressedFile(relPath);
-    std::string brandJS(Poco::format(scriptJS, LOOLWSD::ServiceRoot, std::string(BRANDING)));
+    std::string brandJS(Poco::format(scriptJS, responseRoot, std::string(BRANDING)));
     std::string brandFooter;
 
 #if ENABLE_SUPPORT_KEY
@@ -875,7 +894,7 @@ void FileServerRequestHandler::preprocessAdminFile(const HTTPRequest& request,co
     Poco::replaceInPlace(adminFile, std::string("<!--%BRANDING_JS%-->"), brandJS);
     Poco::replaceInPlace(adminFile, std::string("<!--%FOOTER%-->"), brandFooter);
     Poco::replaceInPlace(adminFile, std::string("%VERSION%"), std::string(LOOLWSD_VERSION_HASH));
-    Poco::replaceInPlace(adminFile, std::string("%SERVICE_ROOT%"), LOOLWSD::ServiceRoot);
+    Poco::replaceInPlace(adminFile, std::string("%SERVICE_ROOT%"), responseRoot);
 
     // Ask UAs to block if they detect any XSS attempt
     response.add("X-XSS-Protection", "1; mode=block");
commit 917591c4a2da5d8480017a8a94eb2a5807c3949a
Author:     gokaysatir <gokaysatir at gmail.com>
AuthorDate: Thu Apr 2 14:31:43 2020 +0300
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Thu Apr 2 12:48:14 2020 +0100

    calc-rename sheet-initially select existing name
    
    Change-Id: Ifb4f28f5f60e7bd514b4fca4dddc000d19237e8b

diff --git a/loleaflet/src/control/Control.Tabs.js b/loleaflet/src/control/Control.Tabs.js
index 182a154c0..505fd51ce 100644
--- a/loleaflet/src/control/Control.Tabs.js
+++ b/loleaflet/src/control/Control.Tabs.js
@@ -218,7 +218,8 @@ L.Control.Tabs = L.Control.extend({
 				$.extend({}, vex.dialog.buttons.YES, { text: _('OK') }),
 				$.extend({}, vex.dialog.buttons.NO, { text: _('Cancel') })
 			],
-			input: '<input name="sheetname" type="text" value="' + $('#spreadsheet-tab' + this._tabForContextMenu).text() + '" required />',
+			input: '<input name="sheetname" id="rename-calc-sheet-modal" type="text" value="' + $('#spreadsheet-tab' + this._tabForContextMenu).text() + '" required />',
+			afterOpen: function() { document.getElementById('rename-calc-sheet-modal').select(); },
 			callback: function(data) {
 				map.renamePage(data.sheetname, nPos);
 			}
commit 9ef17c88740c72cfa98b01771c3c44e810c4ed22
Author:     gokaysatir <gokaysatir at gmail.com>
AuthorDate: Mon Mar 30 02:31:23 2020 +0300
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Wed Apr 1 20:46:47 2020 +0100

    tdf#130568 - Add server os pretty name to help->about
    
    Change-Id: Id6de533dfb8e34a05d348f8ae701bf3c524c9b95

diff --git a/common/Util.cpp b/common/Util.cpp
index 137ec3897..2abbd4d11 100644
--- a/common/Util.cpp
+++ b/common/Util.cpp
@@ -929,6 +929,30 @@ namespace Util
         return false;
 #endif
     }
+
+    std::map<std::string, std::string> stringVectorToMap(std::vector<std::string> sVector, const char delimiter)
+    {
+        std::map<std::string, std::string> result;
+
+        for (std::vector<std::string>::iterator it = sVector.begin(); it != sVector.end(); it++)
+        {
+            size_t delimiterPosition = 0;
+            delimiterPosition = (*it).find(delimiter, 0);
+            if (delimiterPosition != std::string::npos)
+            {
+                std::string key = (*it).substr(0, delimiterPosition);
+                delimiterPosition++;
+                std::string value = (*it).substr(delimiterPosition);
+                result[key] = value;
+            }
+            else
+            {
+                LOG_WRN("Util::stringVectorToMap => record is misformed: " << (*it));
+            }
+        }
+
+        return result;
+    }
 }
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/common/Util.hpp b/common/Util.hpp
index 52043692f..fd1589cfe 100644
--- a/common/Util.hpp
+++ b/common/Util.hpp
@@ -72,7 +72,7 @@ namespace Util
     /// to send data to the child.
     int spawnProcess(const std::string &cmd, const StringVector &args,
                      const std::vector<int>* fdsToKeep = nullptr, int *stdInput = nullptr);
-    
+
 #endif
 
     /// Hex to unsigned char
@@ -972,6 +972,32 @@ int main(int argc, char**argv)
      * test tool targets (typically fuzzing) where start-up speed is critical.
      */
     bool isFuzzing();
+
+    /**
+     * Splits string into vector<string>. Does not accept referenced variables for easy
+     * usage like (splitString("test", ..)) or (splitString(getStringOnTheFly(), ..))
+     */
+    inline std::vector<std::string> splitStringToVector(std::string const str, const char delim)
+    {
+        size_t start;
+        size_t end = 0;
+
+        std::vector<std::string> result;
+
+        while ((start = str.find_first_not_of(delim, end)) != std::string::npos)
+        {
+            end = str.find(delim, start);
+            result.push_back(str.substr(start, end - start));
+        }
+        return result;
+    }
+
+    /**
+     * Converts vector of strings to map. Strings should have formed like this: key + delimiter + value.
+     * In case of a misformed string or zero length vector, returns an empty map.
+     */
+    std::map<std::string, std::string> stringVectorToMap(std::vector<std::string> sVector, const char delimiter);
+
 } // end namespace Util
 
 #endif
diff --git a/loleaflet/html/loleaflet.html.m4 b/loleaflet/html/loleaflet.html.m4
index 1bb0c44c9..a2c1a45cf 100644
--- a/loleaflet/html/loleaflet.html.m4
+++ b/loleaflet/html/loleaflet.html.m4
@@ -220,6 +220,7 @@ m4_ifelse(MOBILEAPP,[true],
       <div id="loolwsd-id"></div>
       <h3>LOKit</h3>
       <div id="lokit-version"></div>
+      <div id="os-name" style="text-align:center"><label>%OS_INFO%</label></div>
       <p>Copyright © _YEAR_, VENDOR.</p>
     </div>
 
diff --git a/wsd/FileServer.cpp b/wsd/FileServer.cpp
index ce9e12756..9d833c510 100644
--- a/wsd/FileServer.cpp
+++ b/wsd/FileServer.cpp
@@ -649,6 +649,7 @@ void FileServerRequestHandler::preprocessFile(const HTTPRequest& request, Poco::
     Poco::replaceInPlace(preprocess, std::string("%HOST%"), host);
     Poco::replaceInPlace(preprocess, std::string("%VERSION%"), std::string(LOOLWSD_VERSION_HASH));
     Poco::replaceInPlace(preprocess, std::string("%SERVICE_ROOT%"), LOOLWSD::ServiceRoot);
+    Poco::replaceInPlace(preprocess, std::string("%OS_INFO%"), LOOLWSD::OSInfo);
 
     std::string protocolDebug = "false";
     if (config.getBool("logging.protocol"))
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 9c040a8fd..54ba8e2b9 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -726,6 +726,7 @@ std::string LOOLWSD::ServerName;
 std::string LOOLWSD::FileServerRoot;
 std::string LOOLWSD::ServiceRoot;
 std::string LOOLWSD::LOKitVersion;
+std::string LOOLWSD::OSInfo;
 std::string LOOLWSD::HostIdentifier;
 std::string LOOLWSD::ConfigFile = LOOLWSD_CONFIGDIR "/loolwsd.xml";
 std::string LOOLWSD::ConfigDir = LOOLWSD_CONFIGDIR "/conf.d";
@@ -749,6 +750,25 @@ std::unique_ptr<TraceFileWriter> LOOLWSD::TraceDumper;
 std::unique_ptr<ClipboardCache> LOOLWSD::SavedClipboards;
 #endif
 
+void LOOLWSD::getOSInfo(){
+    #if !MOBILEAPP
+        // It might be neither mobile nor linux (in the future). That is not handled. If it is not mobile, it is Linux.
+
+        // Read operating system info. We can read "os-release" file, located in /etc.
+        std::ifstream ifs("/etc/os-release");
+        std::string str(std::istreambuf_iterator<char>{ifs}, {});
+        std::vector<std::string> infoList = Util::splitStringToVector(str, '\n');
+        std::map<std::string, std::string> releaseInfo = Util::stringVectorToMap(infoList, '=');
+        LOOLWSD::OSInfo = "unknown";
+
+        if (releaseInfo.find("PRETTY_NAME") != releaseInfo.end()) {
+            LOOLWSD::OSInfo = releaseInfo["PRETTY_NAME"];
+        }
+    #else
+        // Some more cases might be added in the future.
+    #endif
+}
+
 /// This thread polls basic web serving, and handling of
 /// websockets before upgrade: when upgraded they go to the
 /// relevant DocumentBroker poll instead.
@@ -886,6 +906,8 @@ void LOOLWSD::initialize(Application& self)
     AutoPtr<AppConfigMap> defConfig(new AppConfigMap(DefAppConfig));
     conf.addWriteable(defConfig, PRIO_SYSTEM); // Lowest priority
 
+    LOOLWSD::getOSInfo();
+
 #if !MOBILEAPP
 
     // Load default configuration files, if present.
diff --git a/wsd/LOOLWSD.hpp b/wsd/LOOLWSD.hpp
index 383263314..469d26bec 100644
--- a/wsd/LOOLWSD.hpp
+++ b/wsd/LOOLWSD.hpp
@@ -70,6 +70,7 @@ public:
     static std::string FileServerRoot;
     static std::string ServiceRoot; ///< There are installations that need prefixing every page with some path.
     static std::string LOKitVersion;
+    static std::string OSInfo;
     static std::string HostIdentifier; ///< A unique random hash that identifies this server
     static std::string LogLevel;
     static bool AnonymizeUserData;
@@ -219,6 +220,9 @@ public:
 
     static std::string getVersionJSON();
 
+    /// Reads OS information, puts them into LOOLWSD::OSInfo variable
+    static void getOSInfo();
+
     int innerMain();
 
 protected:
commit 3e67e434e4d4976939efbc71502fc573e8d417e7
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Apr 1 14:44:59 2020 +0100
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Wed Apr 1 18:10:23 2020 +0200

    Avoid de-referencing non-existing marker.
    
    This screws up cursors across views in some circumstances,
    when there is no marker in one of the views.
    
    Regression from 3b0478baab1e58a45fc723e9265942bf5d4c10fa
    
    Change-Id: Ia1294d83801d656a919f97206b7b573a1b74b77f
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/91489
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Andras Timar <andras.timar at collabora.com>

diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index 5245d085f..add537854 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -2230,7 +2230,8 @@ L.TileLayer = L.GridLayer.extend({
 			this._viewLayerGroup.removeLayer(viewCursorMarker);
 		}
 
-		this._viewCursors[viewId].marker.showCursorHeader();
+		if (this._viewCursors[viewId].marker)
+			this._viewCursors[viewId].marker.showCursorHeader();
 	},
 
 	updateAllViewCursors : function() {
commit 3f358cbb80797991cc31a79e47e5ac015080815f
Author:     Tamás Zolnai <tamas.zolnai at collabora.com>
AuthorDate: Wed Apr 1 11:35:49 2020 +0200
Commit:     Tamás Zolnai <tamas.zolnai at collabora.com>
CommitDate: Wed Apr 1 15:04:58 2020 +0200

    cypress: mobile: try harder to remove selection in calc spellchecking tests.
    
    It's a known issue that text selection is persistent,
    which made these tests unstable. So use a different method
    to remove the text selection.
    
    Change-Id: I46b0ba268a39bae9e79ce62686b2d1e0eab1adda
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/91486
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Tamás Zolnai <tamas.zolnai at collabora.com>

diff --git a/cypress_test/integration_tests/mobile/calc/calc_helper.js b/cypress_test/integration_tests/mobile/calc/calc_helper.js
index ce3b1608e..0fd771dad 100644
--- a/cypress_test/integration_tests/mobile/calc/calc_helper.js
+++ b/cypress_test/integration_tests/mobile/calc/calc_helper.js
@@ -15,6 +15,27 @@ function clickOnFirstCell() {
 			cy.get('body')
 				.click(XPos, YPos);
 		});
+
+	cy.get('.spreadsheet-cell-resize-marker')
+		.should('exist');
+}
+
+function dblClickOnFirstCell() {
+	// Enable editing if it's in read-only mode
+	helper.enableEditingMobile();
+
+	// Use the tile's edge to find the first cell's position
+	cy.get('.leaflet-tile-container')
+		.then(function(items) {
+			expect(items).to.have.lengthOf(1);
+			var XPos = items[0].getBoundingClientRect().right + 10;
+			var YPos = items[0].getBoundingClientRect().top + 10;
+			cy.get('body')
+				.dblclick(XPos, YPos);
+		});
+
+	cy.get('.leaflet-cursor.blinking-cursor')
+		.should('exist');
 }
 
 function copyContentToClipboard() {
@@ -45,7 +66,7 @@ function copyContentToClipboard() {
 		.should('not.exist');
 }
 
-function selectAllMobile() {
+function removeTextSelection() {
 	// TODO: select all does not work with core/master
 	// if we have a column selected
 	if (Cypress.env('LO_CORE_VERSION') === 'master') {
@@ -61,6 +82,11 @@ function selectAllMobile() {
 		cy.get('.spreadsheet-cell-resize-marker')
 			.should('exist');
 	}
+}
+
+function selectAllMobile() {
+	removeTextSelection();
+
 
 	cy.get('#spreadsheet-header-corner')
 		.click();
@@ -70,5 +96,7 @@ function selectAllMobile() {
 }
 
 module.exports.copyContentToClipboard = copyContentToClipboard;
+module.exports.removeTextSelection = removeTextSelection;
 module.exports.selectAllMobile = selectAllMobile;
 module.exports.clickOnFirstCell = clickOnFirstCell;
+module.exports.dblClickOnFirstCell = dblClickOnFirstCell;
diff --git a/cypress_test/integration_tests/mobile/calc/spellchecking_spec.js b/cypress_test/integration_tests/mobile/calc/spellchecking_spec.js
index 951e567a7..b1a564bca 100644
--- a/cypress_test/integration_tests/mobile/calc/spellchecking_spec.js
+++ b/cypress_test/integration_tests/mobile/calc/spellchecking_spec.js
@@ -18,14 +18,11 @@ describe('Calc spell checking menu.', function() {
 
 	function openContextMenu() {
 		// Step into edit mode
-		calcHelper.clickOnFirstCell();
-		calcHelper.clickOnFirstCell();
-		cy.get('.leaflet-cursor.blinking-cursor')
-			.should('exist');
+		calcHelper.dblClickOnFirstCell();
 
 		// Select text content
 		cy.get('textarea.clipboard')
-			.type('{ctrl}a');
+			.type('{ctrl}a', {force: true});
 
 		// Open context menu
 		cy.get('.leaflet-marker-icon')
@@ -40,10 +37,10 @@ describe('Calc spell checking menu.', function() {
 				}
 
 				// Remove selection
-				cy.get('body')
-					.type('{leftarrow}');
-				cy.get('.leaflet-marker-icon')
-					.should('not.exist');
+				calcHelper.removeTextSelection();
+
+				// Step into edit mode again
+				calcHelper.dblClickOnFirstCell();
 
 				helper.longPressOnDocument(XPos, YPos);
 			});
commit d90aed7cf0da67e6b780a660ad45f99c6577298e
Author:     Tamás Zolnai <tamas.zolnai at collabora.com>
AuthorDate: Wed Apr 1 13:03:31 2020 +0200
Commit:     Tamás Zolnai <tamas.zolnai at collabora.com>
CommitDate: Wed Apr 1 15:04:49 2020 +0200

    cypress: mobile: make impress spellchecking test more stable.
    
    Change-Id: I3fe512eb5da0bcdab905f023f51ce03af7e503f5
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/91487
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Tamás Zolnai <tamas.zolnai at collabora.com>

diff --git a/cypress_test/integration_tests/mobile/impress/spellchecking_spec.js b/cypress_test/integration_tests/mobile/impress/spellchecking_spec.js
index 1d1224db4..d4cbaa256 100644
--- a/cypress_test/integration_tests/mobile/impress/spellchecking_spec.js
+++ b/cypress_test/integration_tests/mobile/impress/spellchecking_spec.js
@@ -43,6 +43,9 @@ describe('Spell checking menu.', function() {
 					}
 				}
 
+				cy.get('.leaflet-cursor.blinking-cursor')
+					.should('exist');
+
 				// Remove selection
 				cy.get('body')
 					.type('{leftarrow}');
commit 59fc10bcd65bfbc8a30b613bc17e83aacc9e06cd
Author:     Tor Lillqvist <tml at collabora.com>
AuthorDate: Wed Apr 1 13:41:31 2020 +0300
Commit:     Tor Lillqvist <tml at collabora.com>
CommitDate: Wed Apr 1 13:42:54 2020 +0300

    Add some (ifdeffed-out) experimentation with file service providers
    
    Kept in #if 0 in case some similar experimentation needed in the
    future.
    
    It might be interesting to be able to tweak behaviour based on knowing
    on what kind of storage the doument is located, but alas, that seems
    not possible.
    
    Change-Id: I877c96fcea3a350faa58a934485714d15b01141d

diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp
index 2f2f1c20a..1289c862c 100644
--- a/kit/ChildSession.cpp
+++ b/kit/ChildSession.cpp
@@ -917,7 +917,39 @@ bool ChildSession::downloadAs(const char* /*buffer*/, int /*length*/, const Stri
     }
 
 #ifdef IOS
-    NSArray<NSString *> *pathComponents = [[NSURL URLWithString:[NSString stringWithUTF8String:getDocURL().c_str()]] pathComponents];
+    NSURL *docURL = [NSURL URLWithString:[NSString stringWithUTF8String:getDocURL().c_str()]];
+
+#if 0
+    // Experimentation
+
+    // Check if we can figure out the name of the file provider service the document is on. (No, the
+    // services dictionary passed to the completion handler is always empty, except for On My iPad
+    // and iCloud Drive.)
+    [NSFileManager.defaultManager
+     getFileProviderServicesForItemAtURL:docURL
+                       completionHandler:^(NSDictionary<NSFileProviderServiceName,NSFileProviderService *> *services,
+                                           NSError *error) {
+            if (services == nil) {
+                LOG_TRC("Could not get file provider services for " << [[docURL absoluteString] UTF8String]);
+            } else if ([services count] == 0) {
+                LOG_TRC("No file provider services returned for " << [[docURL absoluteString] UTF8String]);
+            } else {
+                LOG_TRC("File provider services for " << [[docURL absoluteString] UTF8String]);
+                NSEnumerator *keyEnumerator = [services keyEnumerator];
+                NSString *key;
+                while ((key = (NSString*)[keyEnumerator nextObject])) {
+                    LOG_TRC("  " << key);
+                }
+            }
+        }];
+
+    // Check if we can figure out the "ubiquitous item container" name, which apparently means the file provider extension name.
+    // Alas, this seems to work only for documents on iCloud Drive.
+    NSError *error;
+    auto resources = [docURL promisedItemResourceValuesForKeys:@[NSURLUbiquitousItemContainerDisplayNameKey] error:&error];
+#endif
+
+    NSArray<NSString *> *pathComponents = [docURL pathComponents];
     NSString *baseName = [[pathComponents lastObject] stringByDeletingPathExtension];
     NSURL *documentDirectory = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask][0];
     NSString *dotFormat = [@"." stringByAppendingString:[NSString stringWithUTF8String:format.c_str()]];
commit af8669b9d4134f797079de11cccf62c7aa4c6ee1
Author:     Jan Holesovsky <kendy at collabora.com>
AuthorDate: Tue Mar 31 23:10:37 2020 +0200
Commit:     Jan Holesovsky <kendy at collabora.com>
CommitDate: Wed Apr 1 08:53:22 2020 +0200

    mobile: Kill most of the menu styling.
    
    I believe this is not relevant any more, since we are using the
    mobile-wizard for menus on the phones these days.
    
    Change-Id: Ia6b16fcde1b1e4278a8d40c58968c44492248b69
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/91443
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Jan Holesovsky <kendy at collabora.com>

diff --git a/loleaflet/css/device-mobile.css b/loleaflet/css/device-mobile.css
index 43df49dbe..4414fd4c5 100644
--- a/loleaflet/css/device-mobile.css
+++ b/loleaflet/css/device-mobile.css
@@ -41,3 +41,16 @@ div#w2ui-overlay-actionbar.w2ui-overlay{
 #toolbar-hamburger.menuwizard-opened .main-menu-btn {
 	padding-top: 10px;
 }
+
+.logo {
+	background-size: 100px;
+	max-width: 24px;
+	max-height: 31px;
+	top: 0;
+}
+#toolbar-logo {
+	width: 0px !important;
+}
+#document-header{
+	display: none !important;
+}
diff --git a/loleaflet/css/menubar-mobile.css b/loleaflet/css/menubar-mobile.css
index cccb63eda..af0ccb615 100644
--- a/loleaflet/css/menubar-mobile.css
+++ b/loleaflet/css/menubar-mobile.css
@@ -1,56 +1,4 @@
 @media (max-device-width: 767px) {
-	.logo {
-		background-size: 100px;
-		max-width: 24px;
-		max-height: 31px;
-		top: 0;
-	}
-
-	#toolbar-logo {
-		width: 0px !important;
-	}
-	#main-menu{
-		width: 98%;
-	}
-	#main-menu > li{
-		min-height: 56px;
-		border: none;
-	}
-	#main-menu > li > a{
-		font-size: 18px !important;
-		border: none;
-	}
-	.lo-menu ul{
-		border: none !important;
-		background-color: #f7f7f7 !important;
-	}
-	.lo-menu ul > li{
-		height: auto;
-
-	}
-	.lo-menu ul > li > ul > li > a{
-		height: 56px;
-		display: flex;
-		flex-direction: row;
-		align-items: center;
-	}
-
-	.lo-menu ul >li > .separator{
-		display: none;
-	}
-	.lo-menu ul > li > a{
-		width: 100%;
-		height: 56px;
-		display: flex;
-		flex-direction: row;
-		align-items: center;
-		font-size: 14px;
-		font-weight: bold;
-		color: #404040;
-	}
-	#document-header{
-		display: none !important;
-	}
 	@media (orientation: landscape) {
 		#mobile-wizard.menuwizard #mobile-wizard-content.with-slide-sorter-above {
 			overflow-y: auto !important;
commit 5c95448308f5c40cc3b3a15ffbb2876cbda1a821
Author:     Jan Holesovsky <kendy at collabora.com>
AuthorDate: Tue Mar 31 22:17:13 2020 +0200
Commit:     Jan Holesovsky <kendy at collabora.com>
CommitDate: Wed Apr 1 08:52:52 2020 +0200

    mobile: Position the hamburger icon at the right place already during load.
    
    Change-Id: I93096a3239feccfe5268799b3fbd5544928e5372
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/91442
    Tested-by: Jan Holesovsky <kendy at collabora.com>
    Reviewed-by: Jan Holesovsky <kendy at collabora.com>

diff --git a/loleaflet/css/device-mobile.css b/loleaflet/css/device-mobile.css
index 7fd7e4ca3..43df49dbe 100644
--- a/loleaflet/css/device-mobile.css
+++ b/loleaflet/css/device-mobile.css
@@ -32,6 +32,7 @@ div#w2ui-overlay-actionbar.w2ui-overlay{
 /* Related to loleaflet.css */
 #toolbar-hamburger {
 	width: 36px;
+	height: 36px;
 }
 #toolbar-hamburger.menuwizard-opened {
 	width: 36px;
commit a17c651d6da5303bcb770531b5bfd32543167d3b
Author:     Jan Holesovsky <kendy at collabora.com>
AuthorDate: Tue Mar 31 22:16:40 2020 +0200
Commit:     Jan Holesovsky <kendy at collabora.com>
CommitDate: Wed Apr 1 08:51:12 2020 +0200

    mobile: Move the hamburger menu.
    
    Change-Id: I33da508dc9e8c6a58e85bcca197b6cc39273619d
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/91441
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Jan Holesovsky <kendy at collabora.com>

diff --git a/loleaflet/css/device-mobile.css b/loleaflet/css/device-mobile.css
index bd9018d85..7fd7e4ca3 100644
--- a/loleaflet/css/device-mobile.css
+++ b/loleaflet/css/device-mobile.css
@@ -28,3 +28,15 @@ input#follow-checkbox {
 div#w2ui-overlay-actionbar.w2ui-overlay{
 	margin-left:-7px;
 }
+
+/* Related to loleaflet.css */
+#toolbar-hamburger {
+	width: 36px;
+}
+#toolbar-hamburger.menuwizard-opened {
+	width: 36px;
+	height: 43px;
+}
+#toolbar-hamburger.menuwizard-opened .main-menu-btn {
+	padding-top: 10px;
+}
diff --git a/loleaflet/css/loleaflet.css b/loleaflet/css/loleaflet.css
index 7a85ec727..d56a602c4 100644
--- a/loleaflet/css/loleaflet.css
+++ b/loleaflet/css/loleaflet.css
@@ -178,18 +178,6 @@ body {
 #toolbar-hamburger {
 	width: 0;
 }
- at media (max-width: 767px) and (orientation: portrait), (max-width: 1023px) and (orientation: landscape) {
-	#toolbar-hamburger {
-		width: 36px;
-	}
-	#toolbar-hamburger.menuwizard-opened {
-		width: 36px;
-		height: 43px;
-	}
-	#toolbar-hamburger.menuwizard-opened .main-menu-btn {
-		padding-top: 10px;
-	}
-}
 
 #mobile-edit-button {
 	position: absolute;
commit 7946f593d8d9a6c6b5435c397844084454bb89c2
Author:     Andras Timar <andras.timar at collabora.com>
AuthorDate: Tue Mar 31 22:35:34 2020 +0200
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Wed Apr 1 07:16:51 2020 +0200

    loleaflet: update pot files
    
    Change-Id: I256b724f454ad5cbfdd10759f1b7150cd16564dd
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/91439
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Andras Timar <andras.timar at collabora.com>

diff --git a/loleaflet/po/templates/ios.pot b/loleaflet/po/templates/ios.pot
index fa2c10ac3..028905cff 100644
--- a/loleaflet/po/templates/ios.pot
+++ b/loleaflet/po/templates/ios.pot
@@ -5,7 +5,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-03-15 16:08+0200\n"
+"POT-Creation-Date: 2020-03-31 22:32+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
 "Language-Team: LANGUAGE <LL at li.org>\n"
@@ -14,12 +14,16 @@ msgstr ""
 "Content-Transfer-Encoding: 8bit\n"
 "X-Generator: Translate Toolkit 2.4.0\n"
 
-#: App+info
-msgid "App info"
+#: Version+information
+msgid "Version information"
 msgstr ""
 
-#: Version
-msgid "Version"
+#: Online+git+hash
+msgid "Online git hash"
+msgstr ""
+
+#: Core+git+hash
+msgid "Core git hash"
 msgstr ""
 
 #: Settings
diff --git a/loleaflet/po/templates/loleaflet-help.pot b/loleaflet/po/templates/loleaflet-help.pot
index 927cc90a6..4d92ac417 100644
--- a/loleaflet/po/templates/loleaflet-help.pot
+++ b/loleaflet/po/templates/loleaflet-help.pot
@@ -3,7 +3,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-03-15 22:15+0200\n"
+"POT-Creation-Date: 2020-03-31 22:32+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
 "Language-Team: LANGUAGE <LL at li.org>\n"
diff --git a/loleaflet/po/templates/loleaflet-ui.pot b/loleaflet/po/templates/loleaflet-ui.pot
index 8c45fa8ad..37903d1ca 100644
--- a/loleaflet/po/templates/loleaflet-ui.pot
+++ b/loleaflet/po/templates/loleaflet-ui.pot
@@ -8,7 +8,7 @@ msgid ""
 msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2020-03-15 16:08+0100\n"
+"POT-Creation-Date: 2020-03-31 22:32+0200\n"
 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "Last-Translator: FULL NAME <EMAIL at ADDRESS>\n"
 "Language-Team: LANGUAGE <LL at li.org>\n"
@@ -118,7 +118,7 @@ msgid "Network Graph"
 msgstr ""
 
 #: admin/admin.strings.js:31 src/layer/marker/Annotation.js:252
-#: src/layer/tile/TileLayer.js:395
+#: src/layer/tile/TileLayer.js:396
 msgid "Save"
 msgstr ""
 
@@ -187,17 +187,17 @@ msgid "Are you sure you want to terminate this session?"
 msgstr ""
 
 #: admin/src/AdminSocketOverview.js:107 admin/src/AdminSocketSettings.js:36
-#: src/control/Control.Menubar.js:1156 src/control/Control.Tabs.js:195

... etc. - the rest is truncated


More information about the Libreoffice-commits mailing list