--- a/services/common/storageserver.js
+++ b/services/common/storageserver.js
@@ -232,17 +232,16 @@ ServerBSO.prototype = {
default:
this._log.warn("Unexpected field in BSO record: " + key);
sendMozSvcError(request, response, "8");
return true;
}
}
this.modified = request.timestamp;
- response.newModified = request.timestamp;
response.setHeader("X-Last-Modified", "" + this.modified, false);
response.setStatusLine(request.httpVersion, code, status);
},
};
/**
* Represent a collection on the server.
@@ -257,28 +256,28 @@ ServerBSO.prototype = {
* An object mapping BSO IDs to ServerBSOs.
* @param acceptNew
* If true, POSTs to this collection URI will result in new BSOs being
* created and wired in on the fly.
* @param timestamp
* An optional timestamp value to initialize the modified time of the
* collection. This should be in the format returned by new_timestamp().
*/
-function StorageServerCollection(bsos, acceptNew, timestamp) {
+function StorageServerCollection(bsos, acceptNew, timestamp=new_timestamp()) {
this._bsos = bsos || {};
this.acceptNew = acceptNew || false;
/*
* Track modified timestamp.
* We can't just use the timestamps of contained BSOs: an empty collection
* has a modified time.
*/
CommonUtils.ensureMillisecondsTimestamp(timestamp);
+ this._timestamp = timestamp;
- this.timestamp = timestamp || new_timestamp();
this._log = Log4Moz.repository.getLogger(STORAGE_HTTP_LOGGER);
}
StorageServerCollection.prototype = {
BATCH_MAX_COUNT: 100, // # of records.
BATCH_MAX_SIZE: 1024 * 1024, // # bytes.
_timestamp: null,
@@ -396,23 +395,23 @@ StorageServerCollection.prototype = {
if (options.ids) {
if (options.ids.indexOf(bso.id) == -1) {
return false;
}
}
if (options.newer) {
- if (bso.modified < options.newer) {
+ if (bso.modified <= options.newer) {
return false;
}
}
if (options.older) {
- if (bso.modified > options.older) {
+ if (bso.modified >= options.older) {
return false;
}
}
if (options.index_above) {
if (bso.sortindex === undefined) {
return false;
}
@@ -712,17 +711,16 @@ StorageServerCollection.prototype = {
response.setHeader("Content-Type", "application/newlines", false);
let normalized = data.map(function map(d) {
let result = JSON.stringify(d);
return result.replace("\n", "\\u000a");
});
body = normalized.join("\n") + "\n";
- _(body);
} else {
response.setHeader("Content-Type", "application/json", false);
body = JSON.stringify({items: data});
}
this._log.info("Records: " + data.length);
response.setHeader("X-Num-Records", "" + data.length, false);
response.setHeader("X-Last-Modified", "" + this.timestamp, false);
@@ -768,31 +766,39 @@ StorageServerCollection.prototype = {
input.push(record);
}
} else {
this._log.info("Unknown media type: " + inputMediaType);
throw HTTP_415;
}
+ if (this._ensureUnmodifiedSince(request, response)) {
+ return;
+ }
+
let res = this.post(input, request.timestamp);
let body = JSON.stringify(res);
response.setHeader("Content-Type", "application/json", false);
this.timestamp = request.timestamp;
response.setHeader("X-Last-Modified", "" + this.timestamp, false);
response.setStatusLine(request.httpVersion, "200", "OK");
response.bodyOutputStream.write(body, body.length);
},
deleteHandler: function deleteHandler(request, response) {
this._log.debug("Invoking StorageServerCollection.DELETE.");
let options = this.parseOptions(request);
+ if (this._ensureUnmodifiedSince(request, response)) {
+ return;
+ }
+
let deleted = this.delete(options);
response.deleted = deleted;
this.timestamp = request.timestamp;
response.setStatusLine(request.httpVersion, 204, "No Content");
},
handler: function handler() {
@@ -809,17 +815,38 @@ StorageServerCollection.prototype = {
case "DELETE":
return self.deleteHandler(request, response);
}
request.setHeader("Allow", "GET,POST,DELETE");
response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
};
- }
+ },
+
+ _ensureUnmodifiedSince: function _ensureUnmodifiedSince(request, response) {
+ if (!request.hasHeader("x-if-unmodified-since")) {
+ return false;
+ }
+
+ let requestModified = parseInt(request.getHeader("x-if-unmodified-since"),
+ 10);
+ let serverModified = this.timestamp;
+
+ this._log.debug("Request modified time: " + requestModified +
+ "; Server modified time: " + serverModified);
+ if (serverModified <= requestModified) {
+ return false;
+ }
+
+ this._log.info("Conditional request rejected because client time older " +
+ "than collection timestamp.");
+ response.setStatusLine(request.httpVersion, 412, "Precondition Failed");
+ return true;
+ },
};
//===========================================================================//
// httpd.js-based Storage server. //
//===========================================================================//
/**
@@ -1212,16 +1239,17 @@ StorageServer.prototype = {
storageRE: /^([-_a-zA-Z0-9]+)(?:\/([-_a-zA-Z0-9]+)\/?)?$/,
defaultHeaders: {},
/**
* HTTP response utility.
*/
respond: function respond(req, resp, code, status, body, headers, timestamp) {
+ this._log.info("Response: " + code + " " + status);
resp.setStatusLine(req.httpVersion, code, status);
for each (let [header, value] in Iterator(headers || this.defaultHeaders)) {
resp.setHeader(header, value, false);
}
if (timestamp) {
resp.setHeader("X-Timestamp", "" + timestamp, false);
}
@@ -1496,17 +1524,17 @@ StorageServer.prototype = {
bso = coll.insert(bsoID);
} catch (ex) {
return sendMozSvcError(req, resp, "8");
}
}
bso.putHandler(req, resp);
- coll.timestamp = resp.newModified;
+ coll.timestamp = req.timestamp;
return resp;
}
return coll.collectionHandler(req, resp);
} catch (ex) {
if (ex instanceof HttpError) {
if (!collectionExisted) {
this.deleteCollection(username, collection);