# 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 |
23 | var AdmZip = require('adm-zip'),
24 | fs = require('fs'),
25 | path = require('path'),
26 | util = require('util'),
27 | vm = require('vm');
28 |
29 | var Serializable = require('..').Serializable,
30 | promise = require('..').promise,
31 | _base = require('../_base'),
32 | io = require('../io'),
33 | extension = require('./extension');
34 |
35 |
36 | /** @const */
37 | var WEBDRIVER_PREFERENCES_PATH = _base.isDevMode()
38 | ? path.join(__dirname, '../../../firefox-driver/webdriver.json')
39 | : path.join(__dirname, '../lib/firefox/webdriver.json');
40 |
41 | /** @const */
42 | var 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 */
48 | var WEBDRIVER_EXTENSION_NAME = 'fxdriver@googlecode.com';
49 |
50 |
51 |
52 | /** @type {Object} */
53 | var defaultPreferences = null;
54 |
55 | /**
56 | * Synchronously loads the default preferences used for the FirefoxDriver.
57 | * @return {!Object} The default preferences JSON object.
58 | */
59 | function 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 | */
75 | function 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 | */
107 | function 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 | */
122 | function 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 | */
154 | function 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 | */
200 | function 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 | */
225 | var 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 | };
246 | util.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 | */
254 | Profile.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 | */
265 | Profile.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 | */
284 | Profile.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 | */
294 | Profile.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 | */
303 | Profile.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 | */
312 | Profile.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 | */
322 | Profile.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 | */
331 | Profile.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 | */
340 | Profile.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 | */
349 | Profile.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 | */
358 | Profile.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 | */
372 | Profile.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 | */
400 | Profile.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 | */
419 | Profile.prototype.serialize = function() {
420 | return this.encode();
421 | };
422 |
423 |
424 | // PUBLIC API
425 |
426 |
427 | exports.Profile = Profile;
428 | exports.decode = decode;
429 | exports.loadUserPrefs = loadUserPrefs;