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