[Libreoffice-commits] online.git: Branch 'distro/collabora/collabora-online-2-1' - kit/ChildSession.cpp loleaflet/reference.html loleaflet/src 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/Storage.cpp wsd/Storage.hpp

Jan Holesovsky kendy at collabora.com
Thu Oct 26 07:16:17 UTC 2017


 kit/ChildSession.cpp                  |   33 +++++++++++++--
 loleaflet/reference.html              |    9 +---
 loleaflet/src/map/handler/Map.WOPI.js |    6 +-
 test/Makefile.am                      |    6 +-
 test/UnitOAuth.cpp                    |    5 --
 test/UnitWOPI.cpp                     |   10 ----
 test/UnitWOPISaveAs.cpp               |   72 ++++++++++++++++++++++++++++++++++
 test/WopiTestServer.hpp               |   61 ++++++++++++++++++++++++----
 wsd/ClientSession.cpp                 |   50 +++++++++++++++++------
 wsd/DocumentBroker.cpp                |   50 ++++++++++++++++-------
 wsd/DocumentBroker.hpp                |    7 ++-
 wsd/Storage.cpp                       |   56 +++++++++++++++++---------
 wsd/Storage.hpp                       |    9 ++--
 13 files changed, 282 insertions(+), 92 deletions(-)

New commits:
commit 977c84c798b51f7acf18337a34471bd26cadf55f
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
    Reviewed-on: https://gerrit.libreoffice.org/43617
    Reviewed-by: pranavk <pranavk at collabora.co.uk>
    Tested-by: pranavk <pranavk at collabora.co.uk>

diff --git a/kit/ChildSession.cpp b/kit/ChildSession.cpp
index 633f6b42..ad43c8f0 100644
--- a/kit/ChildSession.cpp
+++ b/kit/ChildSession.cpp
@@ -892,18 +892,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)
         {
@@ -922,7 +941,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 f87fa773..52cbccfb 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 8aa364c1..560be0c8 100644
--- a/loleaflet/src/map/handler/Map.WOPI.js
+++ b/loleaflet/src/map/handler/Map.WOPI.js
@@ -213,11 +213,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 d834dcb2..8c46facd 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -17,7 +17,7 @@ noinst_LTLIBRARIES = \
 	unit-storage.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)
@@ -66,6 +66,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
@@ -79,7 +81,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 # unit-storage.la # unit-admin.la
+TESTS = unit-prefork.la unit-tilecache.la unit-timeout.la unit-oauth.la unit-wopi.la unit-wopi-saveas.la # unit-storage.la # unit-admin.la
 else
 TESTS = ${top_builddir}/test/test
 endif
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 b5e78b7e..5bd105ad 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -616,38 +616,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 fe6bbc61..308aecd6 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -595,11 +595,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 <<
@@ -620,12 +630,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 <<
@@ -641,24 +651,32 @@ bool DocumentBroker::saveToStorageInternal(const std::string& sessionId,
     // storage behind our backs.
 
     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 0bc29996..2b76fbd1 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.
@@ -355,7 +360,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 14482b0c..c36c07ca 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 57fc9ad4..d698ba45 100644
--- a/wsd/Storage.hpp
+++ b/wsd/Storage.hpp
@@ -112,7 +112,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);
 
@@ -184,7 +185,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.
@@ -287,7 +288,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; }
@@ -317,7 +318,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;


More information about the Libreoffice-commits mailing list