firefox/profile.js

1// Copyright 2014 Selenium committers
2// Copyright 2014 Software Freedom Conservancy
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16'use strict';
17
18var AdmZip = require('adm-zip'),
19 fs = require('fs'),
20 path = require('path'),
21 vm = require('vm');
22
23var promise = require('..').promise,
24 _base = require('../_base'),
25 io = require('../io'),
26 extension = require('./extension');
27
28
29/** @const */
30var WEBDRIVER_PREFERENCES_PATH = _base.isDevMode()
31 ? path.join(__dirname, '../../../firefox-driver/webdriver.json')
32 : path.join(__dirname, '../lib/firefox/webdriver.json');
33
34/** @const */
35var WEBDRIVER_EXTENSION_PATH = _base.isDevMode()
36 ? path.join(__dirname,
37 '../../../../build/javascript/firefox-driver/webdriver.xpi')
38 : path.join(__dirname, '../lib/firefox/webdriver.xpi');
39
40/** @const */
41var WEBDRIVER_EXTENSION_NAME = 'fxdriver@googlecode.com';
42
43
44
45/** @type {Object} */
46var defaultPreferences = null;
47
48/**
49 * Synchronously loads the default preferences used for the FirefoxDriver.
50 * @return {!Object} The default preferences JSON object.
51 */
52function getDefaultPreferences() {
53 if (!defaultPreferences) {
54 var contents = fs.readFileSync(WEBDRIVER_PREFERENCES_PATH, 'utf8');
55 defaultPreferences = JSON.parse(contents);
56 }
57 return defaultPreferences;
58}
59
60
61/**
62 * Parses a user.js file in a Firefox profile directory.
63 * @param {string} f Path to the file to parse.
64 * @return {!promise.Promise.<!Object>} A promise for the parsed preferences as
65 * a JSON object. If the file does not exist, an empty object will be
66 * returned.
67 */
68function loadUserPrefs(f) {
69 var done = promise.defer();
70 fs.readFile(f, function(err, contents) {
71 if (err && err.code === 'ENOENT') {
72 done.fulfill({});
73 return;
74 }
75
76 if (err) {
77 done.reject(err);
78 return;
79 }
80
81 var prefs = {};
82 var context = vm.createContext({
83 'user_pref': function(key, value) {
84 prefs[key] = value;
85 }
86 });
87
88 vm.runInContext(contents, context, f);
89 done.fulfill(prefs);
90 });
91 return done.promise;
92}
93
94
95/**
96 * Copies the properties of one object into another.
97 * @param {!Object} a The destination object.
98 * @param {!Object} b The source object to apply as a mixin.
99 */
100function mixin(a, b) {
101 Object.keys(b).forEach(function(key) {
102 a[key] = b[key];
103 });
104}
105
106
107/**
108 * @param {!Object} defaults The default preferences to write. Will be
109 * overridden by user.js preferences in the template directory and the
110 * frozen preferences required by WebDriver.
111 * @param {string} dir Path to the directory write the file to.
112 * @return {!promise.Promise.<string>} A promise for the profile directory,
113 * to be fulfilled when user preferences have been written.
114 */
115function writeUserPrefs(prefs, dir) {
116 var userPrefs = path.join(dir, 'user.js');
117 return loadUserPrefs(userPrefs).then(function(overrides) {
118 mixin(prefs, overrides);
119 mixin(prefs, getDefaultPreferences()['frozen']);
120
121 var contents = Object.keys(prefs).map(function(key) {
122 return 'user_pref(' + JSON.stringify(key) + ', ' +
123 JSON.stringify(prefs[key]) + ');';
124 }).join('\n');
125
126 var done = promise.defer();
127 fs.writeFile(userPrefs, contents, function(err) {
128 err && done.reject(err) || done.fulfill(dir);
129 });
130 return done.promise;
131 });
132};
133
134
135/**
136 * Installs a group of extensions in the given profile directory. If the
137 * WebDriver extension is not included in this set, the default version
138 * bundled with this package will be installed.
139 * @param {!Array.<string>} extensions The extensions to install, as a
140 * path to an unpacked extension directory or a path to a xpi file.
141 * @param {string} dir The profile directory to install to.
142 * @param {boolean=} opt_excludeWebDriverExt Whether to skip installation of
143 * the default WebDriver extension.
144 * @return {!promise.Promise.<string>} A promise for the main profile directory
145 * once all extensions have been installed.
146 */
147function installExtensions(extensions, dir, opt_excludeWebDriverExt) {
148 var hasWebDriver = !!opt_excludeWebDriverExt;
149 var next = 0;
150 var extensionDir = path.join(dir, 'extensions');
151 var done = promise.defer();
152
153 return io.exists(extensionDir).then(function(exists) {
154 if (!exists) {
155 return promise.checkedNodeCall(fs.mkdir, extensionDir);
156 }
157 }).then(function() {
158 installNext();
159 return done.promise;
160 });
161
162 function installNext() {
163 if (!done.isPending()) {
164 return;
165 }
166
167 if (next >= extensions.length) {
168 if (hasWebDriver) {
169 done.fulfill(dir);
170 } else {
171 install(WEBDRIVER_EXTENSION_PATH);
172 }
173 } else {
174 install(extensions[next++]);
175 }
176 }
177
178 function install(ext) {
179 extension.install(ext, extensionDir).then(function(id) {
180 hasWebDriver = hasWebDriver || (id === WEBDRIVER_EXTENSION_NAME);
181 installNext();
182 }, done.reject);
183 }
184}
185
186
187/**
188 * Decodes a base64 encoded profile.
189 * @param {string} data The base64 encoded string.
190 * @return {!promise.Promise.<string>} A promise for the path to the decoded
191 * profile directory.
192 */
193function decode(data) {
194 return io.tmpFile().then(function(file) {
195 var buf = new Buffer(data, 'base64');
196 return promise.checkedNodeCall(fs.writeFile, file, buf).then(function() {
197 return io.tmpDir();
198 }).then(function(dir) {
199 var zip = new AdmZip(file);
200 zip.extractAllTo(dir); // Sync only? Why?? :-(
201 return dir;
202 });
203 });
204}
205
206
207
208/**
209 * Models a Firefox proifle directory for use with the FirefoxDriver. The
210 * {@code Proifle} directory uses an in-memory model until {@link #writeToDisk}
211 * is called.
212 * @param {string=} opt_dir Path to an existing Firefox profile directory to
213 * use a template for this profile. If not specified, a blank profile will
214 * be used.
215 * @constructor
216 */
217var Profile = function(opt_dir) {
218 /** @private {!Object} */
219 this.preferences_ = {};
220
221 mixin(this.preferences_, getDefaultPreferences()['mutable']);
222 mixin(this.preferences_, getDefaultPreferences()['frozen']);
223
224 /** @private {boolean} */
225 this.nativeEventsEnabled_ = true;
226
227 /** @private {(string|undefined)} */
228 this.template_ = opt_dir;
229
230 /** @private {number} */
231 this.port_ = 0;
232
233 /** @private {!Array.<string>} */
234 this.extensions_ = [];
235};
236
237
238/**
239 * Registers an extension to be included with this profile.
240 * @param {string} extension Path to the extension to include, as either an
241 * unpacked extension directory or the path to a xpi file.
242 */
243Profile.prototype.addExtension = function(extension) {
244 this.extensions_.push(extension);
245};
246
247
248/**
249 * Sets a desired preference for this profile.
250 * @param {string} key The preference key.
251 * @param {(string|number|boolean)} value The preference value.
252 * @throws {Error} If attempting to set a frozen preference.
253 */
254Profile.prototype.setPreference = function(key, value) {
255 var frozen = getDefaultPreferences()['frozen'];
256 if (frozen.hasOwnProperty(key) && frozen[key] !== value) {
257 throw Error('You may not set ' + key + '=' + JSON.stringify(value)
258 + '; value is frozen for proper WebDriver functionality ('
259 + key + '=' + JSON.stringify(frozen[key]) + ')');
260 }
261 this.preferences_[key] = value;
262};
263
264
265/**
266 * Returns the currently configured value of a profile preference. This does
267 * not include any defaults defined in the profile's template directory user.js
268 * file (if a template were specified on construction).
269 * @param {string} key The desired preference.
270 * @return {(string|number|boolean|undefined)} The current value of the
271 * requested preference.
272 */
273Profile.prototype.getPreference = function(key) {
274 return this.preferences_[key];
275};
276
277
278/**
279 * @return {number} The port this profile is currently configured to use, or
280 * 0 if the port will be selected at random when the profile is written
281 * to disk.
282 */
283Profile.prototype.getPort = function() {
284 return this.port_;
285};
286
287
288/**
289 * Sets the port to use for the WebDriver extension loaded by this profile.
290 * @param {number} port The desired port, or 0 to use any free port.
291 */
292Profile.prototype.setPort = function(port) {
293 this.port_ = port;
294};
295
296
297/**
298 * @return {boolean} Whether the FirefoxDriver is configured to automatically
299 * accept untrusted SSL certificates.
300 */
301Profile.prototype.acceptUntrustedCerts = function() {
302 return !!this.preferences_['webdriver_accept_untrusted_certs'];
303};
304
305
306/**
307 * Sets whether the FirefoxDriver should automatically accept untrusted SSL
308 * certificates.
309 * @param {boolean} value .
310 */
311Profile.prototype.setAcceptUntrustedCerts = function(value) {
312 this.preferences_['webdriver_accept_untrusted_certs'] = !!value;
313};
314
315
316/**
317 * Sets whether to assume untrusted certificates come from untrusted issuers.
318 * @param {boolean} value .
319 */
320Profile.prototype.setAssumeUntrustedCertIssuer = function(value) {
321 this.preferences_['webdriver_assume_untrusted_issuer'] = !!value;
322};
323
324
325/**
326 * @return {boolean} Whether to assume untrusted certs come from untrusted
327 * issuers.
328 */
329Profile.prototype.assumeUntrustedCertIssuer = function() {
330 return !!this.preferences_['webdriver_assume_untrusted_issuer'];
331};
332
333
334/**
335 * Sets whether to use native events with this profile.
336 * @param {boolean} enabled .
337 */
338Profile.prototype.setNativeEventsEnabled = function(enabled) {
339 this.nativeEventsEnabled_ = enabled;
340};
341
342
343/**
344 * Returns whether native events are enabled in this profile.
345 * @return {boolean} .
346 */
347Profile.prototype.nativeEventsEnabled = function() {
348 return this.nativeEventsEnabled_;
349};
350
351
352/**
353 * Writes this profile to disk.
354 * @param {boolean=} opt_excludeWebDriverExt Whether to exclude the WebDriver
355 * extension from the generated profile. Used to reduce the size of an
356 * {@link #encode() encoded profile} since the server will always install
357 * the extension itself.
358 * @return {!promise.Promise.<string>} A promise for the path to the new
359 * profile directory.
360 */
361Profile.prototype.writeToDisk = function(opt_excludeWebDriverExt) {
362 var profileDir = io.tmpDir();
363 if (this.template_) {
364 profileDir = profileDir.then(function(dir) {
365 return io.copyDir(
366 this.template_, dir, /(parent\.lock|lock|\.parentlock)/);
367 }.bind(this));
368 }
369
370 // Freeze preferences for async operations.
371 var prefs = {};
372 mixin(prefs, this.preferences_);
373
374 // Freeze extensions for async operations.
375 var extensions = this.extensions_.concat();
376
377 return profileDir.then(function(dir) {
378 return writeUserPrefs(prefs, dir);
379 }).then(function(dir) {
380 return installExtensions(extensions, dir, !!opt_excludeWebDriverExt);
381 });
382};
383
384
385/**
386 * Encodes this profile as a zipped, base64 encoded directory.
387 * @return {!promise.Promise.<string>} A promise for the encoded profile.
388 */
389Profile.prototype.encode = function() {
390 return this.writeToDisk(true).then(function(dir) {
391 var zip = new AdmZip();
392 zip.addLocalFolder(dir, '');
393 return io.tmpFile().then(function(file) {
394 zip.writeZip(file); // Sync! Why oh why :-(
395 return promise.checkedNodeCall(fs.readFile, file);
396 });
397 }).then(function(data) {
398 return new Buffer(data).toString('base64');
399 });
400};
401
402
403// PUBLIC API
404
405
406exports.Profile = Profile;
407exports.decode = decode;
408exports.loadUserPrefs = loadUserPrefs;