[Libreoffice-commits] online.git: Branch 'private/mmeeks/clipboard' - 46 commits - bundled/include common/Clipboard.hpp kit/ChildSession.cpp kit/ChildSession.hpp kit/Kit.cpp loleaflet/css loleaflet/src net/Socket.hpp test/UnitCopyPaste.cpp wsd/ClientSession.cpp wsd/ClientSession.hpp wsd/DocumentBroker.cpp wsd/DocumentBroker.hpp wsd/LOOLWSD.cpp wsd/LOOLWSD.hpp

Michael Meeks (via logerrit) logerrit at kemper.freedesktop.org
Tue Aug 6 02:22:22 UTC 2019


 bundled/include/LibreOfficeKit/LibreOfficeKit.hxx |   50 +
 common/Clipboard.hpp                              |   64 ++
 kit/ChildSession.cpp                              |   55 +
 kit/ChildSession.hpp                              |    6 
 kit/Kit.cpp                                       |    9 
 loleaflet/css/toolbar.css                         |    5 
 loleaflet/src/control/Control.ColumnHeader.js     |    2 
 loleaflet/src/control/Control.ContextMenu.js      |   29 -
 loleaflet/src/control/Control.DownloadProgress.js |   56 +
 loleaflet/src/control/Control.LokDialog.js        |    2 
 loleaflet/src/control/Control.RowHeader.js        |    2 
 loleaflet/src/control/Control.Tabs.js             |    2 
 loleaflet/src/layer/marker/ClipboardContainer.js  |    4 
 loleaflet/src/layer/tile/TileLayer.js             |   52 -
 loleaflet/src/map/Clipboard.js                    |  625 +++++++++++++++-------
 loleaflet/src/map/Map.js                          |   11 
 loleaflet/src/map/handler/Map.TouchGesture.js     |    2 
 net/Socket.hpp                                    |    5 
 test/UnitCopyPaste.cpp                            |   18 
 wsd/ClientSession.cpp                             |  179 +++++-
 wsd/ClientSession.hpp                             |   36 -
 wsd/DocumentBroker.cpp                            |  158 ++++-
 wsd/DocumentBroker.hpp                            |   12 
 wsd/LOOLWSD.cpp                                   |   28 
 wsd/LOOLWSD.hpp                                   |    2 
 25 files changed, 1063 insertions(+), 351 deletions(-)

New commits:
commit da0aa0179133f81d891554bae0a484936d11555e
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: Mon Aug 5 22:21:55 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 4ed9986dfa591bbcad7638db1a60ef40f4a347cf
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: Mon Aug 5 22:21:55 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 5587edf10d5f16b9678602ca6c9d48dc8a633c8c
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: Mon Aug 5 22:21:55 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 f28ddca13bb49d34a93da0f9fe8e521c7f518fdf
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: Mon Aug 5 22:21:55 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 261bf1e385d184ea221ca3a6af8b0812ff902b93
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: Mon Aug 5 22:21:55 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 d73ad9db86d47d83590e8be8eeb61d7e7a145c6b
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: Mon Aug 5 22:21:55 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 d17a3eefec42c8eb790a4adffc541486c4c257bc
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: Mon Aug 5 22:21:55 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 9b02a290f9219413722b1a49250994d76460e4a7
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: Mon Aug 5 22:21:54 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 512cf3ba4fc2348ee4b1dfd3689f55c0a417355f
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: Mon Aug 5 22:21:54 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 fa4ed4fb7f61a2c80029f1d1ebf129618d24be40
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: Mon Aug 5 22:21:54 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 750a21644fce6913dbcf5d6710b47051f17d6590
Author:     Ashod Nakashian <ashod.nakashian at collabora.co.uk>
AuthorDate: Sun Jul 7 01:12:50 2019 -0400
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 22:21:54 2019 -0400

    wsd: gracefully disconnect the kit socket
    
    Without the disconnect call, the kit
    doesn't flush the socket buffer, nor
    does it close gracefully. It ends up
    losing the last message, which until
    now wasn't critical. But since we
    pull the clipboard data on tearing
    down the DocBroker, the last message
    from the kit is the clipboard data,
    which can be in the order of megabytes.
    
    Change-Id: I777ce8d06b5a85ffe2051cb389f14a489ce851c9

diff --git a/kit/ChildSession.hpp b/kit/ChildSession.hpp
index a1a4532bb..a632b0abc 100644
--- a/kit/ChildSession.hpp
+++ b/kit/ChildSession.hpp
@@ -235,6 +235,8 @@ public:
 
     bool getClipboard(const char* buffer, int length, const std::vector<std::string>& tokens);
 
+    virtual void disconnect() override;
+
 private:
     bool loadDocument(const char* buffer, int length, const std::vector<std::string>& tokens);
 
