[Libreoffice-commits] online.git: 7 commits - common/Unit.hpp kit/ChildSession.cpp kit/Kit.cpp loleaflet/reference.html loleaflet/src net/Socket.hpp net/WebSocketHandler.hpp test/Makefile.am test/UnitOAuth.cpp test/UnitWOPI.cpp test/UnitWOPISaveAs.cpp test/WopiTestServer.hpp wsd/ClientSession.cpp wsd/DocumentBroker.cpp wsd/DocumentBroker.hpp wsd/LOOLWSD.cpp wsd/protocol.txt wsd/Storage.cpp wsd/Storage.hpp

Jan Holesovsky kendy at collabora.com
Thu Oct 26 09:14:30 UTC 2017


 common/Unit.hpp                          |   10 +
 kit/ChildSession.cpp                     |   40 ++++--
 kit/Kit.cpp                              |    2 
 loleaflet/reference.html                 |    9 -
 loleaflet/src/control/Control.Menubar.js |    4 
 loleaflet/src/control/Toolbar.js         |   14 --
 loleaflet/src/core/Socket.js             |   10 -
 loleaflet/src/map/handler/Map.WOPI.js    |    7 -
 net/Socket.hpp                           |   15 ++
 net/WebSocketHandler.hpp                 |   25 +--
 test/Makefile.am                         |    6 
 test/UnitOAuth.cpp                       |    5 
 test/UnitWOPI.cpp                        |   10 -
 test/UnitWOPISaveAs.cpp                  |   84 +++++++++++++
 test/WopiTestServer.hpp                  |   58 +++++++--
 wsd/ClientSession.cpp                    |   63 ++++++----
 wsd/DocumentBroker.cpp                   |   92 +++++++-------
 wsd/DocumentBroker.hpp                   |   10 -
 wsd/LOOLWSD.cpp                          |    4 
 wsd/Storage.cpp                          |  195 +++++++++++++------------------
 wsd/Storage.hpp                          |   65 ++++++++--
 wsd/protocol.txt                         |   23 +--
 22 files changed, 463 insertions(+), 288 deletions(-)

New commits:
commit 343c5bc69083c844b7896d7a62b40a44d8b06b03
Author: Jan Holesovsky <kendy at collabora.com>
Date:   Thu Oct 26 10:38:57 2017 +0200

    tdf#99744 SaveAs: Extend test to check that the Save As result was sent.
    
    Change-Id: I3788b87d2599c01000af97f496ee2b840c0cae3e

diff --git a/common/Unit.hpp b/common/Unit.hpp
index 9885d073..97db1c39 100644
--- a/common/Unit.hpp
+++ b/common/Unit.hpp
@@ -15,7 +15,7 @@
 #include <string>
 
 #include <LOOLWebSocket.hpp>
-#include "net/WebSocketHandler.hpp"
+#include "net/Socket.hpp"
 
 class UnitBase;
 class UnitWSD;
@@ -24,6 +24,8 @@ class UnitTimeout;
 class UnitHTTPServerRequest;
 class UnitHTTPServerResponse;
 
+class WebSocketHandler;
+
 // Forward declaration to avoid pulling the world here.
 namespace Poco
 {
@@ -95,6 +97,12 @@ public:
         return false;
     }
 
+    /// Message that is about to be sent via the websocket.
+    virtual bool filterSendMessage(const char* /* data */, const size_t /* len */, const WSOpCode /* code */, const bool /* flush */, int& /*unitReturn*/)
+    {
+        return false;
+    }
+
     /// Hook the disk space check
     virtual bool filterCheckDiskSpace(const std::string & /* path */,
                                       bool & /* newResult */)
diff --git a/kit/Kit.cpp b/kit/Kit.cpp
index 34aed6a1..665ab3af 100644
--- a/kit/Kit.cpp
+++ b/kit/Kit.cpp
@@ -1618,7 +1618,7 @@ private:
                     vect.assign(data, data + size);
 
                     // TODO loolnb - this is probably wrong...
-                    session->handleMessage(/* fin = */ false, WebSocketHandler::WSOpCode::Binary, vect);
+                    session->handleMessage(/* fin = */ false, WSOpCode::Binary, vect);
                     return true;
                 }
             }
diff --git a/net/Socket.hpp b/net/Socket.hpp
index 62a743b5..0b6af504 100644
--- a/net/Socket.hpp
+++ b/net/Socket.hpp
@@ -983,6 +983,21 @@ protected:
     friend class SimpleResponseClient;
 };
 
+enum class WSOpCode : unsigned char {
+    Continuation = 0x0,
+    Text         = 0x1,
+    Binary       = 0x2,
+    Reserved1    = 0x3,
+    Reserved2    = 0x4,
+    Reserved3    = 0x5,
+    Reserved4    = 0x6,
+    Reserved5    = 0x7,
+    Close        = 0x8,
+    Ping         = 0x9,
+    Pong         = 0xa
+    // ... reserved
+};
+
 namespace HttpHelper
 {
     /// Sends file as HTTP response.
diff --git a/net/WebSocketHandler.hpp b/net/WebSocketHandler.hpp
index 383f1139..cae461bc 100644
--- a/net/WebSocketHandler.hpp
+++ b/net/WebSocketHandler.hpp
@@ -16,6 +16,7 @@
 
 #include "common/Common.hpp"
 #include "common/Log.hpp"
+#include "common/Unit.hpp"
 #include "Socket.hpp"
 
 #include <Poco/Net/HTTPRequest.h>
@@ -72,21 +73,6 @@ public:
         LOG_TRC("#" << socket->getFD() << " Connected to WS Handler 0x" << std::hex << this << std::dec);
     }
 
-    enum WSOpCode {
-        Continuation, // 0x0
-        Text,         // 0x1
-        Binary,       // 0x2
-        Reserved1,    // 0x3
-        Reserved2,    // 0x4
-        Reserved3,    // 0x5
-        Reserved4,    // 0x6
-        Reserved5,    // 0x7
-        Close,        // 0x8
-        Ping,         // 0x9
-        Pong          // 0xa
-        // ... reserved
-    };
-
     /// Status codes sent to peer on shutdown.
     enum class StatusCodes : unsigned short
     {
@@ -204,7 +190,7 @@ public:
         socket->_inBuffer.erase(socket->_inBuffer.begin(), socket->_inBuffer.begin() + headerLen + payloadLen);
 
         // FIXME: fin, aggregating payloads into _wsPayload etc.
-        LOG_TRC("#" << socket->getFD() << ": Incoming WebSocket message code " << code <<
+        LOG_TRC("#" << socket->getFD() << ": Incoming WebSocket message code " << static_cast<unsigned>(code) <<
                 ", fin? " << fin << ", mask? " << hasMask << ", payload length: " << _wsPayload.size() <<
                 ", residual socket data: " << socket->_inBuffer.size() << " bytes.");
 
@@ -329,11 +315,14 @@ public:
     /// 0 for closed/invalid socket, and -1 for other errors.
     int sendMessage(const char* data, const size_t len, const WSOpCode code, const bool flush = true) const
     {
+        int unitReturn = -1;
+        if (UnitWSD::get().filterSendMessage(data, len, code, flush, unitReturn))
+            return unitReturn;
+
         //TODO: Support fragmented messages.
-        static const unsigned char Fin = static_cast<unsigned char>(WSFrameMask::Fin);
 
         auto socket = _socket.lock();
-        return sendFrame(socket, data, len, static_cast<unsigned char>(Fin | code), flush);
+        return sendFrame(socket, data, len, static_cast<unsigned char>(WSFrameMask::Fin) | static_cast<unsigned char>(code), flush);
     }
 
 protected:
diff --git a/test/UnitWOPISaveAs.cpp b/test/UnitWOPISaveAs.cpp
index cb9ff9c3..a0c5c3df 100644
--- a/test/UnitWOPISaveAs.cpp
+++ b/test/UnitWOPISaveAs.cpp
@@ -35,8 +35,19 @@ public:
     {
         // spec says UTF-7...
         CPPUNIT_ASSERT_EQUAL(std::string("/jan/hole+AWE-ovsk+AP0-/hello world.txt"), request.get("X-WOPI-SuggestedTarget"));
+    }
+
+    bool filterSendMessage(const char* data, const size_t len, const WSOpCode /* code */, const bool /* flush */, int& /*unitReturn*/) override
+    {
+        std::string message(data, len);
+        if (message == "saveas: url=https://127.0.0.1:9980/something%20wopi/files/1?access_token=anything filename=hello%20world.txt")
+        {
+            // successfully exit the test if we also got the outgoing message
+            // notifying about saving the file
+            exitTest(TestResult::Ok);
+        }
 
-        exitTest(TestResult::Ok);
+        return false;
     }
 
     void invokeTest() override
diff --git a/wsd/LOOLWSD.cpp b/wsd/LOOLWSD.cpp
index f1e4c800..879d206f 100644
--- a/wsd/LOOLWSD.cpp
+++ b/wsd/LOOLWSD.cpp
@@ -2012,7 +2012,7 @@ private:
                             URI::encode(docBroker->getPublicUri().getPath(), "", encodedFrom);
                             const std::string load = "load url=" + encodedFrom;
                             std::vector<char> loadRequest(load.begin(), load.end());
-                            clientSession->handleMessage(true, WebSocketHandler::WSOpCode::Text, loadRequest);
+                            clientSession->handleMessage(true, WSOpCode::Text, loadRequest);
 
                             // FIXME: Check for security violations.
                             Path toPath(docBroker->getPublicUri().getPath());
@@ -2024,7 +2024,7 @@ private:
                             // Convert it to the requested format.
                             const auto saveas = "saveas url=" + encodedTo + " format=" + format + " options=";
                             std::vector<char> saveasRequest(saveas.begin(), saveas.end());
-                            clientSession->handleMessage(true, WebSocketHandler::WSOpCode::Text, saveasRequest);
+                            clientSession->handleMessage(true, WSOpCode::Text, saveasRequest);
                         });
                         });
 
