[Libreoffice-commits] online.git: Branch 'distro/collabora/collabora-online-cd-4' - 16 commits - common/Png.hpp configure.ac debian/changelog debian/control ios/config.h.in ios/Mobile kit/Kit.cpp loleaflet/html loleaflet/js loleaflet/Makefile.am loleaflet/plugins loleaflet/src loolwsd.spec.in test/Makefile.am test/UnitWOPIRenameFile.cpp test/WopiTestServer.hpp wsd/ClientSession.cpp wsd/DocumentBroker.cpp wsd/DocumentBroker.hpp wsd/LOOLWSD.cpp wsd/Storage.cpp wsd/Storage.hpp

Libreoffice Gerrit user logerrit at kemper.freedesktop.org
Thu May 9 13:27:36 UTC 2019


 common/Png.hpp                                         |   18 +++
 configure.ac                                           |   22 ++--
 debian/changelog                                       |    6 +
 debian/control                                         |    2 
 ios/Mobile/Document.mm                                 |   92 +++++------------
 ios/Mobile/Info.plist.in                               |    2 
 ios/config.h.in                                        |    2 
 kit/Kit.cpp                                            |   38 ++++---
 loleaflet/Makefile.am                                  |    2 
 loleaflet/html/loleaflet.html.m4                       |    2 
 loleaflet/js/jquery.mCustomScrollbar.js                |   27 +++-
 loleaflet/js/toolbar.js                                |   10 +
 loleaflet/plugins/path-transform/src/Path.Transform.js |   26 ++++
 loleaflet/src/control/Control.Scroll.js                |    5 
 loleaflet/src/control/Toolbar.js                       |    8 +
 loleaflet/src/core/Socket.js                           |   32 ++++-
 loleaflet/src/errormessages.js                         |    3 
 loleaflet/src/layer/tile/TileLayer.js                  |   10 -
 loleaflet/src/layer/tile/WriterTileLayer.js            |    7 +
 loleaflet/src/layer/vector/SVGGroup.js                 |   17 ++-
 loleaflet/src/map/Map.js                               |    9 +
 loleaflet/src/map/handler/Map.Keyboard.js              |   42 +++----
 loleaflet/src/map/handler/Map.WOPI.js                  |    4 
 loolwsd.spec.in                                        |    4 
 test/Makefile.am                                       |    6 -
 test/UnitWOPIRenameFile.cpp                            |   90 ++++++++++++++++
 test/WopiTestServer.hpp                                |   25 +++-
 wsd/ClientSession.cpp                                  |   18 ++-
 wsd/DocumentBroker.cpp                                 |   33 ++++--
 wsd/DocumentBroker.hpp                                 |    4 
 wsd/LOOLWSD.cpp                                        |    3 
 wsd/Storage.cpp                                        |   40 ++++---
 wsd/Storage.hpp                                        |   20 ++-
 33 files changed, 444 insertions(+), 185 deletions(-)

New commits:
commit fce8094f0848ee2b52998d0a61af20449aab9fc9
Author:     Tor Lillqvist <tml at collabora.com>
AuthorDate: Fri Mar 22 15:49:47 2019 +0200
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:44:53 2019 +0200

    For iOS, generate the data: URLs for the PNG tiles already in the Online code
    
    (What we cache is also the textual data: URLs even if we store them
    using .png file names.)
    
    This avoids the current back-and-forth-encoding: First we
    base64-encode the complete binary "tile:" message (one text line
    followed by a newline and the binary PNG) to pass to WebKit, then in
    the JavaScript snippet passed to WebKit we decode the base64 and turn
    it into an ArrayBuffer, and then we unpack the ArrayBuffer and encode
    the PNG part to use as a data: URL.

diff --git a/common/Png.hpp b/common/Png.hpp
index 84861fc48..0d40a37a0 100644
--- a/common/Png.hpp
+++ b/common/Png.hpp
@@ -52,6 +52,10 @@
 #include <cassert>
 #include <chrono>
 
+#ifdef IOS
+#include <Foundation/Foundation.h>
+#endif
+
 #include "Log.hpp"
 #include "SpookyV2.h"
 
