[Libreoffice-commits] online.git: Branch 'feature/proxyhack' - 17 commits - android/app cypress_test/README ios/Mobile loleaflet/html loleaflet/images loleaflet/js loleaflet/src Makefile.am net/Socket.cpp net/Socket.hpp wsd/ClientSession.hpp wsd/DocumentBroker.hpp wsd/FileServer.cpp wsd/LOOLWSD.cpp wsd/ProxyProtocol.cpp wsd/ProxyProtocol.hpp

Michael Meeks (via logerrit) logerrit at kemper.freedesktop.org
Sat Mar 21 15:07:39 UTC 2020


Rebased ref, commits from common ancestor:
commit 634e04aeebcaaf4fe61a684a05e0f3f682234e22
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: Sat Mar 21 15:07:10 2020 +0000

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

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 95ff625f7..9f319215f 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -197,7 +197,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;
@@ -255,7 +255,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);
@@ -281,21 +281,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')
@@ -310,13 +313,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 84db6ac84..05c8c1e9d 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2833,7 +2833,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..19dd70692 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 3b2ec64f4d1fe395f37623d889337bf4caca9f2c
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: Sat Mar 21 14:27:15 2020 +0000

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

diff --git a/net/Socket.cpp b/net/Socket.cpp
index 5bb1fa250..675ee64c6 100644
--- a/net/Socket.cpp
+++ b/net/Socket.cpp
@@ -394,6 +394,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 506d7f0e8e684530c16da7699a746fc590097e44
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: Sat Mar 21 14:19:49 2020 +0000

    Proxy: make eslint happier.
    
    Change-Id: I9ecec787a9a69633a015459eaf39d4b8bd5bb61d

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 98c1d1afe..95ff625f7 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(),
@@ -293,8 +294,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 7975d973749d11976d27eb4b4473f37e6109e632
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: Sat Mar 21 14:07:24 2020 +0000

    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 74d72af98..98c1d1afe 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -196,6 +196,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 e3f6faff933f410b1cabc72775f4120256ea159e
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: Sat Mar 21 14:07:24 2020 +0000

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

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index 4f68e841b..74d72af98 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -187,7 +187,7 @@
 		this.sessionId = 'fetchsession';
 		this.id = window.proxySocketCounter++;
 		this.sendCounter = 0;
-		this.readWaiting = false;
+		this.readWaiting = 0;
 		this.onclose = function() {
 		};
 		this.onerror = function() {
@@ -296,9 +296,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:
@@ -307,13 +307,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 cb5bf54e3..6bcb606f3 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 514060624..84db6ac84 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2831,11 +2831,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());
@@ -2845,7 +2850,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());
@@ -2855,7 +2861,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 ea277d331d67834c8e0f37ef2e25ec620c6e3967
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: Sat Mar 21 14:07:24 2020 +0000

    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 78bceaa9c..4f68e841b 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -317,6 +317,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 0106141140cf987bc4ca977b2cc106bbbc41f8b1
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: Sat Mar 21 14:07:24 2020 +0000

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

diff --git a/loleaflet/js/global.js b/loleaflet/js/global.js
index c1edc9c7f..78bceaa9c 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -176,6 +176,7 @@
 
 	global.proxySocketCounter = 0;
 	global.ProxySocket = function (uri) {
+		var that = this;
 		this.uri = uri;
 		this.binaryType = 'arraybuffer';
 		this.bufferedAmount = 0;
@@ -235,56 +236,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;
@@ -296,7 +289,9 @@
 		};
 		console.debug('New proxy socket ' + this.id + ' ' + this.uri);
 
-		this.send('fetchsession');
+		// queue fetch of session id.
+		this.getSessionId();
+
 		var that = this;
 
 		// horrors ...
@@ -309,7 +304,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 b51605be81774af641b105ea42600e6f8a07dbd6
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: Sat Mar 21 14:07:24 2020 +0000

    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 74e982873..c1edc9c7f 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -193,19 +193,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');
@@ -218,7 +296,6 @@
 		};
 		console.debug('New proxy socket ' + this.id + ' ' + this.uri);
 
-		// FIXME: perhaps a little risky.
 		this.send('fetchsession');
 		var that = this;
 
@@ -231,20 +308,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 2290dadd9..be05a5559 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; }
 
@@ -1035,6 +1039,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)
     {
@@ -1198,6 +1208,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)
@@ -1221,8 +1233,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 dd84960d1..cb5bf54e3 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 ed43262e8..514060624 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2855,7 +2855,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 c4fc7b788215c7549ebe60895de5c3df0216ff31
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: Sat Mar 21 14:07:24 2020 +0000

    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 a08c4cf3b..74e982873 100644
