1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-11-27 03:57:21 +00:00

WebDAV file overwrite protection (#2614)

* putSaver: detect edit conflicts to prevent clobbering, if possible

if the server supplies an ETag, we send it back when saving, allowing
the server to detect edit conflicts and respond with 412 (cf.
https://www.w3.org/1999/04/Editing/)

caveats:
* this only kicks in after the first save, as we don't have access to
  the ETag when first loading the document
* there's no recovery mechanism (e.g. resetting `this.etag` in order to
  force clobbering), other than manually reloading the document

* putSaver: retrieve ETag upon initialization for clobbering protection

this addresses one of the caveats from the previous commit
(2d75cb83af) - while theoretically prone
to a race condition, it seems unlikely that saving will be triggered
before the server responds

* putSaver: simplify URI extraction

this simplifies the approach introduced in
f51f6bf774, with the purpose of removing
the fragment identifier

* putSaver: localize error message

* putSaver: switch to built-in HTTP helper

in the process, fixed ETag assignment in `#save` method (was
`this.etag`, now `self.etag`) as well as a syntax error due to a missing
closing brace

* putSaver: consolidate URI handling
This commit is contained in:
FND 2017-02-17 13:26:15 +01:00 committed by Jeremy Ruston
parent a6b538b308
commit 6505e6f448
2 changed files with 49 additions and 22 deletions

View File

@ -22,6 +22,7 @@ Encryption/RepeatPassword: Repeat password
Encryption/PasswordNoMatch: Passwords do not match
Encryption/SetPassword: Set password
Error/Caption: Error
Error/EditConflict: File changed on server
Error/Filter: Filter error
Error/FilterSyntax: Syntax error in filter expression
Error/IsFilterOperator: Filter Error: Unknown operand for the 'is' filter operator

View File

@ -21,36 +21,62 @@ Select the appropriate saver module and set it up
var PutSaver = function(wiki) {
this.wiki = wiki;
var self = this;
var uri = this.uri();
// Async server probe. Until probe finishes, save will fail fast
// See also https://github.com/Jermolene/TiddlyWiki5/issues/2276
var req = new XMLHttpRequest();
req.open("OPTIONS",encodeURI(document.location.protocol + "//" + document.location.hostname + ":" + document.location.port + document.location.pathname));
req.onload = function() {
// Check DAV header http://www.webdav.org/specs/rfc2518.html#rfc.section.9.1
self.serverAcceptsPuts = (this.status === 200 && !!this.getResponseHeader('dav'));
};
req.send();
$tw.utils.httpRequest({
url: uri,
type: "OPTIONS",
callback: function(err, data, xhr) {
// Check DAV header http://www.webdav.org/specs/rfc2518.html#rfc.section.9.1
self.serverAcceptsPuts = xhr.status === 200 && !!xhr.getResponseHeader("dav");
}
});
// Retrieve ETag if available
$tw.utils.httpRequest({
url: uri,
type: "HEAD",
callback: function(err, data, xhr) {
self.etag = xhr.getResponseHeader("ETag");
}
});
};
PutSaver.prototype.save = function(text,method,callback) {
PutSaver.prototype.uri = function() {
return encodeURI(document.location.toString().split("#")[0]);
};
// TODO: in case of edit conflict
// Prompt: Do you want to save over this? Y/N
// Merging would be ideal, and may be possible using future generic merge flow
PutSaver.prototype.save = function(text, method, callback) {
if (!this.serverAcceptsPuts) {
return false;
}
var req = new XMLHttpRequest();
// TODO: store/check ETags if supported by server, to protect against overwrites
// Prompt: Do you want to save over this? Y/N
// Merging would be ideal, and may be possible using future generic merge flow
req.onload = function() {
if (this.status === 200 || this.status === 201) {
callback(null); // success
var self = this;
var headers = { "Content-Type": "text/html;charset=UTF-8" };
if (this.etag) {
headers["If-Match"] = this.etag;
}
$tw.utils.httpRequest({
url: this.uri(),
type: "PUT",
headers: headers,
data: text,
callback: function(err, data, xhr) {
if (xhr.status === 200 || xhr.status === 201) {
self.etag = xhr.getResponseHeader("ETag");
callback(null); // success
}
else if (xhr.status === 412) { // edit conflict
var message = $tw.language.getString("Error/EditConflict");
callback(message);
}
else {
callback(xhr.responseText); // fail
}
}
else {
callback(this.responseText); // fail
}
};
req.open("PUT", encodeURI(window.location.href));
req.setRequestHeader("Content-Type", "text/html;charset=UTF-8");
req.send(text);
});
return true;
};