Sat, 03 Jan 2015 20:18:00 +0100
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 };