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