# firefox/profile.js

    1// Copyright 2014 Selenium committers2// Copyright 2014 Software Freedom Conservancy3//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 at7//8// http://www.apache.org/licenses/LICENSE-2.09//10// Unless required by applicable law or agreed to in writing, software11// 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 and14// limitations under the License.1516'use strict';1718var AdmZip = require('adm-zip'),19    fs = require('fs'),20    path = require('path'),21    vm = require('vm');2223var promise = require('..').promise,24    _base = require('../_base'),25    io = require('../io'),26    extension = require('./extension');272829/** @const */30var WEBDRIVER_PREFERENCES_PATH = _base.isDevMode()31    ? path.join(__dirname, '../../../firefox-driver/webdriver.json')32    : path.join(__dirname, '../lib/firefox/webdriver.json');3334/** @const */35var WEBDRIVER_EXTENSION_PATH = _base.isDevMode()36    ? path.join(__dirname,37        '../../../../build/javascript/firefox-driver/webdriver.xpi')38    : path.join(__dirname, '../lib/firefox/webdriver.xpi');3940/** @const */41var WEBDRIVER_EXTENSION_NAME = 'fxdriver@googlecode.com';42434445/** @type {Object} */46var defaultPreferences = null;4748/**49 * Synchronously loads the default preferences used for the FirefoxDriver.50 * @return {!Object} The default preferences JSON object.51 */52function getDefaultPreferences() {53  if (!defaultPreferences) {54    var contents = fs.readFileSync(WEBDRIVER_PREFERENCES_PATH, 'utf8');55    defaultPreferences = JSON.parse(contents);56  }57  return defaultPreferences;58}596061/**62 * Parses a user.js file in a Firefox profile directory.63 * @param {string} f Path to the file to parse.64 * @return {!promise.Promise.<!Object>} A promise for the parsed preferences as65 *     a JSON object. If the file does not exist, an empty object will be66 *     returned.67 */68function loadUserPrefs(f) {69  var done = promise.defer();70  fs.readFile(f, function(err, contents) {71    if (err && err.code === 'ENOENT') {72      done.fulfill({});73      return;74    }7576    if (err) {77      done.reject(err);78      return;79    }8081    var prefs = {};82    var context = vm.createContext({83      'user_pref': function(key, value) {84        prefs[key] = value;85      }86    });8788    vm.runInContext(contents, context, f);89    done.fulfill(prefs);90  });91  return done.promise;92}939495/**96 * Copies the properties of one object into another.97 * @param {!Object} a The destination object.98 * @param {!Object} b The source object to apply as a mixin.99 */100function mixin(a, b) {101  Object.keys(b).forEach(function(key) {102    a[key] = b[key];103  });104}105106107/**108 * @param {!Object} defaults The default preferences to write. Will be109 *     overridden by user.js preferences in the template directory and the110 *     frozen preferences required by WebDriver.111 * @param {string} dir Path to the directory write the file to.112 * @return {!promise.Promise.<string>} A promise for the profile directory,113 *     to be fulfilled when user preferences have been written.114 */115function writeUserPrefs(prefs, dir) {116  var userPrefs = path.join(dir, 'user.js');117  return loadUserPrefs(userPrefs).then(function(overrides) {118    mixin(prefs, overrides);119    mixin(prefs, getDefaultPreferences()['frozen']);120121    var contents = Object.keys(prefs).map(function(key) {122      return 'user_pref(' + JSON.stringify(key) + ', ' +123          JSON.stringify(prefs[key]) + ');';124    }).join('\n');125126    var done = promise.defer();127    fs.writeFile(userPrefs, contents, function(err) {128      err && done.reject(err) || done.fulfill(dir);129    });130    return done.promise;131  });132};133134135/**136 * Installs a group of extensions in the given profile directory. If the137 * WebDriver extension is not included in this set, the default version138 * bundled with this package will be installed.139 * @param {!Array.<string>} extensions The extensions to install, as a140 *     path to an unpacked extension directory or a path to a xpi file.141 * @param {string} dir The profile directory to install to.142 * @param {boolean=} opt_excludeWebDriverExt Whether to skip installation of143 *     the default WebDriver extension.144 * @return {!promise.Promise.<string>} A promise for the main profile directory145 *     once all extensions have been installed.146 */147function installExtensions(extensions, dir, opt_excludeWebDriverExt) {148  var hasWebDriver = !!opt_excludeWebDriverExt;149  var next = 0;150  var extensionDir = path.join(dir, 'extensions');151  var done = promise.defer();152153  return io.exists(extensionDir).then(function(exists) {154    if (!exists) {155      return promise.checkedNodeCall(fs.mkdir, extensionDir);156    }157  }).then(function() {158    installNext();159    return done.promise;160  });161162  function installNext() {163    if (!done.isPending()) {164      return;165    }166167    if (next >= extensions.length) {168      if (hasWebDriver) {169        done.fulfill(dir);170      } else {171        install(WEBDRIVER_EXTENSION_PATH);172      }173    } else {174      install(extensions[next++]);175    }176  }177178  function install(ext) {179    extension.install(ext, extensionDir).then(function(id) {180      hasWebDriver = hasWebDriver || (id === WEBDRIVER_EXTENSION_NAME);181      installNext();182    }, done.reject);183 }184}185186187/**188 * Decodes a base64 encoded profile.189 * @param {string} data The base64 encoded string.190 * @return {!promise.Promise.<string>} A promise for the path to the decoded191 *     profile directory.192 */193function decode(data) {194  return io.tmpFile().then(function(file) {195    var buf = new Buffer(data, 'base64');196    return promise.checkedNodeCall(fs.writeFile, file, buf).then(function() {197      return io.tmpDir();198    }).then(function(dir) {199      var zip = new AdmZip(file);200      zip.extractAllTo(dir);  // Sync only? Why?? :-(201      return dir;202    });203  });204}205206207208/**209 * Models a Firefox proifle directory for use with the FirefoxDriver. The210 * {@code Proifle} directory uses an in-memory model until {@link #writeToDisk}211 * is called.212 * @param {string=} opt_dir Path to an existing Firefox profile directory to213 *     use a template for this profile. If not specified, a blank profile will214 *     be used.215 * @constructor216 */217var Profile = function(opt_dir) {218  /** @private {!Object} */219  this.preferences_ = {};220221  mixin(this.preferences_, getDefaultPreferences()['mutable']);222  mixin(this.preferences_, getDefaultPreferences()['frozen']);223224  /** @private {boolean} */225  this.nativeEventsEnabled_ = true;226227  /** @private {(string|undefined)} */228  this.template_ = opt_dir;229230  /** @private {number} */231  this.port_ = 0;232233  /** @private {!Array.<string>} */234  this.extensions_ = [];235};236237238/**239 * Registers an extension to be included with this profile.240 * @param {string} extension Path to the extension to include, as either an241 *     unpacked extension directory or the path to a xpi file.242 */243Profile.prototype.addExtension = function(extension) {244  this.extensions_.push(extension);245};246247248/**249 * Sets a desired preference for this profile.250 * @param {string} key The preference key.251 * @param {(string|number|boolean)} value The preference value.252 * @throws {Error} If attempting to set a frozen preference.253 */254Profile.prototype.setPreference = function(key, value) {255  var frozen = getDefaultPreferences()['frozen'];256  if (frozen.hasOwnProperty(key) && frozen[key] !== value) {257    throw Error('You may not set ' + key + '=' + JSON.stringify(value)258        + '; value is frozen for proper WebDriver functionality ('259        + key + '=' + JSON.stringify(frozen[key]) + ')');260  }261  this.preferences_[key] = value;262};263264265/**266 * Returns the currently configured value of a profile preference. This does267 * not include any defaults defined in the profile's template directory user.js268 * file (if a template were specified on construction).269 * @param {string} key The desired preference.270 * @return {(string|number|boolean|undefined)} The current value of the271 *     requested preference.272 */273Profile.prototype.getPreference = function(key) {274  return this.preferences_[key];275};276277278/**279 * @return {number} The port this profile is currently configured to use, or280 *     0 if the port will be selected at random when the profile is written281 *     to disk.282 */283Profile.prototype.getPort = function() {284  return this.port_;285};286287288/**289 * Sets the port to use for the WebDriver extension loaded by this profile.290 * @param {number} port The desired port, or 0 to use any free port.291 */292Profile.prototype.setPort = function(port) {293  this.port_ = port;294};295296297/**298 * @return {boolean} Whether the FirefoxDriver is configured to automatically299 *     accept untrusted SSL certificates.300 */301Profile.prototype.acceptUntrustedCerts = function() {302  return !!this.preferences_['webdriver_accept_untrusted_certs'];303};304305306/**307 * Sets whether the FirefoxDriver should automatically accept untrusted SSL308 * certificates.309 * @param {boolean} value .310 */311Profile.prototype.setAcceptUntrustedCerts = function(value) {312  this.preferences_['webdriver_accept_untrusted_certs'] = !!value;313};314315316/**317 * Sets whether to assume untrusted certificates come from untrusted issuers.318 * @param {boolean} value .319 */320Profile.prototype.setAssumeUntrustedCertIssuer = function(value) {321  this.preferences_['webdriver_assume_untrusted_issuer'] = !!value;322};323324325/**326 * @return {boolean} Whether to assume untrusted certs come from untrusted327 *     issuers.328 */329Profile.prototype.assumeUntrustedCertIssuer = function() {330  return !!this.preferences_['webdriver_assume_untrusted_issuer'];331};332333334/**335 * Sets whether to use native events with this profile.336 * @param {boolean} enabled .337 */338Profile.prototype.setNativeEventsEnabled = function(enabled) {339  this.nativeEventsEnabled_ = enabled;340};341342343/**344 * Returns whether native events are enabled in this profile.345 * @return {boolean} .346 */347Profile.prototype.nativeEventsEnabled = function() {348  return this.nativeEventsEnabled_;349};350351352/**353 * Writes this profile to disk.354 * @param {boolean=} opt_excludeWebDriverExt Whether to exclude the WebDriver355 *     extension from the generated profile. Used to reduce the size of an356 *     {@link #encode() encoded profile} since the server will always install357 *     the extension itself.358 * @return {!promise.Promise.<string>} A promise for the path to the new359 *     profile directory.360 */361Profile.prototype.writeToDisk = function(opt_excludeWebDriverExt) {362  var profileDir = io.tmpDir();363  if (this.template_) {364    profileDir = profileDir.then(function(dir) {365      return io.copyDir(366          this.template_, dir, /(parent\.lock|lock|\.parentlock)/);367    }.bind(this));368  }369370  // Freeze preferences for async operations.371  var prefs = {};372  mixin(prefs, this.preferences_);373374  // Freeze extensions for async operations.375  var extensions = this.extensions_.concat();376377  return profileDir.then(function(dir) {378    return writeUserPrefs(prefs, dir);379  }).then(function(dir) {380    return installExtensions(extensions, dir, !!opt_excludeWebDriverExt);381  });382};383384385/**386 * Encodes this profile as a zipped, base64 encoded directory.387 * @return {!promise.Promise.<string>} A promise for the encoded profile.388 */389Profile.prototype.encode = function() {390  return this.writeToDisk(true).then(function(dir) {391    var zip = new AdmZip();392    zip.addLocalFolder(dir, '');393    return io.tmpFile().then(function(file) {394      zip.writeZip(file);  // Sync! Why oh why :-(395      return promise.checkedNodeCall(fs.readFile, file);396    });397  }).then(function(data) {398    return new Buffer(data).toString('base64');399  });400};401402403// PUBLIC API404405406exports.Profile = Profile;407exports.decode = decode;408exports.loadUserPrefs = loadUserPrefs;