UNPKG

firefox-profile

Version:

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

530 lines (477 loc) 16.3 kB
'use strict'; var os = require('os'), path = require('path'), fs = require('fs-extra'), parseString = require('xml2js').parseString, // third-party wrench = require('wrench'), AdmZip = require('adm-zip'), archiver = require('archiver'), uuid = require('node-uuid'), Readable = require('lazystream').Readable, async = require('async'), uuid = require('node-uuid'); 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]; } /** * Constructor */ /** * Initialize a new instance of a Firefox Profile * * @param {String|null} profileDirectory optional. if provided, it will copy the directory */ function FirefoxProfile(profileDirectory) { // cloning! this.defaultPreferences = JSON.parse(JSON.stringify(config.DEFAULT_PREFERENCES)); this.profileDir = profileDirectory; // if true, the profile folder is deleted after this._deleteOnExit = true; // can be turned to false when debugging this._deleteZippedProfile = true; this._preferencesModified = false; if (!this.profileDir) { this.profileDir = this._createTempFolder(); } else { // create copy var tmpDir = this._createTempFolder('-copy'); wrench.copyDirSyncRecursive(profileDirectory, tmpDir, { forceDelete: true, filter: /^(parent\.lock|lock|\.parentlock)$/ // excludes parent.lock, lock, .parentlock not copied }); 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() { // console.log('on exit:: ', self.path()); if (self._deleteOnExit) { self.deleteDir(); } }; ['exit', 'SIGINT'].forEach(function(event) { process.addListener(event, self.onExit); }); } /** * Deletes the profile directory. * * Call it only if you do not need the profile. Otherwise use at your own risk. * this function is automatically called by default (= if willDeleteOnExit() returns true) */ FirefoxProfile.prototype.deleteDir = function() { var self = this; ['exit', 'SIGINT'].forEach(function(event) { process.removeListener(event, self.onExit); }); this.shouldDeleteOnExit(false); fs.existsSync(this.profileDir) && wrench.rmdirSyncRecursive(this.profileDir); }; /** * 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 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.addExtensions = function(extensions, cb) { var self = this, functions = extensions.map(function(extension) { return function(callback) { self.addExtension(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() { cb(fs.readFileSync(path.join(tmpFolder,'profile.zip')).toString('base64')); if (self._deleteZippedProfile) { fs.unlinkSync(path.join(tmpFolder,'profile.zip')); fs.rmdirSync(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. * if proxy type is 'manual', then possible settings are: 'ftp', 'http', 'ssl', 'socks' * if proxy type is 'pac', the setting should be 'autoconfig_url' * for other values, only the proxy.type pref will be set * * @param {Object} object a proxy object. Mandatary attribute: 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).split('\n'); contentLines.forEach(function(line) { var found = line.match(regExp); if (found[1]) { 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 = addonDetails.id; var unpack = addonDetails.unpack === undefined ? true : addonDetails.unpack; if (!addonId) { throw new Error('FirefoxProfile: the addon id could not be found!'); } var addonPath = path.join(self.extensionsDir, path.sep, addonId); if (!fs.existsSync(self.extensionsDir)) { fs.mkdirSync(self.extensionsDir); } if (!unpack && xpiFile) { fs.copySync(xpiFile, addonPath + '.xpi'); } else { // copy it! fs.mkdirSync(addonPath); wrench.copyDirSyncRecursive(addon, addonPath, {forceDelete: true}); } if (tmpDir) { wrench.rmdirSyncRecursive(tmpDir); } // done! cb && cb(); }); }; FirefoxProfile.prototype._addonDetails = function(addonPath, cb) { var details = { 'id': null, 'name': null, 'unpack': true, 'version': null }, 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; 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); // console.log('created folder: ', 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', hostDetails[1]); } }; module.exports = FirefoxProfile;