[Libreoffice-commits] online.git: Branch 'feature/proxyhack' - 15 commits - android/app android/lib cypress_test/README loleaflet/css loleaflet/html loleaflet/images loleaflet/js loleaflet/po loleaflet/src Makefile.am net/Socket.hpp test/helpers.hpp test/UnitLoadTorture.cpp 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
Fri Mar 20 15:42:32 UTC 2020


Rebased ref, commits from common ancestor:
commit 3dfc01bc54d4bae63c2ec8250e606b5e20b4d4b0
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: Fri Mar 20 15:42:11 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 683197879..69bf3357e 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..e6bfd9386 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,185 @@ 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);
+
+    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 c6ecd21c8b0c043fd1b840cfa4a7ee1938206cb9
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: Fri Mar 20 11:22:57 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 177360dbc..683197879 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 8c14d17d5fa6a49b5f71378cc8576ce61afec406
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: Fri Mar 20 11:22:57 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 2bba146b28599481dd81fb0d1f57c0a856827107
Author:     Weblate <noreply at documentfoundation.org>
AuthorDate: Fri Mar 20 08:25:31 2020 +0100
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Fri Mar 20 12:19:19 2020 +0100

    update translations
    
    LibreOffice Online/android-app (Bulgarian)
    Currently translated at 100.0% (100 of 100 strings)
    
    Change-Id: Ia0adbe259646f2fda33a25b30f8811dc147cc5a3
    
    update translations
    
    LibreOffice Online/android-lib (Bulgarian)
    Currently translated at 75.0% (9 of 12 strings)
    
    Change-Id: I9206580869c83825c95c9079cc8fe052d860e2a9
    
    update translations
    
    LibreOffice Online/loleaflet-help (Catalan)
    Currently translated at 74.3% (309 of 416 strings)
    
    Change-Id: If5c7bb3dab8e0d84010d7c4c3c149d5eed99183e
    
    update translations
    
    LibreOffice Online/android-lib (Asturian)
    Currently translated at 58.3% (7 of 12 strings)
    
    Change-Id: Ic5b84d1302c52727c27c7acc3d8f23ae39c286d5
    
    update translations
    
    LibreOffice Online/android-lib (Catalan)
    Currently translated at 100.0% (12 of 12 strings)
    
    Change-Id: Id176e6b42a7571fbfbacbbf00f6f9293f255f815
    
    update translations
    
    LibreOffice Online/android-lib (Spanish)
    Currently translated at 100.0% (12 of 12 strings)
    
    Change-Id: Ia75341ad1c6408648600e9f01702ac0408865d50
    
    update translations
    
    LibreOffice Online/loleaflet-help (Catalan)
    Currently translated at 74.3% (309 of 416 strings)
    
    Change-Id: I09fda62f77693d651390db378eb9f2fa62f3c6e0
    
    update translations
    
    LibreOffice Online/loleaflet-help (Spanish)
    Currently translated at 100.0% (416 of 416 strings)
    
    Change-Id: I9b2cb58ac67e3136408b5493d4ab3d4c6e64e9a2
    
    update translations
    
    LibreOffice Online/android-lib (Welsh)
    Currently translated at 100.0% (12 of 12 strings)
    
    Change-Id: I0c4e7d1bfadcd652dc4450abfe65134b055e0060
    
    update translations
    
    LibreOffice Online/android-lib (Welsh)
    Currently translated at 83.3% (10 of 12 strings)
    
    Change-Id: Icda607bf7238045a1ade6f237fb18c61fb0bcff5
    
    update translations
    
    LibreOffice Online/android-lib (Basque)
    Currently translated at 100.0% (12 of 12 strings)
    
    Change-Id: Iebbc610027b26b1202d6e1c78978ed35213e8bcd
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90736
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Andras Timar <andras.timar at collabora.com>

diff --git a/android/app/src/main/res/values-bg/strings.xml b/android/app/src/main/res/values-bg/strings.xml
index 3540dec88..30a5fd173 100644
--- a/android/app/src/main/res/values-bg/strings.xml
+++ b/android/app/src/main/res/values-bg/strings.xml
@@ -99,4 +99,5 @@
     <string name="app_version">Версия: %1$s, Build ID: %2$s</string>
     <string name="pref_enable_chrome_debugger_info">Включва използването на дебъгера на Chrome в документа.</string>
     <string name="pref_enable_chrome_debugger">Дебъгер на Chrome</string>
+    <string name="pref_category_editor">Настройки на редактора</string>
 </resources>
\ No newline at end of file
diff --git a/android/lib/src/main/res/values-ast/strings.xml b/android/lib/src/main/res/values-ast/strings.xml
index ab73e35e0..9f2f7300f 100644
--- a/android/lib/src/main/res/values-ast/strings.xml
+++ b/android/lib/src/main/res/values-ast/strings.xml
@@ -5,4 +5,6 @@
     <string name="preparing_for_the_first_start_after_an_update">Preparando pal primer aniciu dempués d\'un anovamientu.</string>
     <string name="storage_permission_required">Ríquese permisu d\'almacenamientu</string>
     <string name="later">Sero</string>
+    <string name="failed_to_load_file">Nun pudo determinase qué ficheru cargar</string>
+    <string name="exiting">Colando…</string>
 </resources>
\ No newline at end of file
diff --git a/android/lib/src/main/res/values-bg/strings.xml b/android/lib/src/main/res/values-bg/strings.xml
index bcdef509d..affadd8f2 100644
--- a/android/lib/src/main/res/values-bg/strings.xml
+++ b/android/lib/src/main/res/values-bg/strings.xml
@@ -8,4 +8,6 @@
     <string name="temp_file_saving_disabled">Този файл е само за четене, записването е изключено.</string>
     <string name="saving">Записване...</string>
     <string name="preparing_for_the_first_start_after_an_update">Подготвя се първо стартиране след актуализация.</string>
+    <string name="later">По-късно</string>
+    <string name="rate_now">Оценяване сега</string>
 </resources>
\ No newline at end of file
diff --git a/android/lib/src/main/res/values-ca/strings.xml b/android/lib/src/main/res/values-ca/strings.xml
index e7ee2aaae..74682a355 100644
--- a/android/lib/src/main/res/values-ca/strings.xml
+++ b/android/lib/src/main/res/values-ca/strings.xml
@@ -12,4 +12,5 @@
     <string name="later">Més tard</string>
     <string name="rate_our_app_text">Si en gaudiu, poseu-li 5 estrelles al Google Play. Les vostres ressenyes positives són la nostra motivació més gran.</string>
     <string name="rate_our_app_title">Gràcies per utilitzar el %1$s!</string>
+    <string name="exiting">S\'està sortint…</string>
 </resources>
\ No newline at end of file
diff --git a/android/lib/src/main/res/values-cy/strings.xml b/android/lib/src/main/res/values-cy/strings.xml
index be80d2eff..8b9294bab 100644
--- a/android/lib/src/main/res/values-cy/strings.xml
+++ b/android/lib/src/main/res/values-cy/strings.xml
@@ -8,4 +8,9 @@
     <string name="temp_file_saving_disabled">Darllen yn unig yw\'r ffeil hon, mae cadw wedi ei analluogi.</string>
     <string name="saving">Yn cadw...</string>
     <string name="preparing_for_the_first_start_after_an_update">Paratoi am y cychwyn cyntaf ar ôl diweddariad.</string>
+    <string name="rate_our_app_title">Diolch am ddefnyddio %1$s!</string>
+    <string name="later">Hwyrach</string>
+    <string name="rate_now">Graddio nawr</string>
+    <string name="exiting">Gorffen...</string>
+    <string name="rate_our_app_text">Os ydych yn ei hoffi, rhowch 5 seren i ni ar Google Play. Mae eich adolygiadau da yn ein hysbrydoli.</string>
 </resources>
\ No newline at end of file
diff --git a/android/lib/src/main/res/values-es/strings.xml b/android/lib/src/main/res/values-es/strings.xml
index ea77f9dc2..834015b2c 100644
--- a/android/lib/src/main/res/values-es/strings.xml
+++ b/android/lib/src/main/res/values-es/strings.xml
@@ -13,4 +13,5 @@
     <string name="rate_our_app_title">¡Gracias por utilizar %1$s!</string>
     <string name="later">Luego</string>
     <string name="rate_now">Calificar ahora</string>
+    <string name="exiting">Saliendo…</string>
 </resources>
\ No newline at end of file
diff --git a/android/lib/src/main/res/values-eu/strings.xml b/android/lib/src/main/res/values-eu/strings.xml
index c15a6fcb3..4a9ed79a8 100644
--- a/android/lib/src/main/res/values-eu/strings.xml
+++ b/android/lib/src/main/res/values-eu/strings.xml
@@ -12,4 +12,5 @@
     <string name="rate_our_app_title">Eskerrik asko %1$s erabiltzeagatik!</string>
     <string name="later">Geroago</string>
     <string name="rate_now">Eman puntuazioa orain</string>
+    <string name="exiting">Irteten...</string>
 </resources>
\ No newline at end of file
diff --git a/loleaflet/po/help-ca.po b/loleaflet/po/help-ca.po
index d05ad1577..5594c04fb 100644
--- a/loleaflet/po/help-ca.po
+++ b/loleaflet/po/help-ca.po
@@ -3,7 +3,7 @@ msgstr ""
 "Project-Id-Version: PACKAGE VERSION\n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: 2020-03-15 22:15+0200\n"
-"PO-Revision-Date: 2020-03-18 14:17+0000\n"
+"PO-Revision-Date: 2020-03-19 14:34+0000\n"
 "Last-Translator: Adolfo Jayme Barrientos <fito at libreoffice.org>\n"
 "Language-Team: Catalan <https://weblate.documentfoundation.org/projects/libo_online/loleaflet-help/ca/>\n"
 "Language: ca\n"
@@ -1211,7 +1211,7 @@ msgstr "Si no se selecciona cap taula o interval, es mostra el prototip d'un dia
 
 #: html/loleaflet-help.html%2Bdiv.p:319-5
 msgid "<span class=\"def\">Chart editing:</span> Double click the chart to select. Use context menus to add chart elements such as title, axis, and other. Choose <span class=\"ui\">Data Range</span> and <span class=\"ui\">Chart Type</span> to edit chart data and select chart type."
-msgstr ""
+msgstr "<span class=\"def\">Edició dels diagrames:</span> feu doble clic al diagrama per a seleccionar-lo. Feu servir els menús contextuals per a afegir-hi elements com ara el títol i els eixos. Trieu <span class=\"ui\">Interval de dades</span> i <span class=\"ui\">Tipus de diagrama</span> per a modificar les dades del diagrama i seleccionar-ne el tipus, respectivament."
 
 #: html/loleaflet-help.html%2Bdiv.p:320-5
 msgid "<span class=\"def\">Chart formatting:</span> The same context menu brings you to chart data table and chart type selection."
@@ -1415,7 +1415,7 @@ msgstr ""
 
 #: html/loleaflet-help.html%2Bdiv.div.p:415-5
 msgid "<span class=\"def\">Footnotes and endnotes:</span> Footnotes and endnotes are supported."
-msgstr ""
+msgstr "<span class=\"def\">Notes al peu i finals:</span> s'admet l'ús de notes al peu i finals."
 
 #: html/loleaflet-help.html%2Bdiv.div.p:422-5
 msgid "<span class=\"productname\">%productName</span> Impress edits presentations in a way that should be familiar to people. Operations like typing text, cut, copy and pasting contents, selecting text , inserting, resizing, anchoring images, adding and handling tables, are similar to a desktop presentation application. Use the keyboard, menus and toolbars to perform actions in your document."
diff --git a/loleaflet/po/help-es.po b/loleaflet/po/help-es.po
index e1f5fac6a..4f890b5ee 100644
--- a/loleaflet/po/help-es.po
+++ b/loleaflet/po/help-es.po
@@ -3,7 +3,7 @@ msgstr ""
 "Project-Id-Version: \n"
 "Report-Msgid-Bugs-To: \n"
 "POT-Creation-Date: 2020-03-15 22:15+0200\n"
-"PO-Revision-Date: 2020-03-17 18:53+0000\n"
+"PO-Revision-Date: 2020-03-19 13:49+0000\n"
 "Last-Translator: Adolfo Jayme Barrientos <fito at libreoffice.org>\n"
 "Language-Team: Spanish <https://weblate.documentfoundation.org/projects/libo_online/loleaflet-help/es/>\n"
 "Language: es\n"
@@ -1170,8 +1170,6 @@ msgid "Pasting"
 msgstr "Pegar"
 
 #: html/loleaflet-help.html%2Bdiv.p:301-5
-#, fuzzy
-#| msgid "When you paste content copied from within the same document, the format and elements are maintained. If you copy from another document, in another tab or browser window, of from outside of the browser, the pasted content will preserve rich text."
 msgid "When you paste content copied from within the same document, the format and elements are maintained. If you copy from another document, in another tab or browser window, or from outside of the browser, the pasted content will preserve rich text."
 msgstr "Al pegar el contenido copiado del mismo documento, se conserva el formato y el contenido. Si copia de otro documento, en otra pestaña del navegador o ventana, o incluso de otra aplicación que no sea el navegador, el contenido pegado preserva el formato y el contenido."
 
@@ -1188,10 +1186,8 @@ msgid "When you paste text from outside of the document (another browser window
 msgstr "Al pegar texto desde fuera del documento (otra ventana del navegador o aplicación de escritorio), el texto se pegará con texto con formato (texto enriquecido)."
 
 #: html/loleaflet-help.html%2Bdiv.p:305-5
-#, fuzzy
-#| msgid "When you have internal cut or copied content, you can paste this content with the right mouse button menu."
 msgid "When you have internal cut or copied content, you can paste this content using the context menu."
-msgstr "Y cuando haya cortado o copiado contenido interno, puede pegar este contenido con el menú del botón derecho del ratón."
+msgstr "Y cuando haya cortado o copiado contenido interno, puede pegar este contenido con el menú contextual."
 
 #: html/loleaflet-help.html%2Bdiv.h4:308-5
 msgid "Adding charts"
@@ -1366,8 +1362,6 @@ msgid "<span class=\"def\">Direct formatting:</span> You can format text documen
 msgstr "<span class=\"def\">Formato directo:</span> Puede formatear objetos del documento de texto directamente desde los menús, la barra de herramientas y los menús contextuales. El formato directo sólo se aplica al objeto seleccionado."
 
 #: html/loleaflet-help.html%2Bdiv.div.p:397-5
