firefox/profile.js

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