[Libreoffice-commits] online.git: 108 commits - bundled/include common/Clipboard.hpp common/Unit.hpp common/Util.cpp common/Util.hpp kit/ChildSession.cpp kit/ChildSession.hpp kit/Kit.cpp loleaflet/build loleaflet/css loleaflet/html loleaflet/Makefile.am loleaflet/src loleaflet/test Makefile.am net/Socket.hpp test/Makefile.am test/UnitConvert.cpp test/UnitCopyPaste.cpp wsd/ClientSession.cpp wsd/ClientSession.hpp wsd/DocumentBroker.cpp wsd/DocumentBroker.hpp wsd/Exceptions.hpp wsd/FileServer.cpp wsd/FileServer.hpp wsd/LOOLWSD.cpp wsd/LOOLWSD.hpp wsd/protocol.txt

Michael Meeks (via logerrit) logerrit at kemper.freedesktop.org
Tue Aug 6 19:04:28 UTC 2019


 Makefile.am                                       |    1 
 bundled/include/LibreOfficeKit/LibreOfficeKit.hxx |   50 +
 common/Clipboard.hpp                              |  147 +++
 common/Unit.hpp                                   |   12 
 common/Util.cpp                                   |   21 
 common/Util.hpp                                   |   32 
 kit/ChildSession.cpp                              |  151 ++++
 kit/ChildSession.hpp                              |    3 
 kit/Kit.cpp                                       |    6 
 loleaflet/Makefile.am                             |    2 
 loleaflet/build/deps.js                           |    8 
 loleaflet/css/leaflet.css                         |   35 
 loleaflet/css/toolbar.css                         |    5 
 loleaflet/html/clipboard.html                     |  236 ++++++
 loleaflet/src/control/Control.ColumnHeader.js     |    2 
 loleaflet/src/control/Control.ContextMenu.js      |   39 -
 loleaflet/src/control/Control.DownloadProgress.js |  413 +++++++++++
 loleaflet/src/control/Control.LokDialog.js        |    2 
 loleaflet/src/control/Control.Menubar.js          |   14 
 loleaflet/src/control/Control.MobileInput.js      |   14 
 loleaflet/src/control/Control.RowHeader.js        |    2 
 loleaflet/src/control/Control.Tabs.js             |    2 
 loleaflet/src/core/Socket.js                      |    4 
 loleaflet/src/layer/marker/ClipboardContainer.js  |   76 +-
 loleaflet/src/layer/tile/TileLayer.js             |  259 +------
 loleaflet/src/main.js                             |    4 
 loleaflet/src/map/Clipboard.js                    |  810 ++++++++++++++++++++++
 loleaflet/src/map/Map.js                          |   12 
 loleaflet/src/map/handler/Map.TouchGesture.js     |    2 
 loleaflet/test/copy-test.html                     |  146 +++
 net/Socket.hpp                                    |    9 
 test/Makefile.am                                  |    9 
 test/UnitConvert.cpp                              |    1 
 test/UnitCopyPaste.cpp                            |  326 ++++++++
 wsd/ClientSession.cpp                             |  328 ++++++--
 wsd/ClientSession.hpp                             |   58 +
 wsd/DocumentBroker.cpp                            |  183 ++++
 wsd/DocumentBroker.hpp                            |   21 
 wsd/Exceptions.hpp                                |    8 
 wsd/FileServer.cpp                                |   29 
 wsd/FileServer.hpp                                |    3 
 wsd/LOOLWSD.cpp                                   |  140 +++
 wsd/LOOLWSD.hpp                                   |    5 
 wsd/protocol.txt                                  |   44 +
 44 files changed, 3292 insertions(+), 382 deletions(-)

New commits:
commit 617a5d69b0365afe02e6a880e519b93f5629a2d3
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Tue Aug 6 14:49:22 2019 -0400
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:52 2019 -0400

    Account for a state change race.
    
    We can have un-processed incoming mesages in transit after a local
    transition to WAIT_DISCONNECT including some that change state.
    
    Change-Id: I577aebbc0a6ecca695f4bc35b4d226e1a465817d

diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index e4a34ba64..4cdc3fea2 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -104,6 +104,17 @@ void ClientSession::setState(SessionState newState)
 {
     LOG_TRC("ClientSession: transition from " << stateToString(_state) <<
             " to " << stateToString(newState));
+
+    // we can get incoming messages while our disconnection is in transit.
+    if (_state == SessionState::WAIT_DISCONNECT)
+    {
+        if (newState != SessionState::WAIT_DISCONNECT)
+            LOG_WRN("Unusual race - attempts to transition from " <<
+                    stateToString(_state) << " to " <<
+                    stateToString(newState));
+        return;
+    }
+
     switch (newState)
     {
     case SessionState::DETACHED:
commit 80fb9ac96f72db2daf69e9308ca76a96e087dfd2
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Tue Aug 6 11:26:07 2019 -0400
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:52 2019 -0400

    clipboard: unit tests need clipboard tags during init.
    
    Ensure we always have a valid clipboard key at start.
    
    Change-Id: Ia2d37e038d8baa877d147f507a5ff4c867f55813

diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index b832a7d60..e4a34ba64 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -159,8 +159,7 @@ void ClientSession::rotateClipboardKey(bool notifyClient)
     if (_wopiFileInfo && _wopiFileInfo->getDisableCopy())
         return;
 
-    if (_state != SessionState::LIVE &&   // editing
-        _state != SessionState::LOADING) // handshake with client
+    if (_state == SessionState::WAIT_DISCONNECT)
         return;
 
     _clipboardKeys[1] = _clipboardKeys[0];
@@ -201,6 +200,12 @@ std::string ClientSession::getClipboardURI(bool encode)
 
 bool ClientSession::matchesClipboardKeys(const std::string &/*viewId*/, const std::string &tag)
 {
+    if (tag.empty())
+    {
+        LOG_ERR("Invalid, empty clipboard tag");
+        return false;
+    }
+
     // FIXME: check viewId for paranoia if we can.
     for (auto &it : _clipboardKeys)
         if (it == tag)
commit 5cb3423838897439dc26328b396b6576d13f46cb
Author:     Mike Kaganski <mike.kaganski at collabora.com>
AuthorDate: Wed Jul 31 09:50:04 2019 +0000
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:52 2019 -0400

    Use rich paste also for IE
    
    Change-Id: I220af7fb1d2e7326e61c8ef4f9ec73980f65388b

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 7768914e0..079f41e9f 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -657,7 +657,7 @@ L.Clipboard = L.Class.extend({
 			var active = document.activeElement;
 			// Can't get HTML until it is pasted ... so quick timeout
 			setTimeout(function() {
-				that.dataTransferToDocument(null, /* preferInternal = */ false, that._dummyDiv.innerHTML);
+				that.dataTransferToDocument(null, /* preferInternal = */ true, that._dummyDiv.innerHTML);
 				// attempt to restore focus.
 				if (active == null)
 					that._map.focus();
commit 48a998f6f6d7a67c8911767b9ab7a4c9f316b1c5
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Mon Jul 15 12:53:06 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:52 2019 -0400

    copy/paste test - expand to paste.
    
    Change-Id: I69db38511aa70f1c2e7ff9613f49307dfe952bae

diff --git a/loleaflet/test/copy-test.html b/loleaflet/test/copy-test.html
index 761b215eb..a4165b2b2 100644
--- a/loleaflet/test/copy-test.html
+++ b/loleaflet/test/copy-test.html
@@ -23,7 +23,13 @@
       <li id="aclass" data-event="both-aclass"><a class>Both AClass (fails on iOS)</a></li>
       <li/>
       <li/>
+      <li id="paste" data-event="paste"><a href="#">Paste</a></li>
+      <li/>
+      <li/>
     </ul>
+
+    <div id="clipcontent">
+    </div>
 </body>
   <script defer>
     var serial = 42;
@@ -104,5 +110,37 @@
         doSelect();
     };
 
+    // --------- copy function ---------
+    var paste = document.getElementById('paste');
+    paste.onclick = function(ev) {
+
+        if (document.execCommand('paste'))
+            console.log('Paste succeeded');
+        else
+            console.log('Paste failed');
+    };
+
+    document.onpaste = function(ev) {
+        if (!ev.clipboardData) { // non-standard
+            console.log('No clipboard data');
+            return;
+        }
+
+        var dataTransfer = ev.clipboardData;
+        var types = dataTransfer.types;
+
+        console.log('We have ' + types.length + ' types');
+        for (var t = 0; t < types.length; ++t) {
+            var data = new Blob([dataTransfer.getData(types[t])]);
+            console.log('type ' + types[t] + ' length ' + data.size +
+			    ' -> 0x' + data.size.toString(16));
+            if (types[t].startsWith('text/'))
+                 console.log('data: ' + dataTransfer.getData(types[t]));
+        }
+
+        ev.preventDefault();
+        return false;
+    };
+
     </script>
 </body></html>
commit 65c3ba6521d00578102ff6c4af2eaec7121b6dd2
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Mon Jul 15 11:52:49 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:52 2019 -0400

    Add test bits for copy/paste.
    
    Change-Id: I7e1b0bf8c834a414d2f8a87bd16ebf8465dd324a

diff --git a/loleaflet/test/copy-test.html b/loleaflet/test/copy-test.html
new file mode 100644
index 000000000..761b215eb
--- /dev/null
+++ b/loleaflet/test/copy-test.html
@@ -0,0 +1,108 @@
+<!DOCTYPE html>
+<html><head><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
+<title>Copy Test</title>
+<meta charset="utf-8">
+</head>
+  <body id="body" style="user-select: none;"> <!-- text doesn't seem to matter here -->
+    <h1>Test clipboard code: none</h1>
+    <div id="area_id" contenteditable="true" style="-webkit-user-select: text; border: red; background-color: grey">dummy content</div>
+
+    <ul>
+      <li id="select" data-event="select"><a href="#" class="item-link list-button">Select All</a></li>
+      <li/>
+      <li/>
+      <li id="both" data-event="both"><a href="#" class="item-link list-button">Both TouchStart</a></li>
+      <li/>
+      <li/>
+      <li id="bothclick" data-event="both-click"><a href="#" class="item-link list-button">Both Click</a></li>
+      <li/>
+      <li/>
+      <li id="bothstopclick" data-event="both-stop-click"><a href="#" class="item-link list-button">Both StoponTouch Click</a></li>
+      <li/>
+      <li/>
+      <li id="aclass" data-event="both-aclass"><a class>Both AClass (fails on iOS)</a></li>
+      <li/>
+      <li/>
+    </ul>
+</body>
+  <script defer>
+    var serial = 42;
+
+    document.getElementById('area_id').focus();
+
+    // --------- select function ---------
+    var doSelect = function() {
+        console.log('select whole area');
+        var selection     = window.getSelection();
+        selection.removeAllRanges();
+        var rangeToSelect = document.createRange();
+        elem = document.getElementById('area_id');
+        rangeToSelect.selectNodeContents(elem);
+        selection.addRange(rangeToSelect);
+    };
+    var select = document.getElementById('select');
+    select.onclick = function(ev) {
+        doSelect();
+    };
+
+    // --------- copy function ---------
+    var doCopy = function(ev) {
+        console.log('On touch copy invoke');
+
+        try
+        {
+            console.log('exec command copy - before');
+            _ret = document.execCommand('copy');
+           console.log('exec command copy success: ' + _ret);
+        }
+        catch (err)
+        {
+           console.log('exception in copy ' + err);
+           _ret = false;
+        }
+    };
+
+    // --------- both function ---------
+    var both = document.getElementById('both');
+    both.ontouchstart = function(ev) {
+        doCopy();
+    };
+
+    var bothClick = document.getElementById('bothclick');
+    bothClick.onclick = function(ev) {
+        doCopy();
+    };
+
+    // JQuery context menu does this on touch - killing a click event
+    var bothStopClick = document.getElementById('bothstopclick');
+    bothStopClick.ontouchstart = function(ev) {
+        console.log('stop on touch!');
+        ev.preventDefault();
+        ev.stopImmediatePropagation();
+        return false;
+    };
+    bothStopClick.onclick = function(ev) {
+        doCopy();
+    };
+
+    var aclass = document.getElementById('aclass');
+    aclass.onclick = function(ev) {
+        doCopy();
+    };
+
+    // Actually put serial data in the thing ..
+    document.oncopy = function(ev) {
+        ev.preventDefault();
+        var forclip = 'serial ' + serial++ + ' ops';
+        console.log('set clip to ' + forclip);
+        ev.clipboardData.setData('text/plain', forclip);
+    };
+
+    // Do the selection
+    document.onbeforecopy = function(ev) {
+        console.log('we have to select in a before copy event [!]');
+        doSelect();
+    };
+
+    </script>
+</body></html>
commit c855df8c86e576180e22654b509524142bf7f321
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Jul 12 21:45:52 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:52 2019 -0400

    More IE11 work, paste works again, cut/copy pending.
    
    Change-Id: I744e76c7ecb655c42ed3dfb0662c0995d2c48650

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 34aee3649..7768914e0 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -414,13 +414,8 @@ L.Clipboard = L.Class.extend({
 			console.log('Error: collapsed selection - cannot copy/paste');
 	},
 
-	populateClipboard: function(ev) {
+	_getHtmlForClipboard: function() {
 		var text;
-
-		this._checkSelection();
-
-//		this._stopHideDownload(); - this confuses the borwser ruins copy/cut on iOS
-
 		if (this._selectionType === 'complex' ||
 		    this._map._docLayer.hasGraphicSelection()) {
 			console.log('Copy/Cut with complex/graphical selection');
@@ -444,6 +439,24 @@ L.Clipboard = L.Class.extend({
 			console.log('Copy/Cut with simple text selection');
 			text = this._selectionContent;
 		}
+		return text;
+	},
+
+	// returns whether we shold stop processing the event
+	populateClipboard: function(ev) {
+		this._checkSelection();
+
+		if (window.isInternetExplorer)
+		{
+			var that = this;
+			setTimeout(function() { that._resetDiv(); }, 0);
+			this._clipboardSerial++; // we have no way of knowing of course.
+			// We let the browser copy from our div.
+			return false;
+		}
+
+		var text = this._getHtmlForClipboard();
+//		this._stopHideDownload(); - this confuses the borwser ruins copy/cut on iOS
 
 		var plainText = this.stripHTML(text);
 		if (ev.clipboardData) { // Standard
@@ -451,11 +464,9 @@ L.Clipboard = L.Class.extend({
 			ev.clipboardData.setData('text/html', text);
 			console.log('Put "' + text + '" on the clipboard');
 			this._clipboardSerial++;
-
-		} else if (window.clipboardData) { // IE 11 - poor clipboard API
-			if (window.clipboardData.setData('Text', plainText))
-				this._clipboardSerial++;
 		}
+
+		return true; // prevent default
 	},
 
 	// only used by IE.
@@ -466,23 +477,49 @@ L.Clipboard = L.Class.extend({
 		// Now wait for the paste ...
 	},
 
+	// Does the selection of text before an event comes in
 	_beforeSelect: function(ev) {
 		console.log('Got event ' + ev.type + ' setting up selection');
 
-		this._resetDiv();
+		if (window.isInternetExplorer && ev.type != 'paste')
+			// We need populate our content into the div for
+			// the brower to copy.
+			this._dummyDiv.innerHTML = this._getHtmlForClipboard();
+		else
+			// We need some spaces in there ...
+			this._resetDiv();
 
 		var sel = document.getSelection();
 		if (!sel)
 			return;
 
-		sel.removeAllRanges();
-		var rangeToSelect = document.createRange();
-		rangeToSelect.selectNodeContents(this._dummyDiv);
-		sel.addRange(rangeToSelect);
+		var selected = false;
+		var selectRange;
+		if (window.isInternetExplorer && ev.type != 'paste')
+		{
+			this._dummyDiv.focus();
 
-		var checkSelect = document.getSelection();
-		if (checkSelect.isCollapsed)
-			console.log('Error: failed to select - cannot copy/paste');
+			if (document.body.createTextRange) // Internet Explorer
+			{
+				console.log('Legacy IE11 selection');
+				selectRange = document.body.createTextRange();
+				selectRange.moveToElementText(this._dummyDiv);
+				selectRange.select();
+				selected = true;
+			}
+		}
+
+		if (!selected)
+		{
+			sel.removeAllRanges();
+			selectRange = document.createRange();
+			selectRange.selectNodeContents(this._dummyDiv);
+			sel.addRange(selectRange);
+
+			var checkSelect = document.getSelection();
+			if (checkSelect.isCollapsed)
+				console.log('Error: failed to select - cannot copy/paste');
+		}
 
 		return false;
 	},
@@ -595,21 +632,19 @@ L.Clipboard = L.Class.extend({
 		return true;
 	},
 
-	copy: function(ev) {
-		console.log('Copy');
-		ev.preventDefault();
-		this.populateClipboard(ev);
-		this._map._socket.sendMessage('uno .uno:Copy');
-		return false;
+	_doCopyCut: function(ev, unoName) {
+		console.log(unoName);
+		var preventDefault = this.populateClipboard(ev);
+		this._map._socket.sendMessage('uno .uno:' + unoName);
+		if (preventDefault) {
+			ev.preventDefault();
+			return false;
+		}
 	},
 
-	cut: function(ev) {
-		console.log('Cut');
-		ev.preventDefault();
-		this.populateClipboard(ev);
-		this._map._socket.sendMessage('uno .uno:Cut');
-		return false;
-	},
+	cut:  function(ev) { return this._doCopyCut(ev, 'Cut'); },
+
+	copy: function(ev) { return this._doCopyCut(ev, 'Copy'); },
 
 	paste: function(ev) {
 		console.log('Paste');
@@ -622,8 +657,7 @@ L.Clipboard = L.Class.extend({
 			var active = document.activeElement;
 			// Can't get HTML until it is pasted ... so quick timeout
 			setTimeout(function() {
-				var tmpDiv = document.getElementById(that._dummyDivName);
-				that.dataTransferToDocument(null, /* preferInternal = */ false, tmpDiv.innerHTML);
+				that.dataTransferToDocument(null, /* preferInternal = */ false, that._dummyDiv.innerHTML);
 				// attempt to restore focus.
 				if (active == null)
 					that._map.focus();
commit 53bb8261d240fe04269d76bec91fcc1beaee938d
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Jul 10 17:03:09 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    clipboard: remove inadvertent debug.
    
    Change-Id: I59833519d5f4a8b16c7925a8094c974a1e7f5c11

diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index 3ed895cdc..9cccc78df 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2156,7 +2156,7 @@ private:
                 StringTokenizer reqPathTokens(request.getURI(), "/?", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM);
                 if (reqPathTokens.count() > 1 && reqPathTokens[0] == "lool" && reqPathTokens[1] == "clipboard")
                 {
-                    Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ...
+//                    Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ...
                     handleClipboardRequest(request, message, disposition);
                 }
                 else if (!(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) &&
commit cfe70a16c0fb10386b3320f32b72568e0b56e521
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Jul 12 15:38:16 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    clipboard: centralize events, and have a persistent clipboard div.
    
    Change-Id: I94388e144a4a54d98b86c91675e89be932c3502a

diff --git a/loleaflet/src/layer/marker/ClipboardContainer.js b/loleaflet/src/layer/marker/ClipboardContainer.js
index b41fb66e2..c532bff17 100644
--- a/loleaflet/src/layer/marker/ClipboardContainer.js
+++ b/loleaflet/src/layer/marker/ClipboardContainer.js
@@ -16,7 +16,7 @@ L.ClipboardContainer = L.Layer.extend({
 			this.update();
 		}
 
-		L.DomEvent.on(this._textArea, 'copy cut paste ' +
+		L.DomEvent.on(this._textArea,
 		              'keydown keypress keyup ' +
 		              'compositionstart compositionupdate compositionend textInput',
 		              this._map._handleDOMEvent, this._map);
@@ -27,7 +27,7 @@ L.ClipboardContainer = L.Layer.extend({
 			this.getPane().removeChild(this._container);
 		}
 
-		L.DomEvent.off(this._textArea, 'copy cut paste ' +
+		L.DomEvent.off(this._textArea,
 		               'keydown keypress keyup ' +
 		               'compositionstart compositionupdate compositionend textInput',
 		               this._map._handleDOMEvent, this._map);
diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index eb4a504f6..72b1c6558 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -261,9 +261,6 @@ L.TileLayer = L.GridLayer.extend({
 		this._viewReset();
 		map.on('drag resize zoomend', this._updateScrollOffset, this);
 
-		map.on('copy', this._onCopy, this);
-		map.on('cut', this._onCut, this);
-		map.on('paste', this._onPaste, this);
 		map.on('dragover', this._onDragOver, this);
 		map.on('drop', this._onDrop, this);
 
@@ -2644,28 +2641,6 @@ L.TileLayer = L.GridLayer.extend({
 			!this._isEmptyRectangle(this._graphicSelection));
 	},
 
-	_onCopy: function (e) {
-		e = e.originalEvent;
-		this._map._clip.copy(e);
-	},
-
-	_onCut: function (e) {
-		e = e.originalEvent;
-		this._map._clip.cut(e);
-	},
-
-	_onPaste: function (e) {
-		e = e.originalEvent;
-		if (!this._map._activeDialog) {
-			// Paste in document
-			this._map._clip.paste(e);
-		} else {
-			// Paste in dialog
-			e.usePasteKeyEvent = true;
-			this._map._clip.paste(e);
-		}
-	},
-
 	_onDragOver: function (e) {
 		e = e.originalEvent;
 		e.preventDefault();
diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 16fb6ab81..34aee3649 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -17,11 +17,47 @@ L.Clipboard = L.Class.extend({
 		this._accessKey = [ '', '' ];
 		this._clipboardSerial = 0; // incremented on each operation
 		this._failedTimer = null;
-		this._dummyDivName = 'copy-paste-dummy-div';
+		this._dummyDivName = 'copy-paste-container';
+
+		var div = document.createElement('div');
+		this._dummyDiv = div;
+
+		div.setAttribute('id', this._dummyDivName);
+		div.setAttribute('style', 'user-select: text !important');
+		div.style.opacity = '0';
+		div.setAttribute('contenteditable', 'true');
+		div.setAttribute('type', 'text');
+		div.setAttribute('style', 'position: fixed; left: 0px; top: -200px; width: 15000px; height: 200px; ' +
+				 'overflow: hidden; z-index: -1000, -webkit-user-select: text !important; display: block; ' +
+				 'font-size: 6pt">');
+
+		// so we get events to where we want them.
+		var parent = document.getElementById('map');
+		parent.appendChild(div);
+
+		// sensible default content.
+		this._resetDiv();
 
 		var that = this;
-		document.addEventListener(
-			'beforepaste', function(ev) { that.beforepaste(ev); });
+		var beforeSelect = function(ev) { return that._beforeSelect(ev); }
+		if (window.isInternetExplorer)
+		{
+			document.addEventListener('cut',   function(ev)   { return that.cut(ev); });
+			document.addEventListener('copy',  function(ev)   { return that.copy(ev); });
+			document.addEventListener('paste', function(ev)   { return that.paste(ev); });
+			document.addEventListener('beforecut', beforeSelect);
+			document.addEventListener('beforecopy', beforeSelect);
+			document.addEventListener('beforepaste', function(ev) { return that._beforePasteIE(ev); });
+		}
+		else
+		{
+			document.oncut = function(ev)   { return that.cut(ev); };
+			document.oncopy = function(ev)  { return that.copy(ev); };
+			document.onpaste = function(ev) { return that.paste(ev); };
+			document.onbeforecut = beforeSelect;
+			document.onbeforecopy = beforeSelect;
+			document.onbeforepaste = beforeSelect;
+		}
 	},
 
 	compatRemoveNode: function(node) {
@@ -372,9 +408,17 @@ L.Clipboard = L.Class.extend({
 		}
 	},
 
+	_checkSelection: function() {
+		var checkSelect = document.getSelection();
+		if (checkSelect && checkSelect.isCollapsed)
+			console.log('Error: collapsed selection - cannot copy/paste');
+	},
+
 	populateClipboard: function(ev) {
 		var text;
 
+		this._checkSelection();
+
 //		this._stopHideDownload(); - this confuses the borwser ruins copy/cut on iOS
 
 		if (this._selectionType === 'complex' ||
@@ -414,108 +458,46 @@ L.Clipboard = L.Class.extend({
 		}
 	},
 
-	_createDummyDiv: function(htmlContent) {
-		var div = null;
-		if (window.isInternetExplorer)
-		{	// work-around very odd behavior and non-removal of div
-			// could use for other browsers potentially ...
-			div = document.getElementById(this._dummyDivName);
-		}
-		if (div === null)
-			div = document.createElement('div');
-		div.setAttribute('id', this._dummyDivName);
-		div.setAttribute('style', 'user-select: text !important');
-		div.style.opacity = '0';
-		div.setAttribute('contenteditable', 'true');
-		div.setAttribute('type', 'text');
-		div.setAttribute('style', '-webkit-user-select: text !important');
-		div.innerHTML = htmlContent;
+	// only used by IE.
+	_beforePasteIE: function(ev) {
+		console.log('IE11 work ...');
+		this._beforeSelect(ev);
+		this._dummyDiv.focus();
+		// Now wait for the paste ...
+	},
 
-		// so we get events to where we want them.
-		var parent = document.getElementById('map');
-		parent.appendChild(div);
+	_beforeSelect: function(ev) {
+		console.log('Got event ' + ev.type + ' setting up selection');
 
-		return div;
-	},
+		this._resetDiv();
 
-	// only used by IE.
-	beforepaste: function() {
-		console.log('Before paste');
-		if (!window.isInternetExplorer)
+		var sel = document.getSelection();
+		if (!sel)
 			return;
 
-		console.log('IE11 madness ...'); // TESTME ...
-		var div = this._createDummyDiv('---copy-paste-canary---');
-		var sel = document.getSelection();
-		// we need to restore focus.
-		var active = document.activeElement;
-		// get a selection first - FIXME: use Ivan's 2 spaces on input.
-		var range = document.createRange();
-		range.selectNodeContents(div);
 		sel.removeAllRanges();
-		sel.addRange(range);
-		div.focus();
+		var rangeToSelect = document.createRange();
+		rangeToSelect.selectNodeContents(this._dummyDiv);
+		sel.addRange(rangeToSelect);
 
-		var that = this;
-		// Now we wait for paste ...
-		div.addEventListener('paste', function() {
-			// Can't get HTML until it is pasted ... so quick timeout
-			setTimeout(function() {
-				var tmpDiv = document.getElementById(that._dummyDivName);
-				that.dataTransferToDocument(null, /* preferInternal = */ false, tmpDiv.innerHTML);
-				that.compatRemoveNode(tmpDiv);
-				// attempt to restore focus.
-				if (active == null)
-					that._map.focus();
-				else
-					active.focus();
-				that._map._clipboardContainer._abortComposition();
-				that._clipboardSerial++;
-			}, 0 /* ASAP */);
-		});
+		var checkSelect = document.getSelection();
+		if (checkSelect.isCollapsed)
+			console.log('Error: failed to select - cannot copy/paste');
+
+		return false;
+	},
+
+	_resetDiv: function() {
+		// cleanup the content:
+		this._dummyDiv.innerHTML =
+			'<b style="font-weight:normal; background-color: transparent; color: transparent;"><span>  </span></b>';
 	},
 
 	// Try-harder fallbacks for emitting cut/copy/paste events.
 	_execOnElement: function(operation) {
 		var serial = this._clipboardSerial;
 
-		var div = this._createDummyDiv('<b style="font-weight:normal; background-color: transparent; color: transparent;"><span>  </span></b>');
-
-		var that = this;
-		var doInvoke = function(ev) {
-			console.log('Got event ' + ev.type + ' on transient editable');
-
-			var checkSelect = document.getSelection();
-			if (checkSelect.isCollapsed)
-				console.log('Error: failed to select - cannot copy/paste');
-
-			// forward with proper security credentials now.
-			that[operation].call(that, ev);
-			ev.preventDefault();
-			ev.stopPropagation();
-
-			return false;
-		};
-		var doSelect = function(ev) {
-			console.log('Got event ' + ev.type + ' on transient editable');
-			var sel = document.getSelection();
-			if (!sel)
-				return;
-
-			console.log('setup selection');
-			sel.removeAllRanges();
-			var rangeToSelect = document.createRange();
-			rangeToSelect.selectNodeContents(div);
-			sel.addRange(rangeToSelect);
-
-			var checkSelect = document.getSelection();
-			if (checkSelect.isCollapsed)
-				console.log('Error: failed to select - cannot copy/paste');
-
-			return false;
-		};
-		document['on' + operation] = doInvoke;
-		document['onbefore' + operation] = doSelect;
+		this._resetDiv();
 
 		var success = false;
 		var active = null;
@@ -526,11 +508,6 @@ L.Clipboard = L.Class.extend({
 		success = (document.execCommand(operation) &&
 			   serial !== this._clipboardSerial);
 
-		// cleanup
-//		document.removeEventListener('before' + operation, doSelect);
-//		document.removeEventListener(operation, doInvoke);
-		this.compatRemoveNode(div);
-
 		// try to restore focus if we need to.
 		if (active !== null && active !== document.activeElement)
 			active.focus();
@@ -544,7 +521,7 @@ L.Clipboard = L.Class.extend({
 	_execCopyCutPaste: function(operation) {
 		var serial = this._clipboardSerial;
 
-		// try execCommand.
+		// try a direct execCommand.
 		if (document.execCommand(operation) &&
 		    serial !== this._clipboardSerial) {
 			console.log('copied successfully');
@@ -623,6 +600,7 @@ L.Clipboard = L.Class.extend({
 		ev.preventDefault();
 		this.populateClipboard(ev);
 		this._map._socket.sendMessage('uno .uno:Copy');
+		return false;
 	},
 
 	cut: function(ev) {
@@ -630,10 +608,34 @@ L.Clipboard = L.Class.extend({
 		ev.preventDefault();
 		this.populateClipboard(ev);
 		this._map._socket.sendMessage('uno .uno:Cut');
+		return false;
 	},
 
 	paste: function(ev) {
 		console.log('Paste');
+		if (this._map._activeDialog)
+			ev.usePasteKeyEvent = true;
+
+		var that = this;
+		if (window.isInternetExplorer)
+		{
+			var active = document.activeElement;
+			// Can't get HTML until it is pasted ... so quick timeout
+			setTimeout(function() {
+				var tmpDiv = document.getElementById(that._dummyDivName);
+				that.dataTransferToDocument(null, /* preferInternal = */ false, tmpDiv.innerHTML);
+				// attempt to restore focus.
+				if (active == null)
+					that._map.focus();
+				else
+					active.focus();
+				that._map._clipboardContainer._abortComposition(ev);
+				that._clipboardSerial++;
+			}, 0 /* ASAP */);
+			return false;
+		}
+
+
 		if (ev.clipboardData) { // Standard
 			ev.preventDefault();
 			var usePasteKeyEvent = ev.usePasteKeyEvent;
@@ -645,7 +647,7 @@ L.Clipboard = L.Class.extend({
 			this._clipboardSerial++;
 			this._stopHideDownload();
 		}
-		// else: IE 11 - code in beforepaste: above.
+		return false;
 	},
 
 	clearSelection: function() {
commit 1508b2da6ddf0d53b5aaa0432ff75f27f165eaff
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Jul 12 16:06:35 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    clipboard: don't encode empty string as a blob: mends image paste.
    
    Change-Id: I265014ad1186d21aeb8628d8cc124d4d4d3ef91f

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 42ca149a0..16fb6ab81 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -313,7 +313,7 @@ L.Clipboard = L.Class.extend({
 		}
 
 		// Fallback on the html.
-		if (!content) {
+		if (!content && htmlText != '') {
 			content = this._encodeHtmlToBlob(htmlText);
 		}
 
commit 1e759e4d100c338dc842a4a613ee07313df2346e
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Jul 12 15:35:42 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    Handle undefined context menu items.
    
    Change-Id: I98727decc5ad7fe2525a2d28f140fa3a39927e5b

diff --git a/loleaflet/src/control/Control.ContextMenu.js b/loleaflet/src/control/Control.ContextMenu.js
index bab63f3f9..652971f8f 100644
--- a/loleaflet/src/control/Control.ContextMenu.js
+++ b/loleaflet/src/control/Control.ContextMenu.js
@@ -188,6 +188,8 @@ L.installContextMenu = function(options) {
 		var keys = Object.keys(items);
 		for (var i = 0; i < keys.length; i++) {
 			var key = keys[i];
+			if (items[key] === undefined)
+				continue;
 			if (!items[key].isHtmlName) {
 				console.log('re-write name ' + items[key].name);
 				items[key].name = '<a href="#" class="context-menu-link">' + items[key].name + '</a';
commit dd65c20f1292d4607a570c0f0e280a7c5bc48e87
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Jul 12 14:07:51 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    Context menus: stop items looking too much like links.
    
    Change-Id: I9bd469070ba401c97da3547e9d1aaa2ccb83f155

diff --git a/loleaflet/css/toolbar.css b/loleaflet/css/toolbar.css
index a0f1f4e71..ccb7b57d9 100644
--- a/loleaflet/css/toolbar.css
+++ b/loleaflet/css/toolbar.css
@@ -726,3 +726,8 @@ tr.useritem > td.usercolor {
 tr.useritem > td > img {
     border-radius: 100px;
 }
+
+.context-menu-link {
+    text-decoration: none;
+    color: black;
+}
diff --git a/loleaflet/src/control/Control.ContextMenu.js b/loleaflet/src/control/Control.ContextMenu.js
index 19588df89..bab63f3f9 100644
--- a/loleaflet/src/control/Control.ContextMenu.js
+++ b/loleaflet/src/control/Control.ContextMenu.js
@@ -130,8 +130,8 @@ L.Control.ContextMenu = L.Control.extend({
 
 				contextMenu[item.command] = {
 					// Using 'click' and <a href='#' is vital for copy/paste security context.
-					name: '<a href="#">' +  _(itemName) + '</a',
-					isHtmlName: true
+					name: '<a href="#" class="context-menu-link">' +  _(itemName) + '</a',
+					isHtmlName: true,
 				};
 
 				if (item.checktype === 'checkmark') {
@@ -190,7 +190,7 @@ L.installContextMenu = function(options) {
 			var key = keys[i];
 			if (!items[key].isHtmlName) {
 				console.log('re-write name ' + items[key].name);
-				items[key].name = '<a href="#">' + items[key].name + '</a';
+				items[key].name = '<a href="#" class="context-menu-link">' + items[key].name + '</a';
 				items[key].isHtmlName = true;
 			}
 			rewrite(items[key].items);
commit 90c73cbf54fb8714095d15781a2063fa8c052be7
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Jul 12 11:39:16 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    clipboard: Improve selection management.
    
    Change-Id: I3b6318f9a0cc544b74564376426baf7d3759ee3e

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 7b576c48b..42ca149a0 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -479,15 +479,21 @@ L.Clipboard = L.Class.extend({
 	_execOnElement: function(operation) {
 		var serial = this._clipboardSerial;
 
-		var div = this._createDummyDiv('dummy content');
+		var div = this._createDummyDiv('<b style="font-weight:normal; background-color: transparent; color: transparent;"><span>  </span></b>');
 
 		var that = this;
 		var doInvoke = function(ev) {
 			console.log('Got event ' + ev.type + ' on transient editable');
+
+			var checkSelect = document.getSelection();
+			if (checkSelect.isCollapsed)
+				console.log('Error: failed to select - cannot copy/paste');
+
 			// forward with proper security credentials now.
 			that[operation].call(that, ev);
 			ev.preventDefault();
 			ev.stopPropagation();
+
 			return false;
 		};
 		var doSelect = function(ev) {
@@ -501,6 +507,12 @@ L.Clipboard = L.Class.extend({
 			var rangeToSelect = document.createRange();
 			rangeToSelect.selectNodeContents(div);
 			sel.addRange(rangeToSelect);
+
+			var checkSelect = document.getSelection();
+			if (checkSelect.isCollapsed)
+				console.log('Error: failed to select - cannot copy/paste');
+
+			return false;
 		};
 		document['on' + operation] = doInvoke;
 		document['onbefore' + operation] = doSelect;
commit c4f36953c46e1ba7adc29fe9829cc2fe933ddf62
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Thu Jul 11 20:27:58 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    Switch to using beforecopy etc. to setup the selection.
    
    Change-Id: I432bcdaeba84eaac8925673f4d6b33a0d10f0a97

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index d12a965b2..7b576c48b 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -482,37 +482,41 @@ L.Clipboard = L.Class.extend({
 		var div = this._createDummyDiv('dummy content');
 
 		var that = this;
-		var listener = function(e) {
-			e.preventDefault();
-			console.log('Got event ' + operation + ' on transient editable');
+		var doInvoke = function(ev) {
+			console.log('Got event ' + ev.type + ' on transient editable');
 			// forward with proper security credentials now.
-			that[operation].call(that, e);
+			that[operation].call(that, ev);
+			ev.preventDefault();
+			ev.stopPropagation();
+			return false;
+		};
+		var doSelect = function(ev) {
+			console.log('Got event ' + ev.type + ' on transient editable');
+			var sel = document.getSelection();
+			if (!sel)
+				return;
+
+			console.log('setup selection');
+			sel.removeAllRanges();
+			var rangeToSelect = document.createRange();
+			rangeToSelect.selectNodeContents(div);
+			sel.addRange(rangeToSelect);
 		};
-		div.addEventListener('copy', listener);
-		div.addEventListener('cut', listener);
-		div.addEventListener('paste', listener);
+		document['on' + operation] = doInvoke;
+		document['onbefore' + operation] = doSelect;
 
 		var success = false;
 		var active = null;
-		var sel = document.getSelection();
-		if (sel)
-		{
-			// selection can change focus.
-			active = document.activeElement;
 
-			// get a selection first - FIXME: use Ivan's 2 spaces on input.
-			var range = document.createRange();
-			range.selectNodeContents(div);
-			sel.removeAllRanges();
-			sel.addRange(range);
+		// selection can change focus.
+		active = document.activeElement;
+
+		success = (document.execCommand(operation) &&
+			   serial !== this._clipboardSerial);
 
-			success = (document.execCommand(operation) &&
-				   serial !== this._clipboardSerial);
-		}
 		// cleanup
-		div.removeEventListener('paste', listener);
-		div.removeEventListener('cut', listener);
-		div.removeEventListener('copy', listener);
+//		document.removeEventListener('before' + operation, doSelect);
+//		document.removeEventListener(operation, doInvoke);
 		this.compatRemoveNode(div);
 
 		// try to restore focus if we need to.
commit 23d748947c99961a196880f8826cffcd69fdc46f
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Jul 12 11:29:21 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    clipboard: use <a href='#' and click overriding to get copy/paste on iOS
    
    Since context click vs. mouse-up is a global, we have to do all our
    contexts.
    
    Change-Id: Ie50a832d1b9df066cfccc2138f0741f8d407a1a4

diff --git a/loleaflet/src/control/Control.ColumnHeader.js b/loleaflet/src/control/Control.ColumnHeader.js
index 2d340be8d..d83c8303c 100644
--- a/loleaflet/src/control/Control.ColumnHeader.js
+++ b/loleaflet/src/control/Control.ColumnHeader.js
@@ -65,7 +65,7 @@ L.Control.ColumnHeader = L.Control.Header.extend({
 		L.DomEvent.addListener(this._cornerCanvas, 'click', this._onCornerHeaderClick, this);
 
 		var colHeaderObj = this;
-		$.contextMenu({
+		L.installContextMenu({
 			selector: '.spreadsheet-header-columns',
 			className: 'loleaflet-font',
 			items: {
diff --git a/loleaflet/src/control/Control.ContextMenu.js b/loleaflet/src/control/Control.ContextMenu.js
index 1c3bf87e6..19588df89 100644
--- a/loleaflet/src/control/Control.ContextMenu.js
+++ b/loleaflet/src/control/Control.ContextMenu.js
@@ -76,7 +76,7 @@ L.Control.ContextMenu = L.Control.extend({
 		}
 
 		var contextMenu = this._createContextMenuStructure(obj);
-		$.contextMenu({
+		L.installContextMenu({
 			selector: '.leaflet-layer',
 			className: 'loleaflet-font',
 			trigger: 'none',
@@ -129,7 +129,9 @@ L.Control.ContextMenu = L.Control.extend({
 				itemName = _UNO(item.command, docType, true);
 
 				contextMenu[item.command] = {
-					name: _(itemName)
+					// Using 'click' and <a href='#' is vital for copy/paste security context.
+					name: '<a href="#">' +  _(itemName) + '</a',
+					isHtmlName: true
 				};
 
 				if (item.checktype === 'checkmark') {
@@ -176,3 +178,24 @@ L.Control.ContextMenu = L.Control.extend({
 L.control.contextMenu = function (options) {
 	return new L.Control.ContextMenu(options);
 };
+
+// Using 'click' and <a href='#' is vital for copy/paste security context.
+L.installContextMenu = function(options) {
+	options.itemClickEvent = 'click';
+	var rewrite = function(items) {
+		if (items === undefined)
+			return;
+		var keys = Object.keys(items);
+		for (var i = 0; i < keys.length; i++) {
+			var key = keys[i];
+			if (!items[key].isHtmlName) {
+				console.log('re-write name ' + items[key].name);
+				items[key].name = '<a href="#">' + items[key].name + '</a';
+				items[key].isHtmlName = true;
+			}
+			rewrite(items[key].items);
+		}
+	}
+	rewrite(options.items);
+	$.contextMenu(options);
+};
diff --git a/loleaflet/src/control/Control.RowHeader.js b/loleaflet/src/control/Control.RowHeader.js
index 12100b76a..a0116122a 100644
--- a/loleaflet/src/control/Control.RowHeader.js
+++ b/loleaflet/src/control/Control.RowHeader.js
@@ -62,7 +62,7 @@ L.Control.RowHeader = L.Control.Header.extend({
 		this._position = 0;
 
 		var rowHeaderObj = this;
-		$.contextMenu({
+		L.installContextMenu({
 			selector: '.spreadsheet-header-rows',
 			className: 'loleaflet-font',
 			items: {
diff --git a/loleaflet/src/control/Control.Tabs.js b/loleaflet/src/control/Control.Tabs.js
index d120bb08a..5855c584e 100644
--- a/loleaflet/src/control/Control.Tabs.js
+++ b/loleaflet/src/control/Control.Tabs.js
@@ -43,7 +43,7 @@ L.Control.Tabs = L.Control.extend({
 		var docContainer = map.options.documentContainer;
 		this._tabsCont = L.DomUtil.create('div', 'spreadsheet-tabs-container', docContainer.parentElement);
 
-		$.contextMenu({
+		L.installContextMenu({
 			selector: '.spreadsheet-tab',
 			className: 'loleaflet-font',
 			callback: (function(key) {
diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index 926ea62a5..eb4a504f6 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -201,7 +201,7 @@ L.TileLayer = L.GridLayer.extend({
 		this._map._socket.sendMessage('commandvalues command=.uno:LanguageStatus');
 		this._map._socket.sendMessage('commandvalues command=.uno:ViewAnnotations');
 		var that = this;
-		$.contextMenu({
+		L.installContextMenu({
 			selector: '.loleaflet-annotation-menu',
 			trigger: 'none',
 			className: 'loleaflet-font',
@@ -234,7 +234,7 @@ L.TileLayer = L.GridLayer.extend({
 				}
 			}
 		});
-		$.contextMenu({
+		L.installContextMenu({
 			selector: '.loleaflet-annotation-menu-redline',
 			trigger: 'none',
 			className: 'loleaflet-font',
diff --git a/loleaflet/src/map/handler/Map.TouchGesture.js b/loleaflet/src/map/handler/Map.TouchGesture.js
index 1fb90af1c..9c367daa8 100644
--- a/loleaflet/src/map/handler/Map.TouchGesture.js
+++ b/loleaflet/src/map/handler/Map.TouchGesture.js
@@ -62,7 +62,7 @@ L.Map.TouchGesture = L.Handler.extend({
 		/// attach 'touchend' menu clicks event handler
 		if (this._hammer.input instanceof Hammer.TouchInput) {
 			var $doc = $(document);
-			$doc.on('touchend.contextMenu', '.context-menu-item', function (e) {
+			$doc.on('click.contextMenu', '.context-menu-item', function (e) {
 				var $elem = $(this);
 
 				if ($elem.data().contextMenu.selector === '.leaflet-layer') {
commit 8956a8f203476ebc7970daf9d44d84c7e79b1afa
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Jul 10 10:01:31 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    Avoid harmful stopHideDownload while populating the clipboard.
    
    Change-Id: I1e3dec1d76f204fca84dc4a0d53f4ac02175288a

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 722c2ca46..d12a965b2 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -375,7 +375,7 @@ L.Clipboard = L.Class.extend({
 	populateClipboard: function(ev) {
 		var text;
 
-		this._stopHideDownload();
+//		this._stopHideDownload(); - this confuses the borwser ruins copy/cut on iOS
 
 		if (this._selectionType === 'complex' ||
 		    this._map._docLayer.hasGraphicSelection()) {
commit da7790db46abd67d0d6e9e6dc07e005000212d0a
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Jul 10 00:01:14 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    Separate exception handling for iOS and Android postmessages.
    
    Change-Id: I618123dab29fb0e24f92b1bf7fb4bfa76e1dd709

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 59e36525e..722c2ca46 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -550,15 +550,21 @@ L.Clipboard = L.Class.extend({
 				    window.top.webkit.messageHandlers.RichDocumentsMobileInterface) {
 					console.log('We have richdocuments !');
 					window.top.webkit.messageHandlers.RichDocumentsMobileInterface.postMessage(operation);
-				} else if (window.top.RichDocumentsMobileInterface &&
-					   window.top.RichDocumentsMobileInterface.paste) {
+				} else
+					console.log('No webkit messageHandlers');
+			} catch (error) {
+				console.warn('Cannot access webkit hook: ' + error);
+			}
+
+			try {
+				if (window.top.RichDocumentsMobileInterface &&
+				    window.top.RichDocumentsMobileInterface.paste) {
 					console.log('We have richdocuments !');
 					window.top.RichDocumentsMobileInterface.paste();
-				} else {
-					console.log('No richdocuments');
-				}
+				} else
+					console.log('No RichDocumentsMobileInterface');
 			} catch (error) {
-				console.warn('Cannot paste: ' + error);
+				console.warn('Cannot access RichDocumentsMobileInterface hook: ' + error);
 			}
 		}
 
commit b9674d51cdab2f7b9dd71dcc34d521c7f44382be
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Thu Jul 4 23:35:57 2019 -0400
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Tue Aug 6 14:50:51 2019 -0400

    leaflet: Improve handling of paste without source document
    
    Improve on handling paste without download and
    handles onerror properly, which invokes the error
    handler callback (where provided) where previously
    it was ignored.
    
    Change-Id: I3b527516dc4f90484fd1cfba411b45ff948ffc53

diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp
index b3a3fd8b8..9684553e9 100644
--- a/kit/ChildSession.cpp
+++ b/kit/ChildSession.cpp
@@ -1038,6 +1038,8 @@ bool ChildSession::setClipboard(const char* buffer, int length, const std::vecto
             LOG_ERR("set clipboard returned failure");
         else
             LOG_TRC("set clipboard succeeded");
+    } catch (const std::exception& ex) {
+        LOG_ERR("set clipboard failed with exception: " << ex.what());
     } catch (...) {
         LOG_ERR("set clipboard failed with exception");
     }
diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js
index a6efdb86d..926ea62a5 100644
--- a/loleaflet/src/layer/tile/TileLayer.js
+++ b/loleaflet/src/layer/tile/TileLayer.js
@@ -2685,7 +2685,10 @@ L.TileLayer = L.GridLayer.extend({
 		e = e.originalEvent;
 		e.preventDefault();
 
-		this._map._clip.dataTransferToDocument(e.dataTransfer, /* preferInternal = */ false);
+		// Always capture the html content separate as we may lose it when we
+		// pass the clipboard data to a different context (async calls, f.e.).
+		var htmlText = e.dataTransfer.getData('text/html');
+		this._map._clip.dataTransferToDocument(e.dataTransfer, /* preferInternal = */ false, htmlText);
 	},
 
 	_onDragStart: function () {
diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index a8da6d0b8..59e36525e 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -63,14 +63,25 @@ L.Clipboard = L.Class.extend({
 			'&Tag=' + this._accessKey[idx];
 	},
 
-	getStubHtml: function() {
+	// Returns the marker used to identify stub messages.
+	_getHtmlStubMarker: function() {
+		return '<title>Stub HTML Message</title>';
+	},
+
+	// Returns true if the argument is a stub html.
+	_isStubHtml: function(text) {
+		return text.indexOf(this._getHtmlStubMarker()) > 0;
+	},
+
+	_getStubHtml: function() {
 		var lang = 'en_US'; // FIXME: l10n
 		var encodedOrigin = encodeURIComponent(this.getMetaPath());
 		var stub = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">\n' +
 		    '<html>\n' +
 		    '  <head>\n' +
-		    '     <meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n' +
-		    '     <meta name="origin" content="' + encodedOrigin + '"/>\n' +
+		    '    ' + this._getHtmlStubMarker() + '\n' +
+		    '    <meta http-equiv="content-type" content="text/html; charset=utf-8"/>\n' +
+		    '    <meta name="origin" content="' + encodedOrigin + '"/>\n' +
 		    '  </head>\n' +
 		    '  <body lang="' + lang + '" dir="ltr">\n' +
 		    '    <p>' + _('To paste outside %productName, please first click the \'download\' button') + '</p>\n' +
@@ -138,45 +149,61 @@ L.Clipboard = L.Class.extend({
 	// optionalFormData: used for POST for form data
 	// completeFn: called on completion - with response.
 	// progressFn: allows splitting the progress bar up.
-	_doAsyncDownload: function(type,url,optionalFormData,completeFn,progressFn) {
-		var that = this;
-		var request = new XMLHttpRequest();
+	_doAsyncDownload: function(type,url,optionalFormData,completeFn,progressFn,onErrorFn) {
+		try {
+			var that = this;
+			var request = new XMLHttpRequest();
 
-		// avoid to invoke the following code if the download widget depends on user interaction
-		if (!that._downloadProgress || !that._downloadProgress.isVisible()) {
-			that._startProgress();
-			that._downloadProgress.startProgressMode();
-		}
-		request.onload = function() {
-			that._downloadProgress._onComplete();
-			if (type === 'POST') {
-				that._downloadProgress._onClose();
+			// avoid to invoke the following code if the download widget depends on user interaction
+			if (!that._downloadProgress || !that._downloadProgress.isVisible()) {
+				that._startProgress();
+				that._downloadProgress.startProgressMode();
 			}
-			completeFn(this.response);
-		};
-		request.onerror = function() {
-			that._downloadProgress._onComplete();
-			that._downloadProgress._onClose();
-		};
+			request.onload = function() {
+				that._downloadProgress._onComplete();
+				if (type === 'POST') {
+					that._downloadProgress._onClose();
+				}
 
-		request.upload.addEventListener('progress', function (e) {
-			if (e.lengthComputable) {
-				var percent = progressFn(e.loaded / e.total * 100);
-				var progress = { statusType: 'setvalue', value: percent };
-				that._downloadProgress._onUpdateProgress(progress);
-			}
-		}, false);
-		request.open(type, url, true /* isAsync */);
-		request.timeout = 20 * 1000; // 20 secs ...
-		request.responseType = 'blob';
-		if (optionalFormData !== null)
-			request.send(optionalFormData);
-		else
-			request.send();
+				// For some reason 400 error from the server doesn't
+				// invoke onerror callback, but we do get here with
+				// size==0, which signifies no response from the server.
+				// So we check the status code instead.
+				if (this.status == 200) {
+					completeFn(this.response);
+				} else if (onErrorFn) {
+					onErrorFn(this.response);
+				}
+			};
+			request.onerror = function() {
+				if (onErrorFn)
+					onErrorFn();
+				that._downloadProgress._onComplete();
+				that._downloadProgress._onClose();
+			};
+
+			request.upload.addEventListener('progress', function (e) {
+				if (e.lengthComputable) {
+					var percent = progressFn(e.loaded / e.total * 100);
+					var progress = { statusType: 'setvalue', value: percent };
+					that._downloadProgress._onUpdateProgress(progress);
+				}
+			}, false);
+			request.open(type, url, true /* isAsync */);
+			request.timeout = 20 * 1000; // 20 secs ...
+			request.responseType = 'blob';
+			if (optionalFormData !== null)
+				request.send(optionalFormData);
+			else
+				request.send();
+		} catch (error) {
+			if (onErrorFn)
+				onErrorFn();
+		}
 	},
 
 	// Suck the data from one server to another asynchronously ...
-	_dataTransferDownloadAndPasteAsync: function(src, dest) {
+	_dataTransferDownloadAndPasteAsync: function(src, dest, fallbackHtml) {
 		var that = this;
 		// FIXME: add a timestamp in the links (?) ignroe old / un-responsive servers (?)
 		that._doAsyncDownload(
@@ -194,7 +221,37 @@ L.Clipboard = L.Class.extend({
 					function(progress) { return 50 + progress/2; }
 				);
 			},
-			function(progress) { return progress/2; }
+			function(progress) { return progress/2; },
+			function() {
+				console.log('failed to download clipboard using fallback html');
+
+				// If it's the stub, avoid pasting.
+				if (that._isStubHtml(fallbackHtml))
+				{
+					// Let the user know they haven't really copied document content.
+					vex.dialog.alert({
+						message: _('Failed to download clipboard, please re-copy'),
+						callback: function () {
+							that._map.focus();
+						}
+					});
+					return;
+				}
+
+				var formData = new FormData();
+				formData.append('data', new Blob([fallbackHtml]), 'clipboard');
+				that._doAsyncDownload(
+					'POST', dest, formData,
+					function() {
+						console.log('up-load of fallback done, now paste');
+						that._map._socket.sendMessage('uno .uno:Paste')
+					},
+					function(progress) { return 50 + progress/2; },
+					function() {
+						that.dataTransferToDocumentFallback(null, fallbackHtml);
+					}
+				);
+			}
 		);
 	},
 
@@ -220,13 +277,7 @@ L.Clipboard = L.Class.extend({
 		// Look for our HTML meta magic.
 		//   cf. ClientSession.cpp /textselectioncontent:/
 
-		var pasteHtml = null;
-		if (dataTransfer == null) { // IE
-			pasteHtml = htmlText;
-		} else {
-			pasteHtml = dataTransfer.getData('text/html');
-		}
-		var meta = this._getMetaOrigin(pasteHtml);
+		var meta = this._getMetaOrigin(htmlText);
 		var id = this.getMetaPath(0);
 		var idOld = this.getMetaPath(1);
 
@@ -240,26 +291,36 @@ L.Clipboard = L.Class.extend({
 			return;
 		}
 
-		var destination = this.getMetaBase() + this.getMetaPath();
-
 		// Do we have a remote Online we can suck rich data from ?
 		if (meta !== '')
 		{
 			console.log('Transfer between servers\n\t"' + meta + '" vs. \n\t"' + id + '"');
-			this._dataTransferDownloadAndPasteAsync(meta, destination);
+			var destination = this.getMetaBase() + this.getMetaPath();
+			this._dataTransferDownloadAndPasteAsync(meta, destination, htmlText);
 			return;
 		}
 
+		// Fallback.
+		this.dataTransferToDocumentFallback(dataTransfer, htmlText, usePasteKeyEvent);
+	},
+
+	dataTransferToDocumentFallback: function(dataTransfer, htmlText, usePasteKeyEvent) {
+
 		var content;
-		if (dataTransfer == null)
-			content = this._encodeHtmlToBlob(htmlText);
-		else // Suck HTML content out of dataTransfer now while it feels like working.
+		if (dataTransfer) {
+			// Suck HTML content out of dataTransfer now while it feels like working.
 			content = this._readContentSyncToBlob(dataTransfer);
+		}
+
+		// Fallback on the html.
+		if (!content) {
+			content = this._encodeHtmlToBlob(htmlText);
+		}
 
 		// FIXME: do we want this section ?
 
 		// Images get a look in only if we have no content and are async
-		if (content == null && pasteHtml === '' && dataTransfer != null)
+		if (content == null && htmlText === '' && dataTransfer != null)
 		{
 			var types = dataTransfer.types;
 
@@ -291,6 +352,7 @@ L.Clipboard = L.Class.extend({
 			formData.append('file', content);
 
 			var that = this;
+			var destination = this.getMetaBase() + this.getMetaPath();
 			this._doAsyncDownload('POST', destination, formData,
 							function() {
 								console.log('Posted ' + content.size + ' bytes successfully');
@@ -326,14 +388,14 @@ L.Clipboard = L.Class.extend({
 			else
 			{
 				console.log('Downloaded that selection.');
-				text = this.getStubHtml();
+				text = this._getStubHtml();
 				this._onDownloadOnLargeCopyPaste();
 				this._downloadProgress.setURI( // richer, bigger HTML ...
 					this.getMetaBase() + this.getMetaPath() + '&MimeType=text/html');
 			}
 		} else if (this._selectionType === null) {
 			console.log('Copy/Cut with no selection!');
-			text = this.getStubHtml();
+			text = this._getStubHtml();
 		} else {
 			console.log('Copy/Cut with simple text selection');
 			text = this._selectionContent;
@@ -400,7 +462,7 @@ L.Clipboard = L.Class.extend({
 			// Can't get HTML until it is pasted ... so quick timeout
 			setTimeout(function() {
 				var tmpDiv = document.getElementById(that._dummyDivName);
-				that.dataTransferToDocument(null, false, tmpDiv.innerHTML);
+				that.dataTransferToDocument(null, /* preferInternal = */ false, tmpDiv.innerHTML);
 				that.compatRemoveNode(tmpDiv);
 				// attempt to restore focus.
 				if (active == null)
@@ -553,8 +615,11 @@ L.Clipboard = L.Class.extend({
 		if (ev.clipboardData) { // Standard
 			ev.preventDefault();
 			var usePasteKeyEvent = ev.usePasteKeyEvent;
-			this.dataTransferToDocument(ev.clipboardData, /* preferInternal = */ true, null, usePasteKeyEvent);
-			this._map._clipboardContainer._abortComposition();
+			// Always capture the html content separate as we may lose it when we
+			// pass the clipboard data to a different context (async calls, f.e.).
+			var htmlText = ev.clipboardData.getData('text/html');
+			this.dataTransferToDocument(ev.clipboardData, /* preferInternal = */ true, htmlText, usePasteKeyEvent);
+			this._map._clipboardContainer._abortComposition(ev);
 			this._clipboardSerial++;
 			this._stopHideDownload();
 		}
commit 9a0bb531eb40fb59ba765cb7c3e588dde269e86f
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Sun Jul 7 01:04:04 2019 -0400
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 22:21:54 2019 -0400

    wsd: Logging corrections
    
    Change-Id: Ib4472c203d29992f08966a78961ab364e589f8d0

diff --git a/net/Socket.hpp b/net/Socket.hpp
index f914da590..730f7194b 100644
--- a/net/Socket.hpp
+++ b/net/Socket.hpp
@@ -1110,12 +1110,13 @@ public:
 
                 auto& log = Log::logger();
                 if (log.trace() && len > 0) {
-                    LOG_TRC("#" << getFD() << ": Wrote outgoing data " << len << " bytes.");
+                    LOG_TRC("#" << getFD() << ": Wrote outgoing data " << len <<
+                            " bytes of " << _outBuffer.size() << " bytes buffered.");
                     // log.dump("", &_outBuffer[0], len);
                 }
 
                 if (len <= 0 && errno != EAGAIN && errno != EWOULDBLOCK)
-                    LOG_SYS("#" << getFD() << ": Wrote outgoing data " << len << " bytes.");
+                    LOG_SYS("#" << getFD() << ": Socket write returned " << len);
             }
             while (len < 0 && errno == EINTR);
 
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index bbc3fa8fb..3ed895cdc 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2401,7 +2401,7 @@ private:
         else if (!DocumentBroker::lookupSendClipboardTag(_socket.lock(), tag, false))
         {
             LOG_ERR("Invalid clipboard request: " << serverId << " with tag " << tag <<
-                    " and broker: " << (docBroker ? "not" : "") << "found");
+                    " and broker: " << (docBroker ? "" : "not ") << "found");
 
             std::string errMsg;
             if (serverId != LOOLWSD::HostIdentifier)
commit d12227c8390f94152c537ec4c1558a5bfcac3add
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Sat Jul 6 14:31:29 2019 -0400
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 22:21:54 2019 -0400

    wsd: send clipboard key on loading
    
    While we don't send the clipboard key
    to the client during the construction
    of ClientSession, we do so upon handshake
    (loolclient message), and by then our state
    is no longer DETACHED, rather it is LOADING.
    
    This restores copy/paste across documents.
    
    Change-Id: I0db50210f232afa05b1273edeb2cc163fd07c504

diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 29a72a9d5..b832a7d60 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -160,7 +160,7 @@ void ClientSession::rotateClipboardKey(bool notifyClient)
         return;
 
     if (_state != SessionState::LIVE &&   // editing
-        _state != SessionState::DETACHED) // constructor
+        _state != SessionState::LOADING) // handshake with client
         return;
 
     _clipboardKeys[1] = _clipboardKeys[0];
commit 9e791fb0d4d4c701c50b8b50091b5665b832c3f1
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Thu Jul 4 10:50:33 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 22:21:54 2019 -0400

    clipboard: persist selections for a while after a view closes.
    
    re-factor ClientSession state to be a simpler state machine.
    Have a nice disconnect / disconnected handshake on view close.
    
    Change-Id: Ie933cc5c7dfab46c66f4d38a4d75c459aa1cff87

diff --git a/common/Clipboard.hpp b/common/Clipboard.hpp
index fbbcb557c..0b4bee438 100644
--- a/common/Clipboard.hpp
+++ b/common/Clipboard.hpp
@@ -13,9 +13,13 @@
 
 #include <string>
 #include <vector>
+#include <unordered_map>
+#include <mutex>
+
 #include <stdlib.h>
 #include <Log.hpp>
 #include <Exceptions.hpp>
+#include <Poco/MemoryStream.h>
 
 struct ClipboardData
 {
@@ -78,6 +82,66 @@ struct ClipboardData
     }
 };
 
+/// Used to store expired view's clipboards
+class ClipboardCache
+{
+    std::mutex _mutex;
+    struct Entry {
+        std::chrono::steady_clock::time_point _inserted;
+        std::shared_ptr<std::string> _rawData; // big.
+    };
+    // clipboard key -> data
+    std::unordered_map<std::string, Entry> _cache;
+public:
+    ClipboardCache()
+    {
+    }
+
+    void insertClipboard(const std::string key[2],
+                         const char *data, size_t size)
+    {
+        if (size == 0)
+        {
+            LOG_TRC("clipboard cache - ignores empty clipboard data");
+            return;
+        }
+        Entry ent;
+        ent._inserted = std::chrono::steady_clock::now();
+        ent._rawData = std::make_shared<std::string>(data, size);
+        LOG_TRC("insert cached clipboard: " + key[0] + " and " + key[1]);
+        std::lock_guard<std::mutex> lock(_mutex);
+        _cache[key[0]] = ent;
+        _cache[key[1]] = ent;
+    }
+
+    std::shared_ptr<std::string> getClipboard(const std::string &key)
+    {
+        std::lock_guard<std::mutex> lock(_mutex);
+        std::shared_ptr<std::string> data;
+        auto it = _cache.find(key);
+        if (it != _cache.end())
+            data = it->second._rawData;
+        return data;
+    }
+
+    void checkexpiry()
+    {
+        std::lock_guard<std::mutex> lock(_mutex);
+        auto now = std::chrono::steady_clock::now();
+        LOG_TRC("check expiry of cached clipboards");
+        for (auto it = _cache.begin(); it != _cache.end();)
+        {
+            if (std::chrono::duration_cast<std::chrono::minutes>(now - it->second._inserted).count() >= 10)
+            {
+                LOG_TRC("expiring expiry of cached clipboard: " + it->first);
+                it = _cache.erase(it);
+            }
+            else
+                ++it;
+        }
+    }
+};
+
 #endif
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp
index 56700852e..b3a3fd8b8 100644
--- a/kit/ChildSession.cpp
+++ b/kit/ChildSession.cpp
@@ -946,7 +946,7 @@ bool ChildSession::getTextSelection(const char* /*buffer*/, int /*length*/, cons
     return true;
 }
 
-bool ChildSession::getClipboard(const char* /*buffer*/, int /*length*/, const std::vector<std::string>& tokens )
+bool ChildSession::getClipboard(const char* /*buffer*/, int /*length*/, const std::vector<std::string>& tokens)
 {
     const char **pMimeTypes = nullptr; // fetch all for now.
     const char  *pOneType[2];
diff --git a/kit/ChildSession.hpp b/kit/ChildSession.hpp
index b15c8b33a..a1a4532bb 100644
--- a/kit/ChildSession.hpp
+++ b/kit/ChildSession.hpp
@@ -233,6 +233,8 @@ public:
 
     using Session::sendTextFrame;
 
+    bool getClipboard(const char* buffer, int length, const std::vector<std::string>& tokens);
+
 private:
     bool loadDocument(const char* buffer, int length, const std::vector<std::string>& tokens);
 
@@ -245,7 +247,6 @@ private:
     bool downloadAs(const char* buffer, int length, const std::vector<std::string>& tokens);
     bool getChildId();
     bool getTextSelection(const char* buffer, int length, const std::vector<std::string>& tokens);
-    bool getClipboard(const char* buffer, int length, const std::vector<std::string>& tokens);
     bool setClipboard(const char* buffer, int length, const std::vector<std::string>& tokens);
     std::string getTextSelectionInternal(const std::string& mimeType);
     bool paste(const char* buffer, int length, const std::vector<std::string>& tokens);
diff --git a/kit/Kit.cpp b/kit/Kit.cpp
index 832eb6423..a76b6d1f0 100644
--- a/kit/Kit.cpp
+++ b/kit/Kit.cpp
@@ -983,7 +983,7 @@ public:
             // session is being removed.
             for (auto it = _sessions.cbegin(); it != _sessions.cend(); )
             {
-                if (it->second->isCloseFrame())
+               if (it->second->isCloseFrame())
                 {
                     deadSessions.push_back(it->second);
                     it = _sessions.erase(it);
@@ -1901,6 +1901,10 @@ private:
                         _editorId = -1;
                     }
                     LOG_DBG("Removing ChildSession [" << sessionId << "].");
+
+                    // Tell them we're going quietly.
+                    session->sendTextFrame("disconnected:");
+
                     _sessions.erase(it);
                     const size_t count = _sessions.size();
                     LOG_DBG("Have " << count << " child" << (count == 1 ? "" : "ren") <<
diff --git a/test/UnitCopyPaste.cpp b/test/UnitCopyPaste.cpp
index 23697778c..018c60ea7 100644
--- a/test/UnitCopyPaste.cpp
+++ b/test/UnitCopyPaste.cpp
@@ -109,7 +109,7 @@ public:
         std::string value;
 
         // allow empty clipboards
-        if (clipboard && mimeType =="" && clipboard->size() == 0)
+        if (clipboard && mimeType == "" && content == "")
             return true;
 
         if (!clipboard || !clipboard->findType(mimeType, value))
@@ -205,7 +205,9 @@ public:
             assert(sessions.size() > 0 && session < sessions.size());
             clientSession = sessions[session];
 
-            return clientSession->getClipboardURI(false); // nominally thread unsafe
+            std::string tag = clientSession->getClipboardURI(false); // nominally thread unsafe
+            std::cerr << "Got tag '" << tag << "' for session " << session << "\n";
+            return tag;
     }
 
     std::string buildClipboardText(const std::string &text)
@@ -294,6 +296,18 @@ public:
         if (!fetchClipboardAssert(clipURI, "text/plain;charset=utf-8", "herring"))
             return;
 
+        std::cerr << "Close sockets:\n";
+        socket->shutdown();
+        socket2->shutdown();
+
+        sleep(1); // paranoia.
+
+        std::cerr << "Fetch clipboards after shutdown:\n";
+        if (!fetchClipboardAssert(clipURI2, "text/plain;charset=utf-8", "kippers"))
+            return;
+        if (!fetchClipboardAssert(clipURI, "text/plain;charset=utf-8", "herring"))
+            return;
+
         std::cerr << "Clipboard tests succeeded" << std::endl;
         exitTest(TestResult::Ok);
 
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index e02d9dadf..29a72a9d5 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -27,6 +27,7 @@
 #include <common/Common.hpp>
 #include <common/Log.hpp>
 #include <common/Protocol.hpp>
+#include <common/Clipboard.hpp>
 #include <common/Session.hpp>
 #include <common/Unit.hpp>
 #include <common/Util.hpp>
@@ -48,8 +49,7 @@ ClientSession::ClientSession(const std::string& id,
     _docBroker(docBroker),
     _uriPublic(uriPublic),
     _isDocumentOwner(false),
-    _isAttached(false),
-    _isViewLoaded(false),
+    _state(SessionState::DETACHED),
     _keyEvents(1),
     _clientVisibleArea(0, 0, 0, 0),
     _clientSelectedPart(-1),
@@ -67,6 +67,9 @@ ClientSession::ClientSession(const std::string& id,
     // populate with random values.
     for (auto it : _clipboardKeys)
         rotateClipboardKey(false);
+
+    // get timestamp set
+    setState(SessionState::DETACHED);
 }
 
 // Can't take a reference in the constructor.
@@ -85,11 +88,81 @@ ClientSession::~ClientSession()
     SessionMap.erase(getId());
 }
 
+static const char *stateToString(ClientSession::SessionState s)
+{
+    switch (s)
+    {
+    case ClientSession::SessionState::DETACHED:        return "detached";
+    case ClientSession::SessionState::LOADING:         return "loading";
+    case ClientSession::SessionState::LIVE:            return "live";
+    case ClientSession::SessionState::WAIT_DISCONNECT: return "wait_disconnect";
+    }
+    return "invalid";
+}
+
+void ClientSession::setState(SessionState newState)
+{
+    LOG_TRC("ClientSession: transition from " << stateToString(_state) <<
+            " to " << stateToString(newState));
+    switch (newState)
+    {
+    case SessionState::DETACHED:
+        assert(_state == SessionState::DETACHED);
+        break;
+    case SessionState::LOADING:
+        assert(_state == SessionState::DETACHED);
+        break;
+    case SessionState::LIVE:
+        assert(_state == SessionState::LIVE ||
+               _state == SessionState::LOADING);
+        break;
+    case SessionState::WAIT_DISCONNECT:
+        assert(_state == SessionState::LOADING ||
+               _state == SessionState::LIVE);
+        break;
+    }
+    _state = newState;
+    _lastStateTime = std::chrono::steady_clock::now();
+}
+
+bool ClientSession::disconnectFromKit()
+{
+    assert(_state != SessionState::WAIT_DISCONNECT);
+    auto docBroker = getDocumentBroker();
+    if (_state == SessionState::LIVE && docBroker)
+    {
+        setState(SessionState::WAIT_DISCONNECT);
+
+        LOG_TRC("request/rescue clipboard on disconnect for " << getId());
+        // rescue clipboard before shutdown.
+        docBroker->forwardToChild(getId(), "getclipboard");
+
+        // handshake nicely; so wait for 'disconnected'
+        docBroker->forwardToChild(getId(), "disconnect");
+
+        return false;
+    }
+
+    return true; // just get on with it
+}
+
+// Allow 20secs for the clipboard and disconection to come.
+bool ClientSession::staleWaitDisconnect(const std::chrono::steady_clock::time_point &now)
+{
+    if (_state != SessionState::WAIT_DISCONNECT)
+        return false;
+    return std::chrono::duration_cast<std::chrono::seconds>(now - _lastStateTime).count() >= 20;
+}
+
 void ClientSession::rotateClipboardKey(bool notifyClient)
 {
     if (_wopiFileInfo && _wopiFileInfo->getDisableCopy())
         return;
 
+    if (_state != SessionState::LIVE &&   // editing
+        _state != SessionState::DETACHED) // constructor
+        return;
+
     _clipboardKeys[1] = _clipboardKeys[0];
     _clipboardKeys[0] = Util::rng::getHardRandomHexString(16);
     LOG_TRC("Clipboard key on [" << getId() << "] set to " << _clipboardKeys[0] <<
@@ -135,14 +208,39 @@ bool ClientSession::matchesClipboardKeys(const std::string &/*viewId*/, const st
     return false;
 }
 
+
 void ClientSession::handleClipboardRequest(DocumentBroker::ClipboardRequest     type,
                                            const std::shared_ptr<StreamSocket> &socket,
+                                           const std::string                   &tag,
                                            const std::shared_ptr<std::string>  &data)
 {
     // Move the socket into our DocBroker.
     auto docBroker = getDocumentBroker();
     docBroker->addSocketToPoll(socket);
 
+    if (_state == SessionState::WAIT_DISCONNECT)
+    {
+        LOG_TRC("Clipboard request " << tag << " for disconnecting session");
+        if (docBroker->lookupSendClipboardTag(socket, tag, false))
+            return; // the getclipboard already completed.
+        if (type == DocumentBroker::CLIP_REQUEST_SET)
+        {
+            std::ostringstream oss;
+            oss << "HTTP/1.1 400 Bad Request\r\n"
+                << "Date: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n"
+                << "User-Agent: " << WOPI_AGENT_STRING << "\r\n"
+                << "Content-Length: 0\r\n"
+                << "\r\n";
+            socket->send(oss.str());
+            socket->shutdown();
+        }
+        else // will be handled during shutdown
+        {
+            LOG_TRC("Clipboard request " << tag << " queued for shutdown");
+            _clipSockets.push_back(socket);
+        }
+    }
+
     std::string specific;
     if (type == DocumentBroker::CLIP_REQUEST_GET_RICH_HTML_ONLY)
         specific = " text/html";
@@ -1107,7 +1205,8 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
         // 'download' and/or providing our helpful / user page.
 
         // for now just for remote sockets.
-        LOG_TRC("Got clipboard content to send to " << _clipSockets.size() << "sockets");
+        LOG_TRC("Got clipboard content of size " << payload->size() << " to send to " <<
+                _clipSockets.size() << " sockets in state " << stateToString(_state));
 
         postProcessCopyPayload(payload);
 
@@ -1116,6 +1215,13 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
             if (payload->data()[header++] == '\n')
                 break;
         const bool empty = header >= payload->size();
+
+        // final cleanup ...
+        if (!empty && _state == SessionState::WAIT_DISCONNECT &&
+            (!_wopiFileInfo || !_wopiFileInfo->getDisableCopy()))
+            LOOLWSD::SavedClipboards->insertClipboard(
+                _clipboardKeys, &payload->data()[header], payload->size() - header);
+
         for (auto it : _clipSockets)
         {
             std::ostringstream oss;
@@ -1139,6 +1245,11 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
         }
         _clipSockets.clear();
         return true;
+    } else if (tokens[0] == "disconnected:") {
+
+        LOG_INF("End of disconnection handshake for " << getId());
+        docBroker->finalRemoveSession(getId());
+        return true;
     }
 
     if (!isDocPasswordProtected())
@@ -1149,7 +1260,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
         }
         else if (tokens[0] == "status:")
         {
-            setViewLoaded();
+            setState(ClientSession::SessionState::LIVE);
             docBroker->setLoaded();
             // Wopi post load actions
             if (_wopiFileInfo && !_wopiFileInfo->getTemplateSource().empty())
@@ -1454,7 +1565,7 @@ void ClientSession::dumpState(std::ostream& os)
 
     os << "\t\tisReadOnly: " << isReadOnly()
        << "\n\t\tisDocumentOwner: " << _isDocumentOwner
-       << "\n\t\tisAttached: " << _isAttached
+       << "\n\t\tstate: " << stateToString(_state)
        << "\n\t\tkeyEvents: " << _keyEvents
 //       << "\n\t\tvisibleArea: " << _clientVisibleArea
        << "\n\t\tclientSelectedPart: " << _clientSelectedPart
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index 5d5d68249..d4b31dbea 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -44,13 +44,21 @@ public:
 
     void setReadOnly() override;
 
-    /// Returns true if this session is added to a DocBroker.
-    bool isAttached() const { return _isAttached; }
-    void setAttached() { _isAttached = true; }
+    enum SessionState {
+        DETACHED,        // initial
+        LOADING,         // attached to a DocBroker & waiting for load
+        LIVE,            // Document is loaded & editable or viewable.
+        WAIT_DISCONNECT  // closed and waiting for Kit's disconnected message
+    };
 
     /// Returns true if this session has loaded a view (i.e. we got status message).
-    bool isViewLoaded() const { return _isViewLoaded; }
-    void setViewLoaded() { _isViewLoaded = true; }
+    bool isViewLoaded() const { return _state == SessionState::LIVE; }
+
+    /// returns true if we're waiting for the kit to acknowledge disconnect.
+    bool inWaitDisconnected() const { return _state == SessionState::WAIT_DISCONNECT; }
+
+    /// transition to a new state
+    void setState(SessionState newState);
 
     void setDocumentOwner(const bool documentOwner) { _isDocumentOwner = documentOwner; }
     bool isDocumentOwner() const { return _isDocumentOwner; }
@@ -61,6 +69,9 @@ public:
     /// Integer id of the view in the kit process, or -1 if unknown
     int getKitViewId() const { return _kitViewId; }
 
+    /// Disconnect the session and do final cleanup, @returns true if we should not wait.
+    bool disconnectFromKit();
+
     // sendTextFrame that takes std::string and string literal.
     using Session::sendTextFrame;
 
@@ -147,6 +158,7 @@ public:
     /// Handle a clipboard fetch / put request.
     void handleClipboardRequest(DocumentBroker::ClipboardRequest     type,
                                 const std::shared_ptr<StreamSocket> &socket,
+                                const std::string                   &tag,
                                 const std::shared_ptr<std::string>  &data);
 
     /// Create URI for transient clipboard content.
@@ -155,6 +167,9 @@ public:
     /// Adds and/or modified the copied payload before sending on to the client.
     void postProcessCopyPayload(std::shared_ptr<Message> payload);
 
+    /// Returns true if we're expired waiting for a clipboard and should be removed
+    bool staleWaitDisconnect(const std::chrono::steady_clock::time_point &now);
+
     /// Generate and rotate a new clipboard hash, sending it if appropriate
     void rotateClipboardKey(bool notifyClient);
 
@@ -211,11 +226,11 @@ private:
     /// The socket to which the converted (saveas) doc is sent.
     std::shared_ptr<StreamSocket> _saveAsSocket;
 
-    /// If we are added to a DocBroker.
-    bool _isAttached;
+    /// The phase of our lifecycle that we're in.
+    SessionState _state;
 
-    /// If we have loaded a view.
-    bool _isViewLoaded;
+    /// Time of last state transition
+    std::chrono::steady_clock::time_point _lastStateTime;
 
     /// Wopi FileInfo object
     std::unique_ptr<WopiStorage::WOPIFileInfo> _wopiFileInfo;
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index 2d4645747..be1794eb7 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -35,6 +35,7 @@
 #include "TileCache.hpp"
 #include <common/Log.hpp>
 #include <common/Message.hpp>
+#include <common/Clipboard.hpp>
 #include <common/Protocol.hpp>
 #include <common/Unit.hpp>
 #include <common/FileUtil.hpp>
@@ -343,10 +344,23 @@ void DocumentBroker::pollThread()
 
 #if !MOBILEAPP
         if (std::chrono::duration_cast<std::chrono::minutes>(now - lastClipboardHashUpdateTime).count() >= 2)
+        for (auto &it : _sessions)
+        {
+            if (it.second->staleWaitDisconnect(now))
+            {
+                std::string id = it.second->getId();
+                LOG_WRN("Unusual, Kit session " + id + " failed its disconnect handshake, killing");
+                finalRemoveSession(id);
+                break; // it invalid.
+            }
+        }
+
+        if (std::chrono::duration_cast<std::chrono::minutes>(now - lastClipboardHashUpdateTime).count() >= 5)
         {
             LOG_TRC("Rotating clipboard keys");
-            for (auto& it : _sessions)
+            for (auto &it : _sessions)
                 it.second->rotateClipboardKey(true);
+
             lastClipboardHashUpdateTime = now;
         }
 
@@ -810,7 +824,7 @@ bool DocumentBroker::saveToStorage(const std::string& sessionId,
     // If marked to destroy, or session is disconnected, remove.
     const auto it = _sessions.find(sessionId);
     if (_markToDestroy || (it != _sessions.end() && it->second->isCloseFrame()))
-        removeSessionInternal(sessionId);
+        disconnectSessionInternal(sessionId);
 
     // If marked to destroy, then this was the last session.
     if (_markToDestroy || _sessions.empty())
@@ -1025,7 +1039,8 @@ bool DocumentBroker::autoSave(const bool force, const bool dontSaveIfUnmodified)
     for (auto& sessionIt : _sessions)
     {
         // Save the document using an editable session, or first ...
-        if (savingSessionId.empty() || !sessionIt.second->isReadOnly())
+        if (savingSessionId.empty() ||
+            (!sessionIt.second->isReadOnly() && !sessionIt.second->inWaitDisconnected()))
         {
             savingSessionId = sessionIt.second->getId();
         }
@@ -1211,7 +1226,7 @@ size_t DocumentBroker::addSessionInternal(const std::shared_ptr<ClientSession>&
 
     // Add and attach the session.
     _sessions.emplace(session->getId(), session);
-    session->setAttached();
+    session->setState(ClientSession::SessionState::LOADING);
 
     const size_t count = _sessions.size();
     LOG_TRC("Added " << (session->isReadOnly() ? "readonly" : "non-readonly") <<
@@ -1247,7 +1262,7 @@ size_t DocumentBroker::removeSession(const std::string& id)
 
         // If last editable, save and don't remove until after uploading to storage.
         if (!lastEditableSession || !autoSave(isPossiblyModified(), dontSaveIfUnmodified))
-            removeSessionInternal(id);
+            disconnectSessionInternal(id);
     }
     catch (const std::exception& ex)
     {
@@ -1257,7 +1272,7 @@ size_t DocumentBroker::removeSession(const std::string& id)
     return _sessions.size();
 }
 
-size_t DocumentBroker::removeSessionInternal(const std::string& id)
+void DocumentBroker::disconnectSessionInternal(const std::string& id)
 {
     assertCorrectThread();
     try
@@ -1272,10 +1287,51 @@ size_t DocumentBroker::removeSessionInternal(const std::string& id)
             LOOLWSD::dumpEndSessionTrace(getJailId(), id, _uriOrig);
 #endif
 
+            LOG_TRC("Disconnect session internal " << id);
+
+            bool hardDisconnect;
+            if (it->second->inWaitDisconnected())
+            {
+                LOG_TRC("hard disconnecting while waiting for disconnected handshake.");
+                hardDisconnect = true;
+            }
+            else
+            {
+                hardDisconnect = it->second->disconnectFromKit();
+
+                // Let the child know the client has disconnected.
+                const std::string msg("child-" + id + " disconnect");
+                _childProcess->sendTextFrame(msg);
+            }
+
+            if (hardDisconnect)
+                finalRemoveSession(id);
+            // else wait for disconnected.
+        }
+        else
+        {
+            LOG_TRC("Session [" << id << "] not found to disconnect from docKey [" <<
+                    _docKey << "]. Have " << _sessions.size() << " sessions.");
+        }
+    }
+    catch (const std::exception& ex)
+    {
+        LOG_ERR("Error while disconnecting session [" << id << "]: " << ex.what());
+    }
+}
+
+void DocumentBroker::finalRemoveSession(const std::string& id)
+{
+    assertCorrectThread();
+    try
+    {
+        auto it = _sessions.find(id);
+        if (it != _sessions.end())
+        {
             const bool readonly = (it->second ? it->second->isReadOnly() : false);
 
             // Remove. The caller must have a reference to the session
-            // in question, lest we destroy from underneith them.
+            // in question, lest we destroy from underneath them.
             _sessions.erase(it);
             const size_t count = _sessions.size();
 
@@ -1291,11 +1347,7 @@ size_t DocumentBroker::removeSessionInternal(const std::string& id)
                 LOG_END(logger, true);
             }
 
-            // Let the child know the client has disconnected.
-            const std::string msg("child-" + id + " disconnect");
-            _childProcess->sendTextFrame(msg);
-
-            return count;
+            return;
         }
         else
         {
@@ -1307,8 +1359,6 @@ size_t DocumentBroker::removeSessionInternal(const std::string& id)
     {
         LOG_ERR("Error while removing session [" << id << "]: " << ex.what());
     }
-
-    return _sessions.size();
 }
 
 void DocumentBroker::addCallback(const SocketPoll::CallbackFn& fn)
@@ -1333,7 +1383,8 @@ void DocumentBroker::alertAllUsers(const std::string& msg)
     LOG_DBG("Alerting all users of [" << _docKey << "]: " << msg);
     for (auto& it : _sessions)
     {
-        it.second->enqueueSendMessage(payload);
+        if (!it.second->inWaitDisconnected())
+            it.second->enqueueSendMessage(payload);
     }
 }
 
@@ -1421,7 +1472,8 @@ void DocumentBroker::handleTileRequest(TileDesc& tile,
     {
         for (auto& it: _sessions)
         {
-            tileCache().subscribeToTileRendering(tile, it.second);
+            if (!it.second->inWaitDisconnected())
+                tileCache().subscribeToTileRendering(tile, it.second);
         }
     }
     else
@@ -1515,19 +1567,34 @@ void DocumentBroker::handleTileCombinedRequest(TileCombined& tileCombined,
     sendRequestedTiles(session);
 }
 
-void DocumentBroker::handleClipboardRequest(ClipboardRequest type,  const std::shared_ptr<StreamSocket> &socket,
-                                            const std::string &viewId, const std::string &tag,
-                                            const std::shared_ptr<std::string> &data)
+/// lookup in global clipboard cache and send response, send error if missing if @sendError
+bool DocumentBroker::lookupSendClipboardTag(const std::shared_ptr<StreamSocket> &socket,
+                                            const std::string &tag, bool sendError)
 {
-    for (auto& it : _sessions)
+    LOG_TRC("Clipboard request " << tag << " not for a live session - check cache.");
+    std::shared_ptr<std::string> saved =
+        LOOLWSD::SavedClipboards->getClipboard(tag);
+    if (saved)
     {
-        if (it.second->matchesClipboardKeys(viewId, tag))
-        {
-            it.second->handleClipboardRequest(type, socket, data);
-            return;
-        }
+            std::ostringstream oss;
+            oss << "HTTP/1.1 200 OK\r\n"
+                << "Last-Modified: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n"
+                << "User-Agent: " << WOPI_AGENT_STRING << "\r\n"
+                << "Content-Length: " << saved->length() << "\r\n"
+                << "Content-Type: application/octet-stream\r\n"
+                << "X-Content-Type-Options: nosniff\r\n"
+                << "\r\n";
+            oss.write(saved->c_str(), saved->length());
+            socket->setSocketBufferSize(std::min(saved->length() + 256,
+                                                 size_t(Socket::MaximumSendBufferSize)));
+            socket->send(oss.str());
+            socket->shutdown();
+            LOG_INF("Found and queued clipboard response for send of size " << saved->length());
+            return true;
     }
-    LOG_ERR("Could not find matching session to handle clipboard request for " << viewId << " tag: " << tag);
+
+    if (!sendError)
+        return false;
 
     // Bad request.
     std::ostringstream oss;
@@ -1539,6 +1606,24 @@ void DocumentBroker::handleClipboardRequest(ClipboardRequest type,  const std::s
         << "Failed to find this clipboard";
     socket->send(oss.str());
     socket->shutdown();
+
+    return false;
+}
+
+void DocumentBroker::handleClipboardRequest(ClipboardRequest type,  const std::shared_ptr<StreamSocket> &socket,
+                                            const std::string &viewId, const std::string &tag,
+                                            const std::shared_ptr<std::string> &data)
+{
+    for (auto& it : _sessions)
+    {
+        if (it.second->matchesClipboardKeys(viewId, tag))
+        {
+            it.second->handleClipboardRequest(type, socket, tag, data);
+            return;
+        }
+    }
+    if (!lookupSendClipboardTag(socket, tag, true))
+        LOG_ERR("Could not find matching session to handle clipboard request for " << viewId << " tag: " << tag);
 }
 
 void DocumentBroker::sendRequestedTiles(const std::shared_ptr<ClientSession>& session)
@@ -1724,7 +1809,8 @@ bool DocumentBroker::haveAnotherEditableSession(const std::string& id) const
     {
         if (it.second->getId() != id &&
             it.second->isViewLoaded() &&
-            !it.second->isReadOnly())
+            !it.second->isReadOnly() &&
+            !it.second->inWaitDisconnected())
         {
             // This is a loaded session that is non-readonly.
             return true;
@@ -1820,9 +1906,10 @@ bool DocumentBroker::forwardToClient(const std::shared_ptr<Message>& payload)
             // Broadcast to all.
             // Events could cause the removal of sessions.
             std::map<std::string, std::shared_ptr<ClientSession>> sessions(_sessions);
-            for (const auto& pair : sessions)
+            for (const auto& it : _sessions)
             {
-                pair.second->handleKitToClientMessage(data, size);
+                if (!it.second->inWaitDisconnected())
+                    it.second->handleKitToClientMessage(data, size);
             }
         }
         else
@@ -1862,11 +1949,16 @@ void DocumentBroker::shutdownClients(const std::string& closeReason)
         std::shared_ptr<ClientSession> session = pair.second;
         try
         {
-            // Notify the client and disconnect.
-            session->shutdown(WebSocketHandler::StatusCodes::ENDPOINT_GOING_AWAY, closeReason);
+            if (session->inWaitDisconnected())
+                finalRemoveSession(session->getId());
+            else
+            {
+                // Notify the client and disconnect.
+                session->shutdown(WebSocketHandler::StatusCodes::ENDPOINT_GOING_AWAY, closeReason);
 
-            // Remove session, save, and mark to destroy.
-            removeSession(session->getId());
+                // Remove session, save, and mark to destroy.
+                removeSession(session->getId());
+            }
         }
         catch (const std::exception& exc)
         {
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 719f74856..b50f6d706 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -231,6 +231,9 @@ public:
     /// Flag for termination. Note that this doesn't save any unsaved changes in the document
     void stop(const std::string& reason);
 
+    /// Hard removes a session by ID, only for ClientSession.
+    void finalRemoveSession(const std::string& id);
+
     /// Thread safe termination of this broker if it has a lingering thread
     void joinThread();
 
@@ -322,6 +325,8 @@ public:
     void handleClipboardRequest(ClipboardRequest type,  const std::shared_ptr<StreamSocket> &socket,
                                 const std::string &viewId, const std::string &tag,
                                 const std::shared_ptr<std::string> &data);
+    static bool lookupSendClipboardTag(const std::shared_ptr<StreamSocket> &socket,
+                                       const std::string &tag, bool sendError = false);
 
     bool isMarkedToDestroy() const { return _markToDestroy || _stop; }
 
@@ -401,8 +406,8 @@ private:
     /// Loads a new session and adds to the sessions container.
     size_t addSessionInternal(const std::shared_ptr<ClientSession>& session);
 
-    /// Removes a session by ID. Returns the new number of sessions.
-    size_t removeSessionInternal(const std::string& id);
+    /// Starts the Kit <-> DocumentBroker shutdown handshake
+    void disconnectSessionInternal(const std::string& id);
 
     /// Forward a message from child session to its respective client session.
     bool forwardToClient(const std::shared_ptr<Message>& payload);
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index a7f5b8971..bbc3fa8fb 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -104,6 +104,7 @@ using Poco::Net::PartHandler;
 #include "Auth.hpp"
 #include "ClientSession.hpp"
 #include <Common.hpp>
+#include <Clipboard.hpp>
 #include <Crypto.hpp>
 #include <DelaySocket.hpp>
 #include "DocumentBroker.hpp"
@@ -721,6 +722,7 @@ static std::string UnitTestLibrary;
 
 unsigned int LOOLWSD::NumPreSpawnedChildren = 0;
 std::unique_ptr<TraceFileWriter> LOOLWSD::TraceDumper;
+std::unique_ptr<ClipboardCache> LOOLWSD::SavedClipboards;
 
 /// This thread polls basic web serving, and handling of
 /// websockets before upgrade: when upgraded they go to the
@@ -1154,6 +1156,8 @@ void LOOLWSD::initialize(Application& self)
     }
 
 #if !MOBILEAPP
+    SavedClipboards.reset(new ClipboardCache());
+
     FileServerRequestHandler::initialize();
 #endif
 
@@ -2152,7 +2156,7 @@ private:
                 StringTokenizer reqPathTokens(request.getURI(), "/?", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM);
                 if (reqPathTokens.count() > 1 && reqPathTokens[0] == "lool" && reqPathTokens[1] == "clipboard")
                 {
-//                    Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ...
+                    Util::dumpHex(std::cerr, "clipboard:\n", "", socket->getInBuffer()); // lots of data ...
                     handleClipboardRequest(request, message, disposition);
                 }
                 else if (!(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) &&
@@ -2325,7 +2329,8 @@ private:
                                 Poco::MemoryInputStream& message,
                                 SocketDisposition &disposition)
     {
-        LOG_DBG("Clipboard request: " << request.getURI());
+        LOG_DBG("Clipboard " << ((request.getMethod() == HTTPRequest::HTTP_GET) ? "GET" : "POST") <<
+                " request: " << request.getURI());
 
         Poco::URI requestUri(request.getURI());
         Poco::URI::QueryParameters params = requestUri.getQueryParameters();
@@ -2391,7 +2396,10 @@ private:
                         });
                 });
             LOG_TRC("queued clipboard command " << type << " on docBroker fetch");
-        } else {
+        }
+        // fallback to persistent clipboards if we can
+        else if (!DocumentBroker::lookupSendClipboardTag(_socket.lock(), tag, false))
+        {
             LOG_ERR("Invalid clipboard request: " << serverId << " with tag " << tag <<
                     " and broker: " << (docBroker ? "not" : "") << "found");
 
diff --git a/wsd/LOOLWSD.hpp b/wsd/LOOLWSD.hpp
index a23a4f714..0af2caa28 100644
--- a/wsd/LOOLWSD.hpp
+++ b/wsd/LOOLWSD.hpp
@@ -28,6 +28,7 @@
 class ChildProcess;
 class TraceFileWriter;
 class DocumentBroker;
+class ClipboardCache;
 
 std::shared_ptr<ChildProcess> getNewChild_Blocks(
 #if MOBILEAPP
@@ -69,6 +70,7 @@ public:
     static bool AnonymizeUsernames;
     static std::atomic<unsigned> NumConnections;
     static std::unique_ptr<TraceFileWriter> TraceDumper;
+    static std::unique_ptr<ClipboardCache> SavedClipboards;
     static std::set<std::string> EditFileExtensions;
     static unsigned MaxConnections;
     static unsigned MaxDocuments;
commit 2492faf2251c02f775bd6330ba5fddaeda245362
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Thu Jul 4 11:47:03 2019 -0400
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:58:10 2019 -0400

    wsd: add 'meta origin' to clipboardcontent payloads too
    
    Change-Id: I61233fd9b2559a28a0da67dd0a869e97c8b34da7

diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 0d6a56c1a..e02d9dadf 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -806,6 +806,28 @@ void ClientSession::performWrites()
     LOG_TRC(getName() << " ClientSession: performed write.");
 }
 
+void ClientSession::postProcessCopyPayload(std::shared_ptr<Message> payload)
+{
+    // Insert our meta origin if we can
+    payload->rewriteDataBody([=](std::vector<char>& data) {
+            const size_t pos = Util::findInVector(data, "<meta name=\"generator\" content=\"");
+
+            // cf. TileLayer.js /_dataTransferToDocument/
+            if (pos != std::string::npos) // assume text/html
+            {
+                const std::string meta = getClipboardURI();
+                const std::string origin = "<meta name=\"origin\" content=\"" + meta + "\"/>\n";
+                data.insert(data.begin() + pos, origin.begin(), origin.end());
+                return true;
+            }
+            else
+            {
+                LOG_DBG("Missing generator in textselectioncontent/clipboardcontent payload.");
+                return false;
+            }
+        });
+}
+
 bool ClientSession::handleKitToClientMessage(const char* buffer, const int length)
 {
     const auto payload = std::make_shared<Message>(buffer, length, Message::Dir::Out);
@@ -1075,24 +1097,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
         }
     } else if (tokens[0] == "textselectioncontent:") {
 
-        // Insert our meta origin if we can
-        payload->rewriteDataBody([=](std::vector<char>& data) {
-                size_t pos = Util::findInVector(data, "<meta name=\"generator\" content=\"");
-
-                // cf. TileLayer.js /_dataTransferToDocument/
-                if (pos != std::string::npos) // assume text/html
-                {
-                    std::string meta = getClipboardURI();
-                    std::string origin = "<meta name=\"origin\" content=\"" + meta + "\"/>\n";
-                    data.insert(data.begin() + pos, origin.begin(), origin.end());
-                    return true;
-                }
-                else
-                {
-                    LOG_DBG("Missing generator in textselectioncontent");
-                    return false;
-                }
-            });
+        postProcessCopyPayload(payload);
         return forwardToClient(payload);
 
     } else if (tokens[0] == "clipboardcontent:") {
@@ -1103,11 +1108,14 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
 
         // for now just for remote sockets.
         LOG_TRC("Got clipboard content to send to " << _clipSockets.size() << "sockets");
+
+        postProcessCopyPayload(payload);
+
         size_t header;
         for (header = 0; header < payload->size();)
             if (payload->data()[header++] == '\n')
                 break;
-        bool empty = header >= payload->size();
+        const bool empty = header >= payload->size();
         for (auto it : _clipSockets)
         {
             std::ostringstream oss;
diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp
index 1fbe2361e..5d5d68249 100644
--- a/wsd/ClientSession.hpp
+++ b/wsd/ClientSession.hpp
@@ -152,6 +152,9 @@ public:
     /// Create URI for transient clipboard content.
     std::string getClipboardURI(bool encode = true);
 
+    /// Adds and/or modified the copied payload before sending on to the client.
+    void postProcessCopyPayload(std::shared_ptr<Message> payload);
+
     /// Generate and rotate a new clipboard hash, sending it if appropriate
     void rotateClipboardKey(bool notifyClient);
 
commit fae1a967dbcf0c8c7341e4b928224536c9fe3213
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Thu Jul 4 10:31:35 2019 -0400
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:58:03 2019 -0400

    leaflet: catch exceptions form paste postMessage
    
    Change-Id: I06676f0ebdf798db095ea6d70e84907e5c318ac4

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 101963300..a8da6d0b8 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -482,17 +482,21 @@ L.Clipboard = L.Class.extend({
 		// see if we have help for paste
 		if (operation === 'paste')
 		{
-			if (window.top.webkit &&
-			    window.top.webkit.messageHandlers &&
-			    window.top.webkit.messageHandlers.RichDocumentsMobileInterface) {
-				console.log('We have richdocuments !');
-				window.top.webkit.messageHandlers.RichDocumentsMobileInterface.postMessage(operation);
-			} else if (window.top.RichDocumentsMobileInterface &&
-				   window.top.RichDocumentsMobileInterface.paste) {
-				console.log('We have richdocuments !');
-				window.top.RichDocumentsMobileInterface.paste();
-			} else {
-				console.log('No richdocuments');
+			try {
+				if (window.top.webkit &&
+				    window.top.webkit.messageHandlers &&
+				    window.top.webkit.messageHandlers.RichDocumentsMobileInterface) {
+					console.log('We have richdocuments !');
+					window.top.webkit.messageHandlers.RichDocumentsMobileInterface.postMessage(operation);
+				} else if (window.top.RichDocumentsMobileInterface &&
+					   window.top.RichDocumentsMobileInterface.paste) {
+					console.log('We have richdocuments !');
+					window.top.RichDocumentsMobileInterface.paste();
+				} else {
+					console.log('No richdocuments');
+				}
+			} catch (error) {
+				console.warn('Cannot paste: ' + error);
 			}
 		}
 
commit 710ce605c84fb3aa4e8f0247122b15b59842fef8
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Jul 3 13:20:07 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:57:09 2019 -0400

    clipboard: IE11 rich paste works.
    
    Change-Id: Ifa05aa0cf5e84f4ccd5414f45857aee34aa05f1c

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 132a18ef1..101963300 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -17,6 +17,7 @@ L.Clipboard = L.Class.extend({
 		this._accessKey = [ '', '' ];

... etc. - the rest is truncated


More information about the Libreoffice-commits mailing list