# 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 |
25 | var 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 |
32 | var Serializable = require('..').Serializable,
33 | promise = require('..').promise,
34 | _base = require('../_base'),
35 | io = require('../io'),
36 | extension = require('./extension');
37 |
38 |
39 | /** @const */
40 | var WEBDRIVER_PREFERENCES_PATH = _base.isDevMode()
41 | ? path.join(__dirname, '../../../firefox-driver/webdriver.json')
42 | : path.join(__dirname, '../lib/firefox/webdriver.json');
43 |
44 | /** @const */
45 | var 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 */
51 | var WEBDRIVER_EXTENSION_NAME = 'fxdriver@googlecode.com';
52 |
53 |
54 |
55 | /** @type {Object} */
56 | var defaultPreferences = null;
57 |
58 | /**
59 | * Synchronously loads the default preferences used for the FirefoxDriver.
60 | * @return {!Object} The default preferences JSON object.
61 | */
62 | function 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 | */
78 | function 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 | */
110 | function 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 | */
125 | function 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 | */
157 | function 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 | */
203 | function 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 | */
228 | var 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 | };
249 | util.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 | */
257 | Profile.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 | */
268 | Profile.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 | */
287 | Profile.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 | */
297 | Profile.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 | */
306 | Profile.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 | */
315 | Profile.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 | */
325 | Profile.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 | */
334 | Profile.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 | */
343 | Profile.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 | */
352 | Profile.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 | */
361 | Profile.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 | */
375 | Profile.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 | */
403 | Profile.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 | */
423 | Profile.prototype.serialize = function() {
424 | return this.encode();
425 | };
426 |
427 |
428 | // PUBLIC API
429 |
430 |
431 | exports.Profile = Profile;
432 | exports.decode = decode;
433 | exports.loadUserPrefs = loadUserPrefs;