commit a98727b536bc5232f1a78a8147034529f6c483ac
Author: Aron Budea <aron.budea at collabora.com>
Date:   Thu Oct 26 01:27:57 2017 +0200

    Add 'Repair' menu to spreadsheet/presentation.
    
    Change-Id: I7651418c79c711940438ada53d64970e7674d077
    Reviewed-on: https://gerrit.libreoffice.org/43859
    Reviewed-by: Jan Holesovsky <kendy at collabora.com>
    Tested-by: Jan Holesovsky <kendy at collabora.com>

diff --git a/loleaflet/src/control/Control.Menubar.js b/loleaflet/src/control/Control.Menubar.js
index bca73068..120550a8 100644
--- a/loleaflet/src/control/Control.Menubar.js
+++ b/loleaflet/src/control/Control.Menubar.js
@@ -212,6 +212,7 @@ L.Control.Menubar = L.Control.extend({
 			{name: _('Edit'), type: 'menu', menu: [
 				{name: _('Undo'), type: 'unocommand', uno: '.uno:Undo'},
 				{name: _('Redo'), type: 'unocommand', uno: '.uno:Redo'},
+				{name: _('Repair'), id: 'repair',  type: 'action'},
 				{type: 'separator'},
 				{name: _('Cut'), type: 'unocommand', uno: '.uno:Cut'},
 				{name: _('Copy'), type: 'unocommand', uno: '.uno:Copy'},
@@ -279,6 +280,7 @@ L.Control.Menubar = L.Control.extend({
 			{name: _('Edit'), type: 'menu', menu: [
 				{name: _('Undo'), type: 'unocommand', uno: '.uno:Undo'},
 				{name: _('Redo'), type: 'unocommand', uno: '.uno:Redo'},
+				{name: _('Repair'), id: 'repair',  type: 'action'},
 				{type: 'separator'},
 				{name: _('Cut'), type: 'unocommand', uno: '.uno:Cut'},
 				{name: _('Copy'), type: 'unocommand', uno: '.uno:Copy'},
commit 58ec841ad1a5026296eb7f95118f441aedeea184
Author: Jan Holesovsky <kendy at collabora.com>
Date:   Wed Oct 25 21:14:33 2017 +0200

    Move the 'Repair' menu entry under Undo / Redo.
    
    Change-Id: Ic53eec49413de800154ce956747fe0fcf32d96e9

diff --git a/loleaflet/src/control/Control.Menubar.js b/loleaflet/src/control/Control.Menubar.js
index dfae7d89..bca73068 100644
--- a/loleaflet/src/control/Control.Menubar.js
+++ b/loleaflet/src/control/Control.Menubar.js
@@ -26,9 +26,9 @@ L.Control.Menubar = L.Control.extend({
 					{name: _('Microsoft Word (.docx)'), id: 'downloadas-docx', type: 'action'}]}]
 			},
 			{name: _('Edit'), type: 'menu', menu: [
-				{name: _('Repair'), id: 'repair',  type: 'action'},
 				{name: _('Undo'), type: 'unocommand', uno: '.uno:Undo'},
 				{name: _('Redo'), type: 'unocommand', uno: '.uno:Redo'},
+				{name: _('Repair'), id: 'repair',  type: 'action'},
 				{type: 'separator'},
 				{name: _('Cut'), type: 'unocommand', uno: '.uno:Cut'},
 				{name: _('Copy'), type: 'unocommand', uno: '.uno:Copy'},
commit 6745464c70b1adf4c0d523ebe4afe3feed814cac
Author: Jan Holesovsky <kendy at collabora.com>
Date:   Wed Oct 25 14:09:27 2017 +0200

    tdf#99744 SaveAs: Report back to loleaflet that the saveas succeeded.
    
    Change-Id: I670c8b4503c1a4c0a88001a1343f6dec2974e044

diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp
index 9c506389..b9549c06 100644
--- a/kit/ChildSession.cpp
+++ b/kit/ChildSession.cpp
@@ -992,11 +992,10 @@ bool ChildSession::saveAs(const char* /*buffer*/, int /*length*/, const std::vec
     Poco::URI::encode(url, "", encodedURL);
     Poco::URI::encode(wopiFilename, "", encodedWopiFilename);
 
-    sendTextFrame("saveas: url=" + encodedURL + " wopifilename=" + encodedWopiFilename);
-    std::string successStr = success ? "true" : "false";
-    sendTextFrame("unocommandresult: {"
-            "\"commandName\":\"saveas\","
-            "\"success\":\"" + successStr + "\"}");
+    if (success)
+        sendTextFrame("saveas: url=" + encodedURL + " filename=" + encodedWopiFilename);
+    else
+        sendTextFrame("error: cmd=storage kind=savefailed");
 
     return true;
 }
diff --git a/loleaflet/src/control/Toolbar.js b/loleaflet/src/control/Toolbar.js
index a2d70d79..4ec419e3 100644
--- a/loleaflet/src/control/Toolbar.js
+++ b/loleaflet/src/control/Toolbar.js
@@ -93,13 +93,10 @@ L.Map.include({
 		}
 
 		this.showBusy(_('Saving...'), false);
-		// TakeOwnership: we are performing a 'real' save-as, the document
-		// is just getting a new place, ie. it will get the
-		// '.uno:ModifiedStatus' upon completion.
 		this._socket.sendMessage('saveas ' +
 			'url=' + url + ' ' +
 			'format=' + format + ' ' +
-			'options=TakeOwnership,' + options);
+			'options=' + options);
 	},
 
 	applyStyle: function (style, familyName) {
diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js
index d83b6afc..1481f4af 100644
--- a/loleaflet/src/core/Socket.js
+++ b/loleaflet/src/core/Socket.js
@@ -562,10 +562,9 @@ L.Socket = L.Class.extend({
 			this._map._docLayer._debugRenderCount = command.rendercount;
 		}
 		else if (textMsg.startsWith('saveas:')) {
-			textMsg = (textMsg.substring(7)).trim();
-			// var url = textMsg.substring(0, textMsg.indexOf(' '));
-			// var fileName = textMsg.substring(textMsg.indexOf(' '));
-			/// redirect or not?
+			this._map.hideBusy();
+			// var url = command.url; // WOPI url - if needed at some stage
+			// var name = command.name; TODO dialog that the file was saved as "name"
 		}
 		else if (textMsg.startsWith('statusindicator:')) {
 			//FIXME: We should get statusindicator when saving too, no?
@@ -793,6 +792,9 @@ L.Socket = L.Class.extend({
 			else if (tokens[i].substring(0, 5) === 'char=') {
 				command.char = tokens[i].substring(5);
 			}
+			else if (tokens[i].substring(0, 4) === 'url=') {
+				command.url = tokens[i].substring(4);
+			}
 			else if (tokens[i].substring(0, 7) === 'viewid=') {
 				command.viewid = tokens[i].substring(7);
 			}
diff --git a/loleaflet/src/map/handler/Map.WOPI.js b/loleaflet/src/map/handler/Map.WOPI.js
index 7b2a44d4..dc08693e 100644
--- a/loleaflet/src/map/handler/Map.WOPI.js
+++ b/loleaflet/src/map/handler/Map.WOPI.js
@@ -221,15 +221,12 @@ L.Map.WOPI = L.Handler.extend({
 			this._postMessage({msgId: 'Get_Export_Formats_Resp', args: exportFormatsResp});
 		}
 		else if (msg.MessageId === 'Action_SaveAs') {
-			/* TODO
 			if (msg.Values) {
-				if (msg.Values.Filename === null || msg.Values.Filename === undefined) {
-					msg.Values.Filename = '';
+				if (msg.Values.Filename !== null && msg.Values.Filename !== undefined) {
+					this._map.showBusy(_('Creating copy...'), false);
+					map.saveAs('wopi:' + msg.Values.Filename);
 				}
-				this.showBusy(_('Creating copy...'), false);
-				map.saveAs(msg.Values.Filename);
 			}
-			*/
 		}
 	},
 
diff --git a/test/UnitWOPISaveAs.cpp b/test/UnitWOPISaveAs.cpp
index 82e89de0..cb9ff9c3 100644
--- a/test/UnitWOPISaveAs.cpp
+++ b/test/UnitWOPISaveAs.cpp
@@ -31,7 +31,7 @@ public:
     {
     }
 
-    void assertPutFileRelativeRequest(const Poco::Net::HTTPRequest& request) override
+    void assertPutRelativeFileRequest(const Poco::Net::HTTPRequest& request) override
     {
         // spec says UTF-7...
         CPPUNIT_ASSERT_EQUAL(std::string("/jan/hole+AWE-ovsk+AP0-/hello world.txt"), request.get("X-WOPI-SuggestedTarget"));
diff --git a/test/WopiTestServer.hpp b/test/WopiTestServer.hpp
index 6324114d..4f0e14c6 100644
--- a/test/WopiTestServer.hpp
+++ b/test/WopiTestServer.hpp
@@ -64,7 +64,7 @@ public:
     {
     }
 
-    virtual void assertPutFileRelativeRequest(const Poco::Net::HTTPRequest& /*request*/)
+    virtual void assertPutRelativeFileRequest(const Poco::Net::HTTPRequest& /*request*/)
     {
     }
 
@@ -140,17 +140,14 @@ protected:
         }
         else if (request.getMethod() == "POST" && (uriReq.getPath() == "/wopi/files/0" || uriReq.getPath() == "/wopi/files/1"))
         {
-            LOG_INF("Fake wopi host request, handling PutFileRelative: " << uriReq.getPath());
+            LOG_INF("Fake wopi host request, handling PutRelativeFile: " << uriReq.getPath());
 
             CPPUNIT_ASSERT_EQUAL(std::string("PUT_RELATIVE"), request.get("X-WOPI-Override"));
 
-            assertPutFileRelativeRequest(request);
+            assertPutRelativeFileRequest(request);
 
-            Poco::URI wopiURL(helpers::getTestServerURI() + "/wopi/files/1");
-            std::string url;
-            Poco::URI::encode(wopiURL.toString(), ":/?", url);
-
-            std::string content = "{ \"Name\":\"hello world.txt\", \"Url\":\"" + url + "\" }";
+            std::string wopiURL = helpers::getTestServerURI() + "/something wopi/files/1?access_token=anything";
+            std::string content = "{ \"Name\":\"hello world.txt\", \"Url\":\"" + wopiURL + "\" }";
 
             std::ostringstream oss;
             oss << "HTTP/1.1 200 OK\r\n"
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 63fa9eb9..d3cf7106 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -664,7 +664,7 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
         }
 
         std::string encodedWopiFilename;
-        if (!getTokenString(tokens[2], "wopifilename", encodedWopiFilename))
+        if (!getTokenString(tokens[2], "filename", encodedWopiFilename))
         {
             LOG_ERR("Bad syntax for: " << firstLine);
             return false;
@@ -703,11 +703,11 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
             // Normal SaveAs - save to Storage and log result.
             if (resultURL.getScheme() == "file" && !resultURL.getPath().empty())
             {
+                // this also sends the saveas: result
                 docBroker->saveAsToStorage(getId(), resultURL.getPath(), wopiFilename);
             }
-
-            if (!isCloseFrame())
-                forwardToClient(payload);
+            else
+                sendTextFrame("error: cmd=storage kind=savefailed");
         }
         else
         {
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index 1bac7182..77559bfc 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -675,7 +675,7 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
 
     assert(_storage && _tileCache);
     StorageBase::SaveResult storageSaveResult = _storage->saveLocalFileToStorage(auth, saveAsPath, saveAsFilename);
-    if (storageSaveResult == StorageBase::SaveResult::OK)
+    if (storageSaveResult.getResult() == StorageBase::SaveResult::OK)
     {
         if (!isSaveAs)
         {
@@ -697,12 +697,21 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
         }
         else
         {
-            Log::debug() << "Saved As docKey [" << _docKey << "] to URI [" << uri
-                         << "] successfully.";
+            // normalize the url (mainly to " " -> "%20")
+            std::string url = Poco::URI(storageSaveResult.getSaveAsUrl()).toString();
+
+            // encode the name
+            std::string encodedName;
+            Poco::URI::encode(storageSaveResult.getSaveAsName(), "", encodedName);
+
+            it->second->sendTextFrame("saveas: url=" + url + " filename=" + encodedName);
+
+            Log::debug() << "Saved As docKey [" << _docKey << "] to URI [" << url
+                         << " with name '" << encodedName << "'] successfully.";
         }
         return true;
     }
-    else if (storageSaveResult == StorageBase::SaveResult::DISKFULL)
+    else if (storageSaveResult.getResult() == StorageBase::SaveResult::DISKFULL)
     {
         LOG_WRN("Disk full while saving docKey [" << _docKey << "] to URI [" << uri <<
                 "]. Making all sessions on doc read-only and notifying clients.");
@@ -714,18 +723,18 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
             sessionIt.second->sendTextFrame("error: cmd=storage kind=savediskfull");
         }
     }
-    else if (storageSaveResult == StorageBase::SaveResult::UNAUTHORIZED)
+    else if (storageSaveResult.getResult() == StorageBase::SaveResult::UNAUTHORIZED)
     {
         LOG_ERR("Cannot save docKey [" << _docKey << "] to storage URI [" << uri << "]. Invalid or expired access token. Notifying client.");
         it->second->sendTextFrame("error: cmd=storage kind=saveunauthorized");
     }
-    else if (storageSaveResult == StorageBase::SaveResult::FAILED)
+    else if (storageSaveResult.getResult() == StorageBase::SaveResult::FAILED)
     {
         //TODO: Should we notify all clients?
         LOG_ERR("Failed to save docKey [" << _docKey << "] to URI [" << uri << "]. Notifying client.");
         it->second->sendTextFrame("error: cmd=storage kind=savefailed");
     }
-    else if (storageSaveResult == StorageBase::SaveResult::DOC_CHANGED)
+    else if (storageSaveResult.getResult() == StorageBase::SaveResult::DOC_CHANGED)
     {
         LOG_ERR("PutFile says that Document changed in storage");
         _documentChangedInStorage = true;
diff --git a/wsd/Storage.cpp b/wsd/Storage.cpp
index af60c237..95a489cb 100644
--- a/wsd/Storage.cpp
+++ b/wsd/Storage.cpp
@@ -324,7 +324,7 @@ StorageBase::SaveResult LocalStorage::saveLocalFileToStorage(const Authorization
         return StorageBase::SaveResult::FAILED;
     }
 
-    return StorageBase::SaveResult::OK;
+    return StorageBase::SaveResult(StorageBase::SaveResult::OK);
 }
 
 namespace {
@@ -674,7 +674,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
     LOG_INF("Uploading URI via WOPI [" << uriObject.toString() << "] from [" << _jailedFilePath + "].");
 
     std::ostringstream oss;
-    StorageBase::SaveResult saveResult = StorageBase::SaveResult::FAILED;
+    StorageBase::SaveResult saveResult(StorageBase::SaveResult::FAILED);
     try
     {
         std::unique_ptr<Poco::Net::HTTPClientSession> psession(getHTTPClientSession(uriObject));
@@ -748,7 +748,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
         std::istream& rs = psession->receiveResponse(response);
         Poco::StreamCopier::copyStream(rs, oss);
 
-        std::string wopiLog(isSaveAs? "WOPI::PutFileRelative": "WOPI::PutFile");
+        std::string wopiLog(isSaveAs? "WOPI::PutRelativeFile": "WOPI::PutFile");
         LOG_INF(wopiLog << " response: " << oss.str());
         LOG_INF(wopiLog << " uploaded " << size << " bytes from [" << filePath <<
                 "] -> [" << uriObject.toString() << "]: " <<
@@ -756,7 +756,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
 
         if (response.getStatus() == Poco::Net::HTTPResponse::HTTP_OK)
         {
-            saveResult = StorageBase::SaveResult::OK;
+            saveResult.setResult(StorageBase::SaveResult::OK);
             Poco::JSON::Object::Ptr object;
             if (parseJSON(oss.str(), object))
             {
@@ -764,38 +764,49 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
                 LOG_TRC(wopiLog << " returns LastModifiedTime [" << lastModifiedTime << "].");
                 _fileInfo._modifiedTime = iso8601ToTimestamp(lastModifiedTime);
 
+                if (isSaveAs)
+                {
+                    const std::string name = getJSONValue<std::string>(object, "Name");
+                    LOG_TRC(wopiLog << " returns Name [" << name << "].");
+
+                    const std::string url = getJSONValue<std::string>(object, "Url");
+                    LOG_TRC(wopiLog << " returns Url [" << url << "].");
+
+                    saveResult.setSaveAsResult(name, url);
+                }
+
                 // Reset the force save flag now, if any, since we are done saving
                 // Next saves shouldn't be saved forcefully unless commanded
                 _forceSave = false;
             }
             else
             {
-                LOG_WRN("Invalid/Missing JSON found in " << wopiLog << " response");
+                LOG_WRN("Invalid or missing JSON in " << wopiLog << " HTTP_OK response");
             }
         }
         else if (response.getStatus() == Poco::Net::HTTPResponse::HTTP_REQUESTENTITYTOOLARGE)
         {
-            saveResult = StorageBase::SaveResult::DISKFULL;
+            saveResult.setResult(StorageBase::SaveResult::DISKFULL);
         }
         else if (response.getStatus() == Poco::Net::HTTPResponse::HTTP_UNAUTHORIZED)
         {
-            saveResult = StorageBase::SaveResult::UNAUTHORIZED;
+            saveResult.setResult(StorageBase::SaveResult::UNAUTHORIZED);
         }
         else if (response.getStatus() == Poco::Net::HTTPResponse::HTTP_CONFLICT)
         {
-            saveResult = StorageBase::SaveResult::CONFLICT;
+            saveResult.setResult(StorageBase::SaveResult::CONFLICT);
             Poco::JSON::Object::Ptr object;
             if (parseJSON(oss.str(), object))
             {
                 const unsigned loolStatusCode = getJSONValue<unsigned>(object, "LOOLStatusCode");
                 if (loolStatusCode == static_cast<unsigned>(LOOLStatusCode::DOC_CHANGED))
                 {
-                    saveResult = StorageBase::SaveResult::DOC_CHANGED;
+                    saveResult.setResult(StorageBase::SaveResult::DOC_CHANGED);
                 }
             }
             else
             {
-                LOG_WRN("Invalid/missing JSON in " << wopiLog << " response");
+                LOG_WRN("Invalid or missing JSON in " << wopiLog << " HTTP_CONFLICT response");
             }
         }
     }
@@ -803,7 +814,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
     {
         LOG_ERR("Cannot save file to WOPI storage uri [" + uriObject.toString() + "]. Error: " << pexc.displayText() <<
                 (pexc.nested() ? " (" + pexc.nested()->displayText() + ")" : ""));
-        saveResult = StorageBase::SaveResult::FAILED;
+        saveResult.setResult(StorageBase::SaveResult::FAILED);
     }
 
     return saveResult;
@@ -819,7 +830,7 @@ std::string WebDAVStorage::loadStorageFileToLocal(const Authorization& /*auth*/)
 StorageBase::SaveResult WebDAVStorage::saveLocalFileToStorage(const Authorization& /*auth*/, const std::string& /*saveAsPath*/, const std::string& /*saveAsFilename*/)
 {
     // TODO: implement webdav PUT.
-    return StorageBase::SaveResult::OK;
+    return StorageBase::SaveResult(StorageBase::SaveResult::OK);
 }
 
 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/wsd/Storage.hpp b/wsd/Storage.hpp
index 60c92191..52df9d14 100644
--- a/wsd/Storage.hpp
+++ b/wsd/Storage.hpp
@@ -53,14 +53,53 @@ public:
         size_t _size;
     };
 
-    enum class SaveResult
+    class SaveResult
     {
-        OK,
-        DISKFULL,
-        UNAUTHORIZED,
-        DOC_CHANGED, /* Document changed in storage */
-        CONFLICT,
-        FAILED
+    public:
+        enum Result
+        {
+            OK,
+            DISKFULL,
+            UNAUTHORIZED,
+            DOC_CHANGED, /**< Document changed in storage */
+            CONFLICT,
+            FAILED
+        };
+
+        SaveResult(Result result) : _result(result)
+        {
+        }
+
+        void setResult(Result result)
+        {
+            _result = result;
+        }
+
+        Result getResult() const
+        {
+            return _result;
+        }
+
+        void setSaveAsResult(const std::string& name, const std::string& url)
+        {
+            _saveAsName = name;
+            _saveAsUrl = url;
+        }
+
+        const std::string& getSaveAsName() const
+        {
+            return _saveAsName;
+        }
+
+        const std::string& getSaveAsUrl() const
+        {
+            return _saveAsUrl;
+        }
+
+    private:
+        Result _result;
+        std::string _saveAsName;
+        std::string _saveAsUrl;
     };
 
     enum class LOOLStatusCode
diff --git a/wsd/protocol.txt b/wsd/protocol.txt
index 25805b30..3af10f64 100644
--- a/wsd/protocol.txt
+++ b/wsd/protocol.txt
@@ -112,6 +112,8 @@ saveas url=<url> format=<format> options=<options>
 
     <url> is a URL, encoded. <format> is also URL-encoded, i.e. spaces as %20 and it can be empty
     options are the whole rest of the line, not URL-encoded, and can be empty
+    If <url> uses 'wopi:' as the protocol, the path is treated as the path on
+    the wopi host.
 
 selecttext type=<type> x=<x> y=<y>
 
@@ -310,6 +312,12 @@ pong rendercount=<num>
     sent in reply to a 'ping' message, where <num> is the total number
     of rendered tiles of the document.
 
+saveas: url=<url> name=<name>
+
+    <url> is a wopi URL of the newly created file, including the access token.
+    <name> is the resulting name (without path) that was created on the wopi
+    host. It can differ from what was requested in case the file already existed.
+
 status: type=<typeName> parts=<numberOfParts> current=<currentPartNumber> width=<width> height=<height> viewid=<viewId> [partNames]
 
     <typeName> is 'text, 'spreadsheet', 'presentation', 'drawing' or 'other. Others are numbers.
@@ -453,10 +461,11 @@ nextmessage: size=<upperlimit>
     one doesn't need to use a pre-allocated buffer when receiving
     WebSocket messages, this will go away.
 
-saveas: url=<url>
+saveas: url=<url> filename=<filename>
 
     <url> is a URL of the destination, encoded. Sent from the child to the
     parent after a saveAs() completed.
+    <filename> is the resulting jailed filename of the newly created file.
 
 client-<sessionId> <Payload Message>
 
commit 6fe44843954bb68f8b958559b1b0ff37b945cf80
Author: Jan Holesovsky <kendy at collabora.com>
Date:   Tue Oct 24 11:31:39 2017 +0200

    tdf#99744 SaveAs: Use X-WOPI-SuggestedTarget instead of X-WOPI-RelativeTarget.
    
    And the correct encoding - UTF-7 (huh).
    
    Change-Id: I6634fedb598c620128cc25a3e8fdc46e4096a756

diff --git a/test/UnitWOPISaveAs.cpp b/test/UnitWOPISaveAs.cpp
index 4bd3e9e0..82e89de0 100644
--- a/test/UnitWOPISaveAs.cpp
+++ b/test/UnitWOPISaveAs.cpp
@@ -33,7 +33,8 @@ public:
 
     void assertPutFileRelativeRequest(const Poco::Net::HTTPRequest& request) override
     {
-        CPPUNIT_ASSERT_EQUAL(std::string("/path/to/hello world.txt"), request.get("X-WOPI-RelativeTarget"));
+        // spec says UTF-7...
+        CPPUNIT_ASSERT_EQUAL(std::string("/jan/hole+AWE-ovsk+AP0-/hello world.txt"), request.get("X-WOPI-SuggestedTarget"));
 
         exitTest(TestResult::Ok);
     }
@@ -49,7 +50,7 @@ public:
                 initWebsocket("/wopi/files/0?access_token=anything");
 
                 helpers::sendTextFrame(*_ws->getLOOLWebSocket(), "load url=" + _wopiSrc, testName);
-                helpers::sendTextFrame(*_ws->getLOOLWebSocket(), "saveas url=wopi:///path/to/hello%20world.txt", testName);
+                helpers::sendTextFrame(*_ws->getLOOLWebSocket(), "saveas url=wopi:///jan/hole%C5%A1ovsk%C3%BD/hello%20world.txt", testName);
                 SocketPoll::wakeupWorld();
 
                 _phase = Phase::Polling;
diff --git a/wsd/Storage.cpp b/wsd/Storage.cpp
index 6d0aa2c1..af60c237 100644
--- a/wsd/Storage.cpp
+++ b/wsd/Storage.cpp
@@ -14,6 +14,7 @@
 #include <algorithm>
 #include <cassert>
 #include <fstream>
+#include <iconv.h>
 #include <string>
 
 #include <Poco/DateTime.h>
@@ -664,7 +665,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
     // TODO: Check if this URI has write permission (canWrite = true)
     const auto size = getFileSize(_jailedFilePath);
 
-    const bool isSaveAs = !saveAsPath.empty();
+    const bool isSaveAs = !saveAsPath.empty() && !saveAsFilename.empty();
 
     Poco::URI uriObject(_uri);
     uriObject.setPath(isSaveAs? uriObject.getPath(): uriObject.getPath() + "/contents");
@@ -701,7 +702,36 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
         {
             // save as
             request.set("X-WOPI-Override", "PUT_RELATIVE");
-            request.set("X-WOPI-RelativeTarget", saveAsFilename);
+
+            // the suggested target has to be in UTF-7; default to extension
+            // only when the conversion fails
+            std::string suggestedTarget = "." + Poco::Path(saveAsFilename).getExtension();
+
+            iconv_t cd = iconv_open("UTF-7", "UTF-8");
+            if (cd == (iconv_t) -1)
+                LOG_ERR("Failed to initialize iconv for UTF-7 conversion, using '" << suggestedTarget << "'.");
+            else
+            {
+                std::vector<char> input(saveAsFilename.begin(), saveAsFilename.end());
+                std::vector<char> buffer(8 * saveAsFilename.size());
+
+                char* in = &input[0];
+                size_t in_left = input.size();
+                char* out = &buffer[0];
+                size_t out_left = buffer.size();
+
+                if (iconv(cd, &in, &in_left, &out, &out_left) == (size_t) -1)
+                    LOG_ERR("Failed to convert '" << saveAsFilename << "' to UTF-7, using '" << suggestedTarget << "'.");
+                else
+                {
+                    // conversion succeeded
+                    suggestedTarget = std::string(&buffer[0], buffer.size() - out_left);
+                    LOG_TRC("Converted '" << saveAsFilename << "' to UTF-7 as '" << suggestedTarget << "'.");
+                }
+            }
+
+            request.set("X-WOPI-SuggestedTarget", suggestedTarget);
+
             request.set("X-WOPI-Size", std::to_string(size));
         }
 
commit 7ff432a37010031afd3da4012882b160aba51ec9
Author: Jan Holesovsky <kendy at collabora.com>
Date:   Fri Oct 20 18:12:05 2017 +0200

    tdf#99744 SaveAs: Reimplementation of the PutRelativeFile going through Kit.
    
    This is necessary so that changing of the file type works.
    
    Includes a unit test.
    
    Change-Id: Id01d44e555b6bac1002ff950de461fd330602f63

diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp
index c40a6882..9c506389 100644
--- a/kit/ChildSession.cpp
+++ b/kit/ChildSession.cpp
@@ -939,18 +939,37 @@ bool ChildSession::resetSelection(const char* /*buffer*/, int /*length*/, const
 
 bool ChildSession::saveAs(const char* /*buffer*/, int /*length*/, const std::vector<std::string>& tokens)
 {
-    std::string url, format, filterOptions;
+    std::string wopiFilename, url, format, filterOptions;
 
-    if (tokens.size() < 4 ||
+    if (tokens.size() <= 1 ||
         !getTokenString(tokens[1], "url", url))
     {
         sendTextFrame("error: cmd=saveas kind=syntax");
         return false;
     }
 
-    getTokenString(tokens[2], "format", format);
+    // if the url is a 'wopi:///something/blah.odt', then save to a temporary
+    Poco::URI wopiURL(url);
+    if (wopiURL.getScheme() == "wopi")
+    {
+        std::vector<std::string> pathSegments;
+        wopiURL.getPathSegments(pathSegments);
+
+        if (pathSegments.size() == 0)
+        {
+            sendTextFrame("error: cmd=saveas kind=syntax");
+            return false;
+        }
 
-    if (getTokenString(tokens[3], "options", filterOptions))
+        // TODO do we need a tempdir here?
+        url = std::string("file://") + JAILED_DOCUMENT_ROOT + pathSegments[pathSegments.size() - 1];
+        wopiFilename = wopiURL.getPath();
+    }
+
+    if (tokens.size() > 2)
+        getTokenString(tokens[2], "format", format);
+
+    if (tokens.size() > 3 && getTokenString(tokens[3], "options", filterOptions))
     {
         if (tokens.size() > 4)
         {
@@ -969,7 +988,11 @@ bool ChildSession::saveAs(const char* /*buffer*/, int /*length*/, const std::vec
                 filterOptions.size() == 0 ? nullptr : filterOptions.c_str());
     }
 
-    sendTextFrame("saveas: url=" + url);
+    std::string encodedURL, encodedWopiFilename;
+    Poco::URI::encode(url, "", encodedURL);
+    Poco::URI::encode(wopiFilename, "", encodedWopiFilename);
+
+    sendTextFrame("saveas: url=" + encodedURL + " wopifilename=" + encodedWopiFilename);
     std::string successStr = success ? "true" : "false";
     sendTextFrame("unocommandresult: {"
             "\"commandName\":\"saveas\","
diff --git a/loleaflet/reference.html b/loleaflet/reference.html
index 8f1ea583..9a1f39ae 100644
--- a/loleaflet/reference.html
+++ b/loleaflet/reference.html
@@ -2915,13 +2915,10 @@ WOPI host to editor
 	<tr>
 		<td><code><b>Action_SaveAs</b></code></td>
 		<td><code>
-		    <nobr>Name: <String></nobr>
-		    <nobr>Path: <String></nobr>
+		    <nobr>Filename: <String></nobr>
 		</code></td>
-		<td>Creates copy of the document with given Name.<br/>
-		<code>Name</code> is the requested name for the new file.<br/>
-		<code>Path</code> is the relative path in the WOPI host file system where the
-		user wants the new file to be saved.<br/>
+		<td>Creates copy of the document with given Filename.<br/>
+		<code>Filename</code> is the requested filename for the new file.<br/>
 		</td>
 	</tr>
 	<tr>
diff --git a/loleaflet/src/map/handler/Map.WOPI.js b/loleaflet/src/map/handler/Map.WOPI.js
index 26acea7b..7b2a44d4 100644
--- a/loleaflet/src/map/handler/Map.WOPI.js
+++ b/loleaflet/src/map/handler/Map.WOPI.js
@@ -223,11 +223,11 @@ L.Map.WOPI = L.Handler.extend({
 		else if (msg.MessageId === 'Action_SaveAs') {
 			/* TODO
 			if (msg.Values) {
-				if (msg.Values.name === null || msg.Values.name === undefined) {
-					msg.Values.name = '';
+				if (msg.Values.Filename === null || msg.Values.Filename === undefined) {
+					msg.Values.Filename = '';
 				}
 				this.showBusy(_('Creating copy...'), false);
-				map.saveAs(msg.Values.name, msg.Values.path);
+				map.saveAs(msg.Values.Filename);
 			}
 			*/
 		}
diff --git a/test/Makefile.am b/test/Makefile.am
index 7ee4bcf2..aa54e0cb 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -17,7 +17,7 @@ noinst_LTLIBRARIES = \
 	unit-storage.la unit-client.la \
 	unit-admin.la unit-tilecache.la \
 	unit-fuzz.la unit-oob.la unit-oauth.la \
-	unit-wopi.la
+	unit-wopi.la unit-wopi-saveas.la
 
 MAGIC_TO_FORCE_SHLIB_CREATION = -rpath /dummy
 AM_LDFLAGS = -pthread -module $(MAGIC_TO_FORCE_SHLIB_CREATION) $(ZLIB_LIBS)
@@ -80,6 +80,8 @@ unit_oauth_la_SOURCES = UnitOAuth.cpp
 unit_oauth_la_LIBADD = $(CPPUNIT_LIBS)
 unit_wopi_la_SOURCES = UnitWOPI.cpp
 unit_wopi_la_LIBADD = $(CPPUNIT_LIBS)
+unit_wopi_saveas_la_SOURCES = UnitWOPISaveAs.cpp
+unit_wopi_saveas_la_LIBADD = $(CPPUNIT_LIBS)
 
 if HAVE_LO_PATH
 SYSTEM_STAMP = @SYSTEMPLATE_PATH@/system_stamp
@@ -93,7 +95,7 @@ check-local:
 	./run_unit.sh --log-file test.log --trs-file test.trs
 # FIXME 2: unit-oob.la fails with symbol undefined:
 # UnitWSD::testHandleRequest(UnitWSD::TestRequest, UnitHTTPServerRequest&, UnitHTTPServerResponse&) ,
-TESTS = unit-prefork.la unit-tilecache.la unit-timeout.la unit-oauth.la unit-wopi.la
+TESTS = unit-prefork.la unit-tilecache.la unit-timeout.la unit-oauth.la unit-wopi.la unit-wopi-saveas.la
 # TESTS = unit-client.la
 # TESTS += unit-admin.la
 # TESTS += unit-storage.la
diff --git a/test/UnitOAuth.cpp b/test/UnitOAuth.cpp
index f88a4b55..5b2b09c2 100644
--- a/test/UnitOAuth.cpp
+++ b/test/UnitOAuth.cpp
@@ -87,11 +87,6 @@ public:
             exitTest(TestResult::Ok);
     }
 
-    void assertPutFileRequest(const Poco::Net::HTTPRequest& /*request*/) override
-    {
-        // nothing to assert
-    }
-
     void invokeTest() override
     {
         constexpr char testName[] = "UnitOAuth";
diff --git a/test/UnitWOPI.cpp b/test/UnitWOPI.cpp
index a8808d20..338fb145 100644
--- a/test/UnitWOPI.cpp
+++ b/test/UnitWOPI.cpp
@@ -50,16 +50,6 @@ public:
         return _savingPhase == SavingPhase::Modified;
     }
 
-    void assertCheckFileInfoRequest(const Poco::Net::HTTPRequest& /*request*/) override
-    {
-        // nothing to assert in CheckFileInfo
-    }
-
-    void assertGetFileRequest(const Poco::Net::HTTPRequest& /*request*/) override
-    {
-        // nothing to assert in GetFile
-    }
-
     void assertPutFileRequest(const Poco::Net::HTTPRequest& request) override
     {
         if (_savingPhase == SavingPhase::Unmodified)
diff --git a/test/UnitWOPISaveAs.cpp b/test/UnitWOPISaveAs.cpp
new file mode 100644
index 00000000..4bd3e9e0
--- /dev/null
+++ b/test/UnitWOPISaveAs.cpp
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4; fill-column: 100 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include "config.h"
+
+#include "WopiTestServer.hpp"
+#include "Log.hpp"
+#include "Unit.hpp"
+#include "UnitHTTP.hpp"
+#include "helpers.hpp"
+#include <Poco/Net/HTTPRequest.h>
+#include <Poco/Util/LayeredConfiguration.h>
+
+class UnitWOPISaveAs : public WopiTestServer
+{
+    enum class Phase
+    {
+        LoadAndSaveAs,
+        Polling
+    } _phase;
+
+public:
+    UnitWOPISaveAs() :
+        _phase(Phase::LoadAndSaveAs)
+    {
+    }
+
+    void assertPutFileRelativeRequest(const Poco::Net::HTTPRequest& request) override
+    {
+        CPPUNIT_ASSERT_EQUAL(std::string("/path/to/hello world.txt"), request.get("X-WOPI-RelativeTarget"));
+
+        exitTest(TestResult::Ok);
+    }
+
+    void invokeTest() override
+    {
+        constexpr char testName[] = "UnitWOPISaveAs";
+
+        switch (_phase)
+        {
+            case Phase::LoadAndSaveAs:
+            {
+                initWebsocket("/wopi/files/0?access_token=anything");
+
+                helpers::sendTextFrame(*_ws->getLOOLWebSocket(), "load url=" + _wopiSrc, testName);
+                helpers::sendTextFrame(*_ws->getLOOLWebSocket(), "saveas url=wopi:///path/to/hello%20world.txt", testName);
+                SocketPoll::wakeupWorld();
+
+                _phase = Phase::Polling;
+                break;
+            }
+            case Phase::Polling:
+            {
+                // just wait for the results
+                break;
+            }
+        }
+    }
+};
+
+UnitBase *unit_create_wsd(void)
+{
+    return new UnitWOPISaveAs();
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
diff --git a/test/WopiTestServer.hpp b/test/WopiTestServer.hpp
index a3851b5d..6324114d 100644
--- a/test/WopiTestServer.hpp
+++ b/test/WopiTestServer.hpp
@@ -28,8 +28,13 @@ protected:
     /// Websocket to communicate.
     std::unique_ptr<UnitWebSocket> _ws;
 
+    /// Content of the file.
+    std::string _fileContent;
+
 public:
-    WopiTestServer() : UnitWSD()
+    WopiTestServer(std::string fileContent = "Hello, world")
+        : UnitWSD()
+        , _fileContent(fileContent)
     {
     }
 
@@ -47,24 +52,32 @@ public:
         assert(_ws.get());
     }
 
-    virtual void assertCheckFileInfoRequest(const Poco::Net::HTTPRequest& request) = 0;
+    virtual void assertCheckFileInfoRequest(const Poco::Net::HTTPRequest& /*request*/)
+    {
+    }
 
-    virtual void assertGetFileRequest(const Poco::Net::HTTPRequest& request) = 0;
+    virtual void assertGetFileRequest(const Poco::Net::HTTPRequest& /*request*/)
+    {
+    }
+
+    virtual void assertPutFileRequest(const Poco::Net::HTTPRequest& /*request*/)
+    {
+    }
 
-    virtual void assertPutFileRequest(const Poco::Net::HTTPRequest& request) = 0;
+    virtual void assertPutFileRelativeRequest(const Poco::Net::HTTPRequest& /*request*/)
+    {
+    }
 
 protected:
     /// Here we act as a WOPI server, so that we have a server that responds to
     /// the wopi requests without additional expensive setup.
     virtual bool handleHttpRequest(const Poco::Net::HTTPRequest& request, std::shared_ptr<StreamSocket>& socket) override
     {
-        static const std::string hello("Hello, world");
-
         Poco::URI uriReq(request.getURI());
         LOG_INF("Fake wopi host request: " << uriReq.toString());
 
         // CheckFileInfo
-        if (uriReq.getPath() == "/wopi/files/0" || uriReq.getPath() == "/wopi/files/1")
+        if (request.getMethod() == "GET" && (uriReq.getPath() == "/wopi/files/0" || uriReq.getPath() == "/wopi/files/1"))
         {
             LOG_INF("Fake wopi host request, handling CheckFileInfo: " << uriReq.getPath());
 
@@ -73,7 +86,7 @@ protected:
             Poco::LocalDateTime now;
             Poco::JSON::Object::Ptr fileInfo = new Poco::JSON::Object();
             fileInfo->set("BaseFileName", "hello.txt");
-            fileInfo->set("Size", hello.size());
+            fileInfo->set("Size", _fileContent.size());
             fileInfo->set("Version", "1.0");
             fileInfo->set("OwnerId", "test");
             fileInfo->set("UserId", "test");
@@ -115,10 +128,38 @@ protected:
             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: " << hello.size() << "\r\n"
+                << "Content-Length: " << _fileContent.size() << "\r\n"
                 << "Content-Type: " << mimeType << "\r\n"
                 << "\r\n"
-                << hello;
+                << _fileContent;
+
+            socket->send(oss.str());
+            socket->shutdown();
+
+            return true;
+        }
+        else if (request.getMethod() == "POST" && (uriReq.getPath() == "/wopi/files/0" || uriReq.getPath() == "/wopi/files/1"))
+        {
+            LOG_INF("Fake wopi host request, handling PutFileRelative: " << uriReq.getPath());
+
+            CPPUNIT_ASSERT_EQUAL(std::string("PUT_RELATIVE"), request.get("X-WOPI-Override"));
+
+            assertPutFileRelativeRequest(request);
+
+            Poco::URI wopiURL(helpers::getTestServerURI() + "/wopi/files/1");
+            std::string url;
+            Poco::URI::encode(wopiURL.toString(), ":/?", url);
+
+            std::string content = "{ \"Name\":\"hello world.txt\", \"Url\":\"" + url + "\" }";
+
+            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: " << content.size() << "\r\n"
+                << "Content-Type: application/json\r\n"
+                << "\r\n"
+                << content;
 
             socket->send(oss.str());
             socket->shutdown();
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 89d46b92..63fa9eb9 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -654,38 +654,64 @@ bool ClientSession::handleKitToClientMessage(const char* buffer, const int lengt
         int curPart;
         return getTokenInteger(tokens[1], "part", curPart);
     }
-    else if (tokens.size() == 2 && tokens[0] == "saveas:")
+    else if (tokens.size() == 3 && tokens[0] == "saveas:")
     {
-        std::string url;
-        if (!getTokenString(tokens[1], "url", url))
+        std::string encodedURL;
+        if (!getTokenString(tokens[1], "url", encodedURL))
         {
             LOG_ERR("Bad syntax for: " << firstLine);
             return false;
         }
 
+        std::string encodedWopiFilename;
+        if (!getTokenString(tokens[2], "wopifilename", encodedWopiFilename))
+        {
+            LOG_ERR("Bad syntax for: " << firstLine);
+            return false;
+        }
+
+        std::string url, wopiFilename;
+        Poco::URI::decode(encodedURL, url);
+        Poco::URI::decode(encodedWopiFilename, wopiFilename);
+
         // Save-as completed, inform the ClientSession.
-        const std::string filePrefix("file:///");
-        if (url.find(filePrefix) == 0)
+        Poco::URI resultURL(url);
+        if (resultURL.getScheme() == "file")
         {
+            std::string relative(resultURL.getPath());
+            if (relative.size() > 0 && relative[0] == '/')
+                relative = relative.substr(1);
+
             // Rewrite file:// URLs, as they are visible to the outside world.
-            const Path path(docBroker->getJailRoot(), url.substr(filePrefix.length()));
+            const Path path(docBroker->getJailRoot(), relative);
             if (Poco::File(path).exists())
             {
-                url = filePrefix + path.toString().substr(1);
+                resultURL.setPath(path.toString());
             }
             else
             {
                 // Blank for failure.
-                LOG_DBG("SaveAs produced no output, producing blank url.");
-                url.clear();
+                LOG_DBG("SaveAs produced no output in '" << path.toString() << "', producing blank url.");
+                resultURL.clear();
             }
         }
 
-        if (_saveAsSocket)
+        LOG_TRC("Save-as URL: " << resultURL.toString());
+
+        if (!_saveAsSocket)
         {
-            Poco::URI resultURL(url);
-            LOG_TRC("Save-as URL: " << resultURL.toString());
+            // Normal SaveAs - save to Storage and log result.
+            if (resultURL.getScheme() == "file" && !resultURL.getPath().empty())
+            {
+                docBroker->saveAsToStorage(getId(), resultURL.getPath(), wopiFilename);
+            }
 
+            if (!isCloseFrame())
+                forwardToClient(payload);
+        }
+        else
+        {
+            // using the convert-to REST API
             // TODO: Send back error when there is no output.
             if (!resultURL.getPath().empty())
             {
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index cd3da9fb..1bac7182 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -621,11 +621,21 @@ bool DocumentBroker::saveToStorage(const std::string& sessionId,
     return res;
 }
 
+bool DocumentBroker::saveAsToStorage(const std::string& sessionId, const std::string& saveAsPath, const std::string& saveAsFilename)
+{
+    assertCorrectThread();
+
+    return saveToStorageInternal(sessionId, true, "", saveAsPath, saveAsFilename);
+}
+
 bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
-                                           bool success, const std::string& result)
+                                           bool success, const std::string& result,
+                                           const std::string& saveAsPath, const std::string& saveAsFilename)
 {
     assertCorrectThread();
 
+    const bool isSaveAs = !saveAsPath.empty();
+
     // If save requested, but core didn't save because document was unmodified
     // notify the waiting thread, if any.
     LOG_TRC("Saving to storage docKey [" << _docKey << "] for session [" << sessionId <<
@@ -646,12 +656,12 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
     }
 
     const Authorization auth = it->second->getAuthorization();
-    const auto uri = it->second->getPublicUri().toString();
+    const auto uri = isSaveAs? saveAsPath: it->second->getPublicUri().toString();
 
     // If we aren't destroying the last editable session just yet,
     // and the file timestamp hasn't changed, skip saving.
     const auto newFileModifiedTime = Poco::File(_storage->getRootFilePath()).getLastModified();
-    if (!_lastEditableSession && newFileModifiedTime == _lastFileModifiedTime)
+    if (!isSaveAs && !_lastEditableSession && newFileModifiedTime == _lastFileModifiedTime)
     {
         // Nothing to do.
         LOG_DBG("Skipping unnecessary saving to URI [" << uri << "] with docKey [" << _docKey <<
@@ -664,24 +674,32 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
     LOG_DBG("Persisting [" << _docKey << "] after saving to URI [" << uri << "].");
 
     assert(_storage && _tileCache);
-    StorageBase::SaveResult storageSaveResult = _storage->saveLocalFileToStorage(auth);
+    StorageBase::SaveResult storageSaveResult = _storage->saveLocalFileToStorage(auth, saveAsPath, saveAsFilename);
     if (storageSaveResult == StorageBase::SaveResult::OK)
     {
-        setModified(false);
-        _lastFileModifiedTime = newFileModifiedTime;
-        _tileCache->saveLastModified(_lastFileModifiedTime);
-        _lastSaveTime = std::chrono::steady_clock::now();
-        _poll->wakeup();
+        if (!isSaveAs)
+        {
+            setModified(false);
+            _lastFileModifiedTime = newFileModifiedTime;
+            _tileCache->saveLastModified(_lastFileModifiedTime);
+            _lastSaveTime = std::chrono::steady_clock::now();
+            _poll->wakeup();
 
-        // So set _documentLastModifiedTime then
-        _documentLastModifiedTime = _storage->getFileInfo()._modifiedTime;
+            // So set _documentLastModifiedTime then
+            _documentLastModifiedTime = _storage->getFileInfo()._modifiedTime;
 
-        // After a successful save, we are sure that document in the storage is same as ours
-        _documentChangedInStorage = false;
+            // After a successful save, we are sure that document in the storage is same as ours
+            _documentChangedInStorage = false;
 
-        Log::debug() << "Saved docKey [" << _docKey << "] to URI [" << uri
-                     << "] and updated tile cache. Document modified timestamp: "
-                     << _documentLastModifiedTime << Log::end;
+            Log::debug() << "Saved docKey [" << _docKey << "] to URI [" << uri
+                         << "] and updated tile cache. Document modified timestamp: "
+                         << _documentLastModifiedTime << Log::end;
+        }
+        else
+        {
+            Log::debug() << "Saved As docKey [" << _docKey << "] to URI [" << uri
+                         << "] successfully.";
+        }
         return true;
     }
     else if (storageSaveResult == StorageBase::SaveResult::DISKFULL)
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index a7d27a43..0387de20 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -241,6 +241,11 @@ public:
 
     /// Save the document to Storage if it needs persisting.
     bool saveToStorage(const std::string& sesionId, bool success, const std::string& result = "", bool force = false);
+
+    /// Save As the document to Storage.
+    /// @param saveAsPath Absolute path to the jailed file.
+    bool saveAsToStorage(const std::string& sesionId, const std::string& saveAsPath, const std::string& saveAsFilename);
+
     bool isModified() const { return _isModified; }
     void setModified(const bool value);
     /// Save the document if the document is modified.
@@ -359,7 +364,7 @@ private:
     void terminateChild(const std::string& closeReason);
 
     /// Saves the doc to the storage.
-    bool saveToStorageInternal(const std::string& sesionId, bool success, const std::string& result = "");
+    bool saveToStorageInternal(const std::string& sesionId, bool success, const std::string& result = "", const std::string& saveAsPath = std::string(), const std::string& saveAsFilename = std::string());
 
     /// Loads a new session and adds to the sessions container.
     size_t addSessionInternal(const std::shared_ptr<ClientSession>& session);
diff --git a/wsd/Storage.cpp b/wsd/Storage.cpp
index 433885f6..6d0aa2c1 100644
--- a/wsd/Storage.cpp
+++ b/wsd/Storage.cpp
@@ -297,7 +297,7 @@ std::string LocalStorage::loadStorageFileToLocal(const Authorization& /*auth*/)
 #endif
 }
 
-StorageBase::SaveResult LocalStorage::saveLocalFileToStorage(const Authorization& /*auth*/)
+StorageBase::SaveResult LocalStorage::saveLocalFileToStorage(const Authorization& /*auth*/, const std::string& /*saveAsPath*/, const std::string& /*saveAsFilename*/)
 {
     try
     {
@@ -659,13 +659,15 @@ std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth)
     return "";
 }
 
-StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization& auth)
+StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename)
 {
     // TODO: Check if this URI has write permission (canWrite = true)
     const auto size = getFileSize(_jailedFilePath);
 
+    const bool isSaveAs = !saveAsPath.empty();
+
     Poco::URI uriObject(_uri);
-    uriObject.setPath(uriObject.getPath() + "/contents");
+    uriObject.setPath(isSaveAs? uriObject.getPath(): uriObject.getPath() + "/contents");
     auth.authorizeURI(uriObject);
 
     LOG_INF("Uploading URI via WOPI [" << uriObject.toString() << "] from [" << _jailedFilePath + "].");
@@ -677,30 +679,48 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
         std::unique_ptr<Poco::Net::HTTPClientSession> psession(getHTTPClientSession(uriObject));
 
         Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_POST, uriObject.getPathAndQuery(), Poco::Net::HTTPMessage::HTTP_1_1);
-        request.set("X-WOPI-Override", "PUT");
+        request.set("User-Agent", WOPI_AGENT_STRING);
         auth.authorizeRequest(request);
-        if (!_forceSave)
+
+        if (!isSaveAs)
+        {
+            // normal save
+            request.set("X-WOPI-Override", "PUT");
+            request.set("X-LOOL-WOPI-IsModifiedByUser", _isUserModified? "true": "false");
+            request.set("X-LOOL-WOPI-IsAutosave", _isAutosave? "true": "false");
+
+            if (!_forceSave)
+            {
+                // Request WOPI host to not overwrite if timestamps mismatch
+                request.set("X-LOOL-WOPI-Timestamp",
+                            Poco::DateTimeFormatter::format(Poco::DateTime(_fileInfo._modifiedTime),
+                                                            Poco::DateTimeFormat::ISO8601_FRAC_FORMAT));
+            }
+        }
+        else
         {
-            // Request WOPI host to not overwrite if timestamps mismatch
-            request.set("X-LOOL-WOPI-Timestamp",
-                        Poco::DateTimeFormatter::format(Poco::DateTime(_fileInfo._modifiedTime),
-                                                        Poco::DateTimeFormat::ISO8601_FRAC_FORMAT));
+            // save as
+            request.set("X-WOPI-Override", "PUT_RELATIVE");
+            request.set("X-WOPI-RelativeTarget", saveAsFilename);
+            request.set("X-WOPI-Size", std::to_string(size));
         }
-        request.set("X-LOOL-WOPI-IsModifiedByUser", _isUserModified? "true": "false");
-        request.set("X-LOOL-WOPI-IsAutosave", _isAutosave? "true": "false");
 
         request.setContentType("application/octet-stream");
         request.setContentLength(size);
         addStorageDebugCookie(request);
         std::ostream& os = psession->sendRequest(request);
-        std::ifstream ifs(_jailedFilePath);
+
+        const std::string filePath(isSaveAs? saveAsPath: _jailedFilePath);
+        std::ifstream ifs(filePath);
         Poco::StreamCopier::copyStream(ifs, os);
 
         Poco::Net::HTTPResponse response;
         std::istream& rs = psession->receiveResponse(response);
         Poco::StreamCopier::copyStream(rs, oss);
-        LOG_INF("WOPI::PutFile response: " << oss.str());
-        LOG_INF("WOPI::PutFile uploaded " << size << " bytes from [" << _jailedFilePath <<
+
+        std::string wopiLog(isSaveAs? "WOPI::PutFileRelative": "WOPI::PutFile");
+        LOG_INF(wopiLog << " response: " << oss.str());
+        LOG_INF(wopiLog << " uploaded " << size << " bytes from [" << filePath <<
                 "] -> [" << uriObject.toString() << "]: " <<
                 response.getStatus() << " " << response.getReason());
 
@@ -711,7 +731,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
             if (parseJSON(oss.str(), object))
             {
                 const std::string lastModifiedTime = getJSONValue<std::string>(object, "LastModifiedTime");
-                LOG_TRC("WOPI::PutFile returns LastModifiedTime [" << lastModifiedTime << "].");
+                LOG_TRC(wopiLog << " returns LastModifiedTime [" << lastModifiedTime << "].");
                 _fileInfo._modifiedTime = iso8601ToTimestamp(lastModifiedTime);
 
                 // Reset the force save flag now, if any, since we are done saving
@@ -720,7 +740,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
             }
             else
             {
-                LOG_WRN("Invalid/Missing JSON found in WOPI::PutFile response");
+                LOG_WRN("Invalid/Missing JSON found in " << wopiLog << " response");
             }
         }
         else if (response.getStatus() == Poco::Net::HTTPResponse::HTTP_REQUESTENTITYTOOLARGE)
@@ -745,7 +765,7 @@ StorageBase::SaveResult WopiStorage::saveLocalFileToStorage(const Authorization&
             }
             else
             {
-                LOG_WRN("Invalid/missing JSON in WOPI::PutFile response");
+                LOG_WRN("Invalid/missing JSON in " << wopiLog << " response");
             }
         }
     }
@@ -766,7 +786,7 @@ std::string WebDAVStorage::loadStorageFileToLocal(const Authorization& /*auth*/)
     return _uri.toString();
 }
 
-StorageBase::SaveResult WebDAVStorage::saveLocalFileToStorage(const Authorization& /*auth*/)
+StorageBase::SaveResult WebDAVStorage::saveLocalFileToStorage(const Authorization& /*auth*/, const std::string& /*saveAsPath*/, const std::string& /*saveAsFilename*/)
 {
     // TODO: implement webdav PUT.
     return StorageBase::SaveResult::OK;
diff --git a/wsd/Storage.hpp b/wsd/Storage.hpp
index ef0e73ad..60c92191 100644
--- a/wsd/Storage.hpp
+++ b/wsd/Storage.hpp
@@ -114,7 +114,8 @@ public:
     virtual std::string loadStorageFileToLocal(const Authorization& auth) = 0;
 
     /// Writes the contents of the file back to the source.
-    virtual SaveResult saveLocalFileToStorage(const Authorization& auth) = 0;
+    /// @param savedFile When the operation was saveAs, this is the path to the file that was saved.
+    virtual SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename) = 0;
 
     static size_t getFileSize(const std::string& filename);
 
@@ -186,7 +187,7 @@ public:
 
     std::string loadStorageFileToLocal(const Authorization& auth) override;
 
-    SaveResult saveLocalFileToStorage(const Authorization& auth) override;
+    SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename) override;
 
 private:
     /// True if the jailed file is not linked but copied.
@@ -289,7 +290,7 @@ public:
     /// uri format: http://server/<...>/wopi*/files/<id>/content
     std::string loadStorageFileToLocal(const Authorization& auth) override;
 
-    SaveResult saveLocalFileToStorage(const Authorization& auth) override;
+    SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename) override;
 
     /// Total time taken for making WOPI calls during load
     std::chrono::duration<double> getWopiLoadDuration() const { return _wopiLoadDuration; }
@@ -319,7 +320,7 @@ public:
 
     std::string loadStorageFileToLocal(const Authorization& auth) override;
 
-    SaveResult saveLocalFileToStorage(const Authorization& auth) override;
+    SaveResult saveLocalFileToStorage(const Authorization& auth, const std::string& saveAsPath, const std::string& saveAsFilename) override;
 
 private:
     std::unique_ptr<AuthBase> _authAgent;
commit f4198526cad3861582f6a56a82c3a91e696ab391
Author: Jan Holesovsky <kendy at collabora.com>
Date:   Wed Oct 25 22:46:00 2017 +0200

    tdf#99744 SaveAs: Reverts parts of the previous Save As work.
    
    It is necessary to go through the Kit when performing the Save As, so that we
    can change the file type, sync with loading the file, etc.

diff --git a/loleaflet/src/control/Toolbar.js b/loleaflet/src/control/Toolbar.js
index 274e9190..a2d70d79 100644
--- a/loleaflet/src/control/Toolbar.js
+++ b/loleaflet/src/control/Toolbar.js
@@ -84,23 +84,20 @@ L.Map.include({
 		this.downloadAs('print.pdf', 'pdf', null, 'print');
 	},
 
-	saveAs: function (newName, path, format, options) {
+	saveAs: function (url, format, options) {
 		if (format === undefined || format === null) {
 			format = '';
 		}
 		if (options === undefined || options === null) {
 			options = '';
 		}
-		if (path === undefined || path === null) {
-			path = '';
-		}
 
+		this.showBusy(_('Saving...'), false);
 		// TakeOwnership: we are performing a 'real' save-as, the document
 		// is just getting a new place, ie. it will get the
 		// '.uno:ModifiedStatus' upon completion.
 		this._socket.sendMessage('saveas ' +
-			'fileName=' + newName + ' ' +
-			'path=' + path + ' ' +
+			'url=' + url + ' ' +
 			'format=' + format + ' ' +
 			'options=TakeOwnership,' + options);
 	},
diff --git a/loleaflet/src/map/handler/Map.WOPI.js b/loleaflet/src/map/handler/Map.WOPI.js
index f1bd0701..26acea7b 100644
--- a/loleaflet/src/map/handler/Map.WOPI.js
+++ b/loleaflet/src/map/handler/Map.WOPI.js
@@ -221,6 +221,7 @@ L.Map.WOPI = L.Handler.extend({
 			this._postMessage({msgId: 'Get_Export_Formats_Resp', args: exportFormatsResp});
 		}
 		else if (msg.MessageId === 'Action_SaveAs') {
+			/* TODO
 			if (msg.Values) {
 				if (msg.Values.name === null || msg.Values.name === undefined) {
 					msg.Values.name = '';
@@ -228,6 +229,7 @@ L.Map.WOPI = L.Handler.extend({
 				this.showBusy(_('Creating copy...'), false);
 				map.saveAs(msg.Values.name, msg.Values.path);
 			}
+			*/
 		}
 	},
 
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index df6df09c..89d46b92 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -233,17 +233,6 @@ bool ClientSession::_handleInput(const char *buffer, int length)
     {
         return sendCombinedTiles(buffer, length, tokens, docBroker);
     }
-    else if (tokens[0] == "saveas")
-    {
-        std::string newFileName, path;
-        if (tokens.size() > 1)
-            getTokenString(tokens[1], "fileName", newFileName);
-
-        if (tokens.size() > 2)
-            getTokenString(tokens[2], "path", path);
-
-        docBroker->saveFileAs(getId(), newFileName, path);
-    }
     else if (tokens[0] == "save")
     {
         int dontTerminateEdit = 1;
diff --git a/wsd/DocumentBroker.cpp b/wsd/DocumentBroker.cpp
index 76255287..cd3da9fb 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -724,29 +724,6 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
     return false;
 }
 
-void DocumentBroker::saveFileAs(const std::string& sessionId, const std::string& newFileName, const std::string& path)
-{
-    const auto it = _sessions.find(sessionId);
-    if(it == _sessions.end())
-    {
-        return;
-    }
-
-    WopiStorage* wopiStorage = dynamic_cast<WopiStorage*>(_storage.get());
-    if (wopiStorage != nullptr)
-    {
-        const std::string newUrl = wopiStorage->createCopyFile(it->second->getAuthorization(), newFileName, path);
-        if (!newUrl.empty())
-        {
-            it->second->sendTextFrame("saveas: " + newUrl + " " + newFileName);
-        }
-        else
-        {
-            it->second->sendTextFrame("error: cmd=saveas kind=saveasfailed");
-        }
-    }
-}
-
 void DocumentBroker::setLoaded()
 {
     if (!_isLoaded)
diff --git a/wsd/DocumentBroker.hpp b/wsd/DocumentBroker.hpp
index 75e29794..a7d27a43 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -343,9 +343,6 @@ public:
     /// Sends the .uno:Save command to LoKit.
     bool sendUnoSave(const std::string& sessionId, bool dontTerminateEdit = true, bool dontSaveIfUnmodified = true, bool isAutosave = false);
 
-    /// Create copy of the file with a different name
-    void saveFileAs(const std::string& sessionId, const std::string& newFileName, const std::string& path);
-
     /// Sends a message to all sessions
     void broadcastMessage(const std::string& message);
 
diff --git a/wsd/Storage.cpp b/wsd/Storage.cpp
index 7612fa98..433885f6 100644
--- a/wsd/Storage.cpp
+++ b/wsd/Storage.cpp
@@ -590,86 +590,6 @@ std::unique_ptr<WopiStorage::WOPIFileInfo> WopiStorage::getWOPIFileInfo(const Au
     return std::unique_ptr<WopiStorage::WOPIFileInfo>(new WOPIFileInfo({userId, userName, userExtraInfo, watermarkText, canWrite, postMessageOrigin, hidePrintOption, hideSaveOption, hideExportOption, enableOwnerTermination, disablePrint, disableExport, disableCopy, disableInactiveMessages, userCanNotWriteRelative, callDuration}));
 }
 
-/// PutRelativeFile - uri format: http://server/<...>/wopi*/files/<id>/
-std::string WopiStorage::createCopyFile(const Authorization& auth, const std::string& newFileName, const std::string& path)
-{
-    const auto size = getFileSize(_jailedFilePath);
-    std::ostringstream oss;
-    Poco::URI uriObject(_uri);
-    auth.authorizeURI(uriObject);
-
-    LOG_DBG("Wopi PutRelativeFile(save as) request for : " << uriObject.toString());
-
-    try
-    {
-        std::unique_ptr<Poco::Net::HTTPClientSession> psession(getHTTPClientSession(uriObject));
-
-        Poco::Net::HTTPRequest request(Poco::Net::HTTPRequest::HTTP_POST, uriObject.getPathAndQuery(), Poco::Net::HTTPMessage::HTTP_1_1);
-        request.set("User-Agent", WOPI_AGENT_STRING);
-        auth.authorizeRequest(request);
-        request.set("X-WOPI-Override", "PUT_RELATIVE");
-        request.set("X-WOPI-RelativeTarget", newFileName + "." + getFileExtension());
-        request.set("X-WOPI-Size", std::to_string(size));
-        /// custom header
-        request.set("X-WOPI-TargetPath", path);
-        request.setContentType("application/octet-stream");
-        request.setContentLength(size);
-
-        addStorageDebugCookie(request);
-        std::ostream& os = psession->sendRequest(request);
-        std::ifstream ifs(_jailedFilePath);
-        Poco::StreamCopier::copyStream(ifs, os);
-
-        Poco::Net::HTTPResponse response;
-        std::istream& rs = psession->receiveResponse(response);
-        Poco::StreamCopier::copyStream(rs, oss);
-        LOG_INF("WOPI::createCopyFile response: " << oss.str());
-        LOG_INF("WOPI::createCopyFile tried to create a copy of file at [" << uriObject.toString()
-                << "] having a size of " << size << " bytes and suggested name is " << newFileName + "." + getFileExtension() << ". Response recieved "
-                << response.getStatus() << " " << response.getReason());
-
-        auto logger = Log::trace();
-        if (logger.enabled())
-        {
-            logger << "WOPI::createCopyFile header for URI [" << uriObject.toString() << "]:\n";
-            for (const auto& pair : response)
-            {
-                logger << '\t' << pair.first << ": " << pair.second << " / ";
-            }
-
-            LOG_END(logger);
-        }
-
-        if (response.getStatus() != Poco::Net::HTTPResponse::HTTP_OK)
-        {
-            LOG_ERR("WOPI::createCopyFile failed with " << response.getStatus() << ' ' << response.getReason());
-            throw StorageConnectionException("WOPI::createCopyFile failed");
-        }
-    }
-    catch(const Poco::Exception& pexc)
-    {
-        LOG_ERR("createCopyFile cannot create a copy of file with WOPI storage uri [" << uriObject.toString() << "]. Error: " << pexc.displayText() <<
-                (pexc.nested() ? " (" + pexc.nested()->displayText() + ")" : ""));
-        return "";
-    }
-
-    std::string filename;
-    std::string url;
-    std::string hostEditUrl;
-    std::string hostViewUrl;
-
-    LOG_DBG("WOPI::createCopyFile returned: " << oss.str() );
-    Poco::JSON::Object::Ptr object;
-    if (parseJSON(oss.str(), object))
-    {
-        getWOPIValue(object, "Name", filename);
-        getWOPIValue(object, "Url", url);
-        getWOPIValue(object, "HostViewUrl", hostViewUrl);
-        getWOPIValue(object, "HostEditUrl", hostEditUrl);
-    }
-    return hostEditUrl;
-}
-
 /// uri format: http://server/<...>/wopi*/files/<id>/content
 std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth)
 {
diff --git a/wsd/Storage.hpp b/wsd/Storage.hpp
index e94108e0..ef0e73ad 100644
--- a/wsd/Storage.hpp
+++ b/wsd/Storage.hpp
@@ -286,9 +286,6 @@ public:
     /// which can then be obtained using getFileInfo()
     std::unique_ptr<WOPIFileInfo> getWOPIFileInfo(const Authorization& auth);
 
-    /// returns
-    std::string createCopyFile(const Authorization& auth, const std::string& newFileName, const std::string& path);
-
     /// uri format: http://server/<...>/wopi*/files/<id>/content
     std::string loadStorageFileToLocal(const Authorization& auth) override;
 
diff --git a/wsd/protocol.txt b/wsd/protocol.txt
index 69224396..25805b30 100644
--- a/wsd/protocol.txt
+++ b/wsd/protocol.txt
@@ -108,12 +108,10 @@ requestloksession
 
 resetselection
 
-saveas newName=<suggested name for the new file> path=<path as per WOPI host filesystem>
-    format=<format> options=<options>
+saveas url=<url> format=<format> options=<options>
 
-    Creates a copy of the current file with 'fileName' as a suggestion for the
-    name, at the given 'path' in the WOPI host fileSystem. The format and option values
-    are not being used currently, but maybe used for future extension.
+    <url> is a URL, encoded. <format> is also URL-encoded, i.e. spaces as %20 and it can be empty
+    options are the whole rest of the line, not URL-encoded, and can be empty
 
 selecttext type=<type> x=<x> y=<y>
 
@@ -312,14 +310,6 @@ pong rendercount=<num>
     sent in reply to a 'ping' message, where <num> is the total number
     of rendered tiles of the document.
 
-saveas: newUrl=<url> newFileName=<filename>
-
-    sent only if the saveas operation was succesful and the WOPI host sent the
-    HostEditUrl('newUrl') for the new created file. The 'newFileName' represents the
-    name of the newly created file, this is being sent because it is not necessary that
-    the new File will be saved with the requested file name.
-    if the operation fails a 'error' message with cmd='saveas' is sent instead.
-
 status: type=<typeName> parts=<numberOfParts> current=<currentPartNumber> width=<width> height=<height> viewid=<viewId> [partNames]
 
     <typeName> is 'text, 'spreadsheet', 'presentation', 'drawing' or 'other. Others are numbers.


More information about the Libreoffice-commits mailing list