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.

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

mercurial