UNPKG

firefox-profile

Version:

firefox profile for selenium WebDriverJs, admc/wd or any other node selenium driver that supports capabilities

711 lines (647 loc) 21.9 kB
/** * Firefox Profile */ 'use strict'; var os = require('os'), path = require('path'), fs = require('fs-extra'), // third-party parseString = require('xml2js').parseString, wrench = require('wrench'), AdmZip = require('adm-zip'), archiver = require('archiver'), uuid = require('node-uuid'), async = require('async'), uuid = require('node-uuid'), getID = require('jetpack-id'), _ = require('lodash'), Finder = require('./profile_finder'); var config = { // from python... Not used // WEBDRIVER_EXT: 'webdriver.xpi', // EXTENSION_NAME: 'fxdriver@googlecode.com', ANONYMOUS_PROFILE_NAME: 'WEBDRIVER_ANONYMOUS_PROFILE', DEFAULT_PREFERENCES: { 'app.update.auto': 'false', 'app.update.enabled': 'false', 'browser.download.manager.showWhenStarting': 'false', 'browser.EULA.override': 'true', 'browser.EULA.3.accepted': 'true', 'browser.link.open_external': '2', 'browser.link.open_newwindow': '2', 'browser.offline': 'false', 'browser.safebrowsing.enabled': 'false', 'browser.search.update': 'false', 'extensions.blocklist.enabled': 'false', 'browser.sessionstore.resume_from_crash': 'false', 'browser.shell.checkDefaultBrowser': 'false', 'browser.tabs.warnOnClose': 'false', 'browser.tabs.warnOnOpen': 'false', 'browser.startup.page': '0', 'browser.safebrowsing.malware.enabled': 'false', 'startup.homepage_welcome_url': '"about:blank"', 'devtools.errorconsole.enabled': 'true', 'dom.disable_open_during_load': 'false', 'extensions.autoDisableScopes' : 10, 'extensions.logging.enabled': 'true', 'extensions.update.enabled': 'false', 'extensions.update.notifyUser': 'false', 'network.manage-offline-status': 'false', 'network.http.max-connections-per-server': '10', 'network.http.phishy-userpass-length': '255', 'offline-apps.allow_by_default': 'true', 'prompts.tab_modal.enabled': 'false', 'security.fileuri.origin_policy': '3', 'security.fileuri.strict_origin_policy': 'false', 'security.warn_entering_secure': 'false', 'security.warn_entering_secure.show_once': 'false', 'security.warn_entering_weak': 'false', 'security.warn_entering_weak.show_once': 'false', 'security.warn_leaving_secure': 'false', 'security.warn_leaving_secure.show_once': 'false', 'security.warn_submit_insecure': 'false', 'security.warn_viewing_mixed': 'false', 'security.warn_viewing_mixed.show_once': 'false', 'signon.rememberSignons': 'false', 'toolkit.networkmanager.disable': 'true', 'toolkit.telemetry.enabled': 'false', 'toolkit.telemetry.prompted': '2', 'toolkit.telemetry.rejected': 'true', 'javascript.options.showInConsole': 'true', 'browser.dom.window.dump.enabled': 'true', 'webdriver_accept_untrusted_certs': 'true', 'webdriver_enable_native_events': 'true', 'webdriver_assume_untrusted_issuer': 'true', 'dom.max_script_run_time': '30', } }; function unprefix(root, node, prefix) { return root[prefix + ':' + node] || root[node]; } function parseOptions(opts) { if (_.isString(opts)) { return {profileDirectory: opts}; } return opts || {}; } /** * Initialize a new instance of a Firefox Profile. * * Note that this function uses filesystem sync functions to copy an existing profile (id profileDirectory is provided) * which is not optimized. * If you need optimzed async version, use `FirefoxProfile.copy(profileDirectory, cb);` * * @param {Object|String|null} options optional. * * * If it is an object, it can contain the following option: * * profileDirectory: the profile to copy. Not recommended: use FirefoxProfile.copy instead * * destinationDirectory: where the profile will be stored. If not provided, * a tmp directory will be used WARNING: if the tmp directory will be deleted when the process will terminate. * * if it is a string it will copy the directory synchronously * (not recommended at all, kept for backward compatibility). */ function FirefoxProfile(options) { var opts = parseOptions(options), hasDestDir = !!opts.destinationDirectory; this.profileDir = opts.profileDirectory; this.defaultPreferences = _.clone(config.DEFAULT_PREFERENCES); // if true, the profile folder is deleted after this._deleteOnExit = !hasDestDir; // can be turned to false when debugging this._deleteZippedProfile = true; this._preferencesModified = false; if (!this.profileDir) { this.profileDir = opts.destinationDirectory || this._createTempFolder(); } else { // create copy var tmpDir = opts.destinationDirectory || this._createTempFolder('-copy'); wrench.copyDirSyncRecursive(opts.profileDirectory, tmpDir, { forceDelete: true, preserveFiles: hasDestDir, filter: /^(parent\.lock|lock|\.parentlock)$/ // excludes parent.lock, lock, .parentlock }); this.profileDir = tmpDir; } this.extensionsDir = path.join(this.profileDir, 'extensions'); this.userPrefs = path.join(this.profileDir, 'user.js'); // delete on process.exit()... var self = this; this.onExit = function() { if (self._deleteOnExit) { self._cleanOnExit(); } }; ['exit', 'SIGINT'].forEach(function(event) { process.addListener(event, self.onExit); }); } function deleteParallel(files, cb) { async.parallel(files.map(function(file) { return function(next) { fs.unlink(file, next); }; }), function () { cb && cb(); }); } FirefoxProfile.prototype._copy = function(profileDirectory, cb) { var self = this; wrench.copyDirRecursive(profileDirectory, this.profileDir, { forceDelete: true, preserveFiles: true // filter: /^(parent\.lock|lock|\.parentlock)$/ // does not work, not implemented in wrench (async version) }, function() { // remove parent.lock, lock or .parentlock files if they have been copied deleteParallel(['parent.lock', 'lock', '.parentlock'].map(function(file) { return path.join(self.profileDir, file); }), cb); }); }; /** * creates a profile Profile from an existing firefox profile directory asynchronously * * @param {Object|String|null} options * * if it is an object, the following properties are available: * * profileDirectory - required - the profile to copy. * * destinationDirectory: where the profile will be stored. If not provided, * a tmp directory will be used. WARNING: if the tmp directory will be deleted when the process exits. */ FirefoxProfile.copy = function(options, cb) { var opts = parseOptions(options); if (!opts.profileDirectory) { cb && cb(new Error('firefoxProfile: .copy() requires profileDirectory option')); return; } var profile = new FirefoxProfile({destinationDirectory: opts.destinationDirectory}); profile._copy(opts.profileDirectory, function() { cb && cb(null, profile); }); }; /** * copy a profile from the current user profile * * @params {Object} opts an object with the following properties: * - name: property is mandatory. * - userProfilePath optional and passed to Finder constructor. * - destinationDirectory optional */ FirefoxProfile.copyFromUserProfile = function(opts, cb) { if (!opts.name) { cb && cb(new Error('firefoxProfile: .copyFromUserProfile() requires a name options')); return; } var finder = new Finder(opts.userProfilePath); finder.getPath(opts.name, function(err, profilePath) { if (err) { cb(err); return; } FirefoxProfile.copy({ destinationDirectory: opts.destinationDirectory, profileDirectory: profilePath }, cb); }); }; /** * Deletes the profile directory asynchronously. * * Call it only if you do not need the profile. Otherwise use at your own risk. * * @param cb a callback function with boolean parameter (false if the dir is not found) * that will be called when the profileDir is deleted */ FirefoxProfile.prototype.deleteDir = function(cb) { var self = this; ['exit', 'SIGINT'].forEach(function(event) { process.removeListener(event, self.onExit); }); this.shouldDeleteOnExit(false); fs.exists(this.profileDir, function(doesExists) { if (!doesExists) { cb && cb(); return; } wrench.rmdirRecursive(self.profileDir, false, function() { cb && cb(); }); }); }; /** * called on exit to delete the profile directory synchronously. * * this function is automatically called by default (= if willDeleteOnExit() returns true) if a tmp directory is used * * should not be called directly. process.on('exit') cannot be asynchronous: async code is not called * */ FirefoxProfile.prototype._cleanOnExit = function() { if (fs.existsSync(this.profileDir)) { try { wrench.rmdirSyncRecursive(this.profileDir, false); } catch (e) { console.warn('[firefox-profile] cannot delete profileDir on exit', this.profileDir, e); } } }; /** * Specify if the profile Directory should be deleted on process.exit() * * Note: by default: * * if the constructor is called without param: the new profile directory is deleted * * if the constructor is called with param (path to profile dir): the dir is copied at init and the copy is deleted on exit * * @param {boolean} true */ FirefoxProfile.prototype.shouldDeleteOnExit = function(bool) { this._deleteOnExit = bool; }; /** * returns true if the profile directory will be deleted on process.exit() * * @return {boolean} true if (default) */ FirefoxProfile.prototype.willDeleteOnExit = function() { return this._deleteOnExit; }; /** * Set a user preference. * * Any modification to the user preference can be persisted using this.updatePreferences() * If this.setPreference() is called before calling this.encoded(), then this.updatePreferences() * is automatically called. * For a comprehensive list of preference keys, see http://kb.mozillazine.org/About:config_entries * * @param {string} key - the user preference key * @param {boolean|string} value * @see about:config http://kb.mozillazine.org/About:config_entries */ FirefoxProfile.prototype.setPreference = function(key, value) { var cleanValue = ''; if (value === true) { cleanValue = 'true'; } else if (value === false) { cleanValue = 'false'; } else if (typeof(value) === 'string') { cleanValue = '"' + value.replace('\n', '\\n') + '"'; } else { cleanValue = parseInt(value, 10).toString(); } this.defaultPreferences[key] = cleanValue; this._preferencesModified = true; }; /** * Add an extension to the profile. * * @param {string} path - path to a xpi extension file or a unziped extension folder * @param {function} callback - the callback function to call when the extension is added */ FirefoxProfile.prototype.addExtension = function(extension, cb) { this._installExtension(extension, cb); }; /** * Add mutliple extensions to the profile. * * @param {Array} extensions - arrays of paths to xpi extension files or unziped extension folders * @param {function} callback - the callback function to call when the extension is added */ FirefoxProfile.prototype.addExtensions = function(extensions, cb) { var self = this, functions = extensions.map(function(extension) { return function(callback) { self.addExtension(path.normalize(extension), callback); }; }); async.parallel(functions, cb); }; /** * Save user preferences to the user.js profile file. * * updatePreferences() is automatically called when encoded() is called * (if needed = if setPreference() was called before calling encoded()) * */ FirefoxProfile.prototype.updatePreferences = function() { this._writeUserPrefs(this.defaultPreferences); }; /** * @return {string} path of the profile extension directory * */ FirefoxProfile.prototype.path = function () { return this.profileDir; }; /** * @return {boolean} true if webdriver can accept untrusted certificates * */ FirefoxProfile.prototype.canAcceptUntrustedCerts = function () { return this._sanitizePref(this.defaultPreferences['webdriver_accept_untrusted_certs']); }; /** * If not explicitly set, default: true * * @param {boolean} true to accept untrusted certificates, false otherwise. * */ FirefoxProfile.prototype.setAcceptUntrustedCerts = function (val) { this.defaultPreferences['webdriver_accept_untrusted_certs'] = val; }; /** * @return {boolean} true if webdriver can assume untrusted certificate issuer * */ FirefoxProfile.prototype.canAssumeUntrustedCertIssuer = function () { return this._sanitizePref(this.defaultPreferences['webdriver_assume_untrusted_issuer']); }; /** * If not explicitly set, default: true * * @param {boolean} true to make webdriver assume untrusted issuer. * */ FirefoxProfile.prototype.setAssumeUntrustedCertIssuer = function (val) { this.defaultPreferences['webdriver_assume_untrusted_issuer'] = val; }; /** * @return {boolean} true if native events are enabled * */ FirefoxProfile.prototype.nativeEventsEnabled = function () { return this._sanitizePref(this.defaultPreferences['webdriver_enable_native_events']); }; /** * If not explicitly set, default: true * * @param {boolean} boolean true to enable native events. * */ FirefoxProfile.prototype.setNativeEventsEnabled = function (val) { this.defaultPreferences['webdriver_enable_native_events'] = val; }; /** * return zipped, base64 encoded string of the profile directory * for use with remote WebDriver JSON wire protocol * * @param {Function} function a callback function with first params as a zipped, base64 encoded string of the profile directory */ FirefoxProfile.prototype.encoded = function(cb) { var self = this, tmpFolder = this._createTempFolder(), zipStream = fs.createWriteStream(path.join(tmpFolder,'profile.zip')), archive = archiver('zip', { forceUTC: true }); if (this._preferencesModified) { this.updatePreferences(); } zipStream.on('close', function() { fs.readFile(path.join(tmpFolder,'profile.zip'), function(err, content) { cb(content.toString('base64')); deleteParallel([path.join(tmpFolder,'profile.zip'), tmpFolder]); }); }); archive.pipe(zipStream); archive.bulk([ { cwd: self.profileDir, src: ['**'], expand: true } ]); archive.finalize(); }; // only '1' found in proxy.js var ffValues = { 'direct': 0, 'manual': 1, 'pac': 2, 'system': 3 }; /** * Set network proxy settings. * * The parameter `proxy` is a hash which structure depends on the value of mandatory `proxyType` key, * which takes one of the following string values: * * * `direct` - direct connection (no proxy) * * `system` - use operating system proxy settings * * `pac` - use automatic proxy configuration set based on the value of `autoconfigUrl` key * * `manual` - manual proxy settings defined separately for different protocols using values from following keys: * `ftpProxy`, `httpProxy`, `sslProxy`, `socksProxy` * * Examples: * * * set automatic proxy: * * profile.setProxy({ * proxyType: 'pac', * autoconfigUrl: 'http://myserver/proxy.pac' * }); * * * set manual http proxy: * * profile.setProxy({ * proxyType: 'manual', * httpProxy: '127.0.0.1:8080' * }); * * * set manual http and https proxy: * * profile.setProxy({ * proxyType: 'manual', * httpProxy: '127.0.0.1:8080', * sslProxy: '127.0.0.1:8080' * }); * * @param {Object} proxy a proxy hash, mandatory key `proxyType` */ FirefoxProfile.prototype.setProxy = function(proxy) { if (!proxy || !proxy.proxyType) { throw new Error('firefoxProfile: not a valid proxy type'); } this.setPreference('network.proxy.type', ffValues[proxy.proxyType]); switch (proxy.proxyType) { case 'manual': if (proxy.noProxy) { this.setPreference('network.proxy.no_proxies_on', proxy.noProxy); } this._setManualProxyPreference('ftp', proxy.ftpProxy); this._setManualProxyPreference('http', proxy.httpProxy); this._setManualProxyPreference('ssl', proxy.sslProxy); this._setManualProxyPreference('socks', proxy.socksProxy); break; case 'pac': this.setPreference('network.proxy.autoconfig_url', proxy.autoconfigUrl); break; } }; // private FirefoxProfile.prototype._writeUserPrefs = function(userPrefs) { var content = ''; Object.keys(userPrefs).forEach(function(val) { content = content + 'user_pref("' + val +'", ' + userPrefs[val] + ');\n'; }); fs.writeFileSync(this.userPrefs, content); // defaults to utf8 (node 0.8 compat) }; FirefoxProfile.prototype._readExistingUserjs = function() { var self = this, regExp = /user_pref\(['"](.*)["'],\s*['"]?(.*)["']?\)/, contentLines = fs.readFileSync(this.userPrefs, "utf8").split('\n'); contentLines.forEach(function(line) { var found = line.match(regExp); if (found) { self.defaultPreferences[found[1]] = found[2]; } }); }; FirefoxProfile.prototype._installExtension = function(addon, cb) { // from python... not needed. specify full path instead when calling addExtension // if (addon === config.WEBDRIVER_EXT) { // addon = path.join(__dirname, config.WEBDRIVER_EXT); // } var tmpDir = null, // to unzip xpi xpiFile = null, self = this; if (addon.slice(-4) === '.xpi') { tmpDir = this._createTempFolder(addon.split(path.sep).slice(-1)); var zip = new AdmZip(addon); zip.extractAllTo(tmpDir, true); xpiFile = addon; addon = tmpDir; } // find out the addon id this._addonDetails(addon, function(addonDetails) { var addonId = getID(addonDetails); var unpack = addonDetails.unpack === undefined ? true : addonDetails.unpack; if (!addonId) { cb(new Error('FirefoxProfile: the addon id could not be found!')); } var addonPath = path.join(self.extensionsDir, path.sep, addonId); async.series([ // creates extensionsDir function(next) { fs.exists(self.extensionsDir, function(exists) { if (!exists) { fs.mkdir(self.extensionsDir, function() { next(); }); return; } // already exists next(); }); }, function(next) { if (!unpack && xpiFile) { fs.copy(xpiFile, addonPath + '.xpi', function() { next(); }); } else { // copy it! fs.mkdir(addonPath, function() { wrench.copyDirRecursive(addon, addonPath, { forceDelete: true, preserveFiles: true, }, function() { next(); }); }); } }, function (next) { if (tmpDir) { wrench.rmdirRecursive(tmpDir, function() { next(); }); } else { next(); } } ], function() { // done! cb && cb(); }); }); }; FirefoxProfile.prototype._addonDetails = function(addonPath, cb) { var details = { 'id': null, 'name': null, 'unpack': true, 'version': null, 'isNative': false }, self = this; function getNamespaceId(doc, url) { var namespaces = doc[Object.keys(doc)[0]].$, pref = null ; Object.keys(namespaces).forEach(function(prefix) { if (namespaces[prefix] === url) { pref = prefix.replace('xmlns:', ''); return false; } }); return pref; } // Attempt to parse the `install.rdf` inside the extension var doc; try { doc = fs.readFileSync(path.join(addonPath, 'install.rdf')); } // If not found, this is probably a jetpack style addon, so parse // the `package.json` file for addon details catch (e) { var manifest = require(path.join(addonPath, 'package.json')); // Jetpack addons are packed by default details.unpack = false; details.isNative = true; Object.keys(details).forEach(function (prop) { if (manifest[prop] !== undefined) { details[prop] = manifest[prop]; } }); cb && cb(details); return; } parseString(doc, function (err, doc) { var em = getNamespaceId(doc, 'http://www.mozilla.org/2004/em-rdf#'), rdf = getNamespaceId(doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'); // first description var rdfNode = unprefix(doc, 'RDF', rdf); var description = unprefix(rdfNode, 'Description', rdf); if (description && description[0]) { description = description[0]; } Object.keys(description.$).forEach(function(attr) { if (details[attr.replace(em + ':', '')] !== undefined) { details[attr.replace(em + ':', '')] = description.$[attr]; } }); Object.keys(description).forEach(function(attr) { if (details[attr.replace(em + ':', '')] !== undefined) { // to convert boolean strings into booleans details[attr.replace(em + ':', '')] = self._sanitizePref(description[attr][0]); } }); cb && cb(details); }); }; FirefoxProfile.prototype._createTempFolder = function(suffix) { suffix = suffix || ''; var folderName = path.resolve(path.join(os.tmpDir(), uuid.v4() + suffix + path.sep)); fs.mkdirSync(folderName); return folderName; }; FirefoxProfile.prototype._sanitizePref = function(val) { if (val === 'true') { return true; } if (val === 'false') { return false; } else { return val; } }; FirefoxProfile.prototype._setManualProxyPreference = function(key, setting) { if (!setting || setting === '') { return; } var hostDetails = setting.split(':'); this.setPreference('network.proxy.' + key, hostDetails[0]); if (hostDetails[1]) { this.setPreference('network.proxy.' + key + '_port', parseInt(hostDetails[1], 10)); } }; FirefoxProfile.Finder = Finder; module.exports = FirefoxProfile;