// Licensed to the Software Freedom Conservancy (SFC) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The SFC licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. /** * @fileoverview Profile management module. This module is considered internal; * users should use {@link selenium-webdriver/firefox}. */ 'use strict'; const fs = require('fs'), path = require('path'), vm = require('vm'); const isDevMode = require('../lib/devmode'), Symbols = require('../lib/symbols'), io = require('../io'), {Zip, unzip} = require('../io/zip'), extension = require('./extension'); /** * Parses a user.js file in a Firefox profile directory. * @param {string} f Path to the file to parse. * @return {!Promise<!Object>} A promise for the parsed preferences as * a JSON object. If the file does not exist, an empty object will be * returned. */ function loadUserPrefs(f) { return io.read(f).then( function onSuccess(contents) { var prefs = {}; var context = vm.createContext({ 'user_pref': function(key, value) { prefs[key] = value; } }); vm.runInContext(contents.toString(), context, f); return prefs; }, function onError(err) { if (err && err.code === 'ENOENT') { return {}; } throw err; }); } /** * @param {!Object} prefs The default preferences to write. Will be * overridden by user.js preferences in the template directory and the * frozen preferences required by WebDriver. * @param {string} dir Path to the directory write the file to. * @return {!Promise<string>} A promise for the profile directory, * to be fulfilled when user preferences have been written. */ function writeUserPrefs(prefs, dir) { var userPrefs = path.join(dir, 'user.js'); return loadUserPrefs(userPrefs).then(function(overrides) { Object.assign(prefs, overrides); let keys = Object.keys(prefs); if (!keys.length) { return dir; } let contents = Object.keys(prefs).map(function(key) { return 'user_pref(' + JSON.stringify(key) + ', ' + JSON.stringify(prefs[key]) + ');'; }).join('\n'); return new Promise((resolve, reject) => { fs.writeFile(userPrefs, contents, function(err) { err && reject(err) || resolve(dir); }); }); }); }; /** * Installs a group of extensions in the given profile directory. If the * WebDriver extension is not included in this set, the default version * bundled with this package will be installed. * @param {!Array.<string>} extensions The extensions to install, as a * path to an unpacked extension directory or a path to a xpi file. * @param {string} dir The profile directory to install to. * @return {!Promise<string>} A promise for the main profile directory * once all extensions have been installed. */ function installExtensions(extensions, dir) { var next = 0; var extensionDir = path.join(dir, 'extensions'); return new Promise(function(fulfill, reject) { io.mkdir(extensionDir).then(installNext, reject); function installNext() { if (next >= extensions.length) { fulfill(dir); } else { install(extensions[next++]); } } function install(ext) { extension.install(ext, extensionDir).then(function(id) { installNext(); }, reject); } }); } /** * Models a Firefox profile directory for use with the FirefoxDriver. The * {@code Profile} directory uses an in-memory model until * {@link #writeToDisk} or {@link #encode} is called. */ class Profile { /** * @param {string=} opt_dir Path to an existing Firefox profile directory to * use a template for this profile. If not specified, a blank profile will * be used. */ constructor(opt_dir) { /** @private {!Object} */ this.preferences_ = {}; /** @private {(string|undefined)} */ this.template_ = opt_dir; /** @private {!Array<string>} */ this.extensions_ = []; } /** * @return {(string|undefined)} Path to an existing Firefox profile directory * to use as a template when writing this Profile to disk. */ getTemplateDir() { return this.template_; } /** * Registers an extension to be included with this profile. * @param {string} extension Path to the extension to include, as either an * unpacked extension directory or the path to a xpi file. */ addExtension(extension) { this.extensions_.push(extension); } /** * @return {!Array<string>} A list of extensions to install in this profile. */ getExtensions() { return this.extensions_; } /** * Sets a desired preference for this profile. * @param {string} key The preference key. * @param {(string|number|boolean)} value The preference value. * @throws {Error} If attempting to set a frozen preference. */ setPreference(key, value) { this.preferences_[key] = value; } /** * Returns the currently configured value of a profile preference. This does * not include any defaults defined in the profile's template directory user.js * file (if a template were specified on construction). * @param {string} key The desired preference. * @return {(string|number|boolean|undefined)} The current value of the * requested preference. */ getPreference(key) { return this.preferences_[key]; } /** * @return {!Object} A copy of all currently configured preferences. */ getPreferences() { return Object.assign({}, this.preferences_); } /** * Specifies which host the driver should listen for commands on. If not * specified, the driver will default to "localhost". This option should be * specified when "localhost" is not mapped to the loopback address * (127.0.0.1) in `/etc/hosts`. * * @param {string} host the host the driver should listen for commands on */ setHost(host) { this.preferences_['webdriver_firefox_allowed_hosts'] = host; } /** * @return {boolean} Whether the FirefoxDriver is configured to automatically * accept untrusted SSL certificates. */ acceptUntrustedCerts() { return !!this.preferences_['webdriver_accept_untrusted_certs']; } /** * Sets whether the FirefoxDriver should automatically accept untrusted SSL * certificates. * @param {boolean} value . */ setAcceptUntrustedCerts(value) { this.preferences_['webdriver_accept_untrusted_certs'] = !!value; } /** * Sets whether to assume untrusted certificates come from untrusted issuers. * @param {boolean} value . */ setAssumeUntrustedCertIssuer(value) { this.preferences_['webdriver_assume_untrusted_issuer'] = !!value; } /** * @return {boolean} Whether to assume untrusted certs come from untrusted * issuers. */ assumeUntrustedCertIssuer() { return !!this.preferences_['webdriver_assume_untrusted_issuer']; } /** * Writes this profile to disk. * @return {!Promise<string>} A promise for the path to the new profile * directory. */ writeToDisk() { var profileDir = io.tmpDir(); if (this.template_) { profileDir = profileDir.then(function(dir) { return io.copyDir( /** @type {string} */(this.template_), dir, /(parent\.lock|lock|\.parentlock)/); }.bind(this)); } // Freeze preferences for async operations. let prefs = Object.assign({}, this.preferences_); // Freeze extensions for async operations. var extensions = this.extensions_.concat(); return profileDir.then(function(dir) { return writeUserPrefs(prefs, dir); }).then(function(dir) { return installExtensions(extensions, dir); }); } /** * Write profile to disk, compress its containing directory, and return * it as a Base64 encoded string. * * @return {!Promise<string>} A promise for the encoded profile as * Base64 string. * */ encode() { return this.writeToDisk().then(function(dir) { let zip = new Zip; return zip.addDir(dir) .then(() => zip.toBuffer()) .then(buf => buf.toString('base64')); }); } /** * Encodes this profile as a zipped, base64 encoded directory. * @return {!Promise<string>} A promise for the encoded profile. */ [Symbols.serialize]() { return this.encode(); } } // PUBLIC API exports.Profile = Profile; exports.loadUserPrefs = loadUserPrefs;