mirror of
https://github.com/classilla/tenfourfox.git
synced 2024-09-10 11:54:40 +00:00
435 lines
13 KiB
JavaScript
435 lines
13 KiB
JavaScript
|
/* jshint moz: true, esnext: true */
|
||
|
/* 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/. */
|
||
|
|
||
|
"use strict";
|
||
|
|
||
|
const Cu = Components.utils;
|
||
|
Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
|
||
|
Cu.import("resource://gre/modules/Preferences.jsm");
|
||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||
|
Cu.importGlobalProperties(["indexedDB"]);
|
||
|
|
||
|
this.EXPORTED_SYMBOLS = ["PushDB"];
|
||
|
|
||
|
XPCOMUtils.defineLazyGetter(this, "console", () => {
|
||
|
let {ConsoleAPI} = Cu.import("resource://gre/modules/Console.jsm", {});
|
||
|
return new ConsoleAPI({
|
||
|
maxLogLevelPref: "dom.push.loglevel",
|
||
|
prefix: "PushDB",
|
||
|
});
|
||
|
});
|
||
|
|
||
|
this.PushDB = function PushDB(dbName, dbVersion, dbStoreName, keyPath, model) {
|
||
|
console.debug("PushDB()");
|
||
|
this._dbStoreName = dbStoreName;
|
||
|
this._keyPath = keyPath;
|
||
|
this._model = model;
|
||
|
|
||
|
// set the indexeddb database
|
||
|
this.initDBHelper(dbName, dbVersion,
|
||
|
[dbStoreName]);
|
||
|
};
|
||
|
|
||
|
this.PushDB.prototype = {
|
||
|
__proto__: IndexedDBHelper.prototype,
|
||
|
|
||
|
toPushRecord: function(record) {
|
||
|
if (!record) {
|
||
|
return;
|
||
|
}
|
||
|
return new this._model(record);
|
||
|
},
|
||
|
|
||
|
isValidRecord: function(record) {
|
||
|
return record && typeof record.scope == "string" &&
|
||
|
typeof record.originAttributes == "string" &&
|
||
|
record.quota >= 0 &&
|
||
|
typeof record[this._keyPath] == "string";
|
||
|
},
|
||
|
|
||
|
upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
|
||
|
if (aOldVersion <= 3) {
|
||
|
//XXXnsm We haven't shipped Push during this upgrade, so I'm just going to throw old
|
||
|
//registrations away without even informing the app.
|
||
|
if (aDb.objectStoreNames.contains(this._dbStoreName)) {
|
||
|
aDb.deleteObjectStore(this._dbStoreName);
|
||
|
}
|
||
|
|
||
|
let objectStore = aDb.createObjectStore(this._dbStoreName,
|
||
|
{ keyPath: this._keyPath });
|
||
|
|
||
|
// index to fetch records based on endpoints. used by unregister
|
||
|
objectStore.createIndex("pushEndpoint", "pushEndpoint", { unique: true });
|
||
|
|
||
|
// index to fetch records by identifiers.
|
||
|
// In the current security model, the originAttributes distinguish between
|
||
|
// different 'apps' on the same origin. Since ServiceWorkers are
|
||
|
// same-origin to the scope they are registered for, the attributes and
|
||
|
// scope are enough to reconstruct a valid principal.
|
||
|
objectStore.createIndex("identifiers", ["scope", "originAttributes"], { unique: true });
|
||
|
objectStore.createIndex("originAttributes", "originAttributes", { unique: false });
|
||
|
}
|
||
|
|
||
|
if (aOldVersion < 4) {
|
||
|
let objectStore = aTransaction.objectStore(this._dbStoreName);
|
||
|
|
||
|
// index to fetch active and expired registrations.
|
||
|
objectStore.createIndex("quota", "quota", { unique: false });
|
||
|
}
|
||
|
},
|
||
|
|
||
|
/*
|
||
|
* @param aRecord
|
||
|
* The record to be added.
|
||
|
*/
|
||
|
|
||
|
put: function(aRecord) {
|
||
|
console.debug("put()", aRecord);
|
||
|
if (!this.isValidRecord(aRecord)) {
|
||
|
return Promise.reject(new TypeError(
|
||
|
"Scope, originAttributes, and quota are required! " +
|
||
|
JSON.stringify(aRecord)
|
||
|
)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readwrite",
|
||
|
this._dbStoreName,
|
||
|
(aTxn, aStore) => {
|
||
|
aTxn.result = undefined;
|
||
|
|
||
|
aStore.put(aRecord).onsuccess = aEvent => {
|
||
|
console.debug("put: Request successful. Updated record",
|
||
|
aEvent.target.result);
|
||
|
aTxn.result = this.toPushRecord(aRecord);
|
||
|
};
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
/*
|
||
|
* @param aKeyID
|
||
|
* The ID of record to be deleted.
|
||
|
*/
|
||
|
delete: function(aKeyID) {
|
||
|
console.debug("delete()");
|
||
|
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readwrite",
|
||
|
this._dbStoreName,
|
||
|
function txnCb(aTxn, aStore) {
|
||
|
console.debug("delete: Removing record", aKeyID);
|
||
|
aStore.get(aKeyID).onsuccess = event => {
|
||
|
aTxn.result = event.target.result;
|
||
|
aStore.delete(aKeyID);
|
||
|
};
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
// testFn(record) is called with a database record and should return true if
|
||
|
// that record should be deleted.
|
||
|
clearIf: function(testFn) {
|
||
|
console.debug("clearIf()");
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readwrite",
|
||
|
this._dbStoreName,
|
||
|
(aTxn, aStore) => {
|
||
|
aTxn.result = undefined;
|
||
|
|
||
|
aStore.openCursor().onsuccess = event => {
|
||
|
let cursor = event.target.result;
|
||
|
if (cursor) {
|
||
|
let record = this.toPushRecord(cursor.value);
|
||
|
if (testFn(record)) {
|
||
|
let deleteRequest = cursor.delete();
|
||
|
deleteRequest.onerror = e => {
|
||
|
console.error("clearIf: Error removing record",
|
||
|
record.keyID, e);
|
||
|
}
|
||
|
}
|
||
|
cursor.continue();
|
||
|
}
|
||
|
}
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
getByPushEndpoint: function(aPushEndpoint) {
|
||
|
console.debug("getByPushEndpoint()");
|
||
|
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readonly",
|
||
|
this._dbStoreName,
|
||
|
(aTxn, aStore) => {
|
||
|
aTxn.result = undefined;
|
||
|
|
||
|
let index = aStore.index("pushEndpoint");
|
||
|
index.get(aPushEndpoint).onsuccess = aEvent => {
|
||
|
let record = this.toPushRecord(aEvent.target.result);
|
||
|
console.debug("getByPushEndpoint: Got record", record);
|
||
|
aTxn.result = record;
|
||
|
};
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
getByKeyID: function(aKeyID) {
|
||
|
console.debug("getByKeyID()");
|
||
|
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readonly",
|
||
|
this._dbStoreName,
|
||
|
(aTxn, aStore) => {
|
||
|
aTxn.result = undefined;
|
||
|
|
||
|
aStore.get(aKeyID).onsuccess = aEvent => {
|
||
|
let record = this.toPushRecord(aEvent.target.result);
|
||
|
console.debug("getByKeyID: Got record", record);
|
||
|
aTxn.result = record;
|
||
|
};
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Reduces all records associated with an origin to a single value.
|
||
|
*
|
||
|
* @param {String} origin The origin, matched as a prefix against the scope.
|
||
|
* @param {String} originAttributes Additional origin attributes. Requires
|
||
|
* an exact match.
|
||
|
* @param {Function} callback A function with the signature `(result,
|
||
|
* record, cursor)`, where `result` is the value returned by the previous
|
||
|
* invocation, `record` is the registration, and `cursor` is an `IDBCursor`.
|
||
|
* @param {Object} [initialValue] The value to use for the first invocation.
|
||
|
* @returns {Promise} Resolves with the value of the last invocation.
|
||
|
*/
|
||
|
reduceByOrigin: function(origin, originAttributes, callback, initialValue) {
|
||
|
console.debug("forEachOrigin()");
|
||
|
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readwrite",
|
||
|
this._dbStoreName,
|
||
|
(aTxn, aStore) => {
|
||
|
aTxn.result = initialValue;
|
||
|
|
||
|
let index = aStore.index("identifiers");
|
||
|
let range = IDBKeyRange.bound(
|
||
|
[origin, originAttributes],
|
||
|
[origin + "\x7f", originAttributes]
|
||
|
);
|
||
|
index.openCursor(range).onsuccess = event => {
|
||
|
let cursor = event.target.result;
|
||
|
if (!cursor) {
|
||
|
return;
|
||
|
}
|
||
|
let record = this.toPushRecord(cursor.value);
|
||
|
aTxn.result = callback(aTxn.result, record, cursor);
|
||
|
cursor.continue();
|
||
|
};
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
// Perform a unique match against { scope, originAttributes }
|
||
|
getByIdentifiers: function(aPageRecord) {
|
||
|
console.debug("getByIdentifiers()", aPageRecord);
|
||
|
if (!aPageRecord.scope || aPageRecord.originAttributes == undefined) {
|
||
|
console.error("getByIdentifiers: Scope and originAttributes are required",
|
||
|
aPageRecord);
|
||
|
return Promise.reject(new TypeError("Invalid page record"));
|
||
|
}
|
||
|
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readonly",
|
||
|
this._dbStoreName,
|
||
|
(aTxn, aStore) => {
|
||
|
aTxn.result = undefined;
|
||
|
|
||
|
let index = aStore.index("identifiers");
|
||
|
let request = index.get(IDBKeyRange.only([aPageRecord.scope, aPageRecord.originAttributes]));
|
||
|
request.onsuccess = aEvent => {
|
||
|
aTxn.result = this.toPushRecord(aEvent.target.result);
|
||
|
};
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
_getAllByKey: function(aKeyName, aKeyValue) {
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readonly",
|
||
|
this._dbStoreName,
|
||
|
(aTxn, aStore) => {
|
||
|
aTxn.result = undefined;
|
||
|
|
||
|
let index = aStore.index(aKeyName);
|
||
|
// It seems ok to use getAll here, since unlike contacts or other
|
||
|
// high storage APIs, we don't expect more than a handful of
|
||
|
// registrations per domain, and usually only one.
|
||
|
let getAllReq = index.mozGetAll(aKeyValue);
|
||
|
getAllReq.onsuccess = aEvent => {
|
||
|
aTxn.result = aEvent.target.result.map(
|
||
|
record => this.toPushRecord(record));
|
||
|
};
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
// aOriginAttributes must be a string!
|
||
|
getAllByOriginAttributes: function(aOriginAttributes) {
|
||
|
if (typeof aOriginAttributes !== "string") {
|
||
|
return Promise.reject("Expected string!");
|
||
|
}
|
||
|
return this._getAllByKey("originAttributes", aOriginAttributes);
|
||
|
},
|
||
|
|
||
|
getAllKeyIDs: function() {
|
||
|
console.debug("getAllKeyIDs()");
|
||
|
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readonly",
|
||
|
this._dbStoreName,
|
||
|
(aTxn, aStore) => {
|
||
|
aTxn.result = undefined;
|
||
|
aStore.mozGetAll().onsuccess = event => {
|
||
|
aTxn.result = event.target.result.map(
|
||
|
record => this.toPushRecord(record));
|
||
|
};
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
_getAllByPushQuota: function(range) {
|
||
|
console.debug("getAllByPushQuota()");
|
||
|
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readonly",
|
||
|
this._dbStoreName,
|
||
|
(aTxn, aStore) => {
|
||
|
aTxn.result = [];
|
||
|
|
||
|
let index = aStore.index("quota");
|
||
|
index.openCursor(range).onsuccess = event => {
|
||
|
let cursor = event.target.result;
|
||
|
if (cursor) {
|
||
|
aTxn.result.push(this.toPushRecord(cursor.value));
|
||
|
cursor.continue();
|
||
|
}
|
||
|
};
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
getAllUnexpired: function() {
|
||
|
console.debug("getAllUnexpired()");
|
||
|
return this._getAllByPushQuota(IDBKeyRange.lowerBound(1));
|
||
|
},
|
||
|
|
||
|
getAllExpired: function() {
|
||
|
console.debug("getAllExpired()");
|
||
|
return this._getAllByPushQuota(IDBKeyRange.only(0));
|
||
|
},
|
||
|
|
||
|
/**
|
||
|
* Updates an existing push registration.
|
||
|
*
|
||
|
* @param {String} aKeyID The registration ID.
|
||
|
* @param {Function} aUpdateFunc A function that receives the existing
|
||
|
* registration record as its argument, and returns a new record. If the
|
||
|
* function returns `null` or `undefined`, the record will not be updated.
|
||
|
* If the record does not exist, the function will not be called.
|
||
|
* @returns {Promise} A promise resolved with either the updated record, or
|
||
|
* `undefined` if the record was not updated.
|
||
|
*/
|
||
|
update: function(aKeyID, aUpdateFunc) {
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readwrite",
|
||
|
this._dbStoreName,
|
||
|
(aTxn, aStore) => {
|
||
|
aStore.get(aKeyID).onsuccess = aEvent => {
|
||
|
aTxn.result = undefined;
|
||
|
|
||
|
let record = aEvent.target.result;
|
||
|
if (!record) {
|
||
|
console.error("update: Record does not exist", aKeyID);
|
||
|
return;
|
||
|
}
|
||
|
let newRecord = aUpdateFunc(this.toPushRecord(record));
|
||
|
if (!this.isValidRecord(newRecord)) {
|
||
|
console.error("update: Ignoring invalid update",
|
||
|
aKeyID, newRecord);
|
||
|
return;
|
||
|
}
|
||
|
aStore.put(newRecord).onsuccess = aEvent => {
|
||
|
console.debug("update: Update successful", aKeyID, newRecord);
|
||
|
aTxn.result = newRecord;
|
||
|
};
|
||
|
};
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
|
||
|
drop: function() {
|
||
|
console.debug("drop()");
|
||
|
|
||
|
return new Promise((resolve, reject) =>
|
||
|
this.newTxn(
|
||
|
"readwrite",
|
||
|
this._dbStoreName,
|
||
|
function txnCb(aTxn, aStore) {
|
||
|
aStore.clear();
|
||
|
},
|
||
|
resolve,
|
||
|
reject
|
||
|
)
|
||
|
);
|
||
|
},
|
||
|
};
|