--- a/loleaflet/js/global.js
+++ b/loleaflet/js/global.js
@@ -166,16 +166,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;
@@ -200,7 +281,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();
@@ -275,7 +357,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 b4118479f..f4a44fd06 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 a6395b9b4..2290dadd9 100644
--- a/net/Socket.hpp
+++ b/net/Socket.hpp
@@ -446,6 +446,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 68369d274..dd84960d1 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 aa6b9766b..ed43262e8 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);
 
@@ -1764,9 +1767,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;
         }
     }
@@ -1782,9 +1788,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)
     {
@@ -2242,6 +2251,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")
                 {
@@ -2775,6 +2791,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)
     {
@@ -2837,7 +2946,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 52204d304055e66de0aac22ac0c3ee1bc8c7bdd1
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: Sat Mar 21 14:07:24 2020 +0000

    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 84fd50b15..86e54249f 100644
--- a/loleaflet/html/loleaflet.html.m4
+++ b/loleaflet/html/loleaflet.html.m4
@@ -235,6 +235,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%';
@@ -247,6 +248,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
     </script>
diff --git a/wsd/FileServer.cpp b/wsd/FileServer.cpp
index ce9e12756..2b53999c8 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,15 +652,21 @@ 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);
 
+    const auto& config = Application::instance().config();
     std::string protocolDebug = "false";
     if (config.getBool("logging.protocol"))
         protocolDebug = "true";
@@ -658,16 +675,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
 
@@ -850,13 +867,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
@@ -874,7 +893,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 d26aa30cab3335df110110d86fa42750529e9b98
Author:     Miklos Vajna <vmiklos at collabora.com>
AuthorDate: Fri Mar 20 18:04:29 2020 +0100
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Fri Mar 20 22:05:14 2020 +0100

    Fix --with-support-public-key=... build
    
    Change-Id: I4a1ecc16d7b862dae61c80d98d07c0e105c13819
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90829
    Tested-by: Andras Timar <andras.timar at collabora.com>
    Reviewed-by: Andras Timar <andras.timar at collabora.com>

diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 177360dbc..aa6b9766b 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -244,7 +244,7 @@ inline void shutdownLimitReached(const std::shared_ptr<ProtocolHandlerInterface>
     try
     {
         // Let the client know we are shutting down.
-        proto->sendTextMessage(error);
+        proto->sendTextMessage(error.data(), error.size());
 
         // Shutdown.
         proto->shutdown(true, error);
commit d474f060409328c1f40c63074396b1bc579de72e
Author:     Tor Lillqvist <tml at collabora.com>
AuthorDate: Fri Mar 20 16:02:08 2020 +0200
Commit:     Tor Lillqvist <tml at collabora.com>
CommitDate: Fri Mar 20 16:03:19 2020 +0200

    Force portrait for the moment also for iPhone in the iOS app
    
    Change-Id: I1def28e7969cea753e7fc36094fe6514c17d61af

diff --git a/ios/Mobile/Info.plist.in b/ios/Mobile/Info.plist.in
index 628e95e7b..33517ab14 100644
--- a/ios/Mobile/Info.plist.in
+++ b/ios/Mobile/Info.plist.in
@@ -430,8 +430,6 @@
     <key>UISupportedInterfaceOrientations</key>
     <array>
         <string>UIInterfaceOrientationPortrait</string>
-        <string>UIInterfaceOrientationLandscapeLeft</string>
-        <string>UIInterfaceOrientationLandscapeRight</string>
     </array>
     <key>UISupportedInterfaceOrientations~ipad</key>
     <array>
commit a4a5b4aed0992c8430de7414e3a6ba12bdf91fd5
Author:     Henry Castro <hcastro at collabora.com>
AuthorDate: Fri Mar 20 08:37:21 2020 -0400
Commit:     Henry Castro <hcastro at collabora.com>
CommitDate: Fri Mar 20 14:40:12 2020 +0100

    loleaflet: move the code about init of the style combobox, part 2
    
    It fixes mobile:
    TypeError: Cannot read property 'length' of undefined
    
    Change-Id: I620833dfdd736347da86d76531cf1c67e0b5b152
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90792
    Tested-by: Henry Castro <hcastro at collabora.com>
    Reviewed-by: Tamás Zolnai <tamas.zolnai at collabora.com>
    Reviewed-by: Henry Castro <hcastro at collabora.com>

diff --git a/loleaflet/src/control/Control.Toolbar.js b/loleaflet/src/control/Control.Toolbar.js
index 13e8e369c..2621063e3 100644
--- a/loleaflet/src/control/Control.Toolbar.js
+++ b/loleaflet/src/control/Control.Toolbar.js
@@ -1010,25 +1010,6 @@ function initNormalToolbar() {
 			hideTooltip(this, e.target);
 		},
 		onRefresh: function(event) {
-			if (event.target === 'editbar' && map.getDocType() === 'presentation') {
-				// Fill the style select box if not yet filled
-				if ($('.styles-select')[0] && $('.styles-select')[0].length === 1) {
-					var data = [''];
-					// Inserts a separator element
-					data = data.concat({text: '\u2500\u2500\u2500\u2500\u2500\u2500', disabled: true});
-
-					L.Styles.impressLayout.forEach(function(layout) {
-						data = data.concat({id: layout.id, text: _(layout.text)});
-					}, this);
-
-					$('.styles-select').select2({
-						data: data,
-						placeholder: _UNO('.uno:LayoutStatus', 'presentation')
-					});
-					$('.styles-select').on('select2:select', onStyleSelect);
-				}
-			}
-
 			if ((event.target === 'styles' || event.target === 'fonts' || event.target === 'fontsizes') && event.item) {
 				var toolItem = $(this.box).find('#tb_'+ this.name +'_item_'+ w2utils.escapeId(event.item.id));
 				if ((_inDesktopMode() && event.item.desktop == false)
@@ -1594,6 +1575,7 @@ function onDocLayerInit() {
 	var toolbarUp = w2ui['editbar'];
 	var statusbar = w2ui['actionbar'];
 	var docType = map.getDocType();
+	var data;
 
 	switch (docType) {
 	case 'spreadsheet':
@@ -1697,6 +1679,23 @@ function onDocLayerInit() {
 
 		break;
 	case 'presentation':
+		// Fill the style select box if not yet filled
+		if ($('.styles-select')[0] && $('.styles-select')[0].length === 1) {
+			data = [''];
+			// Inserts a separator element
+			data = data.concat({text: '\u2500\u2500\u2500\u2500\u2500\u2500', disabled: true});
+
+			L.Styles.impressLayout.forEach(function(layout) {
+				data = data.concat({id: layout.id, text: _(layout.text)});
+			}, this);
+
+			$('.styles-select').select2({
+				data: data,
+				placeholder: _UNO('.uno:LayoutStatus', 'presentation')
+			});
+			$('.styles-select').on('select2:select', onStyleSelect);
+		}
+
 		if (toolbarUp) {
 			toolbarUp.show('breaksidebar', 'modifypage');
 		}
@@ -1794,7 +1793,7 @@ function onDocLayerInit() {
 			el.resize();
 	}
 
-	var data = [6, 7, 8, 9, 10, 10.5, 11, 12, 13, 14, 15, 16, 18, 20,
+	data = [6, 7, 8, 9, 10, 10.5, 11, 12, 13, 14, 15, 16, 18, 20,
 		22, 24, 26, 28, 32, 36, 40, 44, 48, 54, 60, 66, 72, 80, 88, 96];
 	$('.fontsizes-select').select2({
 		data: data,
commit db585f8c64b32ba423f5c311ddd04d39809d34b4
Author:     Jan Holesovsky <kendy at collabora.com>
AuthorDate: Fri Mar 20 14:29:00 2020 +0100
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Fri Mar 20 14:38:32 2020 +0100

    android: Force portrait for the moment.
    
    Change-Id: Ibd3964fb7e99264104ad46c26e6ff2e4e62b6559
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90794
    Tested-by: Andras Timar <andras.timar at collabora.com>
    Reviewed-by: Andras Timar <andras.timar at collabora.com>

diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 6912a0b57..ecda89bbb 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -51,6 +51,7 @@
         <activity
             android:name="org.libreoffice.androidlib.LOActivity"
             android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
+            android:screenOrientation="sensorPortrait"
             android:launchMode="singleTask">
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
commit fa6e1e36688c0df4c64db84d55e7227ff9c701ad
Author:     Tamás Zolnai <tamas.zolnai at collabora.com>
AuthorDate: Fri Mar 20 12:43:02 2020 +0100
Commit:     Tamás Zolnai <tamas.zolnai at collabora.com>
CommitDate: Fri Mar 20 12:58:19 2020 +0100

    Revert "loleaflet: move the code about initialization of the style combobox"
    
    Breaks mobile view in Impress.
    TypeError: Cannot read property 'length' of undefined
    
    This reverts commit 9db6f855a016891f5c0d59cab014f246752cd907.
    
    Change-Id: I839cde8bb683fc0b933da806f4a4771114eb32f5
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90785
    Tested-by: Tamás Zolnai <tamas.zolnai at collabora.com>
    Reviewed-by: Tamás Zolnai <tamas.zolnai at collabora.com>

diff --git a/loleaflet/src/control/Control.Toolbar.js b/loleaflet/src/control/Control.Toolbar.js
index 84b76d957..13e8e369c 100644
--- a/loleaflet/src/control/Control.Toolbar.js
+++ b/loleaflet/src/control/Control.Toolbar.js
@@ -1010,6 +1010,25 @@ function initNormalToolbar() {
 			hideTooltip(this, e.target);
 		},
 		onRefresh: function(event) {
+			if (event.target === 'editbar' && map.getDocType() === 'presentation') {
+				// Fill the style select box if not yet filled
+				if ($('.styles-select')[0] && $('.styles-select')[0].length === 1) {
+					var data = [''];
+					// Inserts a separator element
+					data = data.concat({text: '\u2500\u2500\u2500\u2500\u2500\u2500', disabled: true});
+
+					L.Styles.impressLayout.forEach(function(layout) {
+						data = data.concat({id: layout.id, text: _(layout.text)});
+					}, this);
+
+					$('.styles-select').select2({
+						data: data,
+						placeholder: _UNO('.uno:LayoutStatus', 'presentation')
+					});
+					$('.styles-select').on('select2:select', onStyleSelect);
+				}
+			}
+
 			if ((event.target === 'styles' || event.target === 'fonts' || event.target === 'fontsizes') && event.item) {
 				var toolItem = $(this.box).find('#tb_'+ this.name +'_item_'+ w2utils.escapeId(event.item.id));
 				if ((_inDesktopMode() && event.item.desktop == false)
@@ -1575,7 +1594,6 @@ function onDocLayerInit() {
 	var toolbarUp = w2ui['editbar'];
 	var statusbar = w2ui['actionbar'];
 	var docType = map.getDocType();
-	var data;
 
 	switch (docType) {
 	case 'spreadsheet':
@@ -1679,23 +1697,6 @@ function onDocLayerInit() {
 
 		break;
 	case 'presentation':
-		if ($('.styles-select')[0].length === 1) {
-			// Fill the style select box if not yet filled
-			data = [''];
-			// Inserts a separator element
-			data = data.concat({text: '\u2500\u2500\u2500\u2500\u2500\u2500', disabled: true});
-
-			L.Styles.impressLayout.forEach(function(layout) {
-				data = data.concat({id: layout.id, text: _(layout.text)});
-			}, this);
-
-			$('.styles-select').select2({
-				data: data,
-				placeholder: _UNO('.uno:LayoutStatus', 'presentation')
-			});
-			$('.styles-select').on('select2:select', onStyleSelect);
-		}
-
 		if (toolbarUp) {
 			toolbarUp.show('breaksidebar', 'modifypage');
 		}
@@ -1793,7 +1794,7 @@ function onDocLayerInit() {
 			el.resize();
 	}
 
-	data = [6, 7, 8, 9, 10, 10.5, 11, 12, 13, 14, 15, 16, 18, 20,
+	var data = [6, 7, 8, 9, 10, 10.5, 11, 12, 13, 14, 15, 16, 18, 20,
 		22, 24, 26, 28, 32, 36, 40, 44, 48, 54, 60, 66, 72, 80, 88, 96];
 	$('.fontsizes-select').select2({
 		data: data,
commit ec65b86390d0ebc1c9f470220862dad0cff5bb77
Author:     Pedro Pinto Silva <pedro.silva at collabora.com>
AuthorDate: Fri Mar 20 12:21:42 2020 +0100
Commit:     Tamás Zolnai <tamas.zolnai at collabora.com>
CommitDate: Fri Mar 20 12:57:56 2020 +0100

    Mobile: Context menus: add missing icons (hyperlink and table)
    
    Change-Id: I85fb6e54d16b7982874fa4ab885c7f2d379b96fd
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90781
    Tested-by: Tamás Zolnai <tamas.zolnai at collabora.com>
    Reviewed-by: Tamás Zolnai <tamas.zolnai at collabora.com>

diff --git a/loleaflet/images/lc_copyhyperlinklocation.svg b/loleaflet/images/lc_copyhyperlinklocation.svg
new file mode 100644
index 000000000..498f956f9
--- /dev/null
+++ b/loleaflet/images/lc_copyhyperlinklocation.svg
@@ -0,0 +1,17 @@
+<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <g transform="translate(2.5,2)">
+  <path d="m2 0.023438c-0.554 0-1 0.446-1 1v11c0 0.554 0.446 1 1 1h8c0.554 0 1-0.446 1-1v-11c0-0.554-0.446-1-1-1zm0 1h8v11h-8z" fill="#808080"/>
+  <path d="m2 1.0234h8v11h-8z" fill="#fff"/>
+  <rect x="3" y="6.0234" width="6" height="1" ry=".5" fill="#4d82b8"/>
+  <rect x="3" y="9.0234" width="6" height="1" ry=".5" fill="#4d82b8"/>
+  <path d="m6 3c-0.554 0-1 0.446-1 1v9.6337c0 0.554 0.446 1 1 1h8c0.554 0 1-0.446 1-1v-9.6337c0-0.554-0.446-1-1-1zm0 1h8v10.634h-8z" fill="#808080"/>
+  <path d="m6 4h8v10.173h-8z" fill="#fff"/>
+  <rect x="7" y="8" width="6" height="1" ry=".5" fill="#4d82b8"/>
+  <rect x="7" y="11" width="6" height="1" ry=".5" fill="#4d82b8"/>
+ </g>
+ <g fill="#4d82b8">
+  <path d="m17.062 15.008c-0.85377 0.1294-1.5678 0.69391-1.9054 1.4672-0.070974 0.16255-0.11815 0.34506-0.15617 0.5307h8c-0.02609-0.12499-0.05348-0.26222-0.09367-0.37462-0.3463-0.96656-1.2687-1.6233-2.3426-1.6233h-3.1273c-0.1269 0-0.25286-0.01848-0.37482 0zm-2.0615 5.9938c0.2352 1.1308 1.2393 1.9979 2.4363 1.9979h3.1273c1.197 0 2.2012-0.86716 2.4363-1.9979z"/>
+  <path d="m7.0615 15.008c-0.85377 0.1294-1.5678 0.69391-1.9054 1.4672-0.070974 0.16255-0.11815 0.34506-0.15617 0.5307h8c-0.02609-0.12499-0.05348-0.26222-0.09367-0.37462-0.3463-0.96656-1.2687-1.6233-2.3426-1.6233h-3.1273c-0.1269 0-0.25286-0.01848-0.37482 0zm-2.0615 5.9938c0.2352 1.1308 1.2393 1.9979 2.4363 1.9979h3.1273c1.197 0 2.2012-0.86716 2.4363-1.9979z"/>
+  <rect x="10" y="18" width="8" height="2" ry=".98861"/>
+ </g>
+</svg>
diff --git a/loleaflet/images/lc_openhyperlinkoncursor.svg b/loleaflet/images/lc_openhyperlinkoncursor.svg
new file mode 100644
index 000000000..5ac4f48ff
--- /dev/null
+++ b/loleaflet/images/lc_openhyperlinkoncursor.svg
@@ -0,0 +1,15 @@
+<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <path d="m10.5 3a7.5 7.5 0 0 0-7.5 7.5 7.5 7.5 0 0 0 0.87305 3.5h13.256a7.5 7.5 0 0 0 0.87109-3.5 7.5 7.5 0 0 0-7.5-7.5z" fill="#fff"/>
+ <g fill="#808080">
+  <path d="m10.5 2a8.5 8.5 0 0 0-8.5 8.5 8.5 8.5 0 0 0 0.76367 3.5h1.1094a7.5 7.5 0 0 1-0.87305-3.5 7.5 7.5 0 0 1 7.5-7.5 7.5 7.5 0 0 1 7.5 7.5 7.5 7.5 0 0 1-0.87109 3.5h1.1055a8.5 8.5 0 0 0 0.76562-3.5 8.5 8.5 0 0 0-8.5-8.5z"/>
+  <path d="m2.6376 10h15.846v1h-15.846z"/>
+  <path d="m9.541 2.2129c-2.4585 3.9164-3.079 7.8785-2.0781 11.787h1.0156c-1.0472-3.73-0.48226-7.4446 1.9102-11.256z" fill-rule="evenodd"/>
+  <path d="m11.459 2.2129-0.84766 0.53125c2.3924 3.8112 2.9574 7.5259 1.9102 11.256h1.0156c1.0008-3.9086 0.38034-7.8707-2.0781-11.787z" fill-rule="evenodd"/>
+ </g>
+ <path d="m4 5.5018c4.2087 1.1064 8.4923 1.5417 13 0" fill="none" stroke="#808080"/>
+ <g fill="#4d82b8">
+  <path d="m17.062 15.008c-0.85377 0.1294-1.5678 0.69391-1.9054 1.4672-0.070974 0.16255-0.11815 0.34506-0.15617 0.5307h8c-0.02609-0.12499-0.05348-0.26222-0.09367-0.37462-0.3463-0.96656-1.2687-1.6233-2.3426-1.6233h-3.1273c-0.1269 0-0.25286-0.01848-0.37482 0zm-2.0615 5.9938c0.2352 1.1308 1.2393 1.9979 2.4363 1.9979h3.1273c1.197 0 2.2012-0.86716 2.4363-1.9979z"/>
+  <path d="m7.0615 15.008c-0.85377 0.1294-1.5678 0.69391-1.9054 1.4672-0.070974 0.16255-0.11815 0.34506-0.15617 0.5307h8c-0.02609-0.12499-0.05348-0.26222-0.09367-0.37462-0.3463-0.96656-1.2687-1.6233-2.3426-1.6233h-3.1273c-0.1269 0-0.25286-0.01848-0.37482 0zm-2.0615 5.9938c0.2352 1.1308 1.2393 1.9979 2.4363 1.9979h3.1273c1.197 0 2.2012-0.86716 2.4363-1.9979z"/>
+  <rect x="10" y="18" width="8" height="2" ry=".98861"/>
+ </g>
+</svg>
diff --git a/loleaflet/images/lc_removehyperlink.svg b/loleaflet/images/lc_removehyperlink.svg
new file mode 100644
index 000000000..c15f04872
--- /dev/null
+++ b/loleaflet/images/lc_removehyperlink.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m10.5 3a7.5 7.5 0 0 0 -7.5 7.5 7.5 7.5 0 0 0 7.5 7.5 7.5 7.5 0 0 0 3.294922-.767578l-1.511719-1.552734a1.0001 1.0001 0 0 1 .029297-1.423829l2.089844-1.980468a1.0001 1.0001 0 0 1 .675781-.275391 1.0001 1.0001 0 0 1 .720703.294922l1.449219 1.457031a7.5 7.5 0 0 0 .751953-3.251953 7.5 7.5 0 0 0 -7.5-7.5zm4.568359 11.398438-.644531.611328 1.056641 1.085937a7.5 7.5 0 0 0 .640625-.640625z" fill="#fff"/><path d="m10.5 2a8.5 8.5 0 0 0 -7.734375 5h-.0019531a8.5 8.5 0 0 0 -.0839844.1914062 8.5 8.5 0 0 0 -.0019531.0039063 8.5 8.5 0 0 0 -.2617188.7128906 8.5 8.5 0 0 0 -.0390625.1191407 8.5 8.5 0 0 0 -.1699219.6582031 8.5 8.5 0 0 0 -.0410156.1816406 8.5 8.5 0 0 0 -.0917968.5976563 8.5 8.5 0 0 0 -.0332032.2792968 8.5 8.5 0 0 0 -.0410156.7558594 8.5 8.5 0 0 0 .0410156.755859 8.5 8.5 0 0 0 .0332032.279297 8.5 8.5 0 0 0 .0917968.597656 8.5 8.5 0 0 0 .0410156.181641 8.5 8.5 0 0 0 .1699219.658203 8.5 8.5 0 0 0 .0390625.119141 8.5 8.5 
 0 0 0 .2617188.712891 8.5 8.5 0 0 0 .0019531.003906 8.5 8.5 0 0 0 .0839844.191406h.0019531a8.5 8.5 0 0 0 7.734375 5 8.5 8.5 0 0 0 2.791016-.484375l.533203-.539063-.515625-.529296a7.5 7.5 0 0 1 -1.316406.402343c.213153-.385908.400167-.77163.578124-1.158203l-.644531-.662109c-.038322-.039314-.066475-.084733-.099609-.126953-.30079.692115-.639641 1.386034-1.0625 2.083984a7.5 7.5 0 0 1 -.263672.013672 7.5 7.5 0 0 1 -.261719-.009766c-.5871482-.968625-1.0554433-1.930371-1.410156-2.888672.5564292-.054563 1.1137625-.08629 1.673828-.095703.33318-.005599.668981.012757 1.003906.023438-.010844-.344928.097725-.688607.322266-.972656-1.119279-.052177-2.2301977-.036936-3.3261719.080078-.0132955-.045676-.0184772-.091055-.03125-.136719h.0078125c-.2808166-1.000216-.4356577-1.99983-.4804687-3h5.0039061c-.032909.734534-.148724 1.468732-.306641 2.203125l1.181641-1.121094c.042532-.36051.088863-.72088.103516-1.082031h3.99414a7.5 7.5 0 0 1 -.076171.662109 7.5 7.5 0 0 1 -.033204.191407 7.5 7.5 0 0 1 -.111328.5
 01953 7.5 7.5 0 0 1 -.076172.277343 7.5 7.5 0 0 1 -.128906.396485 7.5 7.5 0 0 1 -.08789.226562l.542968.546875.523438-.535156a8.5 8.5 0 0 0 .019531-.05664 8.5 8.5 0 0 0 .208984-.732422 8.5 8.5 0 0 0 .03125-.123047 8.5 8.5 0 0 0 .107422-.587891 8.5 8.5 0 0 0 .042969-.292969 8.5 8.5 0 0 0 .039062-.501953 8.5 8.5 0 0 0 .023438-.472656 8.5 8.5 0 0 0 -.023438-.472656 8.5 8.5 0 0 0 -.039062-.5019534 8.5 8.5 0 0 0 -.042969-.2929687 8.5 8.5 0 0 0 -.107422-.5878907 8.5 8.5 0 0 0 -.03125-.1230468 8.5 8.5 0 0 0 -.208984-.7324219 8.5 8.5 0 0 0 -8.046875-5.7890625zm0 1a7.5 7.5 0 0 1 .263672.0136719c.597199.9857061 1.069222 1.9644003 1.425781 2.9394531-1.129272.075053-2.2484697.0544359-3.361328-.0546875.3547127-.958301.8230078-1.9200466 1.410156-2.8886719a7.5 7.5 0 0 1 .261719-.0097656zm1.492188.1503906a7.5 7.5 0 0 1 3.910156 2.1503906c-.909879.2573893-1.811468.4436861-2.705078.5527344-.308001-.9021846-.708295-1.8037121-1.205078-2.703125zm-2.9882818.0078125c-.479075.8683005-.8688159 1.7384591-1.17
 1875 2.609375-.8995078-.1257469-1.7961256-.2959431-2.6914062-.5058593a7.5 7.5 0 0 1 3.8632812-2.1035157zm-4.5820312 2.9609375c1.0222489.2536433 2.0533551.4635562 3.09375.6152344-.1596969.5820098-.2710145 1.1639579-.3515625 1.7460938-.0753467.5059078-.1239541 1.0123644-.1445313 1.5195312h-3.9941406a7.5 7.5 0 0 1 .0722656-.609375 7.5 7.5 0 0 1 .0214844-.1660156 7.5 7.5 0 0 1 .1132813-.5097656 7.5 7.5 0 0 1 .0566406-.2382813 7.5 7.5 0 0 1 .1621094-.4960937 7.5 7.5 0 0 1 .0722656-.2050782 7.5 7.5 0 0 1 .2070313-.4707031 7.5 7.5 0 0 1 .1035156-.2207031 7.5 7.5 0 0 1 .5878906-.9648438zm12.169922.0214844a7.5 7.5 0 0 1 .6875 1.1777344 7.5 7.5 0 0 1 .126953.2871094 7.5 7.5 0 0 1 .142578.3652343 7.5 7.5 0 0 1 .128906.3964844 7.5 7.5 0 0 1 .076172.2773437 7.5 7.5 0 0 1 .111328.5019532 7.5 7.5 0 0 1 .033204.1914062 7.5 7.5 0 0 1 .076171.6621094h-3.99414c-.020603-.5077947-.069024-1.0149517-.144531-1.5214844-.077549-.5515257-.18445-1.1028313-.332032-1.6542968 1.01822-.1339905 2.048215-.3640047 3.
 087891-.6835938zm-8.0898439.7226562c1.321553.141098 2.6602839.166512 4.0156249.0585938.007474.0261041.010279.052025.017578.078125h-.013672c.280816 1.000216.435659 1.99983.480469 3h-5.0039061c.044811-1.00017.1996521-1.999784.4804687-3h-.0078125c.0127728-.0456638.0179545-.0910426.03125-.1367188zm-5.4765625 4.1367188h3.9941406c.0205772.507167.0691846 1.013623.1445313 1.519531.080548.582136.1918656 1.164084.3515625 1.746094-1.0403949.151678-2.0715011.361591-3.09375.615234a7.5 7.5 0 0 1 -.5878906-.964843 7.5 7.5 0 0 1 -.1035156-.220704 7.5 7.5 0 0 1 -.2070313-.470703 7.5 7.5 0 0 1 -.0722656-.205078 7.5 7.5 0 0 1 -.1621094-.496093 7.5 7.5 0 0 1 -.0566406-.238282 7.5 7.5 0 0 1 -.1132813-.509765 7.5 7.5 0 0 1 -.0214844-.166016 7.5 7.5 0 0 1 -.0722656-.609375zm4.8066406 4.232422c.3030591.870916.6928 1.741062 1.171875 2.609375a7.5 7.5 0 0 1 -3.8632812-2.103516c.8952806-.209916 1.7918984-.380112 2.6914062-.505859z" fill="#808080"/><path d="m23 20.944338-2.895447-2.946153 2.7861-3.086414-2.0083
 92-1.911762-2.867282 2.941173-2.925406-2.941173-2.089573 1.98208 2.925403 3.003525-2.925403 2.955562 2.089573 2.058824 2.922011-3.088233 2.928799 3.088233z" fill="#e68497"/></svg>
\ No newline at end of file
diff --git a/loleaflet/images/lc_tabledeletemenu.svg b/loleaflet/images/lc_tabledeletemenu.svg
new file mode 100644
index 000000000..3d0a60e08
--- /dev/null
+++ b/loleaflet/images/lc_tabledeletemenu.svg
@@ -0,0 +1,9 @@
+<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <path d="m3 3h18v18h-18z" fill="#fff"/>
+ <path d="m3 2c-0.554 0-1 0.446-1 1v18c0 0.554 0.446 1 1 1h8.8379l0.24609-1h-3.084v-3h4.3926l1.002-1h-5.3945v-3h6v2.3945l1-0.99805v-1.3965h1.3984l0.90625-0.90625 0.007812 0.007812 0.10156-0.10156h-2.4141v-3h5v1.2715c0.36248 0.036636 0.72126 0.18842 0.99805 0.46484l0.001953 0.001953v-8.7383c0-0.554-0.446-1-1-1h-18zm0 1h5v6h-5v-6zm6 0h6v6h-6v-6zm7 0h5v6h-5v-6zm-13 7h5v3h-5v-3zm6 0h6v3h-6v-3zm-6 4h5v3h-5v-3zm19 3.1465-0.61719 0.5918-0.003906-0.003906-0.37891 0.38086v2.8848h-2.8672l-0.99414 1h3.8613c0.554 0 1-0.446 1-1v-3.8535zm-19 0.85352h5v3h-5v-3z" fill="#808080"/>
+ <path d="m2 2h20v4h-20z" fill="#e68497"/>
+ <g transform="matrix(.86194 0 0 .86196 3.2376 2.775)" fill="#e68497" stroke-width="1.3536">
+  <path d="m17.48 13.611-5.5293 5.5234-0.95117 3.8652 3.8066-1.0117 5.5098-5.5449z"/>
+  <path d="m20.42 11c-0.18956 0-0.37818 0.07172-0.52344 0.2168l-1.6738 1.6699 2.8477 2.8496 1.7109-1.6387c0.29051-0.29013 0.29051-0.75675 0-1.0469l-1.8359-1.834c-0.14525-0.14506-0.33583-0.2168-0.52539-0.2168z"/>
+ </g>
+</svg>
diff --git a/loleaflet/images/lc_tableinsertmenu.svg b/loleaflet/images/lc_tableinsertmenu.svg
new file mode 100644
index 000000000..816ea70bd
--- /dev/null
+++ b/loleaflet/images/lc_tableinsertmenu.svg
@@ -0,0 +1,9 @@
+<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <path d="m3 3h18v18h-18z" fill="#fff"/>
+ <path d="m3 2c-0.554 0-1 0.446-1 1v18c0 0.554 0.446 1 1 1h8.8379l0.24609-1h-3.084v-3h4.3926l1.002-1h-5.3945v-3h6v2.3945l1-0.99805v-1.3965h1.3984l0.19922-0.19922 0.70703-0.70703 0.007812 0.007812 0.10156-0.10156h-2.4141v-3h5v1.2715c0.36248 0.036636 0.72126 0.18842 0.99805 0.46484l0.001953 0.001953v-8.7383c0-0.554-0.446-1-1-1h-18zm0 1h5v6h-5v-6zm6 0h6v6h-6v-6zm7 0h5v6h-5v-6zm-13 7h5v3h-5v-3zm6 0h6v3h-6v-3zm-6 4h5v3h-5v-3zm19 3.1465-0.61719 0.5918-0.003906-0.003906-0.37891 0.38086v2.8848h-2.8672l-0.99414 1h3.8613c0.554 0 1-0.446 1-1v-3.8535zm-19 0.85352h5v3h-5v-3z" fill="#808080"/>
+ <path d="m2 2h20v4h-20z" fill="#4d82b8"/>
+ <g transform="matrix(.86194 0 0 .86196 3.2376 2.775)" fill="#eac282" stroke-width="1.3536">
+  <path d="m17.48 13.611-5.5293 5.5234-0.95117 3.8652 3.8066-1.0117 5.5098-5.5449z"/>
+  <path d="m20.42 11c-0.18956 0-0.37818 0.07172-0.52344 0.2168l-1.6738 1.6699 2.8477 2.8496 1.7109-1.6387c0.29051-0.29013 0.29051-0.75675 0-1.0469l-1.8359-1.834c-0.14525-0.14506-0.33583-0.2168-0.52539-0.2168z"/>
+ </g>
+</svg>
commit 49e97638b2d3b0548440947508862720e22105f2
Author:     Tamás Zolnai <tamas.zolnai at collabora.com>
AuthorDate: Fri Mar 20 11:51:26 2020 +0100
Commit:     Tamás Zolnai <tamas.zolnai at collabora.com>
CommitDate: Fri Mar 20 12:45:23 2020 +0100

    cypress: update README.
    
    Change-Id: Iacf4bd05b01f0ceb4f6d976f979db62afaba091b
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90784
    Tested-by: Tamás Zolnai <tamas.zolnai at collabora.com>
    Reviewed-by: Tamás Zolnai <tamas.zolnai at collabora.com>

diff --git a/cypress_test/README b/cypress_test/README
index d57fc0fc8..51d701d11 100644
--- a/cypress_test/README
+++ b/cypress_test/README
@@ -25,6 +25,12 @@ cypress_test folder to run cypress tests only.
 
     make check
 
+IMPORTANT: Before stepping under cypress_test folder
+and running any command listed here, make sure you've
+done a top-level make, so everything is up-to-date.
+Running commands from under cypress_test folder won't
+trigger a rebuild.
+
 To run cypress test cases selectively, you need to
 go in to the cypress_test folder first and run one of
 the following commands.
@@ -39,13 +45,13 @@ To run all mobile tests:
 
 To run one specific test suit of desktop tests,
 use spec argument with a relative path to
-cypress_test/data/desktop/:
+cypress_test/integration_tests/desktop/:
 
     make check-desktop spec=example_desktop_test_spec.js
 
 To run one specific test suit of mobile tests,
 use spec argument with a relative path to
-cypress_test/data/mobile/:
+cypress_test/integration_tests/mobile/:
 
     make check-mobile spec=writer/toolbar_spec.js
 


More information about the Libreoffice-commits mailing list