diff --git a/loleaflet/reference.html b/loleaflet/reference.html
index dcd0a97761..8f1ea583ac 100644
--- a/loleaflet/reference.html
+++ b/loleaflet/reference.html
@@ -2849,7 +2849,6 @@ Editor to WOPI host
export of the document.
-
Session Management
@@ -2913,6 +2912,18 @@ WOPI host to editor
for details.
+
+ Action_SaveAs |
+
+ Name: <String>
+ Path: <String>
+ |
+ Creates copy of the document with given Name.
+ Name is the requested name for the new file.
+ Path is the relative path in the WOPI host file system where the
+ user wants the new file to be saved.
+ |
+
Action_Print |
@@ -3036,6 +3047,15 @@ Editor to WOPI host
via Insert_Button API above is clicked.
|
+
+ UI_SaveAs |
+ |
+
+ Requests WOPI host to create appropriate UI, so that the user can choose
+ path and File name for creating a copy of the current file.
+ Response to this query is sent via Action_SaveAs message.
+ |
+
Marker
diff --git a/loleaflet/src/control/Control.Menubar.js b/loleaflet/src/control/Control.Menubar.js
index af87725d67..031bbb52cd 100644
--- a/loleaflet/src/control/Control.Menubar.js
+++ b/loleaflet/src/control/Control.Menubar.js
@@ -9,6 +9,7 @@ L.Control.Menubar = L.Control.extend({
text: [
{name: _('File'), id: 'file', type: 'menu', menu: [
{name: _('Save'), id: 'save', type: 'action'},
+ {name: _('Save As'), id: 'saveas', type: 'action'},
{name: _('Print'), id: 'print', type: 'action'},
{name: _('See revision history'), id: 'rev-history', type: 'action'},
{name: _('Download as'), id: 'downloadas', type: 'menu', menu: [
@@ -566,6 +567,8 @@ L.Control.Menubar = L.Control.extend({
var id = $(item).data('id');
if (id === 'save') {
map.save(true, true);
+ } else if (id === 'saveas') {
+ map.fire('postMessage', {msgId: 'UI_SaveAs'});
} else if (id === 'print') {
map.print();
} else if (id.startsWith('downloadas-')) {
diff --git a/loleaflet/src/control/Toolbar.js b/loleaflet/src/control/Toolbar.js
index 832abdb0be..25693170ae 100644
--- a/loleaflet/src/control/Toolbar.js
+++ b/loleaflet/src/control/Toolbar.js
@@ -84,20 +84,23 @@ L.Map.include({
this.downloadAs('print.pdf', 'pdf', null, 'print');
},
- saveAs: function (url, format, options) {
+ saveAs: function (newName, path, 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 ' +
- 'url=' + url + ' ' +
+ 'fileName=' + newName + ' ' +
+ 'path=' + path + ' ' +
'format=' + format + ' ' +
'options=TakeOwnership,' + options);
},
diff --git a/loleaflet/src/core/Socket.js b/loleaflet/src/core/Socket.js
index 9950762770..68f3ea4fdb 100644
--- a/loleaflet/src/core/Socket.js
+++ b/loleaflet/src/core/Socket.js
@@ -438,6 +438,9 @@ L.Socket = L.Class.extend({
return;
}
+ else if (textMsg.startsWith('error:') && command.errorCmd === 'saveas') {
+ this._map.hideBusy();
+ }
else if (textMsg.startsWith('error:') && command.errorCmd === 'load') {
this.close();
@@ -535,6 +538,12 @@ L.Socket = L.Class.extend({
', last: ' + (command.rendercount - this._map._docLayer._debugRenderCount));
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?
+ }
else if (textMsg.startsWith('statusindicator:')) {
//FIXME: We should get statusindicator when saving too, no?
this._map.showBusy(_('Connecting...'), false);
diff --git a/loleaflet/src/map/handler/Map.WOPI.js b/loleaflet/src/map/handler/Map.WOPI.js
index f434c43368..dc6f0fa3ef 100644
--- a/loleaflet/src/map/handler/Map.WOPI.js
+++ b/loleaflet/src/map/handler/Map.WOPI.js
@@ -216,11 +216,19 @@ L.Map.WOPI = L.Handler.extend({
this._postMessage({msgId: 'Get_Export_Formats_Resp', args: exportFormatsResp});
}
+ else if (msg.MessageId === 'Action_SaveAs') {
+ if (msg.Values) {
+ if (msg.Values.name === null || msg.Values.name === undefined) {
+ msg.Values.name = '';
+ }
+ this.showBusy(_('Creating copy...'), false);
+ map.saveAs(msg.Values.name, msg.Values.path);
+ }
+ }
},
_postMessage: function(e) {
if (!this.enabled) { return; }
-
var msgId = e.msgId;
var values = e.args || {};
if (!!this.PostMessageOrigin && window.parent !== window.self) {
@@ -237,7 +245,6 @@ L.Map.WOPI = L.Handler.extend({
'SendTime': Date.now(),
'Values': values
};
-
window.parent.postMessage(JSON.stringify(msg), this.PostMessageOrigin);
}
}
diff --git a/tools/KitClient.cpp b/tools/KitClient.cpp
index dfdb006c08..aa2c012fc3 100644
--- a/tools/KitClient.cpp
+++ b/tools/KitClient.cpp
@@ -78,6 +78,7 @@ extern "C"
CASE(COMMENT);
CASE(INVALIDATE_HEADER);
CASE(CELL_ADDRESS);
+ CASE(RULER_UPDATE);
#undef CASE
}
std::cout << " payload: " << payload << std::endl;
diff --git a/wsd/ClientSession.cpp b/wsd/ClientSession.cpp
index 6a9a88d548..7a4f42b09e 100644
--- a/wsd/ClientSession.cpp
+++ b/wsd/ClientSession.cpp
@@ -214,6 +214,13 @@ bool ClientSession::_handleInput(const char *buffer, int length)
{
return sendCombinedTiles(buffer, length, tokens, docBroker);
}
+ else if (tokens[0] == "saveas")
+ {
+ std::string newFileName, path;
+ getTokenString(tokens[1], "fileName", newFileName);
+ 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 7f2943362e..016d251ddf 100644
--- a/wsd/DocumentBroker.cpp
+++ b/wsd/DocumentBroker.cpp
@@ -718,6 +718,29 @@ 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(_storage.get());
+ if (wopiStorage != nullptr)
+ {
+ const std::string newUrl = wopiStorage->createCopyFile(it->second->getAccessToken(), 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 dd0ace399a..b08fd2e924 100644
--- a/wsd/DocumentBroker.hpp
+++ b/wsd/DocumentBroker.hpp
@@ -339,6 +339,9 @@ public:
/// Sends the .uno:Save command to LoKit.
bool sendUnoSave(const std::string& sessionId, bool dontTerminateEdit = true, bool dontSaveIfUnmodified = true);
+ /// 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 72bae46afe..1472faa604 100644
--- a/wsd/Storage.cpp
+++ b/wsd/Storage.cpp
@@ -584,6 +584,85 @@ std::unique_ptr WopiStorage::getWOPIFileInfo(const Au
return std::unique_ptr(new WOPIFileInfo({userId, userName, userExtraInfo, canWrite, postMessageOrigin, hidePrintOption, hideSaveOption, hideExportOption, enableOwnerTermination, disablePrint, disableExport, disableCopy, callDuration}));
}
+/// PutRelativeFile - uri format: http://server/<...>/wopi*/files//
+std::string WopiStorage::createCopyFile(const std::string& accessToken, const std::string& newFileName, const std::string& path)
+{
+ const auto size = getFileSize(_jailedFilePath);
+ std::ostringstream oss;
+ Poco::URI uriObject(_uri);
+ setQueryParameter(uriObject, "access_token", accessToken);
+
+ LOG_DBG("Wopi PutRelativeFile(save as) request for : " << uriObject.toString());
+
+ try
+ {
+ std::unique_ptr 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);
+ 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//content
std::string WopiStorage::loadStorageFileToLocal(const Authorization& auth)
{
diff --git a/wsd/Storage.hpp b/wsd/Storage.hpp
index 6b26c06654..da30b1b97a 100644
--- a/wsd/Storage.hpp
+++ b/wsd/Storage.hpp
@@ -258,6 +258,9 @@ public:
/// which can then be obtained using getFileInfo()
std::unique_ptr getWOPIFileInfo(const Authorization& auth);
+ /// returns
+ std::string createCopyFile(const std::string& accessToken, const std::string& newFileName, const std::string& path);
+
/// uri format: http://server/<...>/wopi*/files//content
std::string loadStorageFileToLocal(const Authorization& auth) override;
diff --git a/wsd/protocol.txt b/wsd/protocol.txt
index df96f5dce4..b75afe7d5e 100644
--- a/wsd/protocol.txt
+++ b/wsd/protocol.txt
@@ -108,10 +108,12 @@ requestloksession
resetselection
-saveas url= format= options=
+saveas newName= path=
+ format= options=
- is a URL, encoded. 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
+ 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.
selecttext type= x= y=
@@ -306,6 +308,14 @@ pong rendercount=
sent in reply to a 'ping' message, where is the total number
of rendered tiles of the document.
+saveas: newUrl= newFileName=
+
+ 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= parts= current= width= height= viewid= [partNames]
is 'text, 'spreadsheet', 'presentation', 'drawing' or 'other. Others are numbers.