@@ -134,6 +138,10 @@ bool encodeSubBufferToPNG(unsigned char* pixmap, size_t startX, size_t startY,
     png_set_compression_level(png_ptr, Z_BEST_SPEED);
 #endif
 
+#ifdef IOS
+    auto initialSize = output.size();
+#endif
+
     png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB_ALPHA, PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
 
     png_set_write_fn(png_ptr, &output, user_write_fn, user_flush_fn);
@@ -169,6 +177,16 @@ bool encodeSubBufferToPNG(unsigned char* pixmap, size_t startX, size_t startY,
 
     png_destroy_write_struct(&png_ptr, &info_ptr);
 
+#ifdef IOS
+    auto base64 = [[NSData dataWithBytesNoCopy:output.data() + initialSize length:(output.size() - initialSize) freeWhenDone:NO] base64EncodedDataWithOptions:0];
+
+    const char dataURLStart[] = "data:image/png;base64,";
+
+    output.resize(initialSize);
+    output.insert(output.end(), dataURLStart, dataURLStart + sizeof(dataURLStart)-1);
+    output.insert(output.end(), (char*)base64.bytes, (char*)base64.bytes + base64.length);
+#endif
+
     return true;
 }
 
diff --git a/ios/Mobile/Document.mm b/ios/Mobile/Document.mm
index 8550bc3e8..5473ef081 100644
--- a/ios/Mobile/Document.mm
+++ b/ios/Mobile/Document.mm
@@ -84,69 +84,39 @@
 
     NSString *js;
 
-    // Check if the message is binary. We say that any message that isn't just a single line is
-    // "binary" even if that strictly speaking isn't the case; for instance the commandvalues:
-    // message has a long bunch of non-binary JSON on multiple lines. But _onMessage() in Socket.js
-    // handles it fine even if such a message, too, comes in as an ArrayBuffer. (Look for the
-    // "textMsg = String.fromCharCode.apply(null, imgBytes);".)
-
-    const char *newline = (const char *)memchr(buffer, '\n', length);
-    if (newline != nullptr) {
-        // The data needs to be an ArrayBuffer
-        js = @"window.TheFakeWebSocket.onmessage({'data': Base64ToArrayBuffer('";
-        js = [js stringByAppendingString: [[NSData dataWithBytes:buffer length:length] base64EncodedStringWithOptions:0]];
-        js = [js stringByAppendingString:@"')});"];
-        NSString *subjs = [js substringToIndex:std::min(100ul, js.length)];
-        if (subjs.length < js.length)
-            subjs = [subjs stringByAppendingString:@"..."];
-
-        // LOG_TRC("Evaluating JavaScript: " << [subjs UTF8String]);
-
-        dispatch_async(dispatch_get_main_queue(), ^{
-                [self.viewController.webView evaluateJavaScript:js
-                                              completionHandler:^(id _Nullable obj, NSError * _Nullable error)
-                     {
-                         if (error) {
-                             LOG_ERR("Error after " << [subjs UTF8String] << ": " << [error.localizedDescription UTF8String]);
-                         }
-                     }
-                 ];
-        });
-    } else {
-        const unsigned char *ubufp = (const unsigned char *)buffer;
-        std::vector<char> data;
-        for (int i = 0; i < length; i++) {
-            if (ubufp[i] < ' ' || ubufp[i] == '\'' || ubufp[i] == '\\') {
-                data.push_back('\\');
-                data.push_back('x');
-                data.push_back("0123456789abcdef"[(ubufp[i] >> 4) & 0x0F]);
-                data.push_back("0123456789abcdef"[ubufp[i] & 0x0F]);
-            } else {
-                data.push_back(ubufp[i]);
-            }
+    const unsigned char *ubufp = (const unsigned char *)buffer;
+    std::vector<char> data;
+    for (int i = 0; i < length; i++) {
+        if (ubufp[i] < ' ' || ubufp[i] == '\'' || ubufp[i] == '\\') {
+            data.push_back('\\');
+            data.push_back('x');
+            data.push_back("0123456789abcdef"[(ubufp[i] >> 4) & 0x0F]);
+            data.push_back("0123456789abcdef"[ubufp[i] & 0x0F]);
+        } else {
+            data.push_back(ubufp[i]);
         }
-        data.push_back(0);
-
-        js = @"window.TheFakeWebSocket.onmessage({'data': '";
-        js = [js stringByAppendingString:[NSString stringWithUTF8String:data.data()]];
-        js = [js stringByAppendingString:@"'});"];
-
-        // LOG_TRC("Evaluating JavaScript: " << [js UTF8String]);
-
-        dispatch_async(dispatch_get_main_queue(), ^{
-                [self.viewController.webView evaluateJavaScript:js
-                                              completionHandler:^(id _Nullable obj, NSError * _Nullable error)
-                     {
-                         if (error) {
-                             LOG_ERR("Error after " << [js UTF8String] << ": " << [[error localizedDescription] UTF8String]);
-                             NSString *jsException = error.userInfo[@"WKJavaScriptExceptionMessage"];
-                             if (jsException != nil)
-                                 LOG_ERR("JavaScript exception: " << [jsException UTF8String]);
-                         }
-                     }
-                 ];
-            });
     }
+    data.push_back(0);
+
+    js = @"window.TheFakeWebSocket.onmessage({'data': '";
+    js = [js stringByAppendingString:[NSString stringWithUTF8String:data.data()]];
+    js = [js stringByAppendingString:@"'});"];
+
+    // LOG_TRC("Evaluating JavaScript: " << [js UTF8String]);
+
+    dispatch_async(dispatch_get_main_queue(), ^{
+            [self.viewController.webView evaluateJavaScript:js
+                                          completionHandler:^(id _Nullable obj, NSError * _Nullable error)
+                 {
+                     if (error) {
+                         LOG_ERR("Error after " << [js UTF8String] << ": " << [[error localizedDescription] UTF8String]);
+                         NSString *jsException = error.userInfo[@"WKJavaScriptExceptionMessage"];
+                         if (jsException != nil)
+                             LOG_ERR("JavaScript exception: " << [jsException UTF8String]);
+                     }
+                 }
+             ];
+        });
 }
 
 @end
diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js
index f7dc49dda..78d8a00d7 100644
--- a/loleaflet/src/core/Socket.js
+++ b/loleaflet/src/core/Socket.js
@@ -239,7 +239,7 @@ L.Socket = L.Class.extend({
 	},
 
 	_onMessage: function (e) {
-		var imgBytes, index, textMsg;
+		var imgBytes, index, textMsg, img;
 
 		if (typeof (e.data) === 'string') {
 			textMsg = e.data;
@@ -749,13 +749,21 @@ L.Socket = L.Class.extend({
 				textMsg = decodeURIComponent(window.escape(textMsg));
 			}
 		}
+		else if (window.ThisIsTheiOSApp) {
+			// In the iOS app, the native code sends us the PNG tile already as a data: URL after the newline
+			var newlineIndex = textMsg.indexOf('\n');
+			if (newlineIndex > 0) {
+				img = textMsg.substring(newlineIndex+1);
+				textMsg = textMsg.substring(0, newlineIndex);
+			}
+		}
 		else {
 			var data = imgBytes.subarray(index + 1);
 
 			if (data.length > 0 && data[0] == 68 /* D */)
 			{
 				console.log('Socket: got a delta !');
-				var img = data;
+				img = data;
 			}
 			else
 			{
commit fb162606ea6e518a48bb04738235a5953d894d7f
Author:     Henry Castro <hcastro at collabora.com>
AuthorDate: Thu Apr 4 10:51:37 2019 -0400
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:28:52 2019 +0200

    loleaflet: fix "select text with mouse after clicking into text box moves ...
    
    text box instead"
    
    Change-Id: Id867af5ebf7c93cc494be7fad93aae235ffa36d1
    Reviewed-on: https://gerrit.libreoffice.org/70262
    Reviewed-by: Henry Castro <hcastro at collabora.com>
    Tested-by: Henry Castro <hcastro at collabora.com>
    Reviewed-on: https://gerrit.libreoffice.org/70670
    Reviewed-by: Jan Holesovsky <kendy at collabora.com>
    Tested-by: Jan Holesovsky <kendy at collabora.com>

diff --git a/loleaflet/src/layer/vector/SVGGroup.js b/loleaflet/src/layer/vector/SVGGroup.js
index c719a7b1a..da8a40079 100644
--- a/loleaflet/src/layer/vector/SVGGroup.js
+++ b/loleaflet/src/layer/vector/SVGGroup.js
@@ -27,14 +27,21 @@ L.SVGGroup = L.Layer.extend({
 		if (doc.lastChild.localName !== 'svg')
 			return;
 
-		L.DomUtil.remove(this._rect._path);
-		this._svg = this._path.appendChild(doc.lastChild);
+		if (svgString.indexOf('XTEXT_PAINTSHAPE_BEGIN') !== -1) {
+			this._svg = this._path.insertBefore(doc.lastChild, this._rect._path);
+			this._rect._path.setAttribute('pointer-events', 'visibleStroke');
+			this._svg.setAttribute('pointer-events', 'none');
+		} else {
+			L.DomUtil.remove(this._rect._path);
+			this._svg = this._path.appendChild(doc.lastChild);
+			this._svg.setAttribute('pointer-events', 'visiblePainted');
+			L.DomEvent.on(this._svg, 'mousedown', this._onDragStart, this);
+			this._dragShape = this._svg;
+		}
+
 		this._svg.setAttribute('opacity', 0);
 		this._svg.setAttribute('width', size.x);
 		this._svg.setAttribute('height', size.y);
-		this._svg.setAttribute('pointer-events', 'visiblePainted');
-		this._dragShape = this._svg;
-		L.DomEvent.on(this._svg, 'mousedown', this._onDragStart, this);
 
 		this._update();
 	},
commit 2b828db09fd1dd693cc7c0cccc0b7b13f82c3025
Author:     Andras Timar <andras.timar at collabora.com>
AuthorDate: Thu May 9 13:28:22 2019 +0200
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:28:22 2019 +0200

    Bump package version to 4.0.4-1
    
    Change-Id: Ic751162cf1fca53d71a623d03f9b731af5189a87

diff --git a/configure.ac b/configure.ac
index 5f60b3d23..ac8f0db37 100644
--- a/configure.ac
+++ b/configure.ac
@@ -3,7 +3,7 @@
 
 AC_PREREQ([2.63])
 
-AC_INIT([loolwsd], [4.0.3], [libreoffice at lists.freedesktop.org])
+AC_INIT([loolwsd], [4.0.4], [libreoffice at lists.freedesktop.org])
 LT_INIT([shared, disable-static, dlopen])
 
 AM_INIT_AUTOMAKE([1.10 subdir-objects tar-pax -Wno-portability])
diff --git a/debian/changelog b/debian/changelog
index d4fb303c4..57012ad6f 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+loolwsd (4.0.4-1) unstable; urgency=medium
+
+  * see the git log: http://col.la/coolcd4
+
+ -- Andras Timar <andras.timar at collabora.com>  Thu, 09 May 2019 09:10:00 +0200
+
 loolwsd (4.0.3-2) unstable; urgency=medium
 
   * see the git log: http://col.la/coolcd4
diff --git a/debian/control b/debian/control
index bb5e4f5d4..44be55595 100644
--- a/debian/control
+++ b/debian/control
@@ -8,7 +8,7 @@ Standards-Version: 3.9.7
 Package: loolwsd
 Section: web
 Architecture: any
-Depends: ${shlibs:Depends}, ${misc:Depends}, libcurl3-gnutls, libexpat1, libicu55, libjpeg-turbo8, libnss3, libpng12-0, libxml2, libxslt1.1, libsm6, libxinerama1, libxrender1, libgl1-mesa-glx, libcups2, libxcb-render0, libxcb-shm0, locales-all, adduser, expat, fontconfig, cpio, libcap2-bin, collaboraofficebasis6.0-calc (>= 6.0.10.28), collaboraofficebasis6.0-core (>= 6.0.10.28), collaboraofficebasis6.0-graphicfilter (>= 6.0.10.28), collaboraofficebasis6.0-images (>= 6.0.10.28), collaboraofficebasis6.0-impress (>= 6.0.10.28), collaboraofficebasis6.0-ooofonts (>= 6.0.10.28), collaboraofficebasis6.0-writer (>= 6.0.10.28), collaboraoffice6.0 (>= 6.0.10.28), collaboraoffice6.0-ure (>= 6.0.10.28), collaboraofficebasis6.0-en-us (>= 6.0.10.28), collaboraofficebasis6.0-draw (>= 6.0.10.28), collaboraofficebasis6.0-extension-pdf-import (>= 6.0.10.28), collaboraofficebasis6.0-ooolinguistic (>= 6.0.10.28)
+Depends: ${shlibs:Depends}, ${misc:Depends}, libcurl3-gnutls, libexpat1, libicu55, libjpeg-turbo8, libnss3, libpng12-0, libxml2, libxslt1.1, libsm6, libxinerama1, libxrender1, libgl1-mesa-glx, libcups2, libxcb-render0, libxcb-shm0, locales-all, adduser, expat, fontconfig, cpio, libcap2-bin, collaboraofficebasis6.0-calc (= 6.0.10.29-29), collaboraofficebasis6.0-core (= 6.0.10.29-29), collaboraofficebasis6.0-graphicfilter (= 6.0.10.29-29), collaboraofficebasis6.0-images (= 6.0.10.29-29), collaboraofficebasis6.0-impress (= 6.0.10.29-29), collaboraofficebasis6.0-ooofonts (= 6.0.10.29-29), collaboraofficebasis6.0-writer (= 6.0.10.29-29), collaboraoffice6.0 (= 6.0.10.29-29), collaboraoffice6.0-ure (= 6.0.10.29-29), collaboraofficebasis6.0-en-us (= 6.0.10.29-29), collaboraofficebasis6.0-draw (= 6.0.10.29-29), collaboraofficebasis6.0-extension-pdf-import (= 6.0.10.29-29), collaboraofficebasis6.0-ooolinguistic (= 6.0.10.29-29)
 Conflicts: collaboraofficebasis6.0-gnome-integration, collaboraofficebasis6.0-kde-integration
 Description: LibreOffice Online WebSocket Daemon
  LOOLWSD is a daemon that talks to web browser clients and provides LibreOffice
diff --git a/loolwsd.spec.in b/loolwsd.spec.in
index aabcd6cc3..84abe1d5c 100644
--- a/loolwsd.spec.in
+++ b/loolwsd.spec.in
@@ -12,7 +12,7 @@ Name:           loolwsd%{name_suffix}
 Name:           loolwsd
 %endif
 Version:        @PACKAGE_VERSION@
-Release:        2%{?dist}
+Release:        1%{?dist}
 %if 0%{?suse_version} == 1110
 Group:          Productivity/Office/Suite
 BuildRoot:      %{_tmppath}/%{name}-%{version}-build
@@ -38,7 +38,7 @@ BuildRequires:  libcap-progs linux-glibc-devel systemd-rpm-macros python-polib
 BuildRequires:  libcap-progs
 %endif
 
-Requires:       collaboraoffice6.0 >= 6.0.10.28 collaboraoffice6.0-ure >= 6.0.10.28 collaboraofficebasis6.0-core >= 6.0.10.28 collaboraofficebasis6.0-writer >= 6.0.10.28 collaboraofficebasis6.0-impress >= 6.0.10.28 collaboraofficebasis6.0-graphicfilter >= 6.0.10.28 collaboraofficebasis6.0-en-US >= 6.0.10.28 collaboraofficebasis6.0-calc >= 6.0.10.28 collaboraofficebasis6.0-ooofonts >= 6.0.10.28 collaboraofficebasis6.0-images >= 6.0.10.28 collaboraofficebasis6.0-draw >= 6.0.10.28 collaboraofficebasis6.0-extension-pdf-import >= 6.0.10.28 collaboraofficebasis6.0-ooolinguistic >= 6.0.10.28
+Requires:       collaboraoffice6.0 = 6.0.10.29-29 collaboraoffice6.0-ure = 6.0.10.29-29 collaboraofficebasis6.0-core = 6.0.10.29-29 collaboraofficebasis6.0-writer = 6.0.10.29-29 collaboraofficebasis6.0-impress = 6.0.10.29-29 collaboraofficebasis6.0-graphicfilter = 6.0.10.29-29 collaboraofficebasis6.0-en-US = 6.0.10.29-29 collaboraofficebasis6.0-calc = 6.0.10.29-29 collaboraofficebasis6.0-ooofonts = 6.0.10.29-29 collaboraofficebasis6.0-images = 6.0.10.29-29 collaboraofficebasis6.0-draw = 6.0.10.29-29 collaboraofficebasis6.0-extension-pdf-import = 6.0.10.29-29 collaboraofficebasis6.0-ooolinguistic = 6.0.10.29-29
 Conflicts:      collaboraofficebasis6.0-kde-integration collaboraofficebasis6.0-gnome-integration
 Requires(post): coreutils grep sed
 %if 0%{?rhel} == 6
commit 47299c9a440c91a1631eb9f41b522438b9f69487
Author:     merttumer <mert.tumer at collabora.com>
AuthorDate: Tue Apr 30 17:21:44 2019 +0300
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:22:09 2019 +0200

    Added RenameFile Support for WOPI
    
    This patch concerns rename file operation when the integration
    supports it
    Signed-off-by: merttumer <mert.tumer at collabora.com>
    
    Change-Id: Ibb4f615b91dda2491bfcd4d4738198d69eca4e6f
    Reviewed-on: https://gerrit.libreoffice.org/71587
    Reviewed-by: Jan Holesovsky <kendy at collabora.com>
    Tested-by: Jan Holesovsky <kendy at collabora.com>
    Reviewed-on: https://gerrit.libreoffice.org/71918
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>

diff --git a/loleaflet/js/toolbar.js b/loleaflet/js/toolbar.js
index 71327fa7c..bae578585 100644
--- a/loleaflet/js/toolbar.js
+++ b/loleaflet/js/toolbar.js
@@ -1331,7 +1331,15 @@ function onSearchKeyDown(e) {
 function documentNameConfirm() {
 	var value = $('#document-name-input').val();
 	if (value !== null && value != '' && value != map['wopi'].BaseFileName) {
-		map.saveAs(value);
+		if (map['wopi'].UserCanRename && map['wopi'].SupportsRename) {
+			// file name must be without the extension
+			if (value.lastIndexOf('.') > 0)
+				value = value.substr(0, value.lastIndexOf('.'));
+			map.renameFile(value);
+		} else {
+			// saveAs for rename
+			map.saveAs(value);
+		}
 	}
 	map._onGotFocus();
 }
diff --git a/loleaflet/src/control/Toolbar.js b/loleaflet/src/control/Toolbar.js
index 8f3820431..a08642b2e 100644
--- a/loleaflet/src/control/Toolbar.js
+++ b/loleaflet/src/control/Toolbar.js
@@ -107,6 +107,14 @@ L.Map.include({
 			'options=' + options);
 	},
 
+	renameFile: function (filename) {
+		if (!filename) {
+			return;
+		}
+		this.showBusy(_('Renaming...'), false);
+		this._socket.sendMessage('renamefile filename=' + encodeURIComponent(filename));
+	},
+
 	applyStyle: function (style, familyName) {
 		if (!style || !familyName) {
 			this.fire('error', {cmd: 'setStyle', kind: 'incorrectparam'});
diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js
index 30cbe75ef..f7dc49dda 100644
--- a/loleaflet/src/core/Socket.js
+++ b/loleaflet/src/core/Socket.js
@@ -482,6 +482,9 @@ L.Socket = L.Class.extend({
 			else if (command.errorKind === 'savefailed') {
 				storageError = errorMessages.storage.savefailed;
 			}
+			else if (command.errorKind === 'renamefailed') {
+				storageError = errorMessages.storage.renamefailed;
+			}
 			else if (command.errorKind === 'saveunauthorized') {
 				storageError = errorMessages.storage.saveunauthorized;
 			}
@@ -686,7 +689,7 @@ L.Socket = L.Class.extend({
 					', last: ' + (command.rendercount - this._map._docLayer._debugRenderCount));
 			this._map._docLayer._debugRenderCount = command.rendercount;
 		}
-		else if (textMsg.startsWith('saveas:')) {
+		else if (textMsg.startsWith('saveas:') || textMsg.startsWith('renamefile:')) {
 			this._map.hideBusy();
 			if (command !== undefined && command.url !== undefined && command.url !== '') {
 				this.close();
diff --git a/loleaflet/src/errormessages.js b/loleaflet/src/errormessages.js
index 3381b9126..e6eff9365 100644
--- a/loleaflet/src/errormessages.js
+++ b/loleaflet/src/errormessages.js
@@ -26,7 +26,8 @@ errorMessages.storage = {
 	loadfailed: _('Failed to read document from storage. Please contact your storage server (%storageserver) administrator.'),
 	savediskfull: _('Save failed due to no disk space left on storage server. Document will now be read-only. Please contact the server (%storageserver) administrator to continue editing.'),
 	saveunauthorized: _('Document cannot be saved due to expired or invalid access token.'),
-	savefailed: _('Document cannot be saved. Check your permissions or contact the storage server administrator.')
+	savefailed: _('Document cannot be saved. Check your permissions or contact the storage server administrator.'),
+	renamefailed: _('Document cannot be renamed. Check your permissions or contact the storage server administrator.')
 };
 
 if (typeof window !== 'undefined') {
diff --git a/loleaflet/src/map/handler/Map.WOPI.js b/loleaflet/src/map/handler/Map.WOPI.js
index cf0bf7013..ff9200086 100644
--- a/loleaflet/src/map/handler/Map.WOPI.js
+++ b/loleaflet/src/map/handler/Map.WOPI.js
@@ -24,6 +24,8 @@ L.Map.WOPI = L.Handler.extend({
 	EnableShare: false,
 	HideUserList: null,
 	CallPythonScriptSource: null,
+	SupportsRename: false,
+	UserCanRename: false,
 
 	_appLoadedConditions: {
 		docloaded: false,
@@ -79,6 +81,8 @@ L.Map.WOPI = L.Handler.extend({
 		this.DisableInactiveMessages = !!wopiInfo['DisableInactiveMessages'];
 		this.UserCanNotWriteRelative = !!wopiInfo['UserCanNotWriteRelative'];
 		this.EnableInsertRemoteImage = !!wopiInfo['EnableInsertRemoteImage'];
+		this.SupportsRename = !!wopiInfo['SupportsRename'];
+		this.UserCanRename = !!wopiInfo['UserCanRename'];
 		this.EnableShare = !!wopiInfo['EnableShare'];
 		if (wopiInfo['HideUserList'])
 			this.HideUserList = wopiInfo['HideUserList'].split(',');
diff --git a/test/Makefile.am b/test/Makefile.am
index c43e14dd6..bf6d5a473 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -20,7 +20,7 @@ noinst_LTLIBRARIES = \
 	unit-fuzz.la unit-oob.la unit-oauth.la \
 	unit-wopi.la unit-wopi-saveas.la \
 	unit-wopi-ownertermination.la unit-wopi-versionrestore.la \
-	unit-wopi-documentconflict.la
+	unit-wopi-documentconflict.la unit_wopi_renamefile.la
 
 
 MAGIC_TO_FORCE_SHLIB_CREATION = -rpath /dummy
@@ -106,6 +106,8 @@ unit_wopi_versionrestore_la_SOURCES = UnitWOPIVersionRestore.cpp
 unit_wopi_versionrestore_la_LIBADD = $(CPPUNIT_LIBS)
 unit_wopi_documentconflict_la_SOURCES = UnitWOPIDocumentConflict.cpp
 unit_wopi_documentconflict_la_LIBADD = $(CPPUNIT_LIBS)
+unit_wopi_renamefile_la_SOURCES = UnitWOPIRenameFile.cpp
+unit_wopi_renamefile_la_LIBADD = $(CPPUNIT_LIBS)
 
 if HAVE_LO_PATH
 SYSTEM_STAMP = @SYSTEMPLATE_PATH@/system_stamp
@@ -123,7 +125,7 @@ check-local:
 TESTS = unit-convert.la unit-prefork.la unit-tilecache.la unit-timeout.la \
         unit-oauth.la unit-wopi.la unit-wopi-saveas.la \
         unit-wopi-ownertermination.la unit-wopi-versionrestore.la \
-        unit-wopi-documentconflict.la
+        unit-wopi-documentconflict.la unit_wopi_renamefile.la
 # TESTS = unit-client.la
 # TESTS += unit-admin.la
 # TESTS += unit-storage.la
diff --git a/test/UnitWOPIRenameFile.cpp b/test/UnitWOPIRenameFile.cpp
new file mode 100644
index 000000000..04d1fc6d6
--- /dev/null
+++ b/test/UnitWOPIRenameFile.cpp
@@ -0,0 +1,90 @@
+/* -*- 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 <WopiTestServer.hpp>
+#include <Log.hpp>
+#include <Unit.hpp>
+#include <UnitHTTP.hpp>
+#include <helpers.hpp>
+#include <Poco/Net/HTTPRequest.h>
+#include <Poco/Util/LayeredConfiguration.h>
+
+class UnitWOPIRenameFile : public WopiTestServer
+{
+    enum class Phase
+    {
+        Load,
+        RenameFile,
+        Polling
+    } _phase;
+
+public:
+    UnitWOPIRenameFile() :
+        _phase(Phase::Load)
+    {
+    }
+
+    void assertRenameFileRequest(const Poco::Net::HTTPRequest& request) override
+    {
+        // spec says UTF-7...
+        CPPUNIT_ASSERT_EQUAL(std::string("hello"), request.get("X-WOPI-RequestedName"));
+    }
+
+    bool filterSendMessage(const char* data, const size_t len, const WSOpCode /* code */, const bool /* flush */, int& /*unitReturn*/) override
+    {
+        const std::string message(data, len);
+
+        const std::string expected("renamefile: filename=hello");
+        if (message.find(expected) == 0)
+        {
+            // successfully exit the test if we also got the outgoing message
+            // notifying about saving the file
+            exitTest(TestResult::Ok);
+        }
+
+        return false;
+    }
+
+    void invokeTest() override
+    {
+        constexpr char testName[] = "UnitWOPIRenameFile";
+
+        switch (_phase)
+        {
+            case Phase::Load:
+            {
+                initWebsocket("/wopi/files/0?access_token=anything");
+
+                helpers::sendTextFrame(*_ws->getLOOLWebSocket(), "load url=" + _wopiSrc, testName);
+                _phase = Phase::RenameFile;
+                break;
+            }
+            case Phase::RenameFile:
+            {
+                helpers::sendTextFrame(*_ws->getLOOLWebSocket(), "renamefile filename=hello", testName);
+                _phase = Phase::Polling;
+                break;
+            }
+            case Phase::Polling:
+            {
+                // just wait for the results
+                break;
+            }
+        }
+    }
+};
+
+UnitBase *unit_create_wsd(void)
+{
+    return new UnitWOPIRenameFile();
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/test/WopiTestServer.hpp b/test/WopiTestServer.hpp
index 3e95c3b8b..b71212f0f 100644
--- a/test/WopiTestServer.hpp
+++ b/test/WopiTestServer.hpp
@@ -86,6 +86,10 @@ public:
     {
     }
 
+    virtual void assertRenameFileRequest(const Poco::Net::HTTPRequest& /*request*/)
+    {
+    }
+
 protected:
     /// Here we act as a WOPI server, so that we have a server that responds to
     /// the wopi requests without additional expensive setup.
@@ -160,13 +164,22 @@ protected:
         else if (request.getMethod() == "POST" && (uriReq.getPath() == "/wopi/files/0" || uriReq.getPath() == "/wopi/files/1"))
         {
             LOG_INF("Fake wopi host request, handling PutRelativeFile: " << uriReq.getPath());
-
-            CPPUNIT_ASSERT_EQUAL(std::string("PUT_RELATIVE"), request.get("X-WOPI-Override"));
-
-            assertPutRelativeFileRequest(request);
-
             std::string wopiURL = helpers::getTestServerURI() + "/something wopi/files/1?access_token=anything";
-            std::string content = "{ \"Name\":\"hello world%1.pdf\", \"Url\":\"" + wopiURL + "\" }";
+            std::string content;
+
+            if(request.get("X-WOPI-Override") == std::string("PUT_RELATIVE"))
+            {
+                CPPUNIT_ASSERT_EQUAL(std::string("PUT_RELATIVE"), request.get("X-WOPI-Override"));
+                assertPutRelativeFileRequest(request);
+                content = "{ \"Name\":\"hello world%1.pdf\", \"Url\":\"" + wopiURL + "\" }";
+            }
+            else
+            {
+                // rename file; response should be the file name without the url and the extension
+                CPPUNIT_ASSERT_EQUAL(std::string("RENAME_FILE"), request.get("X-WOPI-Override"));
+                assertRenameFileRequest(request);
+                content = "{ \"Name\":\"hello\", \"Url\":\"" + wopiURL + "\" }";
+            }
 
             std::ostringstream oss;
             oss << "HTTP/1.1 200 OK\r\n"
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 665056f30..fd5e159ca 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -177,7 +177,8 @@ bool ClientSession::_handleInput(const char *buffer, int length)
              tokens[0] != "uploadsigneddocument" &&
              tokens[0] != "exportsignanduploaddocument" &&
              tokens[0] != "rendershapeselection" &&
-             tokens[0] != "removesession")
+             tokens[0] != "removesession" &&
+             tokens[0] != "renamefile")
     {
         sendTextFrame("error: cmd=" + tokens[0] + " kind=unknown");
         return false;
@@ -368,6 +369,19 @@ bool ClientSession::_handleInput(const char *buffer, int length)
         docBroker->broadcastMessage(firstLine);
         docBroker->removeSession(sessionId);
     }
+    else if (tokens[0] == "renamefile") {
+        std::string encodedWopiFilename;
+        if (!getTokenString(tokens[1], "filename", encodedWopiFilename))
+        {
+            LOG_ERR("Bad syntax for: " << firstLine);
+            sendTextFrame("error: cmd=renamefile kind=syntax");
+            return false;
+        }
+        std::string wopiFilename;
+        Poco::URI::decode(encodedWopiFilename, wopiFilename);
+        docBroker->saveAsToStorage(getId(), "", wopiFilename, true);
+        return true;
+    }
     else
     {
         if (tokens[0] == "key")
@@ -839,7 +853,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
             {
                 // this also sends the saveas: result
                 LOG_TRC("Save-as path: " << resultURL.getPath());
-                docBroker->saveAsToStorage(getId(), resultURL.getPath(), wopiFilename);
+                docBroker->saveAsToStorage(getId(), resultURL.getPath(), wopiFilename, false);
             }
             else
                 sendTextFrame("error: cmd=storage kind=savefailed");
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index 31d952101..7731776a8 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -558,6 +558,8 @@ bool DocumentBroker::load(const std::shared_ptr<ClientSession>& session, const s
         wopiInfo->set("EnableInsertRemoteImage", wopifileinfo->getEnableInsertRemoteImage());
         wopiInfo->set("EnableShare", wopifileinfo->getEnableShare());
         wopiInfo->set("HideUserList", wopifileinfo->getHideUserList());
+        wopiInfo->set("SupportsRename", wopifileinfo->getSupportsRename());
+        wopiInfo->set("UserCanRename", wopifileinfo->getUserCanRename());
         if (wopifileinfo->getHideChangeTrackingControls() != WopiStorage::WOPIFileInfo::TriState::Unset)
             wopiInfo->set("HideChangeTrackingControls", wopifileinfo->getHideChangeTrackingControls() == WopiStorage::WOPIFileInfo::TriState::True);
 
@@ -780,16 +782,16 @@ bool DocumentBroker::saveToStorage(const std::string& sessionId,
     return res;
 }
 
-bool DocumentBroker::saveAsToStorage(const std::string& sessionId, const std::string& saveAsPath, const std::string& saveAsFilename)
+bool DocumentBroker::saveAsToStorage(const std::string& sessionId, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename)
 {
     assertCorrectThread();
 
-    return saveToStorageInternal(sessionId, true, "", saveAsPath, saveAsFilename);
+    return saveToStorageInternal(sessionId, true, "", saveAsPath, saveAsFilename, isRename);
 }
 
 bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
                                            bool success, const std::string& result,
-                                           const std::string& saveAsPath, const std::string& saveAsFilename)
+                                           const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename)
 {
     assertCorrectThread();
 
@@ -802,7 +804,7 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
     // notify the waiting thread, if any.
     LOG_TRC("Saving to storage docKey [" << _docKey << "] for session [" << sessionId <<
             "]. Success: " << success << ", result: " << result);
-    if (!success && result == "unmodified")
+    if (!success && result == "unmodified" && !isRename)
     {
         LOG_DBG("Save skipped as document [" << _docKey << "] was not modified.");
         _lastSaveTime = std::chrono::steady_clock::now();
@@ -839,7 +841,7 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
 
     // If the file timestamp hasn't changed, skip saving.
     const Poco::Timestamp newFileModifiedTime = Poco::File(_storage->getRootFilePath()).getLastModified();
-    if (!isSaveAs && newFileModifiedTime == _lastFileModifiedTime)
+    if (!isSaveAs && newFileModifiedTime == _lastFileModifiedTime && !isRename)
     {
         // Nothing to do.
         LOG_DBG("Skipping unnecessary saving to URI [" << uriAnonym << "] with docKey [" << _docKey <<
@@ -851,10 +853,10 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
     LOG_DBG("Persisting [" << _docKey << "] after saving to URI [" << uriAnonym << "].");
 
     assert(_storage && _tileCache);
-    StorageBase::SaveResult storageSaveResult = _storage->saveLocalFileToStorage(auth, saveAsPath, saveAsFilename);
+    StorageBase::SaveResult storageSaveResult = _storage->saveLocalFileToStorage(auth, saveAsPath, saveAsFilename, isRename);
     if (storageSaveResult.getResult() == StorageBase::SaveResult::OK)
     {
-        if (!isSaveAs)
+        if (!isSaveAs && !isRename)
         {
             // Saved and stored; update flags.
             setModified(false);
@@ -874,6 +876,19 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
             // Resume polling.
             _poll->wakeup();
         }
+        else if (isRename)
+        {
+            // encode the name
+            const std::string filename = storageSaveResult.getSaveAsName();
+            const std::string url = Poco::URI(storageSaveResult.getSaveAsUrl()).toString();
+            std::string encodedName;
+            Poco::URI::encode(filename, "", encodedName);
+            const std::string filenameAnonym = LOOLWSD::anonymizeUrl(filename);
+
+            std::ostringstream oss;
+            oss << "renamefile: " << "filename=" << encodedName << " url=" << url;
+            broadcastMessage(oss.str());
+        }
         else
         {
             // normalize the url (mainly to " " -> "%20")
@@ -921,7 +936,9 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
     {
         //TODO: Should we notify all clients?
         LOG_ERR("Failed to save docKey [" << _docKey << "] to URI [" << uriAnonym << "]. Notifying client.");
-        it->second->sendTextFrame("error: cmd=storage kind=savefailed");
+        std::ostringstream oss;
+        oss << "error: cmd=storage kind=" << (isRename ? "renamefailed" : "savefailed");
+        it->second->sendTextFrame(oss.str());
     }
     else if (storageSaveResult.getResult() == StorageBase::SaveResult::DOC_CHANGED)
     {
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 6d5a3e63b..db63b9cfc 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -246,7 +246,7 @@ public:
 
     /// Save As the document to Storage.
     /// @param saveAsPath Absolute path to the jailed file.
-    bool saveAsToStorage(const std::string& sesionId, const std::string& saveAsPath, const std::string& saveAsFilename);
+    bool saveAsToStorage(const std::string& sesionId, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename);
 
     bool isModified() const { return _isModified; }
     void setModified(const bool value);
@@ -370,7 +370,7 @@ private:
     void terminateChild(const std::string& closeReason);
 
     /// Saves the doc to the storage.
-    bool saveToStorageInternal(const std::string& sesionId, bool success, const std::string& result = "", const std::string& saveAsPath = std::string(), const std::string& saveAsFilename = std::string());
+    bool saveToStorageInternal(const std::string& sesionId, bool success, const std::string& result = "", const std::string& saveAsPath = std::string(), const std::string& saveAsFilename = std::string(), const bool isRename = false);
 
     /// True iff a save is in progress (requested but not completed).
     bool isSaving() const { return _lastSaveResponseTime < _lastSaveRequestTime; }
diff --git a/wsd/Storage.cpp b/wsd/Storage.cpp
index a5ebcdc39..387a03b50 100644
--- a/wsd/Storage.cpp
+++ b/wsd/Storage.cpp
@@ -336,7 +336,7 @@ std::string LocalStorage::loadStorageFileToLocal(const Authorization& /*auth*/)
 
 }
 
-StorageBase::SaveResult LocalStorage::saveLocalFileToStorage(const Authorization& /*auth*/, const std::string& /*saveAsPath*/, const std::string& /*saveAsFilename*/)
+StorageBase::SaveResult LocalStorage::saveLocalFileToStorage(const Authorization& /*auth*/, const std::string& /*saveAsPath*/, const std::string& /*saveAsFilename*/, bool /*isRename*/)
 {
     try
     {
@@ -499,6 +499,8 @@ std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Au
     bool userCanNotWriteRelative = true;
     bool enableInsertRemoteImage = false;
     bool enableShare = false;
+    bool supportsRename = false;
+    bool userCanRename = false;
     std::string hideUserList("false");
     WOPIFileInfo::TriState disableChangeTrackingRecord = WOPIFileInfo::TriState::Unset;
     WOPIFileInfo::TriState disableChangeTrackingShow = WOPIFileInfo::TriState::Unset;
@@ -585,6 +587,8 @@ std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Au
         JsonUtil::findJSONValue(object, "EnableInsertRemoteImage", enableInsertRemoteImage);
         JsonUtil::findJSONValue(object, "EnableShare", enableShare);
         JsonUtil::findJSONValue(object, "HideUserList", hideUserList);
+        JsonUtil::findJSONValue(object, "SupportsRename", supportsRename);
+        JsonUtil::findJSONValue(object, "UserCanRename", userCanRename);
         bool booleanFlag = false;
         if (JsonUtil::findJSONValue(object, "DisableChangeTrackingRecord", booleanFlag))
             disableChangeTrackingRecord = (booleanFlag ? WOPIFileInfo::TriState::True : WOPIFileInfo::TriState::False);
@@ -614,7 +618,7 @@ std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Au
          enableOwnerTermination, disablePrint, disableExport, disableCopy,
          disableInactiveMessages, userCanNotWriteRelative, enableInsertRemoteImage, enableShare,
          hideUserList, disableChangeTrackingShow, disableChangeTrackingRecord,
-         hideChangeTrackingControls, callDuration}));
+         hideChangeTrackingControls, supportsRename, userCanRename, callDuration}));
 }
 
 /// uri format: http://server/<...>/wopi*/files/<id>/content
@@ -692,7 +696,7 @@ std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth)
     return "";
 }
 
-StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename)
+StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename)
 {
     // TODO: Check if this URI has write permission (canWrite = true)
 
@@ -703,7 +707,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
     const size_t size = getFileSize(filePath);
 
     Poco::URI uriObject(getUri());
-    uriObject.setPath(isSaveAs? uriObject.getPath(): uriObject.getPath() + "/contents");
+    uriObject.setPath(isSaveAs || isRename? uriObject.getPath(): uriObject.getPath() + "/contents");
     auth.authorizeURI(uriObject);
     const std::string uriAnonym = LOOLWSD::anonymizeUrl(uriObject.toString());
 
@@ -718,7 +722,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
         request.set("User-Agent", WOPI_AGENT_STRING);
         auth.authorizeRequest(request);
 
-        if (!isSaveAs)
+        if (!isSaveAs && !isRename)
         {
             // normal save
             request.set("X-WOPI-Override", "PUT");
@@ -736,9 +740,6 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
         }
         else
         {
-            // save as
-            request.set("X-WOPI-Override", "PUT_RELATIVE");
-
             // the suggested target has to be in UTF-7; default to extension
             // only when the conversion fails
             std::string suggestedTarget = "." + Poco::Path(saveAsFilename).getExtension();
@@ -766,9 +767,19 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
                 }
             }
 
-            request.set("X-WOPI-SuggestedTarget", suggestedTarget);
-
-            request.set("X-WOPI-Size", std::to_string(size));
+            if (isRename)
+            {
+                // rename file
+                request.set("X-WOPI-Override", "RENAME_FILE");
+                request.set("X-WOPI-RequestedName", suggestedTarget);
+            }
+            else
+            {
+                // save as
+                request.set("X-WOPI-Override", "PUT_RELATIVE");
+                request.set("X-WOPI-Size", std::to_string(size));
+                request.set("X-WOPI-SuggestedTarget", suggestedTarget);
+            }
         }
 
         request.setContentType("application/octet-stream");
@@ -786,7 +797,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
         Poco::StreamCopier::copyStream(rs, oss);
         std::string responseString = oss.str();
 
-        const std::string wopiLog(isSaveAs ? "WOPI::PutRelativeFile" : "WOPI::PutFile");
+        const std::string wopiLog(isSaveAs ? "WOPI::PutRelativeFile" : (isRename ? "WOPI::RenameFile":"WOPI::PutFile"));
 
         if (Log::infoEnabled())
         {
@@ -834,7 +845,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
                 LOG_TRC(wopiLog << " returns LastModifiedTime [" << lastModifiedTime << "].");
                 getFileInfo().setModifiedTime(iso8601ToTimestamp(lastModifiedTime, "LastModifiedTime"));
 
-                if (isSaveAs)
+                if (isSaveAs || isRename)
                 {
                     const std::string name = JsonUtil::getJSONValue<std::string>(object, "Name");
                     LOG_TRC(wopiLog << " returns Name [" << LOOLWSD::anonymizeUrl(name) << "].");
@@ -844,7 +855,6 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
 
                     saveResult.setSaveAsResult(name, url);
                 }
-
                 // Reset the force save flag now, if any, since we are done saving
                 // Next saves shouldn't be saved forcefully unless commanded
                 forceSave(false);
@@ -897,7 +907,7 @@ std::string WebDAVStorage::loadStorageFileToLocal(const Authorization& /*auth*/)
     return getUri().toString();
 }
 
-StorageBase::SaveResult WebDAVStorage::saveLocalFileToStorage(const Authorization& /*auth*/, const std::string& /*saveAsPath*/, const std::string& /*saveAsFilename*/)
+StorageBase::SaveResult WebDAVStorage::saveLocalFileToStorage(const Authorization& /*auth*/, const std::string& /*saveAsPath*/, const std::string& /*saveAsFilename*/, bool /*isRename*/)
 {
     // TODO: implement webdav PUT.
     return StorageBase::SaveResult(StorageBase::SaveResult::OK);
diff --git a/wsd/Storage.hpp b/wsd/Storage.hpp
index bf228721a..02539b8f1 100644
--- a/wsd/Storage.hpp
+++ b/wsd/Storage.hpp
@@ -193,7 +193,7 @@ public:
 
     /// Writes the contents of the file back to the source.
     /// @param savedFile When the operation was saveAs, this is the path to the file that was saved.
-    virtual SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename) = 0;
+    virtual SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) = 0;
 
     static size_t getFileSize(const std::string& filename);
 
@@ -274,7 +274,7 @@ public:
 
     std::string loadStorageFileToLocal(const Authorization& auth) override;
 
-    SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename) override;
+    SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) override;
 
 private:
     /// True if the jailed file is not linked but copied.
@@ -329,6 +329,8 @@ public:
                      const TriState disableChangeTrackingShow,
                      const TriState disableChangeTrackingRecord,
                      const TriState hideChangeTrackingControls,
+                     const bool supportsRename,
+                     const bool userCanRename,
                      const std::chrono::duration<double> callDuration)
             : _userId(userid),
               _obfuscatedUserId(obfuscatedUserId),
@@ -352,6 +354,8 @@ public:
               _disableChangeTrackingShow(disableChangeTrackingShow),
               _disableChangeTrackingRecord(disableChangeTrackingRecord),
               _hideChangeTrackingControls(hideChangeTrackingControls),
+              _supportsRename(supportsRename),
+              _userCanRename(userCanRename),
               _callDuration(callDuration)
             {
                 _userExtraInfo = userExtraInfo;
@@ -397,6 +401,10 @@ public:
 
         bool getEnableShare() const { return _enableShare; }
 
+        bool getSupportsRename() const { return _supportsRename; }
+
+        bool getUserCanRename() const { return _userCanRename; }
+
         std::string& getHideUserList() { return _hideUserList; }
 
         TriState getDisableChangeTrackingShow() const { return _disableChangeTrackingShow; }
@@ -456,6 +464,10 @@ public:
         TriState _disableChangeTrackingRecord;
         /// If we should hide change-tracking commands for this user.
         TriState _hideChangeTrackingControls;
+        /// If WOPI host supports rename
+        bool _supportsRename;
+        /// If user is allowed to rename the document
+        bool _userCanRename;
 
         /// Time it took to call WOPI's CheckFileInfo
         std::chrono::duration<double> _callDuration;
@@ -470,7 +482,7 @@ public:
     /// uri format: http://server/<...>/wopi*/files/<id>/content
     std::string loadStorageFileToLocal(const Authorization& auth) override;
 
-    SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename) override;
+    SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) override;
 
     /// Total time taken for making WOPI calls during load
     std::chrono::duration<double> getWopiLoadDuration() const { return _wopiLoadDuration; }
@@ -500,7 +512,7 @@ public:
 
     std::string loadStorageFileToLocal(const Authorization& auth) override;
 
-    SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename) override;
+    SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename, const bool isRename) override;
 
 private:
     std::unique_ptr<AuthBase> _authAgent;
commit f8969c67c975cbc91761c655b133352eb6a68614
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri May 3 13:20:14 2019 +0100
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:21:57 2019 +0200

    Fix strange wire-id hash issues.
    
    Remove un-used hash; and ensure that the hash <-> wid mapping is
    suitably unique and helpful, even for duplicate identical tiles in
    a single tilecombine.
    
    Change-Id: I9c752032231e164765615a7c0d6338d6ff7e3fd5
    Reviewed-on: https://gerrit.libreoffice.org/71740
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>

diff --git a/kit/Kit.cpp b/kit/Kit.cpp
index 228af098e..05dbdc722 100644
--- a/kit/Kit.cpp
+++ b/kit/Kit.cpp
@@ -433,13 +433,15 @@ class PngCache
     size_t _cacheSize;
     static const size_t CacheSizeSoftLimit = (1024 * 4 * 32); // 128k of cache
     static const size_t CacheSizeHardLimit = CacheSizeSoftLimit * 2;
+    static const size_t CacheWidHardLimit = 4096;
     size_t _cacheHits;
     size_t _cacheTests;
     TileWireId _nextId;
     DeltaGenerator _deltaGen;
 
-    std::map< TileBinaryHash, CacheEntry > _cache;
-    std::map< TileWireId, TileBinaryHash > _wireToHash;
+    std::unordered_map< TileBinaryHash, CacheEntry > _cache;
+    // This uses little storage so can be much larger
+    std::unordered_map< TileBinaryHash, TileWireId > _hashToWireId;
 
     void clearCache(bool logStats = false)
     {
@@ -447,6 +449,7 @@ class PngCache
             LOG_DBG("cache clear " << _cache.size() << " items total size " <<
                     _cacheSize << " current hits " << _cacheHits);
         _cache.clear();
+        _hashToWireId.clear();
         _cacheSize = 0;
         _cacheHits = 0;
         _cacheTests = 0;
@@ -486,11 +489,6 @@ class PngCache
                     // Shrink cache when we exceed the size to maximize
                     // the chance of hitting these entries in the future.
                     _cacheSize -= it->second.getData()->size();
-
-                    auto wIt = _wireToHash.find(it->second.getWireId());
-                    assert(wIt != _wireToHash.end());
-                    _wireToHash.erase(wIt);
-
                     it = _cache.erase(it);
                 }
                 else
@@ -504,6 +502,20 @@ class PngCache
             LOG_DBG("cache " << _cache.size() << " items total size " <<
                     _cacheSize << " after balance");
         }
+
+        if (_hashToWireId.size() > CacheWidHardLimit)
+        {
+            LOG_DBG("Clear half of wid cache of size " << _hashToWireId.size());
+            TileWireId max = _nextId - CacheWidHardLimit/2;
+            for (auto it = _hashToWireId.begin(); it != _hashToWireId.end();)
+            {
+                if (it->second < max)
+                    it = _hashToWireId.erase(it);
+                else
+                    ++it;
+            }
+            LOG_DBG("Wid cache is now size " << _hashToWireId.size());
+        }
     }
 
     /// Lookup an entry in the cache and store the data in output.
@@ -574,18 +586,18 @@ public:
         clearCache();
     }
 
-    TileWireId hashToWireId(TileBinaryHash id)
+    TileWireId hashToWireId(TileBinaryHash hash)
     {
         TileWireId wid;
-        if (id == 0)
+        if (hash == 0)
             return 0;
-        auto it = _cache.find(id);
-        if (it != _cache.end())
-            wid = it->second.getWireId();
+        auto it = _hashToWireId.find(hash);
+        if (it != _hashToWireId.end())
+            wid = it->second;
         else
         {
             wid = createNewWireId();
-            _wireToHash.emplace(wid, id);
+            _hashToWireId.emplace(hash, wid);
         }
         return wid;
     }
commit 6351f08062923d49a532334335fc58b82e377c0c
Author:     Henry Castro <hcastro at collabora.com>
AuthorDate: Mon Apr 15 14:34:50 2019 -0400
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:21:41 2019 +0200

    loleaflet: fix sending 'compositionupdate' event data
    
    Change-Id: I42142f005d0659b64e5332b5acec52f9ac18a89b
    Reviewed-on: https://gerrit.libreoffice.org/70793
    Reviewed-by: Henry Castro <hcastro at collabora.com>
    Tested-by: Henry Castro <hcastro at collabora.com>
    Reviewed-on: https://gerrit.libreoffice.org/71738
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>

diff --git a/loleaflet/src/map/handler/Map.Keyboard.js b/loleaflet/src/map/handler/Map.Keyboard.js
index b532c16a7..4e3b51354 100644
--- a/loleaflet/src/map/handler/Map.Keyboard.js
+++ b/loleaflet/src/map/handler/Map.Keyboard.js
@@ -399,14 +399,11 @@ L.Map.Keyboard = L.Handler.extend({
 	},
 
 	_onIME: function (e) {
-		if (e.type === 'compositionstart' || e.type === 'compositionupdate') {
+		if (e.type === 'compositionstart') {
 			this._isComposing = true; // we are starting composing with IME
-			if (e.originalEvent.data.length > 0) {
-				this._map._docLayer._postCompositionEvent(0, 'input', e.originalEvent.data);
-			}
-		}
-
-		if (e.type === 'compositionend') {
+		} else if (e.type === 'compositionupdate') {
+			this._map._docLayer._postCompositionEvent(0, 'input', e.originalEvent.data);
+		} else if (e.type === 'compositionend') {
 			this._isComposing = false; // stop of composing with IME
 			// get the composited char codes
 			// clear the input now - best to do this ASAP so the input
commit 297023fa86d156ea8474af062c99ff9bb07177de
Author:     Henry Castro <hcastro at collabora.com>
AuthorDate: Thu Apr 11 14:12:32 2019 -0400
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:21:27 2019 +0200

    loleaflet: fix the input method when receives 'compositionend' event
    
    Change-Id: I296a714f41afb8a497e21661db0edf23d270b821
    Reviewed-on: https://gerrit.libreoffice.org/70607
    Reviewed-by: Henry Castro <hcastro at collabora.com>
    Tested-by: Henry Castro <hcastro at collabora.com>
    Reviewed-on: https://gerrit.libreoffice.org/71737
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>

diff --git a/loleaflet/src/map/handler/Map.Keyboard.js b/loleaflet/src/map/handler/Map.Keyboard.js
index 863473bae..b532c16a7 100644
--- a/loleaflet/src/map/handler/Map.Keyboard.js
+++ b/loleaflet/src/map/handler/Map.Keyboard.js
@@ -401,12 +401,8 @@ L.Map.Keyboard = L.Handler.extend({
 	_onIME: function (e) {
 		if (e.type === 'compositionstart' || e.type === 'compositionupdate') {
 			this._isComposing = true; // we are starting composing with IME
-			var txt = '';
-			for (var i = 0; i < e.originalEvent.data.length; i++) {
-				txt += e.originalEvent.data[i];
-			}
-			if (txt) {
-				this._map._docLayer._postCompositionEvent(0, 'input', txt);
+			if (e.originalEvent.data.length > 0) {
+				this._map._docLayer._postCompositionEvent(0, 'input', e.originalEvent.data);
 			}
 		}
 
@@ -417,10 +413,10 @@ L.Map.Keyboard = L.Handler.extend({
 			// is clear for the next word
 			this._map._clipboardContainer.setValue('');
 			// Set all keycodes to zero
-			this._map._docLayer._postCompositionEvent(0, 'end', '');
+			this._map._docLayer._postCompositionEvent(0, 'end', e.originalEvent.data);
 		}
 
-		if (e.type === 'textInput' && !this._keyHandled) {
+		if (e.type === 'textInput' && !this._keyHandled && !this._isComposing) {
 			// Hack for making space and spell-check text insert work
 			// in Chrome (on Andorid) or Chrome with IME.
 			//
commit edd197fcba364b2aa99c82c7738838b5e0640832
Author:     Iván Sánchez Ortega <ivan.sanchez at collabora.com>
AuthorDate: Thu May 2 09:28:55 2019 +0200
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:21:14 2019 +0200

    tdf#124749:loleaflet: use "KeyboardEvent.key" to detect ignored key events
    
    Replace KeyboardEvent.keyCode with KeyboardEvent.key for detection of "Delete" and
    "Insert" keys. keyCode misbehaves when using an AZERTY/DVORAK keyboard layout, e.g.
    the keyCode for "Delete" in QWERTY is the same as "." in AZERTY.
    
    This works on all major browsers, the only outlier being MSIE8:
    https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key#Browser_compatibility
    
    Change-Id: I5cbfa18ef59ab4989a866fdf4b5708610beccaad
    Reviewed-on: https://gerrit.libreoffice.org/71736
    Reviewed-by: Andras Timar <andras.timar at collabora.com>
    Tested-by: Andras Timar <andras.timar at collabora.com>

diff --git a/loleaflet/src/map/handler/Map.Keyboard.js b/loleaflet/src/map/handler/Map.Keyboard.js
index 28144cc53..863473bae 100644
--- a/loleaflet/src/map/handler/Map.Keyboard.js
+++ b/loleaflet/src/map/handler/Map.Keyboard.js
@@ -182,16 +182,23 @@ L.Map.Keyboard = L.Handler.extend({
 		this._map.off('compositionstart compositionupdate compositionend textInput', this._onIME, this);
 	},
 
+	/*
+	 * Returns true whenever the key event shall be ignored.
+	 * This means shift+insert and shift+delete (or "insert or delete when holding
+	 * shift down"). Those events are handled elsewhere to trigger "cut" and 
+	 * "paste" events, and need to be ignored in order to avoid double-handling them.
+	 */
 	_ignoreKeyEvent: function(e) {
-		var shift = e.originalEvent.shiftKey ? this.keyModifier.shift : 0;
-		if (shift && (e.originalEvent.keyCode === 45 || e.originalEvent.keyCode === 46)) {
-			// don't handle shift+insert, shift+delete
-			// These are converted to 'cut', 'paste' events which are
-			// automatically handled by us, so avoid double-handling
-			return true;
+		var shift = e.originalEvent.shiftKey;
+		if ('key' in e.originalEvent) {
+			var key = e.originalEvent.key;
+			return (shift && (key === 'Delete' || key === 'Insert'));
+		} else {
+			// keyCode is not reliable in AZERTY/DVORAK keyboard layouts, is used
+			// only as a fallback for MSIE8.
+			var keyCode = e.originalEvent.keyCode;
+			return (shift && (keyCode === 45 || keyCode === 46));
 		}
-
-		return false;
 	},
 
 	_setPanOffset: function (pan) {
commit 833304f5a78de80b98c2278d0aab54e9c51c99fe
Author:     Marco Cecchetti <mrcekets at gmail.com>
AuthorDate: Mon Mar 4 20:36:45 2019 +0100
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:20:59 2019 +0200

    leaflet: after resizing a shape, dragging cursor with mouse pans view
    
    The problem was in Path.Transform._apply executed at scale/rotate end
    which enabled map dragging unconditionally.
    
    Change-Id: Id42dc7de397a2ca2774f9d31a698c32b5e1c8514
    Reviewed-on: https://gerrit.libreoffice.org/70559
    Reviewed-by: Henry Castro <hcastro at collabora.com>
    Tested-by: Henry Castro <hcastro at collabora.com>

diff --git a/loleaflet/plugins/path-transform/src/Path.Transform.js b/loleaflet/plugins/path-transform/src/Path.Transform.js
index f7efd72cc..3ea9ddaab 100644
--- a/loleaflet/plugins/path-transform/src/Path.Transform.js
+++ b/loleaflet/plugins/path-transform/src/Path.Transform.js
@@ -282,6 +282,7 @@ L.Handler.PathTransform = L.Handler.extend({
 		var matrix = this._matrix.clone();
 		var angle = this._angle;
 		var scale = this._scale.clone();
+		var moved = this._handleDragged;
 
 		this._transformGeometries();
 
@@ -299,7 +300,11 @@ L.Handler.PathTransform = L.Handler.extend({
 
 		this._updateHandlers();
 
-		map.dragging.enable();
+		if (this._mapDraggingWasEnabled) {
+			if (moved) L.DomEvent._fakeStop({ type: 'click' });
+			map.dragging.enable();
+		}
+
 		this._path.fire('transformed', {
 			matrix: matrix,
 			scale: scale,
@@ -576,7 +581,12 @@ L.Handler.PathTransform = L.Handler.extend({
 	_onRotateStart: function(evt) {
 		var map = this._map;
 
-		map.dragging.disable();
+		this._handleDragged = false;
+		this._mapDraggingWasEnabled = false;
+		if (map.dragging.enabled()) {
+			map.dragging.disable();
+			this._mapDraggingWasEnabled = true;
+		}
 
 		this._originMarker     = null;
 		this._rotationOriginPt = map.latLngToLayerPoint(this._getRotationOrigin());
@@ -604,6 +614,8 @@ L.Handler.PathTransform = L.Handler.extend({
 		var previous = this._rotationStart;
 		var origin   = this._rotationOriginPt;
 
+		this._handleDragged = true;
+
 		// rotation step angle
 		this._angle = Math.atan2(pos.y - origin.y, pos.x - origin.x) -
 			Math.atan2(previous.y - origin.y, previous.x - origin.x);
@@ -649,7 +661,12 @@ L.Handler.PathTransform = L.Handler.extend({
 		var marker = evt.target;
 		var map = this._map;
 
-		map.dragging.disable();
+		this._handleDragged = false;
+		this._mapDraggingWasEnabled = false;
+		if (map.dragging.enabled()) {
+			map.dragging.disable();
+			this._mapDraggingWasEnabled = true;
+		}
 
 		this._activeMarker = marker;
 
@@ -688,6 +705,9 @@ L.Handler.PathTransform = L.Handler.extend({
 	_onScale: function(evt) {
 		var originPoint = this._originMarker._point;
 		var ratioX, ratioY;
+
+		this._handleDragged = true;
+
 		if (this.options.uniformScaling) {
 			ratioX = originPoint.distanceTo(evt.layerPoint) / this._initialDist;
 			ratioY = ratioX;
commit f92af7a5ba98b682686886409d01e460c43ae42f
Author:     Szymon Kłos <szymon.klos at collabora.com>
AuthorDate: Wed Mar 13 18:05:14 2019 +0100
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:20:45 2019 +0200

    Show progressbar centered in the mobile
    
    When Online was embodied in the nextcloud app (Android)
    the progressbar was moved into top-left corner.
    During a loading all functions to get container
    or browser size were returning (0,0).
    This hack assumed that webview size is almost equal
    to the mobile screen size.
    
    Change-Id: I0fff53639a3baa88b57d91bf671e00ad8c71180d
    Reviewed-on: https://gerrit.libreoffice.org/69219
    Reviewed-by: Jan Holesovsky <kendy at collabora.com>
    Tested-by: Jan Holesovsky <kendy at collabora.com>

diff --git a/loleaflet/src/map/Map.js b/loleaflet/src/map/Map.js
index 6f213dd9d..4aad141c5 100644
--- a/loleaflet/src/map/Map.js
+++ b/loleaflet/src/map/Map.js
@@ -108,7 +108,14 @@ L.Map = L.Evented.extend({
 		}
 		this._addLayers(this.options.layers);
 		this._socket = L.socket(this);
-		this._progressBar = L.progressOverlay(this.getCenter(), L.point(150, 25));
+
+		var center = this.getCenter();
+		if (L.Browser.mobile) {
+			var doubledProgressHeight = 200;
+			var size = new L.point(screen.width, screen.height - doubledProgressHeight);
+			center = this.layerPointToLatLng(size._divideBy(2));
+		}
+		this._progressBar = L.progressOverlay(center, new L.point(150, 25));
 
 		if (L.Browser.mobile) {
 			this._clipboardContainer = L.control.mobileInput().addTo(this);
commit 3f63cab0fadc46018b1323b2049aad5ef43cec4e
Author:     Szymon Kłos <szymon.klos at collabora.com>
AuthorDate: Thu Apr 11 13:32:24 2019 +0200
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:20:31 2019 +0200

    Prevent view from jumping on zoom in/out on mobile
    
    Change-Id: I29262d518d61a4a06d66271033c958e56ff16a2b
    Reviewed-on: https://gerrit.libreoffice.org/70592
    Reviewed-by: Tor Lillqvist <tml at collabora.com>
    Tested-by: Tor Lillqvist <tml at collabora.com>

diff --git a/loleaflet/js/jquery.mCustomScrollbar.js b/loleaflet/js/jquery.mCustomScrollbar.js
index 442b20504..2785ec65c 100644
--- a/loleaflet/js/jquery.mCustomScrollbar.js
+++ b/loleaflet/js/jquery.mCustomScrollbar.js
@@ -284,6 +284,11 @@ and dependencies (minified).
 				*/
 				updateOnContentResize:true,
 				/*
+				prevent from updating view position after content resize to avoid jumping on mobile devices
+				values: boolean
+				*/
+				jumpOnContentResize:true,
+				/*
 				auto-update scrollbars each time each image inside the element is fully loaded 
 				values: "auto", boolean
 				*/
@@ -561,31 +566,37 @@ and dependencies (minified).
 						var to=[Math.abs(mCSB_container[0].offsetTop),Math.abs(mCSB_container[0].offsetLeft)];
 						if(o.axis!=="x"){ /* y/yx axis */
 							if(!d.overflowed[0]){ /* y scrolling is not required */
-								_resetContentPosition.call(this); /* reset content position */
+								if(o.advanced.jumpOnContentResize)
+									_resetContentPosition.call(this); /* reset content position */
 								if(o.axis==="y"){
 									_unbindEvents.call(this);
-								}else if(o.axis==="yx" && d.overflowed[1]){
+								}else if(o.axis==="yx" && d.overflowed[1] && o.advanced.jumpOnContentResize){
 									_scrollTo($this,to[1].toString(),{dir:"x",dur:0,overwrite:"none"});
 								}
-							}else if(mCSB_dragger[0].height()>mCSB_dragger[0].parent().height()){
+							}else if(mCSB_dragger[0].height()>mCSB_dragger[0].parent().height()
+									&& o.advanced.jumpOnContentResize){
 								_resetContentPosition.call(this); /* reset content position */
 							}else{ /* y scrolling is required */
-								_scrollTo($this,to[0].toString(),{dir:"y",dur:0,overwrite:"none"});
+								if(o.advanced.jumpOnContentResize)
+									_scrollTo($this,to[0].toString(),{dir:"y",dur:0,overwrite:"none"});
 								d.contentReset.y=null;
 							}
 						}
 						if(o.axis!=="y"){ /* x/yx axis */
 							if(!d.overflowed[1]){ /* x scrolling is not required */
-								_resetContentPosition.call(this); /* reset content position */
+								if(o.advanced.jumpOnContentResize)
+									_resetContentPosition.call(this); /* reset content position */
 								if(o.axis==="x"){
 									_unbindEvents.call(this);
-								}else if(o.axis==="yx" && d.overflowed[0]){
+								}else if(o.axis==="yx" && d.overflowed[0] && o.advanced.jumpOnContentResize){
 									_scrollTo($this,to[0].toString(),{dir:"y",dur:0,overwrite:"none"});
 								}
 							}else if(mCSB_dragger[1].width()>mCSB_dragger[1].parent().width()){
-								_resetContentPosition.call(this); /* reset content position */
+								if(o.advanced.jumpOnContentResize)
+									_resetContentPosition.call(this); /* reset content position */
 							}else{ /* x scrolling is required */
-								_scrollTo($this,to[1].toString(),{dir:"x",dur:0,overwrite:"none"});
+								if(o.advanced.jumpOnContentResize)
+									_scrollTo($this,to[1].toString(),{dir:"x",dur:0,overwrite:"none"});
 								d.contentReset.x=null;
 							}
 						}
diff --git a/loleaflet/src/control/Control.Scroll.js b/loleaflet/src/control/Control.Scroll.js
index e8df36eb8..adb85d55b 100644
--- a/loleaflet/src/control/Control.Scroll.js
+++ b/loleaflet/src/control/Control.Scroll.js
@@ -31,7 +31,10 @@ L.Control.Scroll = L.Control.extend({
 			axis: 'yx',
 			theme: 'minimal-dark',
 			scrollInertia: 0,
-			advanced:{autoExpandHorizontalScroll: true}, /* weird bug, it should be false */
+			advanced:{
+				autoExpandHorizontalScroll: true, /* weird bug, it should be false */
+				jumpOnContentResize: false /* prevent from jumping on a mobile devices */
+			},
 			callbacks:{
 				onScrollStart: function() {
 					control._map.fire('closepopup');
commit 545b2e4262844dacfd8c375cefb91abd9339379f
Author:     Jan Holesovsky <kendy at collabora.com>
AuthorDate: Sat Mar 30 19:12:19 2019 +0100
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:20:17 2019 +0200

    Use the app name more generally, not only on iOS.
    
    Preparation for using it on Android too.
    
    Change-Id: Iee7778b2625a02a98daff5df87c39f4ab1d18144
    Reviewed-on: https://gerrit.libreoffice.org/70651
    Reviewed-by: Tor Lillqvist <tml at collabora.com>
    Tested-by: Tor Lillqvist <tml at collabora.com>

diff --git a/configure.ac b/configure.ac
index dace37db4..5f60b3d23 100644
--- a/configure.ac
+++ b/configure.ac
@@ -69,10 +69,6 @@ AC_ARG_ENABLE([iosapp],
                               to be copied to a Mac where the iOS app is being built, *or* in a tree where
                               you will build the iOS app.]))
 
-AC_ARG_WITH([iosapp-name],
-              AS_HELP_STRING([--with-iosapp-name=<name>],
-                             [Set the user-visible name of the iOS app you build. Default "Mobile".]))
-
 AC_ARG_WITH([iosapp-appicon],
               AS_HELP_STRING([--with-iosapp-appicon=<path>],
                              [Point to a directory containing an icon set to use instead of the default empty one.]))
@@ -88,6 +84,10 @@ AC_ARG_ENABLE([gtkapp],
                               to work similarly to the iOS app, from the JavaScript and the pseudo WebSocket
                               message plumbing point of view. See gtk/README.]))
 
+AC_ARG_WITH([app-name],
+              AS_HELP_STRING([--with-app-name=<name>],
+                             [Set the user-visible name of the app you build.]))
+
 AC_ARG_ENABLE([seccomp],
               AS_HELP_STRING([--disable-seccomp],
                              [Disable use of linux/seccomp.h header when kernel on target system does not support it.
@@ -224,15 +224,18 @@ if test -z "$anonym_msg";  then
   anonym_msg="no anonymization of usernames or filenames"
 fi
 
+APP_NAME="LOOL"
+if test -n "$with_app_name"; then
+   APP_NAME="$with_app_name"
+fi
+AC_DEFINE_UNQUOTED([APP_NAME],["$APP_NAME"],[The user-visible name of the app you build.])
+AC_SUBST(APP_NAME)
+
 ENABLE_IOSAPP=
-MOBILE_APP_NAME="Mobile"
 IOSAPP_BUNDLE_VERSION=
 
 if test "$enable_iosapp" = "yes"; then
    ENABLE_IOSAPP=true
-   if test -n "$with_iosapp_name"; then
-      MOBILE_APP_NAME="$with_iosapp_name"
-   fi
 
    if test -f BUNDLE-VERSION; then
        IOSAPP_BUNDLE_VERSION=$(cat BUNDLE-VERSION)
@@ -287,7 +290,6 @@ if test "$enable_iosapp" = "yes"; then
 fi
 AC_SUBST(ENABLE_IOSAPP)
 AM_CONDITIONAL([ENABLE_IOSAPP], [test "$ENABLE_IOSAPP" = "true"])
-AC_SUBST(MOBILE_APP_NAME)
 AC_SUBST(IOSAPP_BUNDLE_VERSION)
 
 ENABLE_GTKAPP=
diff --git a/ios/Mobile/Info.plist.in b/ios/Mobile/Info.plist.in
index 5dd1931e1..cb16bde94 100644
--- a/ios/Mobile/Info.plist.in
+++ b/ios/Mobile/Info.plist.in
@@ -34,7 +34,7 @@
     <key>CFBundleDevelopmentRegion</key>
     <string>$(DEVELOPMENT_LANGUAGE)</string>
     <key>CFBundleDisplayName</key>
-    <string>@MOBILE_APP_NAME@</string>
+    <string>@APP_NAME@</string>
     <key>CFBundleDocumentTypes</key>
     <array>
        <!-- Document sub-types are listed in order ODF, OOXML, MSO, other -->
diff --git a/ios/config.h.in b/ios/config.h.in
index 18ca58e4e..c1f501fe9 100644
--- a/ios/config.h.in
+++ b/ios/config.h.in
@@ -1,6 +1,8 @@
 /* config.h.  Manually edited from config.h.in.  */
 /* config.h.in.  Generated from configure.ac by autoheader.  */
 
+#define APP_NAME "@APP_NAME@"
+
 /* Whether to disable SECCOMP */
 #define DISABLE_SECCOMP 1
 
diff --git a/loleaflet/Makefile.am b/loleaflet/Makefile.am
index 2732e1928..258801b0e 100644
--- a/loleaflet/Makefile.am
+++ b/loleaflet/Makefile.am
@@ -244,7 +244,7 @@ $(builddir)/dist/loleaflet.html: $(srcdir)/html/loleaflet.html.m4 $(LOLEAFLET_HT
 	@m4 -E -DDEBUG=$(ENABLE_DEBUG) \
 		-DIOSAPP=$(ENABLE_IOSAPP) \
 		-DGTKAPP=$(ENABLE_GTKAPP) \
-		-DMOBILEAPPNAME="$(MOBILE_APP_NAME)" \
+		-DMOBILEAPPNAME="$(APP_NAME)" \
 		-DLOLEAFLET_CSS="$(subst $(SPACE),$(COMMA),$(LOLEAFLET_CSS_M4))" \
 		-DLOLEAFLET_JS="$(subst $(SPACE),$(COMMA),$(GLOBAL_JS) $(NODE_MODULES_JS) \
 		$(call LOLEAFLET_JS,$(srcdir)/build/build.js) \
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 9227f6f3f..4e7437d52 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2792,6 +2792,9 @@ private:
         // Hint to encourage use on mobile devices
         capabilities->set("hasMobileSupport", true);
 
+        // Set the product name
+        capabilities->set("productName", APP_NAME);
+
         std::ostringstream ostrJSON;
         capabilities->stringify(ostrJSON);
         return ostrJSON.str();
commit a1b480731332fa96473ca9b8f81b3446b36774c4
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Sat Mar 30 12:15:09 2019 -0400
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:20:04 2019 +0200

    leaflet: suppress empty error messages
    
    This also adds support to suppress errors
    by setting their message to blank.
    
    Change-Id: I2baabd121afb59c48e950b139f984c64d1720512
    Reviewed-on: https://gerrit.libreoffice.org/70032
    Reviewed-by: Michael Meeks <michael.meeks at collabora.com>
    Tested-by: Michael Meeks <michael.meeks at collabora.com>

diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js
index 76fd8d280..30cbe75ef 100644
--- a/loleaflet/src/core/Socket.js
+++ b/loleaflet/src/core/Socket.js
@@ -548,12 +548,15 @@ L.Socket = L.Class.extend({
 				return;
 			}
 
-			// Parse the storage url as link
-			var tmpLink = document.createElement('a');
-			tmpLink.href = this._map.options.doc;
-			// Insert the storage server address to be more friendly
-			storageError = storageError.replace('%storageserver', tmpLink.host);
-			this._map.fire('warn', {msg: storageError});
+			// Skip empty errors (and allow for suppressing errors by making them blank).
+			if (storageError != '') {
+				// Parse the storage url as link
+				var tmpLink = document.createElement('a');
+				tmpLink.href = this._map.options.doc;
+				// Insert the storage server address to be more friendly
+				storageError = storageError.replace('%storageserver', tmpLink.host);
+				this._map.fire('warn', {msg: storageError});
+			}
 
 			return;
 		}
commit 145c5758fc0cf814f371635fc0b5996a3eeb1eb7
Author:     Andras Timar <andras.timar at collabora.com>
AuthorDate: Wed Apr 3 16:26:51 2019 +0200
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:19:50 2019 +0200

    [cp] add copyright notice to About Box
    
    Change-Id: I51c2c5e7c2b0cf232b8a2f2dbcb3b9f3f04737ea
    Reviewed-on: https://gerrit.libreoffice.org/70205
    Reviewed-by: Michael Meeks <michael.meeks at collabora.com>
    Tested-by: Michael Meeks <michael.meeks at collabora.com>

diff --git a/loleaflet/html/loleaflet.html.m4 b/loleaflet/html/loleaflet.html.m4
index 736dd26a9..bc16fb254 100644
--- a/loleaflet/html/loleaflet.html.m4
+++ b/loleaflet/html/loleaflet.html.m4
@@ -4,6 +4,7 @@ dnl# foreachq(x, `item_1, item_2, ..., item_n', stmt)
 dnl# quoted list, alternate improved version
 define([foreachq],[ifelse([$2],[],[],[pushdef([$1])_$0([$1],[$3],[],$2)popdef([$1])])])dnl
 define([_foreachq],[ifelse([$#],[3],[],[define([$1],[$4])$2[]$0([$1],[$2],shift(shift(shift($@))))])])dnl
+define(_YEAR_,esyscmd(date +%Y|tr -d '\n'))
 <!DOCTYPE html>
 <!-- saved from url=(0054)http://leafletjs.com/examples/quick-start-example.html -->
 <html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
@@ -146,6 +147,7 @@ ifelse(MOBILEAPP,[true],
       <div id="loolwsd-version"></div>
       <h3>LOKit</h3>
       <div id="lokit-version"></div>
+      <p>Copyright _YEAR_, Collabora Productivity Limited.</p>
     </div>
 
     <script>
commit 2d0f5f0fd8e7f16df386c09e33ce97e3ac9cc55e
Author:     Szymon Kłos <szymon.klos at collabora.com>
AuthorDate: Wed Mar 20 09:34:19 2019 +0100
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:19:39 2019 +0200

    Avoid syntax errors while paring command results
    
    Change-Id: I7d778fd62aeb32c8304afb90c21aafd83379c170

diff --git a/loleaflet/src/layer/tile/WriterTileLayer.js b/loleaflet/src/layer/tile/WriterTileLayer.js
index f7bca842b..ece0ddfa4 100644
--- a/loleaflet/src/layer/tile/WriterTileLayer.js
+++ b/loleaflet/src/layer/tile/WriterTileLayer.js
@@ -54,7 +54,12 @@ L.WriterTileLayer = L.TileLayer.extend({
 	},
 
 	_onCommandValuesMsg: function (textMsg) {
-		var values = JSON.parse(textMsg.substring(textMsg.indexOf('{')));
+		var braceIndex = textMsg.indexOf('{');
+		if (braceIndex < 0) {
+			return;
+		}
+
+		var values = JSON.parse(textMsg.substring(braceIndex));
 		if (!values) {
 			return;
 		}
commit 87c76fdb9969ea93c45b42f1b6c70ed62fd2c86d
Author:     Szymon Kłos <szymon.klos at collabora.com>
AuthorDate: Sat Mar 23 14:51:27 2019 +0100
Commit:     Andras Timar <andras.timar at collabora.com>
CommitDate: Thu May 9 13:19:28 2019 +0200

    Make all pages accessible after zooming
    
    After zooming scrollbar was updated and scroll
    position was incorrect what caused first pages
    not accessible.
    
    Regression was introduced by:
    ffd7151443ee360c7764aaa77f9e7fe5f5d64eee
    
    Second problem was jumping to the cursor
    during zooming. Solution was to not update
    the cursors on zooming start.
    
    Change-Id: I0891799b03ed4eccb211ee43eb30e546317a90fc
    Reviewed-on: https://gerrit.libreoffice.org/69606
    Reviewed-by: Szymon Kłos <szymon.klos at collabora.com>
    Tested-by: Szymon Kłos <szymon.klos at collabora.com>

diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index 643a9ba06..f39102829 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -1593,14 +1593,12 @@ L.TileLayer = L.GridLayer.extend({
 
 	_onZoomStart: function () {
 		this._isZooming = true;
-		this._onUpdateCursor();
-		this.updateAllViewCursors();
 	},
 
 
 	_onZoomEnd: function () {
 		this._isZooming = false;
-		this._onUpdateCursor();
+		this._onUpdateCursor(null, true);
 		this.updateAllViewCursors();
 	},
 
@@ -1624,16 +1622,16 @@ L.TileLayer = L.GridLayer.extend({
 	},
 
 	// Update cursor layer (blinking cursor).
-	_onUpdateCursor: function (scroll) {
+	_onUpdateCursor: function (scroll, zoom) {
 		var cursorPos = this._visibleCursor.getNorthWest();
 		var docLayer = this._map._docLayer;
 
-		if ((scroll !== false) && !this._map.getBounds().contains(this._visibleCursor) && this._isCursorVisible) {
+		if ((!zoom && scroll !== false) && !this._map.getBounds().contains(this._visibleCursor) && this._isCursorVisible) {
 			var center = this._map.project(cursorPos);
 			center = center.subtract(this._map.getSize().divideBy(2));
 			center.x = Math.round(center.x < 0 ? 0 : center.x);
 			center.y = Math.round(center.y < 0 ? 0 : center.y);
-			if (!(this._selectionHandles.start && this._selectionHandles.start.isDragged) &&
+			if (!zoom && !(this._selectionHandles.start && this._selectionHandles.start.isDragged) &&
 			    !(this._selectionHandles.end && this._selectionHandles.end.isDragged) &&
 			    !(docLayer._followEditor || docLayer._followUser)) {
 				if (window.ThisIsAMobileApp) {


More information about the Libreoffice-commits mailing list