blob: 3f0bf7e1045dc9009ccbbf085908cdb3555661a8 [file] [log] [blame]
// Copyright 2015 Google Inc. All Rights Reserved.
// Licensed 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
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
import 'dart:collection';
import 'dart:convert' show LineSplitter, base64;
import 'dart:io' as io;
import 'package:archive/archive.dart' show Archive, ArchiveFile, ZipEncoder;
import 'package:path/path.dart' as path;
/// Unmodifiable defaults for 'prefs.js' and 'user.js'.
final List<PrefsOption> lockedPrefs = <PrefsOption>[
new BooleanOption('', false),
new BooleanOption('app.update.enabled', false),
new IntegerOption('browser.displayedE10SNotice', 4),
new BooleanOption('', false),
new BooleanOption('browser.EULA.override', true),
new BooleanOption('browser.EULA.3.accepted', true),
new IntegerOption('', 2),
new IntegerOption('', 2),
new BooleanOption('browser.offline', false),
new BooleanOption('browser.reader.detectedFirstArticle', true),
new BooleanOption('browser.safebrowsing.enabled', false),
new BooleanOption('browser.safebrowsing.malware.enabled', false),
new BooleanOption('', false),
new StringOption('browser.selfsupport.url', ''),
new BooleanOption('browser.sessionstore.resume_from_crash', false),
new BooleanOption('', false),
new BooleanOption('browser.tabs.warnOnClose', false),
new BooleanOption('browser.tabs.warnOnOpen', false),
new BooleanOption('datareporting.healthreport.service.enabled', false),
new BooleanOption('datareporting.healthreport.uploadEnabled', false),
new BooleanOption('datareporting.healthreport.service.firstRun', false),
new BooleanOption('datareporting.healthreport.logging.consoleEnabled', false),
new BooleanOption('datareporting.policy.dataSubmissionEnabled', false),
new BooleanOption('datareporting.policy.dataSubmissionPolicyAccepted', false),
new BooleanOption('devtools.errorconsole.enabled', true),
new BooleanOption('dom.disable_open_during_load', false),
new IntegerOption('extensions.autoDisableScopes', 10),
new BooleanOption('extensions.blocklist.enabled', false),
new BooleanOption('extensions.logging.enabled', true),
new BooleanOption('extensions.update.enabled', false),
new BooleanOption('extensions.update.notifyUser', false),
new BooleanOption('javascript.enabled', true),
new BooleanOption('network.manage-offline-status', false),
new IntegerOption('network.http.phishy-userpass-length', 255),
new BooleanOption('offline-apps.allow_by_default', true),
new BooleanOption('prompts.tab_modal.enabled', false),
new BooleanOption('security.csp.enable', false),
new IntegerOption('security.fileuri.origin_policy', 3),
new BooleanOption('security.fileuri.strict_origin_policy', false),
new BooleanOption('security.warn_entering_secure', false),
new BooleanOption('security.warn_entering_secure.show_once', false),
new BooleanOption('security.warn_entering_weak', false),
new BooleanOption('security.warn_entering_weak.show_once', false),
new BooleanOption('security.warn_leaving_secure', false),
new BooleanOption('security.warn_leaving_secure.show_once', false),
new BooleanOption('security.warn_submit_insecure', false),
new BooleanOption('security.warn_viewing_mixed', false),
new BooleanOption('security.warn_viewing_mixed.show_once', false),
new BooleanOption('signon.rememberSignons', false),
new BooleanOption('toolkit.networkmanager.disable', true),
new IntegerOption('toolkit.telemetry.prompted', 2),
new BooleanOption('toolkit.telemetry.enabled', false),
new BooleanOption('toolkit.telemetry.rejected', true),
new BooleanOption('xpinstall.signatures.required', false),
/// Default values for 'user.js'.
final List<PrefsOption> defaultUserPrefs = <PrefsOption>[
new BooleanOption('browser.dom.window.dump.enabled', true),
new StringOption('browser.newtab.url', 'about:blank'),
new BooleanOption('browser.newtabpage.enabled', false),
new IntegerOption('', 0),
new StringOption('browser.startup.homepage', 'about:blank'),
new IntegerOption('dom.max_chrome_script_run_time', 30),
new IntegerOption('dom.max_script_run_time', 30),
new BooleanOption('dom.report_all_js_exceptions', true),
new BooleanOption('javascript.options.showInConsole', true),
new IntegerOption('network.http.max-connections-per-server', 10),
new StringOption('startup.homepage_welcome_url', 'about:blank'),
new BooleanOption('webdriver_accept_untrusted_certs', true),
new BooleanOption('webdriver_assume_untrusted_issuer', true),
/// Creates a Firefox profile in a format so it can be passed using the
/// `desired` capabilities map.
class FirefoxProfile {
final io.Directory profileDirectory;
Set<PrefsOption> _prefs = new Set<PrefsOption>();
/// The read-only settings of the `prefs.js` file of the profile directory.
List<PrefsOption> get prefs => new UnmodifiableListView<PrefsOption>(_prefs);
/// The settings of the `user.js` file of the profile directory overridden by
/// the settings in [lockedPrefs].
/// [setOption] and [removeOption] allow to update, add, and remove settings
/// except these included in [lockedPrefs].
Set<PrefsOption> _userPrefs = new Set<PrefsOption>();
List<PrefsOption> get userPrefs =>
new UnmodifiableListView<PrefsOption>(_userPrefs);
/// Creates a new FirefoxProfile.
/// If [profileDirectory] is passed the files from this directory will be
/// included in the output generated by [toJson].
/// The files `prefs.js` and `user.js` are loaded from the directory if they
/// exist and their settings are added to [prefs] and [userPrefs].
/// Settings from [lockedPrefs] are also added to [prefs] and always take
/// precedence and can not be overridden or removed.
/// If [profileDirectory] is `null` the content returned by [toJson] only
/// consists of [lockedPrefs] as `prefs.js` and [defaultUserPrefs] and the
/// dynamic updates made with [setOption] and [removeOption] as `user.js`.
FirefoxProfile({this.profileDirectory}) {
if (profileDirectory != null) {
final prefsFile =
new io.File(path.join(profileDirectory.absolute.path, 'prefs.js'));
if (prefsFile.existsSync()) {
_prefs = loadPrefsFile(prefsFile);
final userPrefsFile =
new io.File(path.join(profileDirectory.absolute.path, 'user.js'));
if (userPrefsFile.existsSync()) {
_userPrefs = loadPrefsFile(userPrefsFile)
.where((option) => !lockedPrefs.contains(option))
/// Add an option or replace an option if one already exists with the same
/// name.
/// If option exists in [lockedPrefs] it will not be added.
/// Returns `true` if [userPrefs] was updated, `false` otherwise.
bool setOption(PrefsOption option) {
if (lockedPrefs.contains(option)) {
print('Option "${}" (${option.value}) is locked and therefore '
return false;
return true;
/// Remove the option named [name] from [userPrefs].
/// If [lockedPrefs] contains this option it will not be removed.
/// Returns `true` if [userPrefs] was removed, `false` if it was not removed
/// because [userPrefs] doesn't contain it because [lockedPrefs]
/// contains it.
bool removeOption(String name) {
final option = _userPrefs.firstWhere((o) => == name,
orElse: () => new InvalidOption(name));
if (option is InvalidOption) {
return false;
return _userPrefs.remove(option);
/// Helper for [loadPrefsFile]
static bool _ignoreLine(String line) {
line ??= '';
line = line.trim();
if (line.isEmpty ||
line.startsWith('//') ||
line.startsWith('#') ||
line.startsWith('/*') ||
line.startsWith('*') ||
line.startsWith('*/')) {
return true;
return false;
/// Load a prefs file and parse the content into a set of [PrefsOption].
/// For lines which can't be properly parsed a message is printed and the line
/// is otherwise ignored.
/// Comments, lines starting with `//` are silently ignored.
static Set<PrefsOption> loadPrefsFile(io.File file) {
final prefs = new Set<PrefsOption>();
final lines = LineSplitter.split(file.readAsStringSync())
.where((line) => !_ignoreLine(line));
bool canNotParseCaption = true;
for (final line in lines) {
final option = new PrefsOption.parse(line);
if (option is InvalidOption) {
if (canNotParseCaption) {
print('Can\'t parse lines from file "${file.path}":');
canNotParseCaption = false;
print(' $line');
return prefs;
/// Creates a map like `{'browserName: 'firefox', 'firefox_profile': 'xxxxx'}`
/// where `xxxxx` is the zipped and base64 encoded content of the files in
/// [profileDirectory] if one was pased.
/// The files `prefs.js` and `user.js` are generated from the content of
/// `prefs` and `userPrefs`.
/// It can be uses like
/// `var desired = Capabilities.firefox..addAll(firefoxProfile.toJson()}`
Map toJson() {
Archive archive = new Archive();
if (profileDirectory != null) {
profileDirectory.listSync(recursive: true).forEach((f) {
ArchiveFile archiveFile;
final name = path.relative(f.path, from: profileDirectory.path);
if (f is io.Directory) {
archiveFile = new ArchiveFile('$name/', 0, <int>[]);
} else if (f is io.File) {
if (name == 'prefs.js' || name == 'user.js') {
archiveFile =
new ArchiveFile(name, f.statSync().size, (f).readAsBytesSync());
} else {
throw 'Invalid file type for file "${f.path}" '
final prefsJsContent = => option.asPrefString).join('\n').codeUnits;
new ArchiveFile('prefs.js', prefsJsContent.length, prefsJsContent));
final userJsContent = => option.asPrefString).join('\n').codeUnits;
new ArchiveFile('user.js', userJsContent.length, userJsContent));
final zipData = new ZipEncoder().encode(archive);
return {'firefox_profile': base64.encode(zipData)};
abstract class PrefsOption<T> {
/// This pattern is used to parse preferences in user.js. It is intended to
/// match all preference lines in the format generated by Firefox; it won't
/// necessarily match all possible lines that Firefox will parse.
/// e.g. if you have a line with extra spaces after the end-of-line semicolon,
/// this pattern will not match that line because Firefox never generates
/// lines like that.
static final RegExp _preferencePattern =
new RegExp(r'user_pref\("([^"]+)", ("?.+?"?)\);');
final String name;
T _value;
dynamic get value => _value;
factory PrefsOption(String name, value) {
assert(value is bool || value is int || value is String);
if (value is bool) {
return new BooleanOption(name, value) as PrefsOption<T>;
} else if (value is int) {
return new IntegerOption(name, value) as PrefsOption<T>;
} else if (value is String) {
return new StringOption(name, value) as PrefsOption<T>;
return null;
factory PrefsOption.parse(String prefs) {
final match = _preferencePattern.firstMatch(prefs);
if (match == null) {
return new InvalidOption('Not a valid prefs option: "$prefs".')
as PrefsOption<T>;
final name =;
final valueString =;
if (valueString.startsWith('"') && valueString.endsWith('"')) {
final value = valueString
.substring(1, valueString.length - 1)
.replaceAll(r'\"', r'"')
.replaceAll(r'\\', r'\');
return new StringOption(name, value) as PrefsOption<T>;
if (valueString.toLowerCase() == 'true') {
return new BooleanOption(name, true) as PrefsOption<T>;
} else if (valueString.toLowerCase() == 'false') {
return new BooleanOption(name, false) as PrefsOption<T>;
try {
int value = int.parse(valueString);
return new IntegerOption(name, value) as PrefsOption<T>;
} catch (_) {}
return new InvalidOption('Not a valid prefs option: "$prefs".')
as PrefsOption<T>;
PrefsOption._(, [this._value]) : assert(name.isNotEmpty);
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
return other is PrefsOption && ==;
int get hashCode => name.hashCode;
String get asPrefString => 'user_pref("$name", $_valueAsPrefString);';
String get _valueAsPrefString;
/// Used as a placeholder for unparsable lines.
class InvalidOption extends PrefsOption<String> {
InvalidOption(String value) : super._('invalid', value);
String get _valueAsPrefString =>
throw 'An invalid option can\'t be serialized.';
/// A boolean preferences option with name and value.
class BooleanOption extends PrefsOption<bool> {
BooleanOption(String name, bool value) : super._(name, value);
String get _valueAsPrefString => value.toString();
/// An integer preferences option with name and value.
class IntegerOption extends PrefsOption<int> {
IntegerOption(String name, int value) : super._(name, value);
String get _valueAsPrefString => value.toString();
/// A String preferences option with name and value.
/// [_valueAsPrefString] escapes `"` as `\"` and `\` as `\\`.
class StringOption extends PrefsOption<String> {
StringOption(String name, String value) : super._(name, value);
String _escape(String value) =>
value.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
String get _valueAsPrefString {
return '"${_escape(value)}"';