Tue, 06 Jan 2015 21:39:09 +0100
Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.
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 #ifndef MERGED_COMPARTMENT
9 this.EXPORTED_SYMBOLS = [
10 "ProfileCreationTimeAccessor",
11 "ProfileMetadataProvider",
12 ];
14 const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
16 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
18 Cu.import("resource://gre/modules/Metrics.jsm");
20 #endif
22 const DEFAULT_PROFILE_MEASUREMENT_NAME = "age";
23 const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"};
25 Cu.import("resource://gre/modules/Promise.jsm");
26 Cu.import("resource://gre/modules/osfile.jsm")
27 Cu.import("resource://gre/modules/Task.jsm");
28 Cu.import("resource://gre/modules/Log.jsm");
29 Cu.import("resource://services-common/utils.js");
31 // Profile creation time access.
32 // This is separate from the provider to simplify testing and enable extraction
33 // to a shared location in the future.
34 this.ProfileCreationTimeAccessor = function(profile, log) {
35 this.profilePath = profile || OS.Constants.Path.profileDir;
36 if (!this.profilePath) {
37 throw new Error("No profile directory.");
38 }
39 this._log = log || {"debug": function (s) { dump(s + "\n"); }};
40 }
41 this.ProfileCreationTimeAccessor.prototype = {
42 /**
43 * There are three ways we can get our creation time:
44 *
45 * 1. From our own saved value (to avoid redundant work).
46 * 2. From the on-disk JSON file.
47 * 3. By calculating it from the filesystem.
48 *
49 * If we have to calculate, we write out the file; if we have
50 * to touch the file, we persist in-memory.
51 *
52 * @return a promise that resolves to the profile's creation time.
53 */
54 get created() {
55 if (this._created) {
56 return Promise.resolve(this._created);
57 }
59 function onSuccess(times) {
60 if (times && times.created) {
61 return this._created = times.created;
62 }
63 return onFailure.call(this, null, times);
64 }
66 function onFailure(err, times) {
67 return this.computeAndPersistTimes(times)
68 .then(function onSuccess(created) {
69 return this._created = created;
70 }.bind(this));
71 }
73 return this.readTimes()
74 .then(onSuccess.bind(this),
75 onFailure.bind(this));
76 },
78 /**
79 * Explicitly make `file`, a filename, a full path
80 * relative to our profile path.
81 */
82 getPath: function (file) {
83 return OS.Path.join(this.profilePath, file);
84 },
86 /**
87 * Return a promise which resolves to the JSON contents
88 * of the time file in this accessor's profile.
89 */
90 readTimes: function (file="times.json") {
91 return CommonUtils.readJSON(this.getPath(file));
92 },
94 /**
95 * Return a promise representing the writing of `contents`
96 * to `file` in the specified profile.
97 */
98 writeTimes: function (contents, file="times.json") {
99 return CommonUtils.writeJSON(contents, this.getPath(file));
100 },
102 /**
103 * Merge existing contents with a 'created' field, writing them
104 * to the specified file. Promise, naturally.
105 */
106 computeAndPersistTimes: function (existingContents, file="times.json") {
107 let path = this.getPath(file);
108 function onOldest(oldest) {
109 let contents = existingContents || {};
110 contents.created = oldest;
111 return this.writeTimes(contents, path)
112 .then(function onSuccess() {
113 return oldest;
114 });
115 }
117 return this.getOldestProfileTimestamp()
118 .then(onOldest.bind(this));
119 },
121 /**
122 * Traverse the contents of the profile directory, finding the oldest file
123 * and returning its creation timestamp.
124 */
125 getOldestProfileTimestamp: function () {
126 let self = this;
127 let oldest = Date.now() + 1000;
128 let iterator = new OS.File.DirectoryIterator(this.profilePath);
129 self._log.debug("Iterating over profile " + this.profilePath);
130 if (!iterator) {
131 throw new Error("Unable to fetch oldest profile entry: no profile iterator.");
132 }
134 function onEntry(entry) {
135 function onStatSuccess(info) {
136 // OS.File doesn't seem to be behaving. See Bug 827148.
137 // Let's do the best we can. This whole function is defensive.
138 let date = info.winBirthDate || info.macBirthDate;
139 if (!date || !date.getTime()) {
140 // OS.File will only return file creation times of any kind on Mac
141 // and Windows, where birthTime is defined.
142 // That means we're unable to function on Linux, so we use mtime
143 // instead.
144 self._log.debug("No birth date. Using mtime.");
145 date = info.lastModificationDate;
146 }
148 if (date) {
149 let timestamp = date.getTime();
150 self._log.debug("Using date: " + entry.path + " = " + date);
151 if (timestamp < oldest) {
152 oldest = timestamp;
153 }
154 }
155 }
157 function onStatFailure(e) {
158 // Never mind.
159 self._log.debug("Stat failure: " + CommonUtils.exceptionStr(e));
160 }
162 return OS.File.stat(entry.path)
163 .then(onStatSuccess, onStatFailure);
164 }
166 let promise = iterator.forEach(onEntry);
168 function onSuccess() {
169 iterator.close();
170 return oldest;
171 }
173 function onFailure(reason) {
174 iterator.close();
175 throw new Error("Unable to fetch oldest profile entry: " + reason);
176 }
178 return promise.then(onSuccess, onFailure);
179 },
180 }
182 /**
183 * Measurements pertaining to the user's profile.
184 */
185 function ProfileMetadataMeasurement() {
186 Metrics.Measurement.call(this);
187 }
188 ProfileMetadataMeasurement.prototype = {
189 __proto__: Metrics.Measurement.prototype,
191 name: DEFAULT_PROFILE_MEASUREMENT_NAME,
192 version: 1,
194 fields: {
195 // Profile creation date. Number of days since Unix epoch.
196 profileCreation: {type: Metrics.Storage.FIELD_LAST_NUMERIC},
197 },
198 };
200 /**
201 * Turn a millisecond timestamp into a day timestamp.
202 *
203 * @param msec a number of milliseconds since epoch.
204 * @return the number of whole days denoted by the input.
205 */
206 function truncate(msec) {
207 return Math.floor(msec / MILLISECONDS_PER_DAY);
208 }
210 /**
211 * A Metrics.Provider for profile metadata, such as profile creation time.
212 */
213 this.ProfileMetadataProvider = function() {
214 Metrics.Provider.call(this);
215 }
216 this.ProfileMetadataProvider.prototype = {
217 __proto__: Metrics.Provider.prototype,
219 name: "org.mozilla.profile",
221 measurementTypes: [ProfileMetadataMeasurement],
223 pullOnly: true,
225 getProfileCreationDays: function () {
226 let accessor = new ProfileCreationTimeAccessor(null, this._log);
228 return accessor.created
229 .then(truncate);
230 },
232 collectConstantData: function () {
233 let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME, 1);
235 return Task.spawn(function collectConstant() {
236 let createdDays = yield this.getProfileCreationDays();
238 yield this.enqueueStorageOperation(function storeDays() {
239 return m.setLastNumeric("profileCreation", createdDays);
240 });
241 }.bind(this));
242 },
243 };