addon-sdk/source/lib/sdk/simple-storage.js

Sat, 03 Jan 2015 20:18:00 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Sat, 03 Jan 2015 20:18:00 +0100
branch
TOR_BUG_3246
changeset 7
129ffea94266
permissions
-rw-r--r--

Conditionally enable double key logic according to:
private browsing mode or privacy.thirdparty.isolate preference and
implement in GetCookieStringCommon and FindCookie where it counts...
With some reservations of how to convince FindCookie users to test
condition and pass a nullptr when disabling double key logic.

michael@0 1 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 2 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 4
michael@0 5 "use strict";
michael@0 6
michael@0 7 module.metadata = {
michael@0 8 "stability": "stable"
michael@0 9 };
michael@0 10
michael@0 11 const { Cc, Ci, Cu } = require("chrome");
michael@0 12 const file = require("./io/file");
michael@0 13 const prefs = require("./preferences/service");
michael@0 14 const jpSelf = require("./self");
michael@0 15 const timer = require("./timers");
michael@0 16 const unload = require("./system/unload");
michael@0 17 const { emit, on, off } = require("./event/core");
michael@0 18 const { defer } = require('./core/promise');
michael@0 19
michael@0 20 const { Task } = Cu.import("resource://gre/modules/Task.jsm", {});
michael@0 21
michael@0 22 const WRITE_PERIOD_PREF = "extensions.addon-sdk.simple-storage.writePeriod";
michael@0 23 const WRITE_PERIOD_DEFAULT = 300000; // 5 minutes
michael@0 24
michael@0 25 const QUOTA_PREF = "extensions.addon-sdk.simple-storage.quota";
michael@0 26 const QUOTA_DEFAULT = 5242880; // 5 MiB
michael@0 27
michael@0 28 const JETPACK_DIR_BASENAME = "jetpack";
michael@0 29
michael@0 30 Object.defineProperties(exports, {
michael@0 31 storage: {
michael@0 32 enumerable: true,
michael@0 33 get: function() { return manager.root; },
michael@0 34 set: function(value) { manager.root = value; }
michael@0 35 },
michael@0 36 quotaUsage: {
michael@0 37 get: function() { return manager.quotaUsage; }
michael@0 38 }
michael@0 39 });
michael@0 40
michael@0 41 function getHash(data) {
michael@0 42 let { promise, resolve } = defer();
michael@0 43
michael@0 44 let crypto = Cc["@mozilla.org/security/hash;1"].createInstance(Ci.nsICryptoHash);
michael@0 45 crypto.init(crypto.MD5);
michael@0 46
michael@0 47 let listener = {
michael@0 48 onStartRequest: function() { },
michael@0 49
michael@0 50 onDataAvailable: function(request, context, inputStream, offset, count) {
michael@0 51 crypto.updateFromStream(inputStream, count);
michael@0 52 },
michael@0 53
michael@0 54 onStopRequest: function(request, context, status) {
michael@0 55 resolve(crypto.finish(false));
michael@0 56 }
michael@0 57 };
michael@0 58
michael@0 59 let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
michael@0 60 createInstance(Ci.nsIScriptableUnicodeConverter);
michael@0 61 converter.charset = "UTF-8";
michael@0 62 let stream = converter.convertToInputStream(data);
michael@0 63 let pump = Cc["@mozilla.org/network/input-stream-pump;1"].
michael@0 64 createInstance(Ci.nsIInputStreamPump);
michael@0 65 pump.init(stream, -1, -1, 0, 0, true);
michael@0 66 pump.asyncRead(listener, null);
michael@0 67
michael@0 68 return promise;
michael@0 69 }
michael@0 70
michael@0 71 function writeData(filename, data) {
michael@0 72 let { promise, resolve, reject } = defer();
michael@0 73
michael@0 74 let stream = file.open(filename, "w");
michael@0 75 try {
michael@0 76 stream.writeAsync(data, err => {
michael@0 77 if (err)
michael@0 78 reject(err);
michael@0 79 else
michael@0 80 resolve();
michael@0 81 });
michael@0 82 }
michael@0 83 catch (err) {
michael@0 84 // writeAsync closes the stream after it's done, so only close on error.
michael@0 85 stream.close();
michael@0 86 reject(err);
michael@0 87 }
michael@0 88
michael@0 89 return promise;
michael@0 90 }
michael@0 91
michael@0 92 // A generic JSON store backed by a file on disk. This should be isolated
michael@0 93 // enough to move to its own module if need be...
michael@0 94 function JsonStore(options) {
michael@0 95 this.filename = options.filename;
michael@0 96 this.quota = options.quota;
michael@0 97 this.writePeriod = options.writePeriod;
michael@0 98 this.onOverQuota = options.onOverQuota;
michael@0 99 this.onWrite = options.onWrite;
michael@0 100 this.hash = null;
michael@0 101 unload.ensure(this);
michael@0 102 this.startTimer();
michael@0 103 }
michael@0 104
michael@0 105 JsonStore.prototype = {
michael@0 106 // The store's root.
michael@0 107 get root() {
michael@0 108 return this.isRootInited ? this._root : {};
michael@0 109 },
michael@0 110
michael@0 111 // Performs some type checking.
michael@0 112 set root(val) {
michael@0 113 let types = ["array", "boolean", "null", "number", "object", "string"];
michael@0 114 if (types.indexOf(typeof(val)) < 0) {
michael@0 115 throw new Error("storage must be one of the following types: " +
michael@0 116 types.join(", "));
michael@0 117 }
michael@0 118 this._root = val;
michael@0 119 return val;
michael@0 120 },
michael@0 121
michael@0 122 // True if the root has ever been set (either via the root setter or by the
michael@0 123 // backing file's having been read).
michael@0 124 get isRootInited() {
michael@0 125 return this._root !== undefined;
michael@0 126 },
michael@0 127
michael@0 128 // Percentage of quota used, as a number [0, Inf). > 1 implies over quota.
michael@0 129 // Undefined if there is no quota.
michael@0 130 get quotaUsage() {
michael@0 131 return this.quota > 0 ?
michael@0 132 JSON.stringify(this.root).length / this.quota :
michael@0 133 undefined;
michael@0 134 },
michael@0 135
michael@0 136 startTimer: function JsonStore_startTimer() {
michael@0 137 timer.setTimeout(() => {
michael@0 138 this.write().then(this.startTimer.bind(this));
michael@0 139 }, this.writePeriod);
michael@0 140 },
michael@0 141
michael@0 142 // Removes the backing file and all empty subdirectories.
michael@0 143 purge: function JsonStore_purge() {
michael@0 144 try {
michael@0 145 // This'll throw if the file doesn't exist.
michael@0 146 file.remove(this.filename);
michael@0 147 this.hash = null;
michael@0 148 let parentPath = this.filename;
michael@0 149 do {
michael@0 150 parentPath = file.dirname(parentPath);
michael@0 151 // This'll throw if the dir isn't empty.
michael@0 152 file.rmdir(parentPath);
michael@0 153 } while (file.basename(parentPath) !== JETPACK_DIR_BASENAME);
michael@0 154 }
michael@0 155 catch (err) {}
michael@0 156 },
michael@0 157
michael@0 158 // Initializes the root by reading the backing file.
michael@0 159 read: function JsonStore_read() {
michael@0 160 try {
michael@0 161 let str = file.read(this.filename);
michael@0 162
michael@0 163 // Ideally we'd log the parse error with console.error(), but logged
michael@0 164 // errors cause tests to fail. Supporting "known" errors in the test
michael@0 165 // harness appears to be non-trivial. Maybe later.
michael@0 166 this.root = JSON.parse(str);
michael@0 167 let self = this;
michael@0 168 getHash(str).then(hash => this.hash = hash);
michael@0 169 }
michael@0 170 catch (err) {
michael@0 171 this.root = {};
michael@0 172 this.hash = null;
michael@0 173 }
michael@0 174 },
michael@0 175
michael@0 176 // Cleans up on unload. If unloading because of uninstall, the store is
michael@0 177 // purged; otherwise it's written.
michael@0 178 unload: function JsonStore_unload(reason) {
michael@0 179 timer.clearTimeout(this.writeTimer);
michael@0 180 this.writeTimer = null;
michael@0 181
michael@0 182 if (reason === "uninstall")
michael@0 183 this.purge();
michael@0 184 else
michael@0 185 this.write();
michael@0 186 },
michael@0 187
michael@0 188 // True if the root is an empty object.
michael@0 189 get _isEmpty() {
michael@0 190 if (this.root && typeof(this.root) === "object") {
michael@0 191 let empty = true;
michael@0 192 for (let key in this.root) {
michael@0 193 empty = false;
michael@0 194 break;
michael@0 195 }
michael@0 196 return empty;
michael@0 197 }
michael@0 198 return false;
michael@0 199 },
michael@0 200
michael@0 201 // Writes the root to the backing file, notifying write observers when
michael@0 202 // complete. If the store is over quota or if it's empty and the store has
michael@0 203 // never been written, nothing is written and write observers aren't notified.
michael@0 204 write: Task.async(function JsonStore_write() {
michael@0 205 // Don't write if the root is uninitialized or if the store is empty and the
michael@0 206 // backing file doesn't yet exist.
michael@0 207 if (!this.isRootInited || (this._isEmpty && !file.exists(this.filename)))
michael@0 208 return;
michael@0 209
michael@0 210 let data = JSON.stringify(this.root);
michael@0 211
michael@0 212 // If the store is over quota, don't write. The current under-quota state
michael@0 213 // should persist.
michael@0 214 if ((this.quota > 0) && (data.length > this.quota)) {
michael@0 215 this.onOverQuota(this);
michael@0 216 return;
michael@0 217 }
michael@0 218
michael@0 219 // Hash the data to compare it to any previously written data
michael@0 220 let hash = yield getHash(data);
michael@0 221
michael@0 222 if (hash == this.hash)
michael@0 223 return;
michael@0 224
michael@0 225 // Finally, write.
michael@0 226 try {
michael@0 227 yield writeData(this.filename, data);
michael@0 228
michael@0 229 this.hash = hash;
michael@0 230 if (this.onWrite)
michael@0 231 this.onWrite(this);
michael@0 232 }
michael@0 233 catch (err) {
michael@0 234 console.error("Error writing simple storage file: " + this.filename);
michael@0 235 console.error(err);
michael@0 236 }
michael@0 237 })
michael@0 238 };
michael@0 239
michael@0 240
michael@0 241 // This manages a JsonStore singleton and tailors its use to simple storage.
michael@0 242 // The root of the JsonStore is lazy-loaded: The backing file is only read the
michael@0 243 // first time the root's gotten.
michael@0 244 let manager = ({
michael@0 245 jsonStore: null,
michael@0 246
michael@0 247 // The filename of the store, based on the profile dir and extension ID.
michael@0 248 get filename() {
michael@0 249 let storeFile = Cc["@mozilla.org/file/directory_service;1"].
michael@0 250 getService(Ci.nsIProperties).
michael@0 251 get("ProfD", Ci.nsIFile);
michael@0 252 storeFile.append(JETPACK_DIR_BASENAME);
michael@0 253 storeFile.append(jpSelf.id);
michael@0 254 storeFile.append("simple-storage");
michael@0 255 file.mkpath(storeFile.path);
michael@0 256 storeFile.append("store.json");
michael@0 257 return storeFile.path;
michael@0 258 },
michael@0 259
michael@0 260 get quotaUsage() {
michael@0 261 return this.jsonStore.quotaUsage;
michael@0 262 },
michael@0 263
michael@0 264 get root() {
michael@0 265 if (!this.jsonStore.isRootInited)
michael@0 266 this.jsonStore.read();
michael@0 267 return this.jsonStore.root;
michael@0 268 },
michael@0 269
michael@0 270 set root(val) {
michael@0 271 return this.jsonStore.root = val;
michael@0 272 },
michael@0 273
michael@0 274 unload: function manager_unload() {
michael@0 275 off(this);
michael@0 276 },
michael@0 277
michael@0 278 new: function manager_constructor() {
michael@0 279 let manager = Object.create(this);
michael@0 280 unload.ensure(manager);
michael@0 281
michael@0 282 manager.jsonStore = new JsonStore({
michael@0 283 filename: manager.filename,
michael@0 284 writePeriod: prefs.get(WRITE_PERIOD_PREF, WRITE_PERIOD_DEFAULT),
michael@0 285 quota: prefs.get(QUOTA_PREF, QUOTA_DEFAULT),
michael@0 286 onOverQuota: emit.bind(null, exports, "OverQuota")
michael@0 287 });
michael@0 288
michael@0 289 return manager;
michael@0 290 }
michael@0 291 }).new();
michael@0 292
michael@0 293 exports.on = on.bind(null, exports);
michael@0 294 exports.removeListener = function(type, listener) {
michael@0 295 off(exports, type, listener);
michael@0 296 };

mercurial