common/Message.hpp | 13 ++ common/Util.cpp | 14 ++ common/Util.hpp | 2 loleaflet/src/core/Socket.js | 13 +- loleaflet/src/layer/marker/ClipboardContainer.js | 19 ++- loleaflet/src/layer/tile/TileLayer.js | 141 +++++++++++++++++------ loleaflet/src/map/handler/Map.Keyboard.js | 2 wsd/ClientSession.cpp | 95 +++++++++++++++ wsd/ClientSession.hpp | 27 ++++ wsd/LOOLWSD.cpp | 99 +++++++++++++++- wsd/LOOLWSD.hpp | 2 11 files changed, 372 insertions(+), 55 deletions(-)
New commits: commit c2067a9174748285736549c45276ac741a0fcda6 Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Tue May 28 02:00:46 2019 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Tue May 28 02:26:47 2019 +0100 Start of clipboard server end-point ... Change-Id: I6da502cc1aacef450d612696abcc38f8d8d8873a diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js index 697ab6040..e3d6cd7b5 100644 --- a/loleaflet/src/layer/tile/TileLayer.js +++ b/loleaflet/src/layer/tile/TileLayer.js @@ -2483,9 +2483,11 @@ L.TileLayer = L.GridLayer.extend({ var meta = html.substring(start + match.length, end); // quick sanity checks that it one of ours. - if (meta.indexOf('/clipboard/') > 0 && - meta.indexOf('&tag=') > 0) - return meta; + if (meta.indexOf('%2Fclipboard%3FWOPISrc%3D') > 0 && + meta.indexOf('%26ServerId%3D') > 0 && + meta.indexOf('%26ViewId%3D') > 0 && + meta.indexOf('%26Tag%3D') > 0) + return decodeURIComponent(meta); else console.log('Mis-understood foreign origin: "' + meta + '"'); return ''; @@ -2498,8 +2500,8 @@ L.TileLayer = L.GridLayer.extend({ var pasteHtml = dataTransfer.getData('text/html'); var meta = this._getMetaOrigin(pasteHtml); var id = this._map.options.webserver + this._map.options.serviceRoot + - '/clipboard/' + this._map._socket.WSDServer.Id + '/' + this._viewId + - '?WOPISrc=' + encodeURIComponent(this._map.options.doc); + '/clipboard?WOPISrc='+ encodeURIComponent(this._map.options.doc) + + '&ServerId=' + this._map._socket.WSDServer.Id + '&ViewId=' + this._viewId; // for the paste, we might prefer the internal LOK's copy/paste if (meta.startsWith(id) && preferInternal === true) { @@ -2545,10 +2547,11 @@ L.TileLayer = L.GridLayer.extend({ console.log('Doing async paste of data from remote origin\n\t"' + meta + '" is not\n\t"' + id + '"'); var tilelayer = this; var oReq = new XMLHttpRequest(); - oReq.onload = function() { + oReq.onload = function(e) { var arraybuffer = oReq.response; if (oReq.status == 200) { // OK - var blob = new Blob(['paste mimetype=application/x-openoffice-embed-source-xml;windows_formatname="Star Embed Source (XML)"\n', arraybuffer]); +// var blob = new Blob(['paste mimetype=application/x-openoffice-embed-source-xml;windows_formatname="Star Embed Source (XML)"\n', arraybuffer]); + var blob = new Blob(['paste mimetype=text/plain\n', arraybuffer]); tilelayer._map._socket.sendMessage(blob); console.log('Sent paste blob message'); } else { diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp index 899eae49f..738521d43 100644 --- a/wsd/ClientSession.cpp +++ b/wsd/ClientSession.cpp @@ -97,9 +97,16 @@ std::string ClientSession::getURIAndUpdateClipboardHash() Poco::URI::encode(wopiSrc.toString(), encodeChars, encodedFrom); std::string proto = (LOOLWSD::isSSLEnabled() || LOOLWSD::isSSLTermination()) ? "https://" : "http://"; - std::string meta = proto + _hostNoTrust + "/clipboard/" + LOOLWSD::HostIdentifier + - "/" + std::to_string(getKitViewId()) + "?WOPISrc=" + encodedFrom + "&tag=" + hash; - return meta; + std::string meta = proto + _hostNoTrust + + "/clipboard?WOPISrc=" + encodedFrom + + "&ServerId=" + LOOLWSD::HostIdentifier + + "&ViewId=" + std::to_string(getKitViewId()) + + "&Tag=" + hash; + + std::string metaEncoded; + Poco::URI::encode(meta, encodeChars, metaEncoded); + + return metaEncoded; } // called infrequently @@ -1357,7 +1364,15 @@ void ClientSession::dumpState(std::ostream& os) os << "\t\tisReadOnly: " << isReadOnly() << "\n\t\tisDocumentOwner: " << _isDocumentOwner << "\n\t\tisAttached: " << _isAttached - << "\n\t\tkeyEvents: " << _keyEvents; + << "\n\t\tkeyEvents: " << _keyEvents +// << "\n\t\tvisibleArea: " << _clientVisibleArea + << "\n\t\tclientSelectedPart: " << _clientSelectedPart + << "\n\t\ttile size Pixel: " << _tileWidthPixel << "x" << _tileHeightPixel + << "\n\t\ttile size Twips: " << _tileWidthTwips << "x" << _tileHeightTwips + << "\n\t\tkit ViewId: " << _kitViewId + << "\n\t\thost (un-trusted): " << _hostNoTrust + << "\n\t\tisTextDocument: " << _isTextDocument + << "\n\t\tclipboardKey: " << _clipboardKey; std::shared_ptr<StreamSocket> socket = getSocket().lock(); if (socket) diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp index f9dafd373..c4048b468 100644 --- a/wsd/LOOLWSD.cpp +++ b/wsd/LOOLWSD.cpp @@ -2154,7 +2154,13 @@ private: else { StringTokenizer reqPathTokens(request.getURI(), "/?", StringTokenizer::TOK_IGNORE_EMPTY | StringTokenizer::TOK_TRIM); - if (!(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) && + + if (request.getMethod() == HTTPRequest::HTTP_GET && + reqPathTokens.count() > 0 && reqPathTokens[0] == "clipboard") + { + handleClipboardRequest(request); + } + else if (!(request.find("Upgrade") != request.end() && Poco::icompare(request["Upgrade"], "websocket") == 0) && reqPathTokens.count() > 0 && reqPathTokens[0] == "lool") { // All post requests have url prefix 'lool'. @@ -2318,6 +2324,74 @@ private: LOG_INF("Sent capabilities.json successfully."); } + void handleClipboardRequest(const Poco::Net::HTTPRequest& request) + { + LOG_DBG("Clipboard request: " << request.getURI()); + + Poco::URI requestUri(request.getURI()); + Poco::URI::QueryParameters params = requestUri.getQueryParameters(); + std::string serverId, viewId, tag; + for (auto it : params) + { + if (it.first == "ServerId") + serverId = it.second; + else if (it.first == "ViewId") + viewId = it.second; + else if (it.first == "Tag") + tag = it.second; + } + LOG_TRC("Clipboard request for us: " << serverId << " with tag " << tag); + + // TODO: check WOPISrc too ... + std::shared_ptr<ClientSession> session; + if (serverId == LOOLWSD::HostIdentifier && + (session = ClientSession::getByClipboardHash(tag))) + { + // Do things in the right thread. + auto docBroker = session->getDocumentBroker(); + docBroker->addCallback([=](){ + ; + }); + + std::string result = "Hello world !\n"; + + 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: " << result.size() << "\r\n" + << "Content-Type: application/octet-stream\r\n" + << "X-Content-Type-Options: nosniff\r\n" + << "\r\n" + << result; + + auto socket = _socket.lock(); + socket->send(oss.str()); + socket->shutdown(); + LOG_INF("Sent clipboard content successfully."); + } else { + LOG_ERR("Invalid clipboard request: " << serverId << " with tag " << tag); + + std::string message; + if (serverId != LOOLWSD::HostIdentifier) + message = "Cluster configuration error: mis-matching serverid " + serverId + " vs. " + LOOLWSD::HostIdentifier; + else + message = "Empty clipboard item / session tag " + tag; + + // Bad request. + std::ostringstream oss; + oss << "HTTP/1.1 400\r\n" + << "Date: " << Poco::DateTimeFormatter::format(Poco::Timestamp(), Poco::DateTimeFormat::HTTP_FORMAT) << "\r\n" + << "User-Agent: LOOLWSD WOPI Agent\r\n" + << "Content-Length: 0\r\n" + << "\r\n" + << message; + auto socket = _socket.lock(); + socket->send(oss.str()); + socket->shutdown(); + } + } + void handleRobotsTxtRequest(const Poco::Net::HTTPRequest& request) { LOG_DBG("HTTP request: " << request.getURI()); commit d8195aae3c5008dbfaa6b411ae8f83a92092e02d Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Mon May 27 21:22:06 2019 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Tue May 28 02:26:47 2019 +0100 First cut of remote clipboard access fun. Change-Id: I02e40896753e40ad823beafe1f9da739e3023f43 diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js index 6916ab775..697ab6040 100644 --- a/loleaflet/src/layer/tile/TileLayer.js +++ b/loleaflet/src/layer/tile/TileLayer.js @@ -2480,46 +2480,97 @@ L.TileLayer = L.GridLayer.extend({ if (end < 0) { return ''; } - return html.substring(start + match.length, end); + var meta = html.substring(start + match.length, end); + + // quick sanity checks that it one of ours. + if (meta.indexOf('/clipboard/') > 0 && + meta.indexOf('&tag=') > 0) + return meta; + else + console.log('Mis-understood foreign origin: "' + meta + '"'); + return ''; }, _dataTransferToDocument: function (dataTransfer, preferInternal) { + + // Look for our HTML meta magic. + // cf. ClientSession.cpp /textselectioncontent:/ + var pasteHtml = dataTransfer.getData('text/html'); + var meta = this._getMetaOrigin(pasteHtml); + var id = this._map.options.webserver + this._map.options.serviceRoot + + '/clipboard/' + this._map._socket.WSDServer.Id + '/' + this._viewId + + '?WOPISrc=' + encodeURIComponent(this._map.options.doc); + // for the paste, we might prefer the internal LOK's copy/paste - if (preferInternal === true) { - var pasteHtml = dataTransfer.getData('text/html'); - var meta = this._getMetaOrigin(pasteHtml); - var id = 'https://transient/' + this._map._socket.WSDServer.Id + '/' + this._viewId + - '?WOPISrc=' + encodeURIComponent(this._map.options.doc); - // cf. ClientSession.cpp /textselectioncontent:/ - if (meta == id) { - // Home from home: short-circuit internally. - this._map._socket.sendMessage('uno .uno:Paste'); - return; - } else - console.log('Unusual origin mismatch on paste between:\n\t"' + - meta + '" and\n\t"' + id + '"'); + if (meta.startsWith(id) && preferInternal === true) { + // Home from home: short-circuit internally. + console.log('short-circuit, internal paste'); + this._map._socket.sendMessage('uno .uno:Paste'); + return; } - var types = dataTransfer.types; + // Suck HTML content out of dataTransfer now while it feels like working. + var content = this._readContentSync(dataTransfer); - // first try to transfer images - // TODO if we have both Files and a normal mimetype, should we handle - // both, or prefer one or the other? - for (var t = 0; t < types.length; ++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); + // Images get a look in only if we have no content and are async + if (content == null && pasteHtml == '') + { + var types = dataTransfer.types; + + console.log('Attempting to paste image(s)'); + + // first try to transfer images + // TODO if we have both Files and a normal mimetype, should we handle + // both, or prefer one or the other? + for (var t = 0; t < types.length; ++t) { + 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); + } } } } + return; } - // now try various mime types + // Try Fetch the data directly ourselves instead. + if (meta != '') { + // FIXME: really should short-circuit on the server. + console.log('Doing async paste of data from remote origin\n\t"' + meta + '" is not\n\t"' + id + '"'); + var tilelayer = this; + var oReq = new XMLHttpRequest(); + oReq.onload = function() { + var arraybuffer = oReq.response; + if (oReq.status == 200) { // OK + var blob = new Blob(['paste mimetype=application/x-openoffice-embed-source-xml;windows_formatname="Star Embed Source (XML)"\n', arraybuffer]); + tilelayer._map._socket.sendMessage(blob); + console.log('Sent paste blob message'); + } else { + console.log('Error code ' + oReq.status + ' fetching from URL "' + meta + '": ' + e + ' falling back to local.'); + tilelayer._map._socket.sendMessage(content); + } + } + oReq.onerror = function(e) { + console.log('Error fetching from URL "' + meta + '": ' + e + ' falling back to local.'); + tilelayer._map._socket.sendMessage(content); + }; + oReq.open('GET', meta); + oReq.responseType = 'arraybuffer'; + oReq.send(); + // user abort - if they do stops paste. + } else { + console.log('Received a paste but nothing on the clipboard'); + } + }, + + _readContentSync: function(dataTransfer) { + // Try various content mime types var mimeTypes; if (this._docType === 'spreadsheet') { // FIXME apparently we cannot paste the text/html or text/rtf as @@ -2547,15 +2598,15 @@ L.TileLayer = L.GridLayer.extend({ ]; } + var types = dataTransfer.types; for (var i = 0; i < mimeTypes.length; ++i) { - for (t = 0; t < types.length; ++t) { + for (var t = 0; t < types.length; ++t) { if (mimeTypes[i][0] === types[t]) { - var blob = new Blob(['paste mimetype=' + mimeTypes[i][1] + '\n', dataTransfer.getData(types[t])]); - this._map._socket.sendMessage(blob); - return; + return new Blob(['paste mimetype=' + mimeTypes[i][1] + '\n', dataTransfer.getData(types[t])]); } } } + return null; }, _onFileLoadFunc: function(file) { diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp index f906e690d..899eae49f 100644 --- a/wsd/ClientSession.cpp +++ b/wsd/ClientSession.cpp @@ -14,6 +14,7 @@ #include <fstream> #include <sstream> #include <memory> +#include <unordered_map> #include <Poco/Net/HTTPResponse.h> #include <Poco/StreamCopier.h> @@ -35,10 +36,14 @@ using namespace LOOLProtocol; using Poco::Path; using Poco::StringTokenizer; +static std::mutex SessionMapMutex; +static std::unordered_map<std::string, std::weak_ptr<ClientSession>> SessionMap; + ClientSession::ClientSession(const std::string& id, const std::shared_ptr<DocumentBroker>& docBroker, const Poco::URI& uriPublic, - const bool readOnly) : + const bool readOnly, + const std::string& hostNoTrust) : Session("ToClient-" + id, id, readOnly), _docBroker(docBroker), _uriPublic(uriPublic), @@ -53,16 +58,61 @@ ClientSession::ClientSession(const std::string& id, _tileWidthTwips(0), _tileHeightTwips(0), _kitViewId(-1), + _hostNoTrust(hostNoTrust), _isTextDocument(false) { const size_t curConnections = ++LOOLWSD::NumConnections; LOG_INF("ClientSession ctor [" << getName() << "], current number of connections: " << curConnections); } +// Can't take a reference in the constructor. +void ClientSession::construct() +{ + std::unique_lock<std::mutex> lock(SessionMapMutex); + SessionMap[getId()] = shared_from_this(); +} + ClientSession::~ClientSession() { const size_t curConnections = --LOOLWSD::NumConnections; LOG_INF("~ClientSession dtor [" << getName() << "], current number of connections: " << curConnections); + + std::unique_lock<std::mutex> lock(SessionMapMutex); + SessionMap.erase(getId()); +} + +std::string ClientSession::getURIAndUpdateClipboardHash() +{ + std::string hash = Util::rng::getHexString(16); + { + std::unique_lock<std::mutex> lock(SessionMapMutex); + _clipboardKey = hash; + } + + std::string encodedFrom; + Poco::URI wopiSrc = getDocumentBroker()->getPublicUri(); + wopiSrc.setQueryParameters(Poco::URI::QueryParameters()); + + std::string encodeChars = ",/?:@&=+$#"; // match JS encodeURIComponent + Poco::URI::encode(wopiSrc.toString(), encodeChars, encodedFrom); + + std::string proto = (LOOLWSD::isSSLEnabled() || LOOLWSD::isSSLTermination()) ? "https://" : "http://"; + std::string meta = proto + _hostNoTrust + "/clipboard/" + LOOLWSD::HostIdentifier + + "/" + std::to_string(getKitViewId()) + "?WOPISrc=" + encodedFrom + "&tag=" + hash; + return meta; +} + +// called infrequently +std::shared_ptr<ClientSession> ClientSession::getByClipboardHash(std::string &key) +{ + std::unique_lock<std::mutex> lock(SessionMapMutex); + for (auto &it : SessionMap) + { + auto session = it.second.lock(); + if (session && session->_clipboardKey == key) + return session; + } + return std::shared_ptr<ClientSession>(); } void ClientSession::handleIncomingMessage(SocketDisposition &disposition) @@ -986,14 +1036,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt // cf. TileLayer.js /_dataTransferToDocument/ if (pos != std::string::npos) // assume text/html { - // FIXME: expose other content types ? provide an RTF back-channel ? - std::string encodedFrom; - Poco::URI wopiSrc = docBroker->getPublicUri(); - wopiSrc.setQueryParameters(Poco::URI::QueryParameters()); - // matching encodeURIComponent - Poco::URI::encode(wopiSrc.toString(), ",/?:@&=+$#", encodedFrom); - std::string meta = "https://transient/" + LOOLWSD::HostIdentifier + "/" + - std::to_string(getKitViewId()) + "?WOPISrc=" + encodedFrom; + std::string meta = getURIAndUpdateClipboardHash(); std::string origin = "<meta name=\"origin\" content=\"" + meta + "\"/>\n"; data.insert(data.begin() + pos, origin.begin(), origin.end()); return true; diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp index 9562c8f39..61b3c037c 100644 --- a/wsd/ClientSession.hpp +++ b/wsd/ClientSession.hpp @@ -33,10 +33,14 @@ public: ClientSession(const std::string& id, const std::shared_ptr<DocumentBroker>& docBroker, const Poco::URI& uriPublic, - const bool isReadOnly = false); - + const bool isReadOnly, + const std::string& hostNoTrust); + void construct(); virtual ~ClientSession(); + /// Lookup any session by id. + static std::shared_ptr<ClientSession> getById(const std::string &id); + void handleIncomingMessage(SocketDisposition &) override; void setReadOnly() override; @@ -132,8 +136,15 @@ public: void resetWireIdMap(); bool isTextDocument() const { return _isTextDocument; } + + /// Find clipboard for session + static std::shared_ptr<ClientSession> getByClipboardHash(std::string &key); + private: + /// Create URI for transient clipboard content. + std::string getURIAndUpdateClipboardHash(); + /// SocketHandler: disconnection event. void onDisconnect() override; /// Does SocketHandler: have data or timeouts to setup. @@ -217,9 +228,15 @@ private: /// The integer id of the view in the Kit process int _kitViewId; + /// Un-trusted hostname of our service from the client + const std::string _hostNoTrust; + /// Client is using a text document? bool _isTextDocument; + /// Transient clipboard identifier - protected by SessionMapMutex + std::string _clipboardKey; + /// TileID's of the sent tiles. Push by sending and pop by tileprocessed message from the client. std::list<std::pair<std::string, std::chrono::steady_clock::time_point>> _tilesOnFly; diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp index 44652cb34..f9dafd373 100644 --- a/wsd/LOOLWSD.cpp +++ b/wsd/LOOLWSD.cpp @@ -687,7 +687,7 @@ inline std::string getAdminURI(const Poco::Util::LayeredConfiguration &config) #endif // MOBILEAPP -std::atomic<unsigned> LOOLWSD::NextSessionId; +std::atomic<uint64_t> LOOLWSD::NextSessionId; #ifndef KIT_IN_PROCESS std::atomic<int> LOOLWSD::ForKitWritePipe(-1); std::atomic<int> LOOLWSD::ForKitProcId(-1); @@ -1762,7 +1762,8 @@ static std::shared_ptr<ClientSession> createNewClientSession(const WebSocketHand const std::string& id, const Poco::URI& uriPublic, const std::shared_ptr<DocumentBroker>& docBroker, - const bool isReadOnly) + const bool isReadOnly, + const std::string& hostNoTrust) { LOG_CHECK_RET(docBroker && "Null docBroker instance", nullptr); try @@ -1778,7 +1779,10 @@ static std::shared_ptr<ClientSession> createNewClientSession(const WebSocketHand // In case of WOPI, if this session is not set as readonly, it might be set so // later after making a call to WOPI host which tells us the permission on files // (UserCanWrite param). - return std::make_shared<ClientSession>(id, docBroker, uriPublic, isReadOnly); + auto session = std::make_shared<ClientSession>(id, docBroker, uriPublic, isReadOnly, hostNoTrust); + session->construct(); + + return session; } catch (const std::exception& exc) { @@ -2426,7 +2430,8 @@ private: // Load the document. // TODO: Move to DocumentBroker. const bool isReadOnly = true; - std::shared_ptr<ClientSession> clientSession = createNewClientSession(nullptr, _id, uriPublic, docBroker, isReadOnly); + std::shared_ptr<ClientSession> clientSession = createNewClientSession( + nullptr, _id, uriPublic, docBroker, isReadOnly, "nocliphost"); if (clientSession) { disposition.setMove([docBroker, clientSession, format] @@ -2475,7 +2480,10 @@ private: sent = true; } else + { LOG_WRN("Failed to create Client Session with id [" << _id << "] on docKey [" << docKey << "]."); + cleanupDocBrokers(); + } } if (!sent) @@ -2673,7 +2681,11 @@ private: std::shared_ptr<DocumentBroker> docBroker = findOrCreateDocBroker(ws, url, docKey, _id, uriPublic); if (docBroker) { - std::shared_ptr<ClientSession> clientSession = createNewClientSession(&ws, _id, uriPublic, docBroker, isReadOnly); + // We can send this back to whomever sent it to us though. + const std::string hostNoTrust = (LOOLWSD::ServerName.empty() ? request.getHost() : LOOLWSD::ServerName); + + std::shared_ptr<ClientSession> clientSession = createNewClientSession(&ws, _id, uriPublic, + docBroker, isReadOnly, hostNoTrust); if (clientSession) { // Transfer the client socket to the DocumentBroker when we get back to the poll: @@ -2730,7 +2742,6 @@ private: else { LOG_WRN("Failed to create Client Session with id [" << _id << "] on docKey [" << docKey << "]."); - cleanupDocBrokers(); } } else diff --git a/wsd/LOOLWSD.hpp b/wsd/LOOLWSD.hpp index 7cb03920f..97cec967f 100644 --- a/wsd/LOOLWSD.hpp +++ b/wsd/LOOLWSD.hpp @@ -44,7 +44,7 @@ public: // An Application is a singleton anyway, // so just keep these as statics. - static std::atomic<unsigned> NextSessionId; + static std::atomic<uint64_t> NextSessionId; static unsigned int NumPreSpawnedChildren; static bool NoCapsForKit; static bool NoSeccomp; commit df4e17dc26a26cba05155f5102f66b5237dec074 Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Sun May 26 00:16:18 2019 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Tue May 28 02:26:47 2019 +0100 Ensure we have a unique ID for each document and view. Change-Id: I9f38dd137d1617aa39b1ca505b07ab7740a986b9 diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js index a32fa9044..6916ab775 100644 --- a/loleaflet/src/layer/tile/TileLayer.js +++ b/loleaflet/src/layer/tile/TileLayer.js @@ -2488,14 +2488,16 @@ L.TileLayer = L.GridLayer.extend({ if (preferInternal === true) { var pasteHtml = dataTransfer.getData('text/html'); var meta = this._getMetaOrigin(pasteHtml); - var id = this._map._socket.WSDServer.Id; + var id = 'https://transient/' + this._map._socket.WSDServer.Id + '/' + this._viewId + + '?WOPISrc=' + encodeURIComponent(this._map.options.doc); + // cf. ClientSession.cpp /textselectioncontent:/ if (meta == id) { // Home from home: short-circuit internally. this._map._socket.sendMessage('uno .uno:Paste'); return; } else - console.log('Unusual origin mismatch on paste between: "' + - meta + '" and "' + id); + console.log('Unusual origin mismatch on paste between:\n\t"' + + meta + '" and\n\t"' + id + '"'); } var types = dataTransfer.types; diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp index c6d6def28..f906e690d 100644 --- a/wsd/ClientSession.cpp +++ b/wsd/ClientSession.cpp @@ -52,6 +52,7 @@ ClientSession::ClientSession(const std::string& id, _tileHeightPixel(0), _tileWidthTwips(0), _tileHeightTwips(0), + _kitViewId(-1), _isTextDocument(false) { const size_t curConnections = ++LOOLWSD::NumConnections; @@ -979,12 +980,21 @@ 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) { + 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 { - // FIXME: expose other content types ? provide an RTF back-channel ? WOPISRC ? - std::string origin = "<meta name=\"origin\" content=\"" + LOOLWSD::HostIdentifier + "\"/>\n"; + // FIXME: expose other content types ? provide an RTF back-channel ? + std::string encodedFrom; + Poco::URI wopiSrc = docBroker->getPublicUri(); + wopiSrc.setQueryParameters(Poco::URI::QueryParameters()); + // matching encodeURIComponent + Poco::URI::encode(wopiSrc.toString(), ",/?:@&=+$#", encodedFrom); + std::string meta = "https://transient/" + LOOLWSD::HostIdentifier + "/" + + std::to_string(getKitViewId()) + "?WOPISrc=" + encodedFrom; + std::string origin = "<meta name=\"origin\" content=\"" + meta + "\"/>\n"; data.insert(data.begin() + pos, origin.begin(), origin.end()); return true; } @@ -1024,6 +1034,11 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt { _isTextDocument = docType.find("text") != std::string::npos; } + + // Store our Kit ViewId + int viewId = -1; + if(getTokenInteger(token, "viewid", viewId)) + _kitViewId = viewId; } // Forward the status response to the client. diff --git a/wsd/ClientSession.hpp b/wsd/ClientSession.hpp index b701e5568..9562c8f39 100644 --- a/wsd/ClientSession.hpp +++ b/wsd/ClientSession.hpp @@ -55,6 +55,9 @@ public: /// Handle kit-to-client message. bool handleKitToClientMessage(const char* data, const int size); + /// Integer id of the view in the kit process, or -1 if unknown + int getKitViewId() const { return _kitViewId; } + // sendTextFrame that takes std::string and string literal. using Session::sendTextFrame; @@ -211,6 +214,9 @@ private: int _tileWidthTwips; int _tileHeightTwips; + /// The integer id of the view in the Kit process + int _kitViewId; + /// Client is using a text document? bool _isTextDocument; commit 4645bbbd36ade52e5b1b3557453c515f665c6824 Author: Michael Meeks <michael.me...@collabora.com> AuthorDate: Sat May 25 16:13:54 2019 +0100 Commit: Michael Meeks <michael.me...@collabora.com> CommitDate: Tue May 28 02:26:47 2019 +0100 Initial switch of outbound copy format to HTML. Use a content-editable instead of an input Switch format specifiers left and right. Embed a <meta name="origin" content="<Id>"/> to reliably detect internal copy/paste not based on content. Lots of testing. Change-Id: I36723298e392331515b055b5ebe8132fb2e4fa3c diff --git a/common/Message.hpp b/common/Message.hpp index a62cfe375..3fdbc5d36 100644 --- a/common/Message.hpp +++ b/common/Message.hpp @@ -13,6 +13,7 @@ #include <atomic> #include <string> #include <vector> +#include <functional> #include "Protocol.hpp" #include "Log.hpp" @@ -117,6 +118,18 @@ public: /// Returns true if and only if the payload is considered Binary. bool isBinary() const { return _type == Type::Binary; } + /// Allows some in-line re-writing of the message + void rewriteDataBody(std::function<bool (std::vector<char> &)> func) + { + if (func(_data)) + { + // Check - just the body. + assert(_firstLine == LOOLProtocol::getFirstLine(_data.data(), _data.size())); + assert(_abbr == _id + ' ' + LOOLProtocol::getAbbreviatedMessage(_data.data(), _data.size())); + assert(_type == detectType()); + } + } + private: /// Constructs a unique ID. diff --git a/common/Util.cpp b/common/Util.cpp index 6f2322c8f..dcdbd115b 100644 --- a/common/Util.cpp +++ b/common/Util.cpp @@ -739,6 +739,20 @@ namespace Util return base + Util::anonymize(filename, nAnonymizationSalt) + ext + params; } + + size_t findInVector(const std::vector<char>& tokens, const char *cstring) + { + assert(cstring); + for (size_t i = 0; i < tokens.size(); ++i) + { + size_t j; + for (j = 0; i + j < tokens.size() && cstring[j] != '\0' && tokens[i + j] == cstring[j]; ++j) + ; + if (cstring[j] == '\0') + return i; + } + return std::string::npos; + } } /* vim:set shiftwidth=4 softtabstop=4 expandtab: */ diff --git a/common/Util.hpp b/common/Util.hpp index 9021b7ed1..488f9d525 100644 --- a/common/Util.hpp +++ b/common/Util.hpp @@ -256,6 +256,8 @@ namespace Util return oss.str(); } + size_t findInVector(const std::vector<char>& tokens, const char *cstring); + /// Trim spaces from the left. Just spaces. inline std::string& ltrim(std::string& s) { diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js index b48338420..17ea4bb09 100644 --- a/loleaflet/src/core/Socket.js +++ b/loleaflet/src/core/Socket.js @@ -36,6 +36,7 @@ L.Socket = L.Class.extend({ ProtocolVersionNumber: '0.1', ReconnectCount: 0, WasShownLimitDialog: false, + WSDServer: {}, getParameterValue: function (s) { var i = s.indexOf('='); @@ -275,25 +276,25 @@ L.Socket = L.Class.extend({ var command = this.parseServerCmd(textMsg); if (textMsg.startsWith('loolserver ')) { // This must be the first message, unless we reconnect. - var loolwsdVersionObj = JSON.parse(textMsg.substring(textMsg.indexOf('{'))); - var h = loolwsdVersionObj.Hash; + this.WSDServer = JSON.parse(textMsg.substring(textMsg.indexOf('{'))); + var h = this.WSDServer.Hash; if (parseInt(h,16).toString(16) === h.toLowerCase().replace(/^0+/, '')) { if (!window.ThisIsTheiOSApp) { h = '<a target="_blank" href="https://hub.libreoffice.org/git-online/' + h + '">' + h + '</a>'; } - $('#loolwsd-version').html(loolwsdVersionObj.Version + ' (git hash: ' + h + ')'); + $('#loolwsd-version').html(this.WSDServer.Version + ' (git hash: ' + h + ')'); } else { - $('#loolwsd-version').text(loolwsdVersionObj.Version); + $('#loolwsd-version').text(this.WSDServer.Version); } var idUri = this._map.options.server + this._map.options.serviceRoot + '/hosting/discovery'; idUri = idUri.replace(/^ws:/, 'http:'); idUri = idUri.replace(/^wss:/, 'https:'); - $('#loolwsd-id').html('<a href="' + idUri + '">' + loolwsdVersionObj.Id + '</a>'); + $('#loolwsd-id').html('<a href="' + idUri + '">' + this.WSDServer.Id + '</a>'); // TODO: For now we expect perfect match in protocol versions - if (loolwsdVersionObj.Protocol !== this.ProtocolVersionNumber) { + if (this.WSDServer.Protocol !== this.ProtocolVersionNumber) { this._map.fire('error', {msg: _('Unsupported server version.')}); } } diff --git a/loleaflet/src/layer/marker/ClipboardContainer.js b/loleaflet/src/layer/marker/ClipboardContainer.js index 41aae051d..f22ed2feb 100644 --- a/loleaflet/src/layer/marker/ClipboardContainer.js +++ b/loleaflet/src/layer/marker/ClipboardContainer.js @@ -41,15 +41,25 @@ L.ClipboardContainer = L.Layer.extend({ }, select: function() { - this._textArea.select(); + window.getSelection().selectAllChildren(this._textArea); + }, + + resetToSelection: function() { + var sel = window.getSelection(); + if (sel.anchorNode == this._textArea) { + // already selected, don't reset to plain-text from toString() + } else { + this.setValue(sel.toString()); + } }, getValue: function() { - return this._textArea.value; + return this._textArea.innerHTML; }, setValue: function(val) { - this._textArea.value = val; + this._textArea.innerHTML = val; + this.select(); }, setLatLng: function (latlng) { @@ -67,7 +77,8 @@ L.ClipboardContainer = L.Layer.extend({ _initLayout: function () { this._container = L.DomUtil.create('div', 'clipboard-container'); this._container.id = 'doc-clipboard-container'; - this._textArea = L.DomUtil.create('input', 'clipboard', this._container); + this._textArea = L.DomUtil.create('div', 'clipboard', this._container); + this._textArea.setAttribute('contenteditable', 'true'); this._textArea.setAttribute('type', 'text'); this._textArea.setAttribute('autocorrect', 'off'); this._textArea.setAttribute('autocapitalize', 'off'); diff --git a/loleaflet/src/layer/tile/TileLayer.js b/loleaflet/src/layer/tile/TileLayer.js index a1488669a..a32fa9044 100644 --- a/loleaflet/src/layer/tile/TileLayer.js +++ b/loleaflet/src/layer/tile/TileLayer.js @@ -26,12 +26,18 @@ function hex2string(inData) } L.Compatibility = { + stripHTML: function(html) { // grim. + var tmp = document.createElement('div'); + tmp.innerHTML = html; + return tmp.textContent || tmp.innerText || ''; + }, + clipboardSet: function (event, text) { if (event.clipboardData) { // Standard - event.clipboardData.setData('text/plain', text); + event.clipboardData.setData('text/html', text); } - else if (window.clipboardData) { // IE 11 - window.clipboardData.setData('Text', text); + else if (window.clipboardData) { // IE 11 - poor clipboard API + window.clipboardData.setData('Text', this.stripHTML(text)); } } }; @@ -1227,7 +1233,7 @@ L.TileLayer = L.GridLayer.extend({ clearTimeout(this._selectionContentRequest); } this._selectionContentRequest = setTimeout(L.bind(function () { - this._map._socket.sendMessage('gettextselection mimetype=text/plain;charset=utf-8');}, this), 100); + this._map._socket.sendMessage('gettextselection mimetype=text/html');}, this), 100); } this._onUpdateTextSelection(); }, @@ -1267,6 +1273,7 @@ L.TileLayer = L.GridLayer.extend({ _onTextSelectionContentMsg: function (textMsg) { this._selectionTextContent = textMsg.substr(22); + this._map._clipboardContainer.setValue(this._selectionTextContent); }, _updateScrollOnCellSelection: function (oldSelection, newSelection) { @@ -2463,18 +2470,32 @@ L.TileLayer = L.GridLayer.extend({ this._dataTransferToDocument(e.dataTransfer, /* preferInternal = */ false); }, + _getMetaOrigin: function (html) { + var match = '<meta name="origin" content="'; + var start = html.indexOf(match); + if (start < 0) { + return ''; + } + var end = html.indexOf('"', start + match.length); + if (end < 0) { + return ''; + } + return html.substring(start + match.length, end); + }, + _dataTransferToDocument: function (dataTransfer, preferInternal) { // for the paste, we might prefer the internal LOK's copy/paste if (preferInternal === true) { - var pasteString = dataTransfer.getData('text/plain'); - if (!pasteString) { - pasteString = dataTransfer.getData('Text'); // IE 11 - } - - if (pasteString && pasteString === this._selectionTextHash) { + var pasteHtml = dataTransfer.getData('text/html'); + var meta = this._getMetaOrigin(pasteHtml); + var id = this._map._socket.WSDServer.Id; + if (meta == id) { + // Home from home: short-circuit internally. this._map._socket.sendMessage('uno .uno:Paste'); return; - } + } else + console.log('Unusual origin mismatch on paste between: "' + + meta + '" and "' + id); } var types = dataTransfer.types; diff --git a/loleaflet/src/map/handler/Map.Keyboard.js b/loleaflet/src/map/handler/Map.Keyboard.js index 4e3b51354..95a5269b9 100644 --- a/loleaflet/src/map/handler/Map.Keyboard.js +++ b/loleaflet/src/map/handler/Map.Keyboard.js @@ -528,7 +528,7 @@ L.Map.Keyboard = L.Handler.extend({ case 91: // Left Cmd (Safari) case 93: // Right Cmd (Safari) // we prepare for a copy or cut event - this._map._clipboardContainer.setValue(window.getSelection().toString()); + this._map._clipboardContainer.resetToSelection(); this._map.focus(); this._map._clipboardContainer.select(); return true; diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp index 3de6a8651..c6d6def28 100644 --- a/wsd/ClientSession.cpp +++ b/wsd/ClientSession.cpp @@ -977,6 +977,24 @@ 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=\""); + if (pos != std::string::npos) // assume text/html + { + // FIXME: expose other content types ? provide an RTF back-channel ? WOPISRC ? + std::string origin = "<meta name=\"origin\" content=\"" + LOOLWSD::HostIdentifier + "\"/>\n"; + data.insert(data.begin() + pos, origin.begin(), origin.end()); + return true; + } + else + { + LOG_DBG("Missing generator in textselectioncontent"); + return false; + } + }); + return forwardToClient(payload); } if (!isDocPasswordProtected()) _______________________________________________ Libreoffice-commits mailing list libreoffice-comm...@lists.freedesktop.org https://lists.freedesktop.org/mailman/listinfo/libreoffice-commits