--- a/browser/components/extensions/ext-history.js
+++ b/browser/components/extensions/ext-history.js
@@ -9,17 +9,16 @@ Cu.import("resource://gre/modules/Extens
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
const {
- normalizeTime,
SingletonEventManager,
} = ExtensionUtils;
let nsINavHistoryService = Ci.nsINavHistoryService;
const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
["link", nsINavHistoryService.TRANSITION_LINK],
["typed", nsINavHistoryService.TRANSITION_TYPED],
["auto_bookmark", nsINavHistoryService.TRANSITION_BOOKMARK],
@@ -136,17 +135,17 @@ extensions.registerSchemaAPI("history",
addUrl: function(details) {
let transition, date;
try {
transition = getTransitionType(details.transition);
} catch (error) {
return Promise.reject({message: error.message});
}
if (details.visitTime) {
- date = normalizeTime(details.visitTime);
+ date = details.visitTime;
}
let pageInfo = {
title: details.title,
url: details.url,
visits: [
{
transition,
date,
@@ -161,36 +160,36 @@ extensions.registerSchemaAPI("history",
},
deleteAll: function() {
return PlacesUtils.history.clear();
},
deleteRange: function(filter) {
let newFilter = {
- beginDate: normalizeTime(filter.startTime),
- endDate: normalizeTime(filter.endTime),
+ beginDate: filter.startTime,
+ endDate: filter.endTime,
};
// History.removeVisitsByFilter returns a boolean, but our API should return nothing
return PlacesUtils.history.removeVisitsByFilter(newFilter).then(() => undefined);
},
deleteUrl: function(details) {
let url = details.url;
// History.remove returns a boolean, but our API should return nothing
return PlacesUtils.history.remove(url).then(() => undefined);
},
search: function(query) {
let beginTime = (query.startTime == null) ?
PlacesUtils.toPRTime(Date.now() - 24 * 60 * 60 * 1000) :
- PlacesUtils.toPRTime(normalizeTime(query.startTime));
+ PlacesUtils.toPRTime(query.startTime);
let endTime = (query.endTime == null) ?
Number.MAX_VALUE :
- PlacesUtils.toPRTime(normalizeTime(query.endTime));
+ PlacesUtils.toPRTime(query.endTime);
if (beginTime > endTime) {
return Promise.reject({message: "The startTime cannot be after the endTime"});
}
let options = PlacesUtils.history.getNewQueryOptions();
options.sortingMode = options.SORT_BY_DATE_DESCENDING;
options.maxResults = query.maxResults || 100;
--- a/browser/components/extensions/schemas/history.json
+++ b/browser/components/extensions/schemas/history.json
@@ -86,29 +86,16 @@
"type": "string",
"description": "The visit ID of the referrer."
},
"transition": {
"$ref": "TransitionType",
"description": "The $(topic:transition-types)[transition type] for this visit from its referrer."
}
}
- },
- {
- "id": "HistoryTime",
- "description": "A time specified as a Date object, a number or string representing milliseconds since the epoch, or an ISO 8601 string",
- "choices": [
- {
- "type": "string",
- "pattern": "^[1-9]\\d*$"
- },
- {
- "$ref": "extensionTypes.Date"
- }
- ]
}
],
"functions": [
{
"name": "search",
"type": "function",
"description": "Searches the history for the last visit time of each page matching the query.",
"async": "callback",
@@ -117,22 +104,22 @@
"name": "query",
"type": "object",
"properties": {
"text": {
"type": "string",
"description": "A free-text query to the history service. Leave empty to retrieve all pages."
},
"startTime": {
- "$ref": "HistoryTime",
+ "type": "date",
"optional": true,
"description": "Limit results to those visited after this date. If not specified, this defaults to 24 hours in the past."
},
"endTime": {
- "$ref": "HistoryTime",
+ "type": "date",
"optional": true,
"description": "Limit results to those visited before this date."
},
"maxResults": {
"type": "integer",
"optional": true,
"minimum": 1,
"description": "The maximum number of results to retrieve. Defaults to 100."
@@ -205,17 +192,17 @@
"description": "The title of the page."
},
"transition": {
"$ref": "TransitionType",
"optional": true,
"description": "The $(topic:transition-types)[transition type] for this visit from its referrer."
},
"visitTime": {
- "$ref": "HistoryTime",
+ "type": "date",
"optional": true,
"description": "The date when this visit occurred."
}
}
},
{
"name": "callback",
"type": "function",
@@ -254,21 +241,21 @@
"description": "Removes all items within the specified date range from the history. Pages will not be removed from the history unless all visits fall within the range.",
"async": "callback",
"parameters": [
{
"name": "range",
"type": "object",
"properties": {
"startTime": {
- "$ref": "HistoryTime",
+ "type": "date",
"description": "Items added to history after this date."
},
"endTime": {
- "$ref": "HistoryTime",
+ "type": "date",
"description": "Items added to history before this date."
}
}
},
{
"name": "callback",
"type": "function",
"parameters": []
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1558,44 +1558,25 @@ class ChildAPIManager {
hasListener(path, name, listener) {
let ref = path.concat(name).join(".");
let set = this.listeners.get(ref) || new Set();
return set.has(listener);
}
}
-/**
- * Convert any of several different representations of a date/time to a Date object.
- * Accepts several formats:
- * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
- * either a number or a string.
- *
- * @param {Date|string|number} date
- * The date to convert.
- * @returns {Date}
- * A Date object
- */
-function normalizeTime(date) {
- // Of all the formats we accept the "number of milliseconds since the epoch as a string"
- // is an outlier, everything else can just be passed directly to the Date constructor.
- return new Date((typeof date == "string" && /^\d+$/.test(date))
- ? parseInt(date, 10) : date);
-}
-
this.ExtensionUtils = {
detectLanguage,
extend,
flushJarCache,
getConsole,
getInnerWindowID,
ignoreEvent,
injectAPI,
instanceOf,
- normalizeTime,
promiseDocumentLoaded,
promiseDocumentReady,
promiseObserved,
runSafe,
runSafeSync,
runSafeSyncWithoutClone,
runSafeWithoutClone,
BaseContext,
--- a/toolkit/components/extensions/Schemas.jsm
+++ b/toolkit/components/extensions/Schemas.jsm
@@ -12,17 +12,17 @@ const Cr = Components.results;
Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
var {
- instanceOf,
+ instanceOf
} = ExtensionUtils;
XPCOMUtils.defineLazyServiceGetter(this, "contentPolicyService",
"@mozilla.org/addons/content-policy;1",
"nsIAddonContentPolicy");
this.EXPORTED_SYMBOLS = ["Schemas"];
@@ -77,16 +77,43 @@ function getValueBaseType(value) {
} else if (t == "number") {
if (value % 1 == 0) {
return "integer";
}
}
return t;
}
+/**
+ * Convert any of several different representations of a date/time to a Date object.
+ * Accepts several formats:
+ * a Date object, an ISO8601 string, or a number of milliseconds since the epoch as
+ * either a number or a string.
+ *
+ * @param {Date|string|number} date
+ * The date to convert.
+ * @returns {Date}
+ * A Date object
+ */
+function normalizeTime(date) {
+ // Of all the formats we accept the "number of milliseconds since the epoch as a string"
+ // is an outlier, everything else can just be passed directly to the Date constructor.
+ // A valid ISO 8601 timestamp or a positive integer
+ const PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$|^\d+$/;
+ if (typeof date == "string" && !PATTERN.test(date)) {
+ throw new Error(`${date} is not a date in a valid format`);
+ }
+ let result = new Date((typeof date == "string" && /^\d+$/.test(date))
+ ? parseInt(date, 10) : date);
+ if (isNaN(result)) {
+ throw new Error(`${date} is not a date in a valid format`);
+ }
+ return result;
+}
+
// Methods of Context that are used by Schemas.normalize. These methods can be
// overridden at the construction of Context.
const CONTEXT_FOR_VALIDATION = [
"checkLoadURL",
"hasPermission",
"logError",
];
@@ -500,31 +527,16 @@ const FORMATS = {
contentSecurityPolicy(string, context) {
let error = contentPolicyService.validateAddonCSP(string);
if (error != null) {
throw new SyntaxError(error);
}
return string;
},
-
- date(string, context) {
- // A valid ISO 8601 timestamp.
- const PATTERN = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2}(\.\d{3})?(Z|([-+]\d{2}:?\d{2})))?$/;
- if (!PATTERN.test(string)) {
- throw new Error(`Invalid date string ${string}`);
- }
- // Our pattern just checks the format, we could still have invalid
- // values (e.g., month=99 or month=02 and day=31). Let the Date
- // constructor do the dirty work of validating.
- if (isNaN(new Date(string))) {
- throw new Error(`Invalid date string ${string}`);
- }
- return string;
- },
};
// Schema files contain namespaces, and each namespace contains types,
// properties, functions, and events. An Entry is a base class for
// types, properties, functions, and events.
class Entry {
constructor(schema = {}) {
/**
@@ -985,16 +997,33 @@ class ObjectType extends Type {
// SubModuleProperty. No value is ever expected to have this type.
class SubModuleType extends Type {
constructor(functions) {
super();
this.functions = functions;
}
}
+class DateType extends Type {
+ normalize(value, context) {
+ let normalized;
+ try {
+ normalized = {value: normalizeTime(value)};
+ } catch (e) {
+ return context.error(`${value} is not a date in a valid format`,
+ "be a date in a valid format");
+ }
+ return normalized;
+ }
+
+ checkBaseType(baseType) {
+ return baseType == "string" || baseType == "integer" || baseType == "object";
+ }
+}
+
class NumberType extends Type {
normalize(value, context) {
let r = this.normalizeBase("number", value, context);
if (r.error) {
return r;
}
if (isNaN(r.value) || !Number.isFinite(r.value)) {
@@ -1557,16 +1586,19 @@ this.Schemas = {
} else {
checkTypeProperties("properties", "additionalProperties", "patternProperties", "isInstanceOf");
}
return new ObjectType(type, properties, additionalProperties, patternProperties, type.isInstanceOf || null);
} else if (type.type == "array") {
checkTypeProperties("items", "minItems", "maxItems");
return new ArrayType(type, this.parseType(path, type.items),
type.minItems || 0, type.maxItems || Infinity);
+ } else if (type.type == "date") {
+ checkTypeProperties();
+ return new DateType(type);
} else if (type.type == "number") {
checkTypeProperties();
return new NumberType(type);
} else if (type.type == "integer") {
checkTypeProperties("minimum", "maximum");
return new IntegerType(type, type.minimum || -Infinity, type.maximum || Infinity);
} else if (type.type == "boolean") {
checkTypeProperties();
--- a/toolkit/components/extensions/ext-downloads.js
+++ b/toolkit/components/extensions/ext-downloads.js
@@ -15,17 +15,16 @@ XPCOMUtils.defineLazyModuleGetter(this,
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
ignoreEvent,
- normalizeTime,
runSafeSync,
SingletonEventManager,
} = ExtensionUtils;
const DOWNLOAD_ITEM_FIELDS = ["id", "url", "referrer", "filename", "incognito",
"danger", "mime", "startTime", "endTime",
"estimatedEndTime", "state",
"paused", "canResume", "error",
@@ -241,17 +240,17 @@ function downloadQuery(query) {
}
}
}
function normalizeDownloadTime(arg, before) {
if (arg == null) {
return before ? Number.MAX_VALUE : 0;
}
- return normalizeTime(arg).getTime();
+ return arg.getTime();
}
const startedBefore = normalizeDownloadTime(query.startedBefore, true);
const startedAfter = normalizeDownloadTime(query.startedAfter, false);
// const endedBefore = normalizeDownloadTime(query.endedBefore, true);
// const endedAfter = normalizeDownloadTime(query.endedAfter, false);
const totalBytesGreater = query.totalBytesGreater || 0;
--- a/toolkit/components/extensions/schemas/downloads.json
+++ b/toolkit/components/extensions/schemas/downloads.json
@@ -206,58 +206,45 @@
},
"previous": {
"optional": true,
"type": "boolean"
}
}
},
{
- "id": "DownloadTime",
- "description": "A time specified as a Date object, a number or string representing milliseconds since the epoch, or an ISO 8601 string",
- "choices": [
- {
- "type": "string",
- "pattern": "^[1-9]\\d*$"
- },
- {
- "$ref": "extensionTypes.Date"
- }
- ]
- },
- {
"id": "DownloadQuery",
"description": "Parameters that combine to specify a predicate that can be used to select a set of downloads. Used for example in search() and erase()",
"type": "object",
"properties": {
"query": {
"description": "This array of search terms limits results to <a href='#type-DownloadItem'>DownloadItems</a> whose <code>filename</code> or <code>url</code> contain all of the search terms that do not begin with a dash '-' and none of the search terms that do begin with a dash.",
"optional": true,
"type": "array",
"items": { "type": "string" }
},
"startedBefore": {
"description": "Limits results to downloads that started before the given ms since the epoch.",
"optional": true,
- "$ref": "DownloadTime"
+ "type": "date"
},
"startedAfter": {
"description": "Limits results to downloads that started after the given ms since the epoch.",
"optional": true,
- "$ref": "DownloadTime"
+ "type": "date"
},
"endedBefore": {
"description": "Limits results to downloads that ended before the given ms since the epoch.",
"optional": true,
- "$ref": "DownloadTime"
+ "type": "date"
},
"endedAfter": {
"description": "Limits results to downloads that ended after the given ms since the epoch.",
"optional": true,
- "$ref": "DownloadTime"
+ "type": "date"
},
"totalBytesGreater": {
"description": "Limits results to downloads whose totalBytes is greater than the given integer.",
"optional": true,
"type": "number"
},
"totalBytesLess": {
"description": "Limits results to downloads whose totalBytes is less than the given integer.",
--- a/toolkit/components/extensions/schemas/extension_types.json
+++ b/toolkit/components/extensions/schemas/extension_types.json
@@ -54,30 +54,12 @@
"description": "The ID of the frame to inject the script into. This may not be used in combination with <code>allFrames</code>."
},
"runAt": {
"$ref": "RunAt",
"optional": true,
"description": "The soonest that the JavaScript or CSS will be injected into the tab. Defaults to \"document_idle\"."
}
}
- },
- {
- "id": "Date",
- "choices": [
- {
- "type": "string",
- "format": "date"
- },
- {
- "type": "integer",
- "minimum": 0
- },
- {
- "type": "object",
- "isInstanceOf": "Date",
- "additionalProperties": { "type": "any" }
- }
- ]
}
]
}
]
--- a/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_schemas.js
@@ -211,25 +211,23 @@ let json = [
relativeUrl: {type: "string", "format": "relativeUrl", "optional": true},
strictRelativeUrl: {type: "string", "format": "strictRelativeUrl", "optional": true},
},
},
],
},
{
- name: "formatDate",
+ name: "dateType",
type: "function",
parameters: [
{
name: "arg",
- type: "object",
- properties: {
- date: {type: "string", format: "date", optional: true},
- },
+ type: "date",
+ optional: true,
},
],
},
{
name: "deep",
type: "function",
parameters: [
@@ -623,50 +621,63 @@ add_task(function* () {
}
for (let url of ["//foo.html", "http://foo/bar.html"]) {
Assert.throws(() => root.testing.format({strictRelativeUrl: url}),
/must be a relative URL/,
"should throw for non-relative URL");
}
- const dates = [
+ const isoDates = [
"2016-03-04",
"2016-03-04T08:00:00Z",
"2016-03-04T08:00:00.000Z",
"2016-03-04T08:00:00-08:00",
"2016-03-04T08:00:00.000-08:00",
"2016-03-04T08:00:00+08:00",
"2016-03-04T08:00:00.000+08:00",
"2016-03-04T08:00:00+0800",
"2016-03-04T08:00:00-0800",
];
- dates.forEach(str => {
- root.testing.formatDate({date: str});
- verify("call", "testing", "formatDate", [{date: str}]);
+ isoDates.forEach(str => {
+ root.testing.dateType(str);
+ verify("call", "testing", "dateType", [new Date(str)]);
});
// Make sure that a trivial change to a valid date invalidates it.
- dates.forEach(str => {
- Assert.throws(() => root.testing.formatDate({date: "0" + str}),
- /Invalid date string/,
+ isoDates.forEach(str => {
+ Assert.throws(() => root.testing.dateType("0" + str),
+ /is not a date in a valid format/,
"should throw for invalid iso date string");
- Assert.throws(() => root.testing.formatDate({date: str + "0"}),
- /Invalid date string/,
+ Assert.throws(() => root.testing.dateType(str + "0"),
+ /is not a date in a valid format/,
"should throw for invalid iso date string");
});
+ const goodDates = [
+ 1,
+ "1",
+ new Date(),
+ ]
+
+ goodDates.forEach(str => {
+ root.testing.dateType(str);
+ let checkDate = new Date(typeof str == "string" ? parseInt(str) : str);
+ verify("call", "testing", "dateType", [checkDate]);
+ });
+
const badDates = [
+ "-1",
"I do not look anything like a date string",
"2016-99-99",
"2016-03-04T25:00:00Z",
];
badDates.forEach(str => {
- Assert.throws(() => root.testing.formatDate({date: str}),
- /Invalid date string/,
+ Assert.throws(() => root.testing.dateType(str),
+ /is not a date in a valid format/,
"should throw for invalid iso date string");
});
root.testing.deep({foo: {bar: [{baz: {required: 12, optional: "42"}}]}});
verify("call", "testing", "deep", [{foo: {bar: [{baz: {required: 12, optional: "42"}}]}}]);
tallied = null;
Assert.throws(() => root.testing.deep({foo: {bar: [{baz: {optional: "42"}}]}}),