-#, fuzzy
-#| msgid "<span class=\"def\">Style formatting:</span> <span class=\"productname\">%productName</span> support paragraph styles. You can apply an existing paragraph style to a paragraph. Choose menu <span class=\"ui\">Edit</span> → <span class=\"ui\">Edit styles</span> to change style."
 msgid "<span class=\"def\">Style formatting:</span> <span class=\"productname\">%productName</span> supports paragraph styles. You can apply an existing paragraph style to a paragraph. Choose menu <span class=\"ui\">Edit</span> → <span class=\"ui\">Edit styles</span> to change style."
 msgstr "<span class=\"def\">Formato por estilo:</span> <span class=\"productname\">%productName</span> admite estilos de párrafo. Puede aplicar un estilo de párrafo existente a varios párrafos. Seleccione el menú <span class=\"ui\">Editar</span> ▸ <span class=\"ui\">Editar estilo</span> para modificar el estilo."
 
@@ -1420,10 +1414,8 @@ msgid "<span class=\"def\">Page header and footer:</span> Headers and footers ar
 msgstr "<span class=\"def\">Cabecera y pie de página:</span> Cabeceras y pies de página están disponibles para el estilo de página existente aplicado en el documento en la posición del cursor."
 
 #: html/loleaflet-help.html%2Bdiv.div.p:415-5
-#, fuzzy
-#| msgid "<span class=\"def\">Footnotes and endnotes:</span> Footnotes and endnotes are supported, being rendered as they are shown."
 msgid "<span class=\"def\">Footnotes and endnotes:</span> Footnotes and endnotes are supported."
-msgstr "<span class=\"def\">Notas al pie y notas finales:</span> se admiten las notas al pie de página y finales y se visualizan en cuanto se insertan."
+msgstr "<span class=\"def\">Notas al pie y notas finales:</span> se admite la utilización de notas al pie de página y finales."
 
 #: html/loleaflet-help.html%2Bdiv.div.p:422-5
 msgid "<span class=\"productname\">%productName</span> Impress edits presentations in a way that should be familiar to people. Operations like typing text, cut, copy and pasting contents, selecting text , inserting, resizing, anchoring images, adding and handling tables, are similar to a desktop presentation application. Use the keyboard, menus and toolbars to perform actions in your document."
@@ -1542,8 +1534,6 @@ msgid "What is the blue wavy underline in some words in the text?"
 msgstr "¿Y las líneas onduladas azules en el texto?"
 
 #: html/loleaflet-help.html%2Bdiv.p:465-5
-#, fuzzy
-#| msgid "Grammar errors in the text is marked with the blue wavy underline. Click on the underlined text with the right mouse button to open a menu with suggestions to correct the grammar error and the offending grammar rules. Select the right suggestion to change the text."
 msgid "Grammar errors in the text is marked with the blue wavy underline. Click on the underlined text with the right mouse button to open a menu with suggestions to correct the grammar error and the offended grammar rules. Select the right suggestion to change the text."
 msgstr "Los errores gramaticales en el texto se marcan con un subrayado ondulado azul. Pulse con el botón secundario del ratón para abrir un menú con sugerencias para corregir el error gramatical y conocer la regla infringida. Seleccione la sugerencia adecuada para cambiar el texto."
 
@@ -1608,8 +1598,6 @@ msgid "You have the Track Changes feature enabled for the document. Track Change
 msgstr "Usted tiene el seguimiento de cambios activado. El seguimiento de cambios escribe todos los cambios en el texto y muestra las ediciones para una revisión posterior. Es muy útil para marcar cambios en textos y hojas de cálculo. El seguimiento de cambios se activa al elegir <span class=\"ui\">Editar</span> ▸ <span class=\"ui\">Seguimiento de cambios</span> ▸ <span class=\"ui\">Grabar</span>."
 
 #: html/loleaflet-help.html%2Bdiv.div.p:491-5
-#, fuzzy
-#| msgid "With track changes enabled your document show the following settings:"
 msgid "With track changes enabled your document is shown the following manner:"
 msgstr "Con el seguimiento de cambio activado, su documento presenta las siguientes características:"
 
commit 9db6f855a016891f5c0d59cab014f246752cd907
Author:     Henry Castro <hcastro at collabora.com>
AuthorDate: Thu Mar 19 11:40:04 2020 -0400
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Fri Mar 20 12:18:40 2020 +0100

    loleaflet: move the code about initialization of the style combobox
    
    It is prefereable to move it when the document is initializing
    rather than the iteration of toolbar items
    
    Change-Id: I6fd856ed8b7e1b4e3224be09d37d2e86b9dce166
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90759
    Tested-by: Andras Timar <andras.timar at collabora.com>
    Reviewed-by: Andras Timar <andras.timar at collabora.com>

diff --git a/loleaflet/src/control/Control.Toolbar.js b/loleaflet/src/control/Control.Toolbar.js
index 13e8e369c..84b76d957 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':
+		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');
 		}
@@ -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 ea1c818b9a11c9a39989561608e100c822c26658
Author:     Henry Castro <hcastro at collabora.com>
AuthorDate: Thu Mar 19 10:55:08 2020 -0400
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Fri Mar 20 12:16:55 2020 +0100

    loleaflet: fill the "style" combobox when data is received
    
    This approach is preferable rather than iterating the toolbar items
    
    Change-Id: I81107dde1d6c99b3e5af9793c7b2dff517747ff9
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90758
    Tested-by: Andras Timar <andras.timar at collabora.com>
    Reviewed-by: Andras Timar <andras.timar at collabora.com>

diff --git a/loleaflet/src/control/Control.Toolbar.js b/loleaflet/src/control/Control.Toolbar.js
index 18aae5c52..13e8e369c 100644
--- a/loleaflet/src/control/Control.Toolbar.js
+++ b/loleaflet/src/control/Control.Toolbar.js
@@ -1037,8 +1037,6 @@ function initNormalToolbar() {
 				} else {
 					toolItem.css('display', '');
 				}
-
-				updateCommandValues(event.target);
 			}
 
 			if (event.target === 'inserttable')
@@ -2090,41 +2088,15 @@ function onCommandValues(e) {
 	}
 }
 
