UNPKG

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