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 | ;
|
24 |
|
25 | const AdmZip = require('adm-zip'),
|
26 | fs = require('fs'),
|
27 | path = require('path'),
|
28 | vm = require('vm');
|
29 |
|
30 | const isDevMode = require('../lib/devmode'),
|
31 | Symbols = require('../lib/symbols'),
|
32 | io = require('../io'),
|
33 | extension = require('./extension');
|
34 |
|
35 |
|
36 | /** @const */
|
37 | const WEBDRIVER_PREFERENCES_PATH = isDevMode
|
38 | ? path.join(__dirname, '../../../firefox-driver/webdriver.json')
|
39 | : path.join(__dirname, '../lib/firefox/webdriver.json');
|
40 |
|
41 | /** @const */
|
42 | const 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 */
|
48 | const 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 = /** @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 | */
|
76 | function 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 | */
|
106 | function 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 | */
|
138 | function 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 | */
|
174 | function 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 | */
|
194 | class 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 |
|
428 | exports.Profile = Profile;
|
429 | exports.decode = decode;
|
430 | exports.loadUserPrefs = loadUserPrefs;
|