-function updateToolbarCommandValues(e) {
-	if (e.commandName === '.uno:CharFontName') {
-		// 2) For .uno:CharFontName
-		var commandValues = map.getToolbarCommandValues(e.commandName);
-		if (typeof commandValues === 'undefined') {
-			return;
-		}
-
-		var data = []; // reset data in order to avoid that the font select box is populated with styles, too.
-		// Old browsers like IE11 et al don't like Object.keys with
-		// empty arguments
-		if (typeof commandValues === 'object') {
-			data = data.concat(Object.keys(commandValues));
-		}
-
-		$('.fonts-select').select2({
-			data: data.sort(function (a, b) {  // also sort(localely)
-				return a.localeCompare(b);
-			}),
-			placeholder: _('Font')
-		});
-		$('.fonts-select').on('select2:select', onFontSelect);
-		$('.fonts-select').val(fontsSelectValue).trigger('change');
-		w2ui['editbar'].resize();
-	}
-}
-
-function updateCommandValues(targetName) {
+function updateCommandValues(e) {
 	var data = [];
+	var commandValues;
 	// 1) For .uno:StyleApply
 	// we need an empty option for the place holder to work
-	if (targetName === 'styles' && $('.styles-select option').length === 1) {
+	if (e.commandName === '.uno:StyleApply') {
 		var styles = [];
 		var topStyles = [];
-		var commandValues = map.getToolbarCommandValues('.uno:StyleApply');
+		commandValues = map.getToolbarCommandValues(e.commandName);
 		if (typeof commandValues === 'undefined')
 			return;
 		var commands = commandValues.Commands;
@@ -2188,10 +2160,32 @@ function updateCommandValues(targetName) {
 		$('.styles-select').val(stylesSelectValue).trigger('change');
 		$('.styles-select').on('select2:select', onStyleSelect);
 		w2ui['editbar'].resize();
+	} else if (e.commandName === '.uno:CharFontName') {
+		// 2) For .uno:CharFontName
+		commandValues = map.getToolbarCommandValues(e.commandName);
+		if (typeof commandValues === 'undefined') {
+			return;
+		}
+
+		data = []; // reset data in order to avoid that the font select box is populated with styles, too.
+		// Old browsers like IE11 et al don't like Object.keys with
+		// empty arguments
+		if (typeof commandValues === 'object') {
+			data = data.concat(Object.keys(commandValues));
+		}
+
+		$('.fonts-select').select2({
+			data: data.sort(function (a, b) {  // also sort(localely)
+				return a.localeCompare(b);
+			}),
+			placeholder: _('Font')
+		});
+		$('.fonts-select').on('select2:select', onFontSelect);
+		$('.fonts-select').val(fontsSelectValue).trigger('change');
+		w2ui['editbar'].resize();
 	}
 }
 
-
 function onUpdateParts(e) {
 	if (e.docType === 'text') {
 		var current = e.currentPage;
@@ -2688,8 +2682,7 @@ function setupToolbar(e) {
 
 	if (!window.mode.isMobile()) {
 		map.on('updatetoolbarcommandvalues', function(e) {
-			updateToolbarCommandValues(e);
-			w2ui['editbar'].refresh();
+			updateCommandValues(e);
 		});
 
 		map.on('showbusy', function(e) {
@@ -2738,7 +2731,6 @@ global.onAddressInput = onAddressInput;
 global.onFormulaInput = onFormulaInput;
 global.onFormulaBarBlur = onFormulaBarBlur;
 global.onFormulaBarFocus = onFormulaBarFocus;
-global.updateCommandValues = updateCommandValues;
 global.onStyleSelect = onStyleSelect;
 global.insertTable = insertTable;
 global.insertShapes = insertShapes;
commit a83dd9a1608b1bdb2569937762a88aeaadee831c
Author:     Miklos Vajna <vmiklos at collabora.com>
AuthorDate: Fri Mar 20 09:16:22 2020 +0100
Commit:     Miklos Vajna <vmiklos at collabora.com>
CommitDate: Fri Mar 20 10:47:59 2020 +0100

    test: increase load timeout in UnitLoadTorture
    
    This test sometimes failed on me just because the load was aborted due
    to the timeout, doubling the limit makes the problem go away.
    
    Change-Id: Ided724d67a513391b86836065d4e627581857a92
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90771
    Tested-by: Jenkins CollaboraOffice <jenkinscollaboraoffice at gmail.com>
    Reviewed-by: Miklos Vajna <vmiklos at collabora.com>

diff --git a/test/UnitLoadTorture.cpp b/test/UnitLoadTorture.cpp
index 430658635..739157c0e 100644
--- a/test/UnitLoadTorture.cpp
+++ b/test/UnitLoadTorture.cpp
@@ -64,7 +64,8 @@ int UnitLoadTorture::loadTorture(const std::string& testname, const std::string&
                     Poco::URI(helpers::getTestServerURI()), request, response, testname);
                 helpers::sendTextFrame(socket, "load url=" + documentURL, testname);
 
-                const auto status = helpers::assertResponseString(socket, "status:", testname);
+                // 20s is double of the default.
+                const auto status = helpers::assertResponseString(socket, "status:", testname, 20000);
                 int viewid = -1;
                 LOOLProtocol::getTokenIntegerFromMessage(status, "viewid", viewid);
                 sum_view_ids += viewid;
diff --git a/test/helpers.hpp b/test/helpers.hpp
index 46aef7605..1b831a289 100644
--- a/test/helpers.hpp
+++ b/test/helpers.hpp
@@ -311,7 +311,7 @@ std::vector<char> getResponseMessage(LOOLWebSocket& ws, const std::string& prefi
             auto now = std::chrono::steady_clock::now();
             if (now > endTime) // timedout
             {
-                TST_LOG("Timeout.");
+                TST_LOG("Timeout after " << timeoutMs << " ms.");
                 break;
             }
             long waitTimeUs = std::chrono::duration_cast<std::chrono::microseconds>(endTime - now).count();
@@ -378,9 +378,9 @@ std::string getResponseString(T& ws, const std::string& prefix, const std::strin
 }
 
 template <typename T>
-std::string assertResponseString(T& ws, const std::string& prefix, const std::string& testname)
+std::string assertResponseString(T& ws, const std::string& prefix, const std::string& testname, const size_t timeoutMs = 10000)
 {
-    const auto res = getResponseString(ws, prefix, testname);
+    const auto res = getResponseString(ws, prefix, testname, timeoutMs);
     LOK_ASSERT_EQUAL(prefix, res.substr(0, prefix.length()));
     return res;
 }
commit 7285fad0f9b076b9667b9c254dd2f54a88ea67f9
Author:     Tamás Zolnai <tamas.zolnai at collabora.com>
AuthorDate: Thu Mar 19 15:23:19 2020 +0100
Commit:     Tamás Zolnai <tamas.zolnai at collabora.com>
CommitDate: Fri Mar 20 10:01:13 2020 +0100

    mobile: better way for identifying submenus.
    
    We actually have commands for submenus too, so
    we can use them to generate IDs.
    
    Adjust the icon names, which are now generated from
    commands.
    
    Change-Id: I76bb69fb8f83cddfb3d36c3ee74739091bc08f2d
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90763
    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_anchormenu.svg b/loleaflet/images/lc_anchormenu.svg
index 66bcde439..94262ca3d 100644
--- a/loleaflet/images/lc_anchormenu.svg
+++ b/loleaflet/images/lc_anchormenu.svg
@@ -1,4 +1,53 @@
-<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
- <path d="m8.0488 2-7.0488 20h2.9863c0.32039 0 0.58719-0.09076 0.80078-0.27148 0.2233-0.19076 0.3735-0.3963 0.45117-0.61719l1.209-3.916h5.8047l0.044922-0.03125c0.25925-0.18599 0.57769-0.29488 0.90625-0.29687 0.73878-0.003821 1.2821 0.55054 1.4883 1.123v-1.6133h-0.24414c-0.87923 0-1.623-0.73808-1.623-1.6211 0-0.71181 0.53066-1.2243 1.1797-1.4375-0.34798-0.47875-0.55598-1.0542-0.55664-1.6719v-0.001953c-2.55e-4 -0.90942 0.44146-1.7171 1.1113-2.2441l-2.6074-7.4004h-3.9023zm1.9375 3.6445c0.12621 0.49197 0.25074 0.94941 0.37695 1.3711 0.12621 0.41165 0.24873 0.77274 0.36524 1.084l1.9648 6.3242h-5.3867l1.9648-6.3086c0.1068-0.31124 0.2234-0.67792 0.34961-1.0996 0.12621-0.42169 0.24873-0.87913 0.36523-1.3711z" fill="#696969"/>
- <path d="m16.314 9.7778a1.8667 1.8667 0 0 0-1.8667 1.8667 1.8667 1.8667 0 0 0 1.2444 1.7585v0.73036h-1.2444c-0.34471 0-0.62222 0.27751-0.62222 0.62222s0.27751 0.62222 0.62222 0.62222h1.2444v5.5563c-1.0596-0.13504-2.0494-0.60481-2.8207-1.3466l0.66111-0.46302a0.61243 0.67872 0 0 0-0.32326-1.2578 0.61243 0.67872 0 0 0-0.33056 0.11059l-2.4876 1.7379a0.61265 0.67896 0 1 0 0.65383 1.1484l0.79476-0.55538c1.1666 1.213 2.7771 1.9128 4.4746 1.9141 1.698-9.33e-4 3.309-0.70087 4.4759-1.9141l0.79358 0.55538a0.61265 0.67896 0 1 0 0.65383-1.1484l-2.4876-1.7379a0.61243 0.67872 0 0 0-0.33056-0.11059 0.61243 0.67872 0 0 0-0.32326 1.2578l0.66478 0.46545c-0.7723 0.74169-1.7638 1.2114-2.8243 1.3453v-5.5574h1.2444c0.34471 0 0.62222-0.27751 0.62222-0.62222 0-0.34471-0.27751-0.62222-0.62222-0.62222h-1.2444v-0.73161a1.8667 1.8667 0 0 0 1.2444-1.7573 1.8667 1.8667 0 0 0-1.8667-1.8667zm0 1.2444a0.62222 0.62222 0 0 1 0.62222 0.62222 0.62222 0.62222 0 0 1-0.62222 0.62222 0.62222 0.62222 0 0 1-0.62222-0.62222 0
 .62222 0.62222 0 0 1 0.62222-0.62222z" fill="#4d82b8" stroke-width=".62222"/>
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   viewBox="0 0 24 24"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="lc_submenu_setanchoratchar.svg"
+   inkscape:version="0.92.4 (unknown)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="748"
+     inkscape:window-height="480"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="9.8333333"
+     inkscape:cx="12"
+     inkscape:cy="12"
+     inkscape:window-x="0"
+     inkscape:window-y="30"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg4" />
+  <path
+     d="m12 2a3 3 0 0 0 -3 3 3 3 0 0 0 2 2.8261719v1.1738281h-2c-.554 0-1 .446-1 1s .446 1 1 1h2v8.929688c-1.7029249-.217027-3.2936626-.97202-4.5332031-2.164063l1.0625-.744141a.9842562 1.0907768 0 0 0 -.5195313-2.021484.9842562 1.0907768 0 0 0 -.53125.177734l-3.9980468 2.792969a.9842562 1.0907768 0 1 0 1.0507812 1.845703l1.2773438-.892578c1.8749123 1.949387 4.4631707 3.074058 7.1914062 3.076172 2.728857-.001543 5.317991-1.126379 7.193359-3.076172l1.275391.892578a.98425669 1.0907768 0 1 0 1.050781-1.845703l-3.998047-2.792969a.98425669 1.0907768 0 0 0 -.53125-.177734.98425669 1.0907768 0 0 0 -.519531 2.021484l1.068359.748047c-1.24124 1.192002-2.8347 1.946875-4.539062 2.16211v-8.931641h2c .554 0 1-.446 1-1s-.446-1-1-1h-2v-1.1757812a3 3 0 0 0 2-2.8242188 3 3 0 0 0 -3-3zm0 2a1 1 0 0 1 1 1 1 1 0 0 1 -1 1 1 1 0 0 1 -1-1 1 1 0 0 1 1-1z"
+     fill="#4d82b8"
+     id="path2" />
 </svg>
diff --git a/loleaflet/images/lc_submenu_bringtofront.svg b/loleaflet/images/lc_arrangeframemenu.svg
similarity index 100%
rename from loleaflet/images/lc_submenu_bringtofront.svg
rename to loleaflet/images/lc_arrangeframemenu.svg
diff --git a/loleaflet/images/lc_arrangemenu.svg b/loleaflet/images/lc_arrangemenu.svg
new file mode 100644
index 000000000..7caa5b828
--- /dev/null
+++ b/loleaflet/images/lc_arrangemenu.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m2.9492188 14c-.5263 0-.9492188.422919-.9492188.949219v6.101562c0 .5263.4229188.949219.9492188.949219h8.1015622c.5263 0 .949219-.422919.949219-.949219v-2.050781h-1v2h-8v-6h1v-1z" fill="#808080"/><path d="m3 15v6h8v-2h-6c-.554 0-1-.446-1-1v-3z" fill="#fff"/><path d="m12.949219 2c-.5263 0-.949219.4229187-.949219.9492188v2.0507812h1v-2h8v6h-1v1h1.050781c.5263 0 .949219-.4229188.949219-.9492188v-6.1015624c0-.5263-.422919-.9492188-.949219-.9492188z" fill="#808080"/><path d="m13 3v2h6c.554 0 1 .446 1 1v3h1v-6z" fill="#fff"/><rect fill="#eac282" height="12" ry="1.254237" width="14" x="5" y="6"/></svg>
\ No newline at end of file
diff --git a/loleaflet/images/lc_submenu_rotateright.svg b/loleaflet/images/lc_rotateflipmenu.svg
similarity index 100%
rename from loleaflet/images/lc_submenu_rotateright.svg
rename to loleaflet/images/lc_rotateflipmenu.svg
diff --git a/loleaflet/images/lc_submenu_setanchoratchar.svg b/loleaflet/images/lc_submenu_setanchoratchar.svg
deleted file mode 100644
index 94262ca3d..000000000
--- a/loleaflet/images/lc_submenu_setanchoratchar.svg
+++ /dev/null
@@ -1,53 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" standalone="no"?>
-<svg
-   xmlns:dc="http://purl.org/dc/elements/1.1/"
-   xmlns:cc="http://creativecommons.org/ns#"
-   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
-   xmlns:svg="http://www.w3.org/2000/svg"
-   xmlns="http://www.w3.org/2000/svg"
-   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
-   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
-   viewBox="0 0 24 24"
-   version="1.1"
-   id="svg4"
-   sodipodi:docname="lc_submenu_setanchoratchar.svg"
-   inkscape:version="0.92.4 (unknown)">
-  <metadata
-     id="metadata10">
-    <rdf:RDF>
-      <cc:Work
-         rdf:about="">
-        <dc:format>image/svg+xml</dc:format>
-        <dc:type
-           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
-      </cc:Work>
-    </rdf:RDF>
-  </metadata>
-  <defs
-     id="defs8" />
-  <sodipodi:namedview
-     pagecolor="#ffffff"
-     bordercolor="#666666"
-     borderopacity="1"
-     objecttolerance="10"
-     gridtolerance="10"
-     guidetolerance="10"
-     inkscape:pageopacity="0"
-     inkscape:pageshadow="2"
-     inkscape:window-width="748"
-     inkscape:window-height="480"
-     id="namedview6"
-     showgrid="false"
-     inkscape:zoom="9.8333333"
-     inkscape:cx="12"
-     inkscape:cy="12"
-     inkscape:window-x="0"
-     inkscape:window-y="30"
-     inkscape:window-maximized="0"
-     inkscape:current-layer="svg4" />
-  <path
-     d="m12 2a3 3 0 0 0 -3 3 3 3 0 0 0 2 2.8261719v1.1738281h-2c-.554 0-1 .446-1 1s .446 1 1 1h2v8.929688c-1.7029249-.217027-3.2936626-.97202-4.5332031-2.164063l1.0625-.744141a.9842562 1.0907768 0 0 0 -.5195313-2.021484.9842562 1.0907768 0 0 0 -.53125.177734l-3.9980468 2.792969a.9842562 1.0907768 0 1 0 1.0507812 1.845703l1.2773438-.892578c1.8749123 1.949387 4.4631707 3.074058 7.1914062 3.076172 2.728857-.001543 5.317991-1.126379 7.193359-3.076172l1.275391.892578a.98425669 1.0907768 0 1 0 1.050781-1.845703l-3.998047-2.792969a.98425669 1.0907768 0 0 0 -.53125-.177734.98425669 1.0907768 0 0 0 -.519531 2.021484l1.068359.748047c-1.24124 1.192002-2.8347 1.946875-4.539062 2.16211v-8.931641h2c .554 0 1-.446 1-1s-.446-1-1-1h-2v-1.1757812a3 3 0 0 0 2-2.8242188 3 3 0 0 0 -3-3zm0 2a1 1 0 0 1 1 1 1 1 0 0 1 -1 1 1 1 0 0 1 -1-1 1 1 0 0 1 1-1z"
-     fill="#4d82b8"
-     id="path2" />
-</svg>
diff --git a/loleaflet/images/lc_submenu_wrapoff.svg b/loleaflet/images/lc_wrapmenu.svg
similarity index 100%
rename from loleaflet/images/lc_submenu_wrapoff.svg
rename to loleaflet/images/lc_wrapmenu.svg
diff --git a/loleaflet/src/control/Control.ContextMenu.js b/loleaflet/src/control/Control.ContextMenu.js
index 3a81d430c..1c8dee80a 100644
--- a/loleaflet/src/control/Control.ContextMenu.js
+++ b/loleaflet/src/control/Control.ContextMenu.js
@@ -280,6 +280,7 @@ L.Control.ContextMenu = L.Control.extend({
 
 				contextMenu['submenu' + subMenuIdx++] = {
 					name: _(itemName).replace(/\(~[A-Za-z]\)/, '').replace('~', ''),
+					command: item.command,
 					items: submenu
 				};
 				isLastItemText = true;
diff --git a/loleaflet/src/control/Control.JSDialogBuilder.js b/loleaflet/src/control/Control.JSDialogBuilder.js
index 125dbae90..cb0b607ee 100644
--- a/loleaflet/src/control/Control.JSDialogBuilder.js
+++ b/loleaflet/src/control/Control.JSDialogBuilder.js
@@ -1658,17 +1658,6 @@ L.Control.JSDialogBuilder = L.Control.extend({
 	}
 });
 
-L.Control.JSDialogBuilder.generateIDForSubMenu = function(menuStructure) {
-	for (var child = 0; child < menuStructure['children'].length; ++child) {
-		if (menuStructure['children'][child]['command'] === '.uno:SetAnchorAtChar' || menuStructure['children'][child]['command'] === '.uno:WrapOff' || menuStructure['children'][child]['command'] === '.uno:BringToFront' || menuStructure['children'][child]['command'] === '.uno:RotateRight') {
-			var tempstring = menuStructure['children'][child]['command'];
-			tempstring = tempstring.substring(5);
-			menuStructure['id'] = 'submenu_' + tempstring.toLowerCase();
-			break;
-		}
-	}
-};
-
 L.Control.JSDialogBuilder.getMenuStructureForMobileWizard = function(menu, mainMenu, itemCommand) {
 	if (itemCommand.includes('sep'))
 		return null;
@@ -1708,7 +1697,8 @@ L.Control.JSDialogBuilder.getMenuStructureForMobileWizard = function(menu, mainM
 
 	if (mainMenu) {
 		for (var menuItem in menu) {
-			var element = this.getMenuStructureForMobileWizard(menu[menuItem], false, menuItem);
+			var subItemCommand = menu[menuItem].command ? menu[menuItem].command : menuItem;
+			var element = this.getMenuStructureForMobileWizard(menu[menuItem], false, subItemCommand);
 			if (element)
 				menuStructure['children'].push(element);
 		}
@@ -1718,7 +1708,9 @@ L.Control.JSDialogBuilder.getMenuStructureForMobileWizard = function(menu, mainM
 			if (element)
 				menuStructure['children'].push(element);
 		}
-		this.generateIDForSubMenu(menuStructure);
+		if (menu.command) {
+			menuStructure.id = menu.command.substring(5).toLowerCase();
+		}
 	}
 
 	return menuStructure;
commit 15157f9e679c2f2cdf11aacbda6aea96b299bc32
Author:     Tor Lillqvist <tml at collabora.com>
AuthorDate: Fri Mar 20 10:20:23 2020 +0200
Commit:     Tor Lillqvist <tml at collabora.com>
CommitDate: Fri Mar 20 10:20:23 2020 +0200

    Mention --enable-cypress
    
    Change-Id: Ic29138650f70c0eaa0d7a60b1b6dd4f4929b6b96

diff --git a/cypress_test/README b/cypress_test/README
index 11a05cd32..d57fc0fc8 100644
--- a/cypress_test/README
+++ b/cypress_test/README
@@ -2,9 +2,11 @@ Cypress based test framework for LibreOffice Online
 ====================================================
 
 
-
 Installation
 ------------------
+
+You need to have run configure with the --enable-cypress option.
+
 In a normal desktop environment you only need to install npm
 packages for running cypress tests. This is done by the build
 system, so running 'make check' will do the basic installation.
commit 6240c5381bc791e921d6c4156c200197cba8d4c8
Author:     Tamás Zolnai <tamas.zolnai at collabora.com>
AuthorDate: Thu Mar 19 17:37:04 2020 +0100
Commit:     Tor Lillqvist <tml at collabora.com>
CommitDate: Thu Mar 19 22:55:52 2020 +0200

    Restore cypress test framework's mobile tests.
    
    Broken since: b62dcc025555076a7522809b9f12f873c278205d
    
    The issue here cypress sets the window size and not the device size
    when tries to emulate mobile view.
    https://github.com/cypress-io/cypress/issues/970
    
    For now, let's keep window size media rules too.
    
    [tml: Window size changes depending on orientation. Device size does
    not. We need to change each rule that used only max-device-width into
    one using two subrules using max-width combined with comma (i.e. the
    OR operator), one for portrait using 767px as the limit, one for
    landscape using 1023px as the limit.]
    
    We do this in other places of the css code anyway. In a long term, it
    would be good to find a better way to emulate mobile view by cypress
    or to detect mobile from JS code, so we can enable mobile mode easily
    for cypress tests.
    
    Change-Id: Ic7974f44fcbf6ed2883e93acd28153709514c216

diff --git a/loleaflet/css/loleaflet.css b/loleaflet/css/loleaflet.css
index 334b721ee..2016bc131 100644
--- a/loleaflet/css/loleaflet.css
+++ b/loleaflet/css/loleaflet.css
@@ -178,7 +178,7 @@ body {
 #toolbar-hamburger {
 	width: 0;
 }
- at media (max-device-width: 767px) {
+ at media (max-width: 767px) and (orientation: portrait), (max-width: 1023px) and (orientation: landscape) {
 	#toolbar-hamburger {
 		width: 36px;
 	}
diff --git a/loleaflet/css/mobilewizard.css b/loleaflet/css/mobilewizard.css
index eb4d613fa..0dde3030a 100644
--- a/loleaflet/css/mobilewizard.css
+++ b/loleaflet/css/mobilewizard.css
@@ -1,4 +1,4 @@
- at media (max-device-width: 767px)  {
+ at media (max-width: 767px) and (orientation: portrait), (max-width: 1023px) and (orientation: landscape) {
 	.menuwizard .menu-entry-icon{
 		padding-left: 4%;
 	}
commit e030e434610437ecd1f4e48471639d9314e8ffe1
Author:     Henry Castro <hcastro at collabora.com>
AuthorDate: Thu Mar 19 10:24:48 2020 -0400
Commit:     Henry Castro <hcastro at collabora.com>
CommitDate: Thu Mar 19 19:18:54 2020 +0100

    loleaflet: remove unnecessary debug console log
    
    Change-Id: I1d8b789e7b777dba52362b750475bfd1de1585cd
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90757
    Tested-by: Henry Castro <hcastro 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 d6e8ce67d..18aae5c52 100644
--- a/loleaflet/src/control/Control.Toolbar.js
+++ b/loleaflet/src/control/Control.Toolbar.js
@@ -2105,12 +2105,6 @@ function updateToolbarCommandValues(e) {
 			data = data.concat(Object.keys(commandValues));
 		}
 
-		/* debug messages it will be removed later */
-		if (data.length < 3) {
-			console.log('ALERT!, the server is sending a small font list');
-		}
-		/* debug end*/
-
 		$('.fonts-select').select2({
 			data: data.sort(function (a, b) {  // also sort(localely)
 				return a.localeCompare(b);
commit af338541a88d4f90d1859ba60d8de6e3e90f1423
Author:     Henry Castro <hcastro at collabora.com>
AuthorDate: Thu Mar 19 11:52:54 2020 -0400
Commit:     Henry Castro <hcastro at collabora.com>
CommitDate: Thu Mar 19 19:18:18 2020 +0100

    loleaflet: check if  "parts" variable is a number of the preview thumbnail
    
    While I was debugging Impress, I received unexpected "undefined"
    value of the variable "parts"
    
    Change-Id: Id8b6737155516fd0d8cfaa350170e9c631c55dcf
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90756
    Tested-by: Henry Castro <hcastro at collabora.com>
    Reviewed-by: Henry Castro <hcastro at collabora.com>

diff --git a/loleaflet/src/control/Control.PartsPreview.js b/loleaflet/src/control/Control.PartsPreview.js
index 3ed9f09d4..3e31133e7 100644
--- a/loleaflet/src/control/Control.PartsPreview.js
+++ b/loleaflet/src/control/Control.PartsPreview.js
@@ -65,7 +65,7 @@ L.Control.PartsPreview = L.Control.extend({
 		var selectedPart = e.selectedPart;
 		var selectedParts = e.selectedParts;
 		var docType = e.docType;
-		if (docType === 'text') {
+		if (docType === 'text' || isNaN(parts)) {
 			return;
 		}
 
commit 3dd1cefde8dc81b57465c292d409f1673b980c32
Author:     Pedro Pinto Silva <pedro.silva at collabora.com>
AuthorDate: Thu Mar 19 15:35:24 2020 +0100
Commit:     Tamás Zolnai <tamas.zolnai at collabora.com>
CommitDate: Thu Mar 19 16:27:37 2020 +0100

    Mobile: mobile wizard: improve aditional ids by making them dependent of their respective children uno commands and
    
    add icons
    
    Change-Id: Iae1fb597272a684539700ed0732cfd19d43724f6
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90755
    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_submenu_bringtofront.svg b/loleaflet/images/lc_submenu_bringtofront.svg
new file mode 100644
index 000000000..7caa5b828
--- /dev/null
+++ b/loleaflet/images/lc_submenu_bringtofront.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m2.9492188 14c-.5263 0-.9492188.422919-.9492188.949219v6.101562c0 .5263.4229188.949219.9492188.949219h8.1015622c.5263 0 .949219-.422919.949219-.949219v-2.050781h-1v2h-8v-6h1v-1z" fill="#808080"/><path d="m3 15v6h8v-2h-6c-.554 0-1-.446-1-1v-3z" fill="#fff"/><path d="m12.949219 2c-.5263 0-.949219.4229187-.949219.9492188v2.0507812h1v-2h8v6h-1v1h1.050781c.5263 0 .949219-.4229188.949219-.9492188v-6.1015624c0-.5263-.422919-.9492188-.949219-.9492188z" fill="#808080"/><path d="m13 3v2h6c.554 0 1 .446 1 1v3h1v-6z" fill="#fff"/><rect fill="#eac282" height="12" ry="1.254237" width="14" x="5" y="6"/></svg>
\ No newline at end of file
diff --git a/loleaflet/images/lc_submenu_rotateright.svg b/loleaflet/images/lc_submenu_rotateright.svg
new file mode 100644
index 000000000..40e86d9b3
--- /dev/null
+++ b/loleaflet/images/lc_submenu_rotateright.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g transform="matrix(-1 0 0 1 23.999864 0)"><path d="m1.499971 21.5h7.000029v-15.0000002z" fill="#4d82b8" fill-rule="evenodd" stroke="#4d82b8" stroke-linecap="round" stroke-linejoin="round"/><path d="m10.5 14.5v7.000008h11.999999z" fill="#fff" fill-rule="evenodd" stroke="#808080" stroke-linecap="round" stroke-linejoin="round"/><g transform="matrix(0 -1 -1 0 30.992857 27)"><path d="m21.5 16.5-2.995 2.992879-3.005-2.992879" fill="none" stroke="#4d82b8" stroke-linecap="round" stroke-linejoin="round"/><path d="m13.5 11.007943c-.276142 0-.5.223858-.5.5s.223858.5.5.5h2c1.380712 0 2.5 1.119287 2.5 2.5h.0059v.423829 4.068228h1v-4.068228-.5c-.0059-2.154663-1.88341-3.373799-3.5059-3.423829z" fill="#4d82b8" fill-rule="evenodd"/></g></g></svg>
\ No newline at end of file
diff --git a/loleaflet/images/lc_submenu_setanchoratchar.svg b/loleaflet/images/lc_submenu_setanchoratchar.svg
new file mode 100644
index 000000000..94262ca3d
--- /dev/null
+++ b/loleaflet/images/lc_submenu_setanchoratchar.svg
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   viewBox="0 0 24 24"
+   version="1.1"
+   id="svg4"
+   sodipodi:docname="lc_submenu_setanchoratchar.svg"
+   inkscape:version="0.92.4 (unknown)">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8" />
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="748"
+     inkscape:window-height="480"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="9.8333333"
+     inkscape:cx="12"
+     inkscape:cy="12"
+     inkscape:window-x="0"
+     inkscape:window-y="30"
+     inkscape:window-maximized="0"
+     inkscape:current-layer="svg4" />
+  <path
+     d="m12 2a3 3 0 0 0 -3 3 3 3 0 0 0 2 2.8261719v1.1738281h-2c-.554 0-1 .446-1 1s .446 1 1 1h2v8.929688c-1.7029249-.217027-3.2936626-.97202-4.5332031-2.164063l1.0625-.744141a.9842562 1.0907768 0 0 0 -.5195313-2.021484.9842562 1.0907768 0 0 0 -.53125.177734l-3.9980468 2.792969a.9842562 1.0907768 0 1 0 1.0507812 1.845703l1.2773438-.892578c1.8749123 1.949387 4.4631707 3.074058 7.1914062 3.076172 2.728857-.001543 5.317991-1.126379 7.193359-3.076172l1.275391.892578a.98425669 1.0907768 0 1 0 1.050781-1.845703l-3.998047-2.792969a.98425669 1.0907768 0 0 0 -.53125-.177734.98425669 1.0907768 0 0 0 -.519531 2.021484l1.068359.748047c-1.24124 1.192002-2.8347 1.946875-4.539062 2.16211v-8.931641h2c .554 0 1-.446 1-1s-.446-1-1-1h-2v-1.1757812a3 3 0 0 0 2-2.8242188 3 3 0 0 0 -3-3zm0 2a1 1 0 0 1 1 1 1 1 0 0 1 -1 1 1 1 0 0 1 -1-1 1 1 0 0 1 1-1z"
+     fill="#4d82b8"
+     id="path2" />
+</svg>
diff --git a/loleaflet/images/lc_submenu_wrapoff.svg b/loleaflet/images/lc_submenu_wrapoff.svg
new file mode 100644
index 000000000..b34b507e0
--- /dev/null
+++ b/loleaflet/images/lc_submenu_wrapoff.svg
@@ -0,0 +1,12 @@
+<svg version="1.1" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
+ <rect x="2" y="3" width="20" height="1" ry=".44607" fill="#808080"/>
+ <g fill="#808080">
+  <rect x="2" y="6" width="20" height="1" ry=".44607"/>
+  <rect x="2" y="21" width="20" height="1" ry=".44607"/>
+  <rect x="2" y="12" width="20" height="1" ry=".44607"/>
+  <rect x="2" y="18" width="20" height="1" ry=".44607"/>
+  <rect x="2" y="9" width="20" height="1" ry=".44607"/>
+  <rect x="2" y="15" width="20" height="1" ry=".44607"/>
+ </g>
+ <rect x="7" y="8" width="10" height="9" ry="1" fill="#4d82b8"/>
+</svg>
diff --git a/loleaflet/src/control/Control.JSDialogBuilder.js b/loleaflet/src/control/Control.JSDialogBuilder.js
index f9f536a8e..125dbae90 100644
--- a/loleaflet/src/control/Control.JSDialogBuilder.js
+++ b/loleaflet/src/control/Control.JSDialogBuilder.js
@@ -1660,8 +1660,10 @@ L.Control.JSDialogBuilder = L.Control.extend({
 
 L.Control.JSDialogBuilder.generateIDForSubMenu = function(menuStructure) {
 	for (var child = 0; child < menuStructure['children'].length; ++child) {
-		if (menuStructure['children'][child]['command'] === '.uno:SetAnchorAtChar') {
-			menuStructure['id'] = 'submenu_anchor';
+		if (menuStructure['children'][child]['command'] === '.uno:SetAnchorAtChar' || menuStructure['children'][child]['command'] === '.uno:WrapOff' || menuStructure['children'][child]['command'] === '.uno:BringToFront' || menuStructure['children'][child]['command'] === '.uno:RotateRight') {
+			var tempstring = menuStructure['children'][child]['command'];
+			tempstring = tempstring.substring(5);
+			menuStructure['id'] = 'submenu_' + tempstring.toLowerCase();
 			break;
 		}
 	}
commit b62dcc025555076a7522809b9f12f873c278205d
Author:     Tor Lillqvist <tml at collabora.com>
AuthorDate: Thu Mar 19 16:27:36 2020 +0200
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu Mar 19 15:40:44 2020 +0100

    Hacks to improve user experience when changing orientation on phones
    
    I now think that much of our media queries with (max-device-height:
    767px) actually should use (max-device-width: 767px) if the intent is
    that they detect the case of mobile phones, as opposed to tablets.
    
    Many mobile phones have a screen.height (which is what
    max-device-height in CSS refers to) that is clearly larger than 767.
    But a screen.width of 768 is what at least my 9.7 iPad has.
    
    Change-Id: I3b44e32ec583837897b4aa390fc0ab92be59d163
    Reviewed-on: https://gerrit.libreoffice.org/c/online/+/90752
    Tested-by: Andras Timar <andras.timar at collabora.com>
    Reviewed-by: Andras Timar <andras.timar at collabora.com>

diff --git a/loleaflet/css/loleaflet.css b/loleaflet/css/loleaflet.css
index a0cf27e43..334b721ee 100644
--- a/loleaflet/css/loleaflet.css
+++ b/loleaflet/css/loleaflet.css
@@ -174,9 +174,28 @@ body {
 	width: 0;
 }
 
+/* Hide hamburger except on mobile phones */
 #toolbar-hamburger {
 	width: 0;
 }
+ at media (max-device-width: 767px) {
+	#toolbar-hamburger {
+		width: 36px;
+	}
+	/* In Safari on iPhone, the address bar is auto-hidden in landscape
+	 * orientation. For some reason this prevents jQuery from
+	 * getting events when tapping on the things in the toolbar at
+	 * the top, like the hamburger button. To work around this,
+	 * add a bit of padding to the body element. This is of course
+	 * an extremely silly waste of precious vertical space and
+	 * some other solution should be found.
+	 */
+	@media (orientation: landscape) {
+		body {
+			padding-top: 20px;
+		}
+	}
+}
 
 #mobile-edit-button {
 	position: absolute;
@@ -284,9 +303,6 @@ body {
 		bottom: 35px !important;
 	}
 
-	#toolbar-hamburger {
-	width: 36px;
-	}
 	#closebuttonwrapper {
 		display: none;
 	}
@@ -296,7 +312,7 @@ body {
 	}
 }
 
- at media (max-width: 767px) and (orientation: portrait), (max-device-height: 767px) and (orientation: portrait) {
+ at media (max-width: 767px) and (orientation: portrait) {
 	#presentation-controls-wrapper {
 		top: initial;
 		left: initial;
@@ -327,7 +343,7 @@ body {
 		bottom: 33px;
 	}
 }
- at media (max-width: 767px) and (orientation: landscape), (max-device-height: 767px) and (orientation: landscape) {
+ at media (max-width: 1023px) and (orientation: landscape) {
 	#presentation-controls-wrapper {
 		top: 41px;
 		bottom: 33px;
diff --git a/loleaflet/css/menubar-mobile.css b/loleaflet/css/menubar-mobile.css
index 564eedc3e..cccb63eda 100644
--- a/loleaflet/css/menubar-mobile.css
+++ b/loleaflet/css/menubar-mobile.css
@@ -1,4 +1,4 @@
- at media (max-width: 767px), (max-device-height: 767px) {
+ at media (max-device-width: 767px) {
 	.logo {
 		background-size: 100px;
 		max-width: 24px;
diff --git a/loleaflet/css/menubar.css b/loleaflet/css/menubar.css
index f25fb9cb9..df11acf55 100644
--- a/loleaflet/css/menubar.css
+++ b/loleaflet/css/menubar.css
@@ -151,12 +151,11 @@
     overflow: hidden;
 }
 
-/* Mobile menu toggle button */
+/* Hamburger button */
 
 .main-menu-btn {
     margin: 2px 10px;
     position: relative;
-    display: none;
     width: 17px;
     height: 21px;
     text-indent: 17px;
@@ -167,7 +166,7 @@
 }
 
 
-/* hamburger icon */
+/* Hamburger icon */
 
 .main-menu-btn-icon,
 .main-menu-btn-icon:before,
@@ -270,15 +269,6 @@
 	display: none;
     }
 
-    /* hide the menu in mobile view */
-    #main-menu-state:not(:checked) ~ #main-menu {
-        display: none;
-    }
-
-    .main-menu-btn {
-        display: inline-block;
-    }
-
     .main-nav {
         position: absolute;
         height: 0;
diff --git a/loleaflet/css/mobilewizard.css b/loleaflet/css/mobilewizard.css
index bb6addd51..eb4d613fa 100644
--- a/loleaflet/css/mobilewizard.css
+++ b/loleaflet/css/mobilewizard.css
@@ -1,4 +1,4 @@
- at media (max-width: 767px), (max-device-height: 767px) {
+ at media (max-device-width: 767px)  {
 	.menuwizard .menu-entry-icon{
 		padding-left: 4%;

... etc. - the rest is truncated


More information about the Libreoffice-commits mailing list