]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/storage.jsm
Import r6976 from upstream hg supporting Firefox up to 25.*
[dactyl.git] / common / modules / storage.jsm
index 896daf4461189867811231b5c916dba25887f93b..6cc704f0040c477242a61d3ee7705160621a9961 100644 (file)
@@ -1,35 +1,21 @@
-// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
+// Copyright (c) 2008-2013 Kris Maglione <maglione.k at Gmail>
 //
 // This work is licensed for reuse under an MIT license. Details are
 // given in the LICENSE.txt file included with this file.
 "use strict";
 
-Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("storage", {
     exports: ["File", "Storage", "storage"],
     require: ["services", "util"]
-}, this);
+});
+
+lazyRequire("config", ["config"]);
+lazyRequire("io", ["IO"]);
+lazyRequire("overlay", ["overlay"]);
 
 var win32 = /^win(32|nt)$/i.test(services.runtime.OS);
 var myObject = JSON.parse("{}").constructor;
 
-function loadData(name, store, type) {
-    try {
-        let data = storage.infoPath.child(name).read();
-        let result = JSON.parse(data);
-        if (result instanceof type)
-            return result;
-    }
-    catch (e) {}
-}
-
-function saveData(obj) {
-    if (obj.privateData && storage.privateMode)
-        return;
-    if (obj.store && storage.infoPath)
-        storage.infoPath.child(obj.name).write(obj.serial);
-}
-
 var StoreBase = Class("StoreBase", {
     OPTIONS: ["privateData", "replacer"],
 
@@ -39,16 +25,24 @@ var StoreBase = Class("StoreBase", {
 
     init: function (name, store, load, options) {
         this._load = load;
+        this._options = options;
 
-        this.__defineGetter__("store", function () store);
-        this.__defineGetter__("name", function () name);
+        this.__defineGetter__("store", () => store);
+        this.__defineGetter__("name", () => name);
         for (let [k, v] in Iterator(options))
             if (this.OPTIONS.indexOf(k) >= 0)
                 this[k] = v;
         this.reload();
     },
 
-    changed: function () { this.timer.tell(); },
+    clone: function (storage) {
+        let store = storage.privateMode ? false : this.store;
+        let res = this.constructor(this.name, store, this._load, this._options);
+        res.storage = storage;
+        return res;
+    },
+
+    changed: function () { this.timer && this.timer.tell(); },
 
     reload: function reload() {
         this._object = this._load() || this._constructor();
@@ -61,7 +55,7 @@ var StoreBase = Class("StoreBase", {
         storage.infoPath.child(this.name).remove(false);
     },
 
-    save: function () { saveData(this); },
+    save: function () { (self.storage || storage)._saveData(this); },
 
     __iterator__: function () Iterator(this._object)
 });
@@ -71,10 +65,13 @@ var ArrayStore = Class("ArrayStore", StoreBase, {
 
     get length() this._object.length,
 
-    set: function set(index, value) {
+    set: function set(index, value, quiet) {
         var orig = this._object[index];
         this._object[index] = value;
-        this.fireEvent("change", index);
+        if (!quiet)
+            this.fireEvent("change", index);
+
+        return orig;
     },
 
     push: function push(value) {
@@ -82,12 +79,32 @@ var ArrayStore = Class("ArrayStore", StoreBase, {
         this.fireEvent("push", this._object.length);
     },
 
-    pop: function pop(value) {
-        var res = this._object.pop();
-        this.fireEvent("pop", this._object.length);
+    pop: function pop(value, ord) {
+        if (ord == null)
+            var res = this._object.pop();
+        else
+            res = this._object.splice(ord, 1)[0];
+
+        this.fireEvent("pop", this._object.length, ord);
+        return res;
+    },
+
+    shift: function shift(value) {
+        var res = this._object.shift();
+        this.fireEvent("shift", this._object.length);
         return res;
     },
 
+    insert: function insert(value, ord) {
+        if (ord == 0)
+            this._object.unshift(value);
+        else
+            this._object = this._object.slice(0, ord)
+                               .concat([value])
+                               .concat(this._object.slice(ord));
+        this.fireEvent("insert", this._object.length, ord);
+    },
+
     truncate: function truncate(length, fromEnd) {
         var res = this._object.length;
         if (this._object.length > length) {
@@ -145,15 +162,24 @@ var ObjectStore = Class("ObjectStore", StoreBase, {
     }
 });
 
+var sessionGlobal = Cu.import("resource://gre/modules/Services.jsm", {});
+
 var Storage = Module("Storage", {
+    Local: function Local(dactyl, modules, window) ({
+        init: function init() {
+            this.privateMode = PrivateBrowsingUtils.isWindowPrivate(window);
+        }
+    }),
+
     alwaysReload: {},
 
-    init: function () {
+    init: function init() {
         this.cleanup();
 
-        if (services.bootstrap && !services.bootstrap.session)
-            services.bootstrap.session = {};
-        this.session = services.bootstrap ? services.bootstrap.session : {};
+        let { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
+        if (!Services.dactylSession)
+            Services.dactylSession = Cu.createObjectIn(sessionGlobal);
+        this.session = Services.dactylSession;
     },
 
     cleanup: function () {
@@ -164,28 +190,80 @@ var Storage = Module("Storage", {
                 this[key].timer.flush();
             delete this[key];
         }
-        for (let ary in values(this.observers))
-            for (let obj in values(ary))
-                if (obj.ref && obj.ref.get())
-                    delete obj.ref.get().dactylStorageRefs;
 
         this.keys = {};
         this.observers = {};
     },
 
-    exists: function exists(name) this.infoPath.child(name).exists(),
+    _loadData: function loadData(name, store, type) {
+        try {
+            let file = storage.infoPath.child(name);
+            if (file.exists()) {
+                let data = file.read();
+                let result = JSON.parse(data);
+                if (result instanceof type)
+                    return result;
+            }
+        }
+        catch (e) {
+            util.reportError(e);
+        }
+    },
+
+    _saveData: function saveData(obj) {
+        if (obj.privateData && storage.privateMode)
+            return;
+        if (obj.store && storage.infoPath)
+            storage.infoPath.child(obj.name).write(obj.serial);
+    },
+
+    storeForSession: function storeForSession(key, val) {
+        if (val)
+            this.session[key] = sessionGlobal.JSON.parse(JSON.stringify(val));
+        else
+            delete this.dactylSession[key];
+    },
+
+    infoPath: Class.Memoize(() =>
+        File(IO.runtimePath.replace(/,.*/, ""))
+            .child("info").child(config.profileName)),
+
+    exists: function exists(key) this.infoPath.child(key).exists(),
+
+    remove: function remove(key) {
+        if (this.exists(key)) {
+            if (this[key] && this[key].timer)
+                this[key].timer.flush();
+            delete this[key];
+            delete this.keys[key];
+            this.infoPath.child(key).remove(false);
+        }
+    },
 
     newObject: function newObject(key, constructor, params) {
         if (params == null || !isObject(params))
             throw Error("Invalid argument type");
 
-        if (!(key in this.keys) || params.reload || this.alwaysReload[key]) {
-            if (key in this && !(params.reload || this.alwaysReload[key]))
-                throw Error();
-            let load = function () loadData(key, params.store, params.type || myObject);
+        if (this.isLocalModule) {
+            this.globalInstance.newObject.apply(this.globalInstance, arguments);
+
+            if (!(key in this.keys) && this.privateMode && key in this.globalInstance.keys) {
+                let obj = this.globalInstance.keys[key];
+                this.keys[key] = this._privatize(obj);
+            }
+
+            return this.keys[key];
+        }
+
+        let reload = params.reload || this.alwaysReload[key];
+        if (!(key in this.keys) || reload) {
+            if (key in this && !reload)
+                throw Error("Cannot add storage key with that name.");
+
+            let load = () => this._loadData(key, params.store, params.type || myObject);
 
             this.keys[key] = new constructor(key, params.store, load, params);
-            this.keys[key].timer = new Timer(1000, 10000, function () storage.save(key));
+            this.keys[key].timer = new Timer(1000, 10000, () => this.save(key));
             this.__defineGetter__(key, function () this.keys[key]);
         }
         return this.keys[key];
@@ -201,33 +279,46 @@ var Storage = Module("Storage", {
 
     addObserver: function addObserver(key, callback, ref) {
         if (ref) {
-            if (!ref.dactylStorageRefs)
-                ref.dactylStorageRefs = [];
-            ref.dactylStorageRefs.push(callback);
-            var callbackRef = Cu.getWeakReference(callback);
+            let refs = overlay.getData(ref, "storage-refs");
+            refs.push(callback);
+            var callbackRef = util.weakReference(callback);
         }
         else {
             callbackRef = { get: function () callback };
         }
+
         this.removeDeadObservers();
+
         if (!(key in this.observers))
             this.observers[key] = [];
-        if (!this.observers[key].some(function (o) o.callback.get() == callback))
-            this.observers[key].push({ ref: ref && Cu.getWeakReference(ref), callback: callbackRef });
+
+        if (!this.observers[key].some(o => o.callback.get() == callback))
+            this.observers[key].push({ ref: ref && Cu.getWeakReference(ref),
+                                       callback: callbackRef });
     },
 
     removeObserver: function (key, callback) {
         this.removeDeadObservers();
+
         if (!(key in this.observers))
             return;
-        this.observers[key] = this.observers[key].filter(function (elem) elem.callback.get() != callback);
+
+        this.observers[key] = this.observers[key].filter(elem => elem.callback.get() != callback);
         if (this.observers[key].length == 0)
             delete obsevers[key];
     },
 
     removeDeadObservers: function () {
+        function filter(o) {
+            if (!o.callback.get())
+                return false;
+
+            let ref = o.ref && o.ref.get();
+            return ref && !ref.closed && overlay.getData(ref, "storage-refs", null);
+        }
+
         for (let [key, ary] in Iterator(this.observers)) {
-            this.observers[key] = ary = ary.filter(function (o) o.callback.get() && (!o.ref || o.ref.get() && o.ref.get().dactylStorageRefs));
+            this.observers[key] = ary = ary.filter(filter);
             if (!ary.length)
                 delete this.observers[key];
         }
@@ -235,11 +326,13 @@ var Storage = Module("Storage", {
 
     fireEvent: function fireEvent(key, event, arg) {
         this.removeDeadObservers();
+
         if (key in this.observers)
             // Safe, since we have our own Array object here.
             for each (let observer in this.observers[key])
                 observer.callback.get()(key, event, arg);
-        if (key in this.keys)
+
+        if (key in this.keys && this.keys[key].timer)
             this[key].timer.tell();
     },
 
@@ -250,37 +343,46 @@ var Storage = Module("Storage", {
 
     save: function save(key) {
         if (this[key])
-            saveData(this.keys[key]);
+            this._saveData(this.keys[key]);
     },
 
     saveAll: function storeAll() {
         for each (let obj in this.keys)
-            saveData(obj);
+            this._saveData(obj);
     },
 
     _privateMode: false,
     get privateMode() this._privateMode,
-    set privateMode(val) {
-        if (val && !this._privateMode)
+    set privateMode(enabled) {
+        this._privateMode = Boolean(enabled);
+
+        if (this.isLocalModule) {
             this.saveAll();
-        if (!val && this._privateMode)
-            for (let key in this.keys)
-                this.load(key);
-        return this._privateMode = Boolean(val);
-    }
+
+            if (!enabled)
+                delete this.keys;
+            else {
+                let { keys } = this;
+                this.keys = {};
+                for (let [k, v] in Iterator(keys))
+                    this.keys[k] = this._privatize(v);
+            }
+        }
+        return this._privateMode;
+    },
+
+    _privatize: function privatize(obj) {
+        if (obj.privateData && obj.clone)
+            return obj.clone(this);
+        return obj;
+    },
 }, {
     Replacer: {
         skipXpcom: function skipXpcom(key, val) val instanceof Ci.nsISupports ? null : val
     }
 }, {
-    init: function init(dactyl, modules) {
-        init.superapply(this, arguments);
-        storage.infoPath = File(modules.IO.runtimePath.replace(/,.*/, ""))
-                             .child("info").child(dactyl.profileName);
-    },
-
     cleanup: function (dactyl, modules, window) {
-        delete window.dactylStorageRefs;
+        overlay.setData(window, "storage-refs", null);
         this.removeDeadObservers();
     }
 });
@@ -292,12 +394,19 @@ var Storage = Module("Storage", {
  * @param {nsIFile|string} path Expanded according to {@link IO#expandPath}
  * @param {boolean} checkPWD Whether to allow expansion relative to the
  *          current directory. @default true
+ * @param {string} charset The charset of the file. @default File.defaultEncoding
  */
 var File = Class("File", {
-    init: function (path, checkPWD) {
+    init: function (path, checkPWD, charset) {
         let file = services.File();
 
-        if (path instanceof Ci.nsIFile)
+        if (charset)
+            this.charset = charset;
+
+        if (path instanceof Ci.nsIFileURL)
+            path = path.file;
+
+        if (path instanceof Ci.nsIFile || path instanceof File)
             file = path.clone();
         else if (/file:\/\//.test(path))
             file = services["file:"].getFileFromURLSpec(path);
@@ -315,20 +424,26 @@ var File = Class("File", {
                 return File.DoesNotExist(path, e);
             }
         }
-        let self = XPCSafeJSObjectWrapper(file.QueryInterface(Ci.nsILocalFile));
-        self.__proto__ = this;
-        return self;
+        this.file = file.QueryInterface(Ci.nsILocalFile);
+        return this;
     },
 
+    charset: Class.Memoize(() => File.defaultEncoding),
+
     /**
      * @property {nsIFileURL} Returns the nsIFileURL object for this file.
      */
-    get URI() services.io.newFileURI(this),
+    URI: Class.Memoize(function () {
+        let uri = services.io.newFileURI(this.file)
+                          .QueryInterface(Ci.nsIFileURL);
+        uri.QueryInterface(Ci.nsIMutable).mutable = false;
+        return uri;
+    }),
 
     /**
      * Iterates over the objects in this directory.
      */
-    iterDirectory: function () {
+    iterDirectory: function iterDirectory() {
         if (!this.exists())
             throw Error(_("io.noSuchFile"));
         if (!this.isDirectory())
@@ -340,26 +455,32 @@ var File = Class("File", {
     /**
      * Returns a new file for the given child of this directory entry.
      */
-    child: function (name) {
+    child: function child() {
         let f = this.constructor(this);
-        for each (let elem in name.split(File.pathSplit))
-            f.append(elem);
+        for (let [, name] in Iterator(arguments))
+            for each (let elem in name.split(File.pathSplit))
+                f.append(elem);
         return f;
     },
 
+    /**
+     * Returns an iterator for all lines in a file.
+     */
+    get lines() File.readLines(services.FileInStream(this.file, -1, 0, 0),
+                               this.charset),
+
     /**
      * Reads this file's entire contents in "text" mode and returns the
      * content as a string.
      *
      * @param {string} encoding The encoding from which to decode the file.
-     *          @default options["fileencoding"]
+     *          @default #charset
      * @returns {string}
      */
-    read: function (encoding) {
-        let ifstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(Ci.nsIFileInputStream);
-        ifstream.init(this, -1, 0, 0);
+    read: function read(encoding) {
+        let ifstream = services.FileInStream(this.file, -1, 0, 0);
 
-        return File.readStream(ifstream, encoding);
+        return File.readStream(ifstream, encoding || this.charset);
     },
 
     /**
@@ -369,13 +490,14 @@ var File = Class("File", {
      *     entries.
      * @returns {[nsIFile]}
      */
-    readDirectory: function (sort) {
+    readDirectory: function readDirectory(sort) {
         if (!this.isDirectory())
             throw Error(_("io.eNotDir"));
 
         let array = [e for (e in this.iterDirectory())];
         if (sort)
-            array.sort(function (a, b) b.isDirectory() - a.isDirectory() || String.localeCompare(a.path, b.path));
+            array.sort((a, b) => (b.isDirectory() - a.isDirectory() ||
+                                  String.localeCompare(a.path, b.path)));
         return array;
     },
 
@@ -384,7 +506,7 @@ var File = Class("File", {
      *
      * @returns {nsIFileURL}
      */
-    toURI: function toURI() services.io.newFileURI(this),
+    toURI: function toURI() services.io.newFileURI(this.file),
 
     /**
      * Writes the string *buf* to this file.
@@ -408,20 +530,17 @@ var File = Class("File", {
      *     permissions if the file exists.
      * @default 0644
      * @param {string} encoding The encoding to used to write the file.
-     * @default options["fileencoding"]
+     * @default #charset
      */
-    write: function (buf, mode, perms, encoding) {
-        let ofstream = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
+    write: function write(buf, mode, perms, encoding) {
         function getStream(defaultChar) {
-            let stream = Cc["@mozilla.org/intl/converter-output-stream;1"].createInstance(Ci.nsIConverterOutputStream);
-            stream.init(ofstream, encoding, 0, defaultChar);
-            return stream;
+            return services.ConvOutStream(ofstream, encoding, 0, defaultChar);
         }
         if (buf instanceof File)
             buf = buf.read();
 
         if (!encoding)
-            encoding = File.defaultEncoding;
+            encoding = this.charset;
 
         if (mode == ">>")
             mode = File.MODE_WRONLY | File.MODE_CREATE | File.MODE_APPEND;
@@ -433,7 +552,7 @@ var File = Class("File", {
         if (!this.exists()) // OCREAT won't create the directory
             this.create(this.NORMAL_FILE_TYPE, perms);
 
-        ofstream.init(this, mode, perms, 0);
+        let ofstream = services.FileOutStream(this.file, mode, perms, 0);
         try {
             var ocstream = getStream(0);
             ocstream.writeString(buf);
@@ -452,7 +571,34 @@ var File = Class("File", {
             ofstream.close();
         }
         return true;
-    }
+    },
+
+    // Wrapped native methods:
+    copyTo: function copyTo(dir, name)
+        this.file.copyTo(this.constructor(dir).file,
+                         name),
+
+    copyToFollowingLinks: function copyToFollowingLinks(dir, name)
+        this.file.copyToFollowingLinks(this.constructor(dir).file,
+                                       name),
+
+    moveTo: function moveTo(dir, name)
+        this.file.moveTo(this.constructor(dir).file,
+                         name),
+
+    equals: function equals(file)
+        this.file.equals(this.constructor(file).file),
+
+    contains: function contains(dir, recur)
+        this.file.contains(this.constructor(dir).file,
+                           recur),
+
+    getRelativeDescriptor: function getRelativeDescriptor(file)
+        this.file.getRelativeDescriptor(this.constructor(file).file),
+
+    setRelativeDescriptor: function setRelativeDescriptor(file, path)
+        this.file.setRelativeDescriptor(this.constructor(file).file,
+                                        path)
 }, {
     /**
      * @property {number} Open for reading only.
@@ -510,15 +656,15 @@ var File = Class("File", {
     /**
      * @property {string} The current platform's path separator.
      */
-    PATH_SEP: Class.memoize(function () {
+    PATH_SEP: Class.Memoize(function () {
         let f = services.directory.get("CurProcD", Ci.nsIFile);
         f.append("foo");
         return f.path.substr(f.parent.path.length, 1);
     }),
 
-    pathSplit: Class.memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
+    pathSplit: Class.Memoize(function () util.regexp("(?:/|" + util.regexp.escape(this.PATH_SEP) + ")", "g")),
 
-    DoesNotExist: function (path, error) ({
+    DoesNotExist: function DoesNotExist(path, error) ({
         path: path,
         exists: function () false,
         __noSuchMethod__: function () { throw error || Error("Does not exist"); }
@@ -541,7 +687,7 @@ var File = Class("File", {
      * @param {boolean} relative Whether the path is relative or absolute.
      * @returns {string}
      */
-    expandPath: function (path, relative) {
+    expandPath: function expandPath(path, relative) {
         function getenv(name) services.environment.get(name);
 
         // expand any $ENV vars - this is naive but so is Vim and we like to be compatible
@@ -549,10 +695,9 @@ var File = Class("File", {
         // Kris reckons we shouldn't replicate this 'bug'. --djk
         // TODO: should we be doing this for all paths?
         function expand(path) path.replace(
-            !win32 ? /\$(\w+)\b|\${(\w+)}/g
-                   : /\$(\w+)\b|\${(\w+)}|%(\w+)%/g,
-            function (m, n1, n2, n3) getenv(n1 || n2 || n3) || m
-        );
+            win32 ? /\$(\w+)\b|\${(\w+)}|%(\w+)%/g
+                  : /\$(\w+)\b|\${(\w+)}/g,
+            (m, n1, n2, n3) => (getenv(n1 || n2 || n3) || m));
         path = expand(path);
 
         // expand ~
@@ -577,11 +722,18 @@ var File = Class("File", {
 
     expandPathList: function (list) list.map(this.expandPath),
 
-    readStream: function (ifstream, encoding) {
+    readURL: function readURL(url, encoding) {
+        let channel = services.io.newChannel(url, null, null);
+        channel.contentType = "text/plain";
+        return this.readStream(channel.open(), encoding);
+    },
+
+    readStream: function readStream(ifstream, encoding) {
         try {
-            var icstream = Cc["@mozilla.org/intl/converter-input-stream;1"].createInstance(Ci.nsIConverterInputStream);
-            icstream.init(ifstream, encoding || File.defaultEncoding, 4096, // 4096 bytes buffering
-                          Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
+            var icstream = services.CharsetStream(
+                    ifstream, encoding || File.defaultEncoding, 4096, // buffer size
+                    services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
+
             let buffer = [];
             let str = {};
             while (icstream.readString(4096, str) != 0)
@@ -594,7 +746,23 @@ var File = Class("File", {
         }
     },
 
-    isAbsolutePath: function (path) {
+    readLines: function readLines(ifstream, encoding) {
+        try {
+            var icstream = services.CharsetStream(
+                    ifstream, encoding || File.defaultEncoding, 4096, // buffer size
+                    services.CharsetStream.DEFAULT_REPLACEMENT_CHARACTER);
+
+            var value = {};
+            while (icstream.readLine(value))
+                yield value.value;
+        }
+        finally {
+            icstream.close();
+            ifstream.close();
+        }
+    },
+
+    isAbsolutePath: function isAbsolutePath(path) {
         try {
             services.File().initWithPath(path);
             return true;
@@ -604,7 +772,7 @@ var File = Class("File", {
         }
     },
 
-    joinPaths: function (head, tail, cwd) {
+    joinPaths: function joinPaths(head, tail, cwd) {
         let path = this(head, cwd);
         try {
             // FIXME: should only expand environment vars and normalize path separators
@@ -619,8 +787,30 @@ var File = Class("File", {
     replacePathSep: function (path) path.replace("/", File.PATH_SEP, "g")
 });
 
+let (file = services.directory.get("ProfD", Ci.nsIFile)) {
+    Object.keys(file).forEach(function (prop) {
+        if (!(prop in File.prototype)) {
+            let isFunction;
+            try {
+                isFunction = callable(file[prop]);
+            }
+            catch (e) {}
+
+            if (isFunction)
+                File.prototype[prop] = util.wrapCallback(function wrapper() this.file[prop].apply(this.file, arguments));
+            else
+                Object.defineProperty(File.prototype, prop, {
+                    configurable: true,
+                    get: function wrap_get() this.file[prop],
+                    set: function wrap_set(val) { this.file[prop] = val; }
+                });
+        }
+    });
+    file = null;
+}
+
 endModule();
 
-// catch(e){dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack);}
+// catch(e){ dump(e + "\n" + (e.stack || Error().stack)); Components.utils.reportError(e) }
 
-// vim: set fdm=marker sw=4 sts=4 et ft=javascript:
+// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: