blob: ed3b4ff5bd12f59dbc9b93ed8d001544d1ee9b15 [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
//
// 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.
// We should migrate this file to Null Safety once package:archive is migrated,
// but for now annotate it with the older language version.
// @dart = 2.9
library webdriver.support.firefox_profile;
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>[
BooleanOption('app.update.auto', false),
BooleanOption('app.update.enabled', false),
IntegerOption('browser.displayedE10SNotice', 4),
BooleanOption('browser.download.manager.showWhenStarting', false),
BooleanOption('browser.EULA.override', true),
BooleanOption('browser.EULA.3.accepted', true),
IntegerOption('browser.link.open_external', 2),
IntegerOption('browser.link.open_newwindow', 2),
BooleanOption('browser.offline', false),
BooleanOption('browser.reader.detectedFirstArticle', true),
BooleanOption('browser.safebrowsing.enabled', false),
BooleanOption('browser.safebrowsing.malware.enabled', false),
BooleanOption('browser.search.update', false),
StringOption('browser.selfsupport.url', ''),
BooleanOption('browser.sessionstore.resume_from_crash', false),
BooleanOption('browser.shell.checkDefaultBrowser', false),
BooleanOption('browser.tabs.warnOnClose', false),
BooleanOption('browser.tabs.warnOnOpen', false),
BooleanOption('datareporting.healthreport.service.enabled', false),
BooleanOption('datareporting.healthreport.uploadEnabled', false),
BooleanOption('datareporting.healthreport.service.firstRun', false),
BooleanOption('datareporting.healthreport.logging.consoleEnabled', false),
BooleanOption('datareporting.policy.dataSubmissionEnabled', false),
BooleanOption('datareporting.policy.dataSubmissionPolicyAccepted', false),
BooleanOption('devtools.errorconsole.enabled', true),
BooleanOption('dom.disable_open_during_load', false),
IntegerOption('extensions.autoDisableScopes', 10),
BooleanOption('extensions.blocklist.enabled', false),
BooleanOption('extensions.logging.enabled', true),
BooleanOption('extensions.update.enabled', false),
BooleanOption('extensions.update.notifyUser', false),
BooleanOption('javascript.enabled', true),
BooleanOption('network.manage-offline-status', false),
IntegerOption('network.http.phishy-userpass-length', 255),
BooleanOption('offline-apps.allow_by_default', true),
BooleanOption('prompts.tab_modal.enabled', false),
BooleanOption('security.csp.enable', false),
IntegerOption('security.fileuri.origin_policy', 3),
BooleanOption('security.fileuri.strict_origin_policy', false),
BooleanOption('security.warn_entering_secure', false),
BooleanOption('security.warn_entering_secure.show_once', false),
BooleanOption('security.warn_entering_weak', false),
BooleanOption('security.warn_entering_weak.show_once', false),
BooleanOption('security.warn_leaving_secure', false),
BooleanOption('security.warn_leaving_secure.show_once', false),
BooleanOption('security.warn_submit_insecure', false),
BooleanOption('security.warn_viewing_mixed', false),
BooleanOption('security.warn_viewing_mixed.show_once', false),
BooleanOption('signon.rememberSignons', false),
BooleanOption('toolkit.networkmanager.disable', true),
IntegerOption('toolkit.telemetry.prompted', 2),
BooleanOption('toolkit.telemetry.enabled', false),
BooleanOption('toolkit.telemetry.rejected', true),
BooleanOption('xpinstall.signatures.required', false),
];
/// Default values for 'user.js'.
final List<PrefsOption> defaultUserPrefs = <PrefsOption>[
BooleanOption('browser.dom.window.dump.enabled', true),
StringOption('browser.newtab.url', 'about:blank'),
BooleanOption('browser.newtabpage.enabled', false),
IntegerOption('browser.startup.page', 0),
StringOption('browser.startup.homepage', 'about:blank'),
IntegerOption('dom.max_chrome_script_run_time', 30),
IntegerOption('dom.max_script_run_time', 30),
BooleanOption('dom.report_all_js_exceptions', true),
BooleanOption('javascript.options.showInConsole', true),
IntegerOption('network.http.max-connections-per-server', 10),
StringOption('startup.homepage_welcome_url', 'about:blank'),
BooleanOption('webdriver_accept_untrusted_certs', true),
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 = <PrefsOption>{};
/// The read-only settings of the `prefs.js` file of the profile directory.
List<PrefsOption> get prefs => 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 = <PrefsOption>{};
List<PrefsOption> get userPrefs =>
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}) {
_userPrefs.addAll(defaultUserPrefs);
if (profileDirectory != null) {
final prefsFile =
io.File(path.join(profileDirectory.absolute.path, 'prefs.js'));
if (prefsFile.existsSync()) {
_prefs = loadPrefsFile(prefsFile);
}
final userPrefsFile =
io.File(path.join(profileDirectory.absolute.path, 'user.js'));
if (userPrefsFile.existsSync()) {
_userPrefs = loadPrefsFile(userPrefsFile)
.where((option) => !lockedPrefs.contains(option))
.toSet();
}
}
_prefs.addAll(lockedPrefs);
}
/// 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.name}" (${option.value}) is locked and therefore '
'ignored');
return false;
}
_userPrefs.add(option);
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) => o.name == name,
orElse: () => 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 = <PrefsOption>{};
final lines = LineSplitter.split(file.readAsStringSync())
.where((line) => !_ignoreLine(line));
bool canNotParseCaption = true;
for (final line in lines) {
final option = PrefsOption.parse(line);
if (option is InvalidOption) {
if (canNotParseCaption) {
print('Can\'t parse lines from file "${file.path}":');
canNotParseCaption = false;
}
print(' $line');
continue;
}
prefs.add(option);
}
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<String, dynamic> toJson() {
Archive archive = 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 = ArchiveFile('$name/', 0, <int>[]);
} else if (f is io.File) {
if (name == 'prefs.js' || name == 'user.js') {
return;
}
archiveFile =
ArchiveFile(name, f.statSync().size, (f).readAsBytesSync());
} else {
throw 'Invalid file type for file "${f.path}" '
'(${io.FileSystemEntity.typeSync(f.path)}).';
}
archive.addFile(archiveFile);
});
}
final prefsJsContent =
prefs.map((option) => option.asPrefString).join('\n').codeUnits;
archive.addFile(
ArchiveFile('prefs.js', prefsJsContent.length, prefsJsContent));
final userJsContent =
userPrefs.map((option) => option.asPrefString).join('\n').codeUnits;
archive
.addFile(ArchiveFile('user.js', userJsContent.length, userJsContent));
final zipData = 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 =
RegExp(r'user_pref\("([^"]+)", ("?.+?"?)\);');
final String name;
final T value;
factory PrefsOption(String name, T value) {
if (value is bool) {
return BooleanOption(name, value) as PrefsOption<T>;
} else if (value is int) {
return IntegerOption(name, value) as PrefsOption<T>;
} else if (value is String) {
return StringOption(name, value) as PrefsOption<T>;
}
throw ArgumentError(
'Only `bool`, `int` and `String` are valid parameter types.');
}
factory PrefsOption.parse(String prefs) {
final match = _preferencePattern.firstMatch(prefs);
if (match == null) {
return InvalidOption('Not a valid prefs option: "$prefs".')
as PrefsOption<T>;
}
final name = match.group(1);
final valueString = match.group(2);
if (valueString.startsWith('"') && valueString.endsWith('"')) {
final value = valueString
.substring(1, valueString.length - 1)
.replaceAll(r'\"', r'"')
.replaceAll(r'\\', r'\');
return StringOption(name, value) as PrefsOption<T>;
}
if (valueString.toLowerCase() == 'true') {
return BooleanOption(name, true) as PrefsOption<T>;
} else if (valueString.toLowerCase() == 'false') {
return BooleanOption(name, false) as PrefsOption<T>;
}
try {
int value = int.parse(valueString);
return IntegerOption(name, value) as PrefsOption<T>;
} catch (_) {}
return InvalidOption('Not a valid prefs option: "$prefs".')
as PrefsOption<T>;
}
PrefsOption._(this.name, [this.value]) : assert(name.isNotEmpty);
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is PrefsOption && this.name == other.name;
}
@override
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);
@override
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);
@override
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);
@override
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);
static String _escape(String value) =>
value.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
@override
String get _valueAsPrefString {
return '"${_escape(value)}"';
}
}