@@ -273,7 +275,6 @@ private:
 
     void rememberEventsForInactiveUser(const int type, const std::string& payload);
 
-    virtual void disconnect() override;
     virtual bool _handleInput(const char* buffer, int length) override;
 
     std::shared_ptr<lok::Document> getLOKitDocument()
diff --git a/kit/Kit.cpp b/kit/Kit.cpp
index a76b6d1f0..91a498b97 100644
--- a/kit/Kit.cpp
+++ b/kit/Kit.cpp
@@ -1905,6 +1905,9 @@ private:
                     // Tell them we're going quietly.
                     session->sendTextFrame("disconnected:");
 
+                    // Disconnect to gracefully flush and close the socket.
+                    session->disconnect();
+
                     _sessions.erase(it);
                     const size_t count = _sessions.size();
                     LOG_DBG("Have " << count << " child" << (count == 1 ? "" : "ren") <<
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 = [ '', '' ];
 		this._clipboardSerial = 0; // incremented on each operation
 		this._failedTimer = null;
+		this._dummyDivName = 'copy-paste-dummy-div';
 
 		var that = this;
 		document.addEventListener(
@@ -352,7 +353,15 @@ L.Clipboard = L.Class.extend({
 	},
 
 	_createDummyDiv: function(htmlContent) {
-		var div = document.createElement('div');
+		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');
@@ -390,9 +399,9 @@ L.Clipboard = L.Class.extend({
 		div.addEventListener('paste', function() {
 			// Can't get HTML until it is pasted ... so quick timeout
 			setTimeout(function() {
-				console.log('Content pasted');
-				that.dataTransferToDocument(null, false, div.innerHTML);
-				that.compatRemoveNode(div);
+				var tmpDiv = document.getElementById(that._dummyDivName);
+				that.dataTransferToDocument(null, false, tmpDiv.innerHTML);
+				that.compatRemoveNode(tmpDiv);
 				// attempt to restore focus.
 				if (active == null)
 					that._map.focus();
commit 98d120d19b4356cf7ef058d755cdf7385e53b8bf
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Jul 3 12:52:29 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:57:03 2019 -0400

    This to that.
    
    Change-Id: Ic690751e42b0f906db7b327d5d8082028ef492dd

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 1f5e989ad..132a18ef1 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -392,7 +392,7 @@ L.Clipboard = L.Class.extend({
 			setTimeout(function() {
 				console.log('Content pasted');
 				that.dataTransferToDocument(null, false, div.innerHTML);
-				this.compatRemoveNode(div);
+				that.compatRemoveNode(div);
 				// attempt to restore focus.
 				if (active == null)
 					that._map.focus();
commit 3fd1423a290c2deb0fd71b787f6db2df2ef8c736
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Jul 3 12:13:51 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:56:58 2019 -0400

    clipboard: get image paste working for Edge.
    
    Change-Id: I35c12e094e16f966f1be9c631c6d7023954504f0

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 9789011fc..1f5e989ad 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -197,7 +197,6 @@ L.Clipboard = L.Class.extend({
 		);
 	},
 
-	// FIXME: do we want this really ?
 	_onFileLoadFunc: function(file) {
 		var socket = this._map._socket;
 		return function(e) {
@@ -206,6 +205,16 @@ L.Clipboard = L.Class.extend({
 		};
 	},
 
+	_asyncReadPasteImage: function(file) {
+		if (file.type.match(/image.*/)) {
+			var reader = new FileReader();
+			reader.onload = this._onFileLoadFunc(file);
+			reader.readAsArrayBuffer(file);
+			return true;
+		}
+		return false;
+	},
+
 	dataTransferToDocument: function (dataTransfer, preferInternal, htmlText, usePasteKeyEvent) {
 		// Look for our HTML meta magic.
 		//   cf. ClientSession.cpp /textselectioncontent:/
@@ -262,14 +271,13 @@ L.Clipboard = L.Class.extend({
 				console.log('\ttype' + types[t]);
 				if (types[t] === 'Files') {
 					var files = dataTransfer.files;
-					for (var f = 0; f < files.length; ++f) {
-						var file = files[f];
-						if (file.type.match(/image.*/)) {
-							var reader = new FileReader();
-							reader.onload = this._onFileLoadFunc(file);
-							reader.readAsArrayBuffer(file);
-						}
+					if (files !== null)
+					{
+						for (var f = 0; f < files.length; ++f)
+							this._asyncReadPasteImage(files[f])
 					}
+					else // IE / Edge
+						this._asyncReadPasteImage(dataTransfer.items[t].getAsFile());
 				}
 			}
 			return;
commit 6fbb3fef3604a1fa6e54b4c3b845f10d8a80cab3
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Wed Jul 3 11:41:27 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:56:53 2019 -0400

    clipboard: ensure image fallback works for paste.
    
    Don't return an empty blob for no textual content case.
    
    Change-Id: I592d0e7f876b7ecbe86f769cbb7fdd4a2183531b

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 93e649c14..9789011fc 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -125,7 +125,10 @@ L.Clipboard = L.Class.extend({
 			content.push(data);
 			content.push('\n');
 		}
-		return new Blob(content, {type : 'application/octet-stream', endings: 'transparent'});
+		if (content.length > 0)
+			return new Blob(content, {type : 'application/octet-stream', endings: 'transparent'});
+		else
+			return null;
 	},
 
 	// Abstract async post & download for our progress wrappers
commit 23db67341c010f6409bb5f8d7f97b774fdcd58f8
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Mon Jul 1 12:36:40 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:55:01 2019 -0400

    IE11 - get copy/paste happy again.
    
    Change-Id: I42a74a04a6f3f3fb7eff617a2003084174108464

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 141b59a85..93e649c14 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -23,6 +23,13 @@ L.Clipboard = L.Class.extend({
 			'beforepaste', function(ev) { that.beforepaste(ev); });
 	},
 
+	compatRemoveNode: function(node) {
+		if (window.isInternetExplorer)
+			node.removeNode(true);
+		else // standard
+			node.parentNode.removeChild(node);
+	},
+
 	// We can do a much better job when we fetch text/plain too.
 	stripHTML: function(html) {
 		var tmp = document.createElement('div');
@@ -30,7 +37,7 @@ L.Clipboard = L.Class.extend({
 		// attempt to cleanup unwanted elements
 		var styles = tmp.querySelectorAll('style');
 		for (var i = 0; i < styles.length; i++) {
-			styles[i].parentNode.removeChild(styles[i]);
+			this.compatRemoveNode(styles[i]);
 		}
 		return tmp.textContent.trim() || tmp.innerText.trim() || '';
 	},
@@ -374,7 +381,7 @@ L.Clipboard = L.Class.extend({
 			setTimeout(function() {
 				console.log('Content pasted');
 				that.dataTransferToDocument(null, false, div.innerHTML);
-				div.parentNode.removeChild(div);
+				this.compatRemoveNode(div);
 				// attempt to restore focus.
 				if (active == null)
 					that._map.focus();
@@ -424,7 +431,7 @@ L.Clipboard = L.Class.extend({
 		div.removeEventListener('paste', listener);
 		div.removeEventListener('cut', listener);
 		div.removeEventListener('copy', listener);
-		div.parentNode.removeChild(div);
+		this.compatRemoveNode(div);
 
 		// try to restore focus if we need to.
 		if (active !== null && active !== document.activeElement)
commit 5f27b235e375d8be9deb8509e975c11319179a1e
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Sat Jun 29 10:42:38 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:53:19 2019 -0400

    Fix this to that.
    
    Change-Id: I978092973244ac80cdf68ed539f30c3272b6a615

diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 34df9aadf..141b59a85 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -477,7 +477,7 @@ L.Clipboard = L.Class.extend({
 			{
 				console.log('successful ' + operation);
 				if (operation === 'paste')
-					this._stopHideDownload();
+					that._stopHideDownload();
 			}
 			else
 			{
commit 326391325e02508a89b17c44eab5502a0a15e8b8
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Jun 28 20:37:10 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:53:11 2019 -0400

    clipboard: having downloaded the complex selection, don't do it again.
    
    Instead use the result.
    
    Change-Id: Ife2093d6d69c2598079fee7a543044378e2b6829

diff --git a/loleaflet/src/control/Control.DownloadProgress.js b/loleaflet/src/control/Control.DownloadProgress.js
index 76d17d13b..8df498997 100644
--- a/loleaflet/src/control/Control.DownloadProgress.js
+++ b/loleaflet/src/control/Control.DownloadProgress.js
@@ -121,7 +121,8 @@ L.Control.DownloadProgress = L.Control.extend({
 			return;
 		this._setNormalCursor();
 		this._complete = true;
-		this._content.removeChild(this._progress);
+		if (this._content.contains(this._progress))
+			this._content.removeChild(this._progress);
 		this._content.style.width  = '150px';
 		this._content.appendChild(this._confirmPasteButton);
 	},
@@ -159,7 +160,6 @@ L.Control.DownloadProgress = L.Control.extend({
 					if (idx > 0)
 						text = text.substring(idx, text.length);
 					that._map._clip.setTextSelectionContent(text);
-					that._onComplete();
 				};
 				// TODO: failure to parse ? ...
 				reader.readAsText(response);
diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 5d4abc348..34df9aadf 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -299,10 +299,19 @@ L.Clipboard = L.Class.extend({
 		if (this._selectionType === 'complex' ||
 		    this._map._docLayer.hasGraphicSelection()) {
 			console.log('Copy/Cut with complex/graphical selection');
-			text = this.getStubHtml();
-			this._onDownloadOnLargeCopyPaste();
-			this._downloadProgress.setURI( // richer, bigger HTML ...
-				this.getMetaBase() + this.getMetaPath() + '&MimeType=text/html');
+			if (this._selectionType === 'text' && this._selectionContent !== '')
+			{ // back here again having downloaded it ...
+				text = this._selectionContent;
+				console.log('Use downloaded selection.');
+			}
+			else
+			{
+				console.log('Downloaded that selection.');
+				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();
commit 7f43d5d38507988845a96a1d5c8301321b4d3c87
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Jun 28 18:26:32 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:53:06 2019 -0400

    Use isComplex for calc too.
    
    Change-Id: Ib209cca53079cb9d63a8955caf80945c7ea455d4

diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp
index bb135f3a2..56700852e 100644
--- a/kit/ChildSession.cpp
+++ b/kit/ChildSession.cpp
@@ -917,7 +917,8 @@ bool ChildSession::getTextSelection(const char* /*buffer*/, int /*length*/, cons
         return false;
     }
 
-    if (getLOKitDocument()->getDocumentType() != LOK_DOCTYPE_TEXT)
+    if (getLOKitDocument()->getDocumentType() != LOK_DOCTYPE_TEXT &&
+        getLOKitDocument()->getDocumentType() != LOK_DOCTYPE_SPREADSHEET)
     {
         const std::string selection = getTextSelectionInternal(mimeType);
         if (selection.size() >= 1024 * 1024) // Don't return huge data.
commit bc9ea43af1be0cbaacd1e6b07f5dc5efa1798305
Author:     Michael Meeks <michael.meeks at collabora.com>
AuthorDate: Fri Jun 28 13:56:48 2019 +0100
Commit:     Michael Meeks <michael.meeks at collabora.com>
CommitDate: Mon Aug 5 21:53:01 2019 -0400

    Don't let the clipboard 'Download' button linger around continually.
    
    NB. can't use text-selection as a trigger to hide it, but use another
    copy/cut/paste etc.
    
    Change-Id: Iebf07a2fb8900b71134fcdac011336d87ab01e6a

diff --git a/loleaflet/src/control/Control.DownloadProgress.js b/loleaflet/src/control/Control.DownloadProgress.js
index bda2d645f..76d17d13b 100644
--- a/loleaflet/src/control/Control.DownloadProgress.js
+++ b/loleaflet/src/control/Control.DownloadProgress.js
@@ -138,7 +138,8 @@ L.Control.DownloadProgress = L.Control.extend({
 			this._content.removeChild(this._confirmPasteButton);
 		if (this._content.contains(this._progress))
 			this._content.removeChild(this._progress);
-		this._map.focus();
+		if (this._map)
+			this._map.focus();
 		this.remove();
 		this._closed = true;
 	},
@@ -158,7 +159,7 @@ L.Control.DownloadProgress = L.Control.extend({
 					if (idx > 0)
 						text = text.substring(idx, text.length);
 					that._map._clip.setTextSelectionContent(text);
-					// TODO: now swap to the 'copy' button (?)
+					that._onComplete();
 				};
 				// TODO: failure to parse ? ...
 				reader.readAsText(response);
diff --git a/loleaflet/src/map/Clipboard.js b/loleaflet/src/map/Clipboard.js
index 9afb59ee3..5d4abc348 100644
--- a/loleaflet/src/map/Clipboard.js
+++ b/loleaflet/src/map/Clipboard.js
@@ -293,6 +293,9 @@ L.Clipboard = L.Class.extend({
 
 	populateClipboard: function(ev) {
 		var text;
+
+		this._stopHideDownload();
+
 		if (this._selectionType === 'complex' ||
 		    this._map._docLayer.hasGraphicSelection()) {
 			console.log('Copy/Cut with complex/graphical selection');
@@ -462,7 +465,11 @@ L.Clipboard = L.Class.extend({
 		clearTimeout(this._failedTimer);
 		setTimeout(function() {
 			if (that._clipboardSerial !== serial)
+			{
 				console.log('successful ' + operation);
+				if (operation === 'paste')
+					this._stopHideDownload();
+			}
 			else
 			{
 				console.log('help did not arive for ' + operation);
@@ -509,6 +516,7 @@ L.Clipboard = L.Class.extend({
 			this.dataTransferToDocument(ev.clipboardData, /* preferInternal = */ true, null, usePasteKeyEvent);
 			this._map._clipboardContainer._abortComposition();
 			this._clipboardSerial++;
+			this._stopHideDownload();
 		}
 		// else: IE 11 - code in beforepaste: above.
 	},
@@ -516,6 +524,15 @@ L.Clipboard = L.Class.extend({
 	clearSelection: function() {
 		this._selectionContent = '';

... etc. - the rest is truncated


More information about the Libreoffice-commits mailing list