summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozprofile/mozprofile
diff options
context:
space:
mode:
Diffstat (limited to 'testing/mozbase/mozprofile/mozprofile')
-rw-r--r--testing/mozbase/mozprofile/mozprofile/__init__.py20
-rw-r--r--testing/mozbase/mozprofile/mozprofile/addons.py354
-rwxr-xr-xtesting/mozbase/mozprofile/mozprofile/cli.py204
-rw-r--r--testing/mozbase/mozprofile/mozprofile/diff.py86
-rw-r--r--testing/mozbase/mozprofile/mozprofile/permissions.py335
-rw-r--r--testing/mozbase/mozprofile/mozprofile/prefs.py263
-rw-r--r--testing/mozbase/mozprofile/mozprofile/profile.py595
-rw-r--r--testing/mozbase/mozprofile/mozprofile/view.py47
8 files changed, 1904 insertions, 0 deletions
diff --git a/testing/mozbase/mozprofile/mozprofile/__init__.py b/testing/mozbase/mozprofile/mozprofile/__init__.py
new file mode 100644
index 0000000000..454514a7d1
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/__init__.py
@@ -0,0 +1,20 @@
+# flake8: noqa
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+To use mozprofile as an API you can import mozprofile.profile_ and/or the AddonManager_.
+
+``mozprofile.profile`` features a generic ``Profile`` class. In addition,
+subclasses ``FirefoxProfile`` and ``ThundebirdProfile`` are available
+with preset preferences for those applications.
+"""
+
+from mozprofile.addons import *
+from mozprofile.cli import *
+from mozprofile.diff import *
+from mozprofile.permissions import *
+from mozprofile.prefs import *
+from mozprofile.profile import *
+from mozprofile.view import *
diff --git a/testing/mozbase/mozprofile/mozprofile/addons.py b/testing/mozbase/mozprofile/mozprofile/addons.py
new file mode 100644
index 0000000000..e2450e61dc
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/addons.py
@@ -0,0 +1,354 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import binascii
+import hashlib
+import json
+import os
+import shutil
+import sys
+import tempfile
+import zipfile
+from xml.dom import minidom
+
+import mozfile
+from mozlog.unstructured import getLogger
+from six import reraise, string_types
+
+_SALT = binascii.hexlify(os.urandom(32))
+_TEMPORARY_ADDON_SUFFIX = "@temporary-addon"
+
+# Logger for 'mozprofile.addons' module
+module_logger = getLogger(__name__)
+
+
+class AddonFormatError(Exception):
+ """Exception for not well-formed add-on manifest files"""
+
+
+class AddonManager(object):
+ """
+ Handles all operations regarding addons in a profile including:
+ installing and cleaning addons
+ """
+
+ def __init__(self, profile, restore=True):
+ """
+ :param profile: the path to the profile for which we install addons
+ :param restore: whether to reset to the previous state on instance garbage collection
+ """
+ self.profile = profile
+ self.restore = restore
+
+ # Initialize all class members
+ self._internal_init()
+
+ def _internal_init(self):
+ """Internal: Initialize all class members to their default value"""
+
+ # Add-ons installed; needed for cleanup
+ self._addons = []
+
+ # Backup folder for already existing addons
+ self.backup_dir = None
+
+ # Information needed for profile reset (see http://bit.ly/17JesUf)
+ self.installed_addons = []
+
+ def __del__(self):
+ # reset to pre-instance state
+ if self.restore:
+ self.clean()
+
+ def clean(self):
+ """Clean up addons in the profile."""
+
+ # Remove all add-ons installed
+ for addon in self._addons:
+ # TODO (bug 934642)
+ # Once we have a proper handling of add-ons we should kill the id
+ # from self._addons once the add-on is removed. For now lets forget
+ # about the exception
+ try:
+ self.remove_addon(addon)
+ except IOError:
+ pass
+
+ # restore backups
+ if self.backup_dir and os.path.isdir(self.backup_dir):
+ extensions_path = os.path.join(self.profile, "extensions")
+
+ for backup in os.listdir(self.backup_dir):
+ backup_path = os.path.join(self.backup_dir, backup)
+ shutil.move(backup_path, extensions_path)
+
+ if not os.listdir(self.backup_dir):
+ mozfile.remove(self.backup_dir)
+
+ # reset instance variables to defaults
+ self._internal_init()
+
+ def get_addon_path(self, addon_id):
+ """Returns the path to the installed add-on
+
+ :param addon_id: id of the add-on to retrieve the path from
+ """
+ # By default we should expect add-ons being located under the
+ # extensions folder.
+ extensions_path = os.path.join(self.profile, "extensions")
+ paths = [
+ os.path.join(extensions_path, addon_id),
+ os.path.join(extensions_path, addon_id + ".xpi"),
+ ]
+ for path in paths:
+ if os.path.exists(path):
+ return path
+
+ raise IOError("Add-on not found: %s" % addon_id)
+
+ @classmethod
+ def is_addon(self, addon_path):
+ """
+ Checks if the given path is a valid addon
+
+ :param addon_path: path to the add-on directory or XPI
+ """
+ try:
+ self.addon_details(addon_path)
+ return True
+ except AddonFormatError:
+ return False
+
+ def _install_addon(self, path, unpack=False):
+ addons = [path]
+
+ # if path is not an add-on, try to install all contained add-ons
+ try:
+ self.addon_details(path)
+ except AddonFormatError as e:
+ module_logger.warning("Could not install %s: %s" % (path, str(e)))
+
+ # If the path doesn't exist, then we don't really care, just return
+ if not os.path.isdir(path):
+ return
+
+ addons = [
+ os.path.join(path, x)
+ for x in os.listdir(path)
+ if self.is_addon(os.path.join(path, x))
+ ]
+ addons.sort()
+
+ # install each addon
+ for addon in addons:
+ # determine the addon id
+ addon_details = self.addon_details(addon)
+ addon_id = addon_details.get("id")
+
+ # if the add-on has to be unpacked force it now
+ # note: we might want to let Firefox do it in case of addon details
+ orig_path = None
+ if os.path.isfile(addon) and (unpack or addon_details["unpack"]):
+ orig_path = addon
+ addon = tempfile.mkdtemp()
+ mozfile.extract(orig_path, addon)
+
+ # copy the addon to the profile
+ extensions_path = os.path.join(self.profile, "extensions")
+ addon_path = os.path.join(extensions_path, addon_id)
+
+ if os.path.isfile(addon):
+ addon_path += ".xpi"
+
+ # move existing xpi file to backup location to restore later
+ if os.path.exists(addon_path):
+ self.backup_dir = self.backup_dir or tempfile.mkdtemp()
+ shutil.move(addon_path, self.backup_dir)
+
+ # copy new add-on to the extension folder
+ if not os.path.exists(extensions_path):
+ os.makedirs(extensions_path)
+ shutil.copy(addon, addon_path)
+ else:
+ # move existing folder to backup location to restore later
+ if os.path.exists(addon_path):
+ self.backup_dir = self.backup_dir or tempfile.mkdtemp()
+ shutil.move(addon_path, self.backup_dir)
+
+ # copy new add-on to the extension folder
+ shutil.copytree(addon, addon_path, symlinks=True)
+
+ # if we had to extract the addon, remove the temporary directory
+ if orig_path:
+ mozfile.remove(addon)
+ addon = orig_path
+
+ self._addons.append(addon_id)
+ self.installed_addons.append(addon)
+
+ def install(self, addons, **kwargs):
+ """
+ Installs addons from a filepath or directory of addons in the profile.
+
+ :param addons: paths to .xpi or addon directories
+ :param unpack: whether to unpack unless specified otherwise in the install.rdf
+ """
+ if not addons:
+ return
+
+ # install addon paths
+ if isinstance(addons, string_types):
+ addons = [addons]
+ for addon in set(addons):
+ self._install_addon(addon, **kwargs)
+
+ @classmethod
+ def _gen_iid(cls, addon_path):
+ hash = hashlib.sha1(_SALT)
+ hash.update(addon_path.encode())
+ return hash.hexdigest() + _TEMPORARY_ADDON_SUFFIX
+
+ @classmethod
+ def addon_details(cls, addon_path):
+ """
+ Returns a dictionary of details about the addon.
+
+ :param addon_path: path to the add-on directory or XPI
+
+ Returns::
+
+ {'id': u'rainbow@colors.org', # id of the addon
+ 'version': u'1.4', # version of the addon
+ 'name': u'Rainbow', # name of the addon
+ 'unpack': False } # whether to unpack the addon
+ """
+
+ details = {"id": None, "unpack": False, "name": None, "version": None}
+
+ def get_namespace_id(doc, url):
+ attributes = doc.documentElement.attributes
+ namespace = ""
+ for i in range(attributes.length):
+ if attributes.item(i).value == url:
+ if ":" in attributes.item(i).name:
+ # If the namespace is not the default one remove 'xlmns:'
+ namespace = attributes.item(i).name.split(":")[1] + ":"
+ break
+ return namespace
+
+ def get_text(element):
+ """Retrieve the text value of a given node"""
+ rc = []
+ for node in element.childNodes:
+ if node.nodeType == node.TEXT_NODE:
+ rc.append(node.data)
+ return "".join(rc).strip()
+
+ if not os.path.exists(addon_path):
+ raise IOError("Add-on path does not exist: %s" % addon_path)
+
+ is_webext = False
+ try:
+ if zipfile.is_zipfile(addon_path):
+ with zipfile.ZipFile(addon_path, "r") as compressed_file:
+ filenames = [f.filename for f in (compressed_file).filelist]
+ if "install.rdf" in filenames:
+ manifest = compressed_file.read("install.rdf")
+ elif "manifest.json" in filenames:
+ is_webext = True
+ manifest = compressed_file.read("manifest.json").decode()
+ manifest = json.loads(manifest)
+ else:
+ raise KeyError("No manifest")
+ elif os.path.isdir(addon_path):
+ entries = os.listdir(addon_path)
+ # Beginning with https://phabricator.services.mozilla.com/D126174
+ # directories may exist that contain one single XPI. If that's
+ # the case we need to process it just as we do above.
+ if len(entries) == 1 and zipfile.is_zipfile(
+ os.path.join(addon_path, entries[0])
+ ):
+ with zipfile.ZipFile(
+ os.path.join(addon_path, entries[0]), "r"
+ ) as compressed_file:
+ filenames = [f.filename for f in (compressed_file).filelist]
+ if "install.rdf" in filenames:
+ manifest = compressed_file.read("install.rdf")
+ elif "manifest.json" in filenames:
+ is_webext = True
+ manifest = compressed_file.read("manifest.json").decode()
+ manifest = json.loads(manifest)
+ else:
+ raise KeyError("No manifest")
+ # Otherwise, treat is an already unpacked XPI.
+ else:
+ try:
+ with open(os.path.join(addon_path, "install.rdf")) as f:
+ manifest = f.read()
+ except IOError:
+ with open(os.path.join(addon_path, "manifest.json")) as f:
+ manifest = json.loads(f.read())
+ is_webext = True
+ else:
+ raise IOError(
+ "Add-on path is neither an XPI nor a directory: %s" % addon_path
+ )
+ except (IOError, KeyError) as e:
+ reraise(AddonFormatError, AddonFormatError(str(e)), sys.exc_info()[2])
+
+ if is_webext:
+ details["version"] = manifest["version"]
+ details["name"] = manifest["name"]
+ # Bug 1572404 - we support two locations for gecko-specific
+ # metadata.
+ for location in ("applications", "browser_specific_settings"):
+ try:
+ details["id"] = manifest[location]["gecko"]["id"]
+ break
+ except KeyError:
+ pass
+ if details["id"] is None:
+ details["id"] = cls._gen_iid(addon_path)
+ details["unpack"] = False
+ else:
+ try:
+ doc = minidom.parseString(manifest)
+
+ # Get the namespaces abbreviations
+ em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")
+ rdf = get_namespace_id(
+ doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ )
+
+ description = doc.getElementsByTagName(rdf + "Description").item(0)
+ for entry, value in description.attributes.items():
+ # Remove the namespace prefix from the tag for comparison
+ entry = entry.replace(em, "")
+ if entry in details.keys():
+ details.update({entry: value})
+ for node in description.childNodes:
+ # Remove the namespace prefix from the tag for comparison
+ entry = node.nodeName.replace(em, "")
+ if entry in details.keys():
+ details.update({entry: get_text(node)})
+ except Exception as e:
+ reraise(AddonFormatError, AddonFormatError(str(e)), sys.exc_info()[2])
+
+ # turn unpack into a true/false value
+ if isinstance(details["unpack"], string_types):
+ details["unpack"] = details["unpack"].lower() == "true"
+
+ # If no ID is set, the add-on is invalid
+ if details.get("id") is None and not is_webext:
+ raise AddonFormatError("Add-on id could not be found.")
+
+ return details
+
+ def remove_addon(self, addon_id):
+ """Remove the add-on as specified by the id
+
+ :param addon_id: id of the add-on to be removed
+ """
+ path = self.get_addon_path(addon_id)
+ mozfile.remove(path)
diff --git a/testing/mozbase/mozprofile/mozprofile/cli.py b/testing/mozbase/mozprofile/mozprofile/cli.py
new file mode 100755
index 0000000000..7addb1165d
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/cli.py
@@ -0,0 +1,204 @@
+#!/usr/bin/env python
+
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Creates and/or modifies a Firefox profile.
+The profile can be modified by passing in addons to install or preferences to set.
+If no profile is specified, a new profile is created and the path of the
+resulting profile is printed.
+"""
+import sys
+from optparse import OptionParser
+
+from .prefs import Preferences
+from .profile import FirefoxProfile, Profile
+
+__all__ = [
+ "MozProfileCLI",
+ "cli",
+ "KeyValueParseError",
+ "parse_key_value",
+ "parse_preferences",
+]
+
+
+class KeyValueParseError(Exception):
+ """Error when parsing strings of serialized key-values."""
+
+ def __init__(self, msg, errors=()):
+ self.errors = errors
+ Exception.__init__(self, msg)
+
+
+def parse_key_value(strings, separator="=", context="key, value"):
+ """Parse string-serialized key-value pairs in the form of `key = value`.
+
+ Args:
+ strings (list): List of strings to parse.
+ separator (str): Identifier used to split the strings.
+
+ Returns:
+ list: A list of (<key>, <value>) tuples. Whitespace is not stripped.
+
+ Raises:
+ KeyValueParseError
+ """
+
+ # syntax check
+ missing = [string for string in strings if separator not in string]
+ if missing:
+ raise KeyValueParseError(
+ "Error: syntax error in %s: %s" % (context, ",".join(missing)),
+ errors=missing,
+ )
+ return [string.split(separator, 1) for string in strings]
+
+
+def parse_preferences(prefs, context="--setpref="):
+ """Parse preferences specified on the command line.
+
+ Args:
+ prefs (list): A list of strings, usually of the form "<pref>=<value>".
+
+ Returns:
+ dict: A dictionary of the form {<pref>: <value>} where values have been
+ cast.
+ """
+ try:
+ prefs = dict(parse_key_value(prefs, context=context))
+ except KeyValueParseError as e:
+ print(str(e))
+ sys.exit(1)
+
+ return {k: Preferences.cast(v) for k, v in prefs.items()}
+
+
+class MozProfileCLI(object):
+ """The Command Line Interface for ``mozprofile``."""
+
+ module = "mozprofile"
+ profile_class = Profile
+
+ def __init__(self, args=sys.argv[1:], add_options=None):
+ self.parser = OptionParser(description=__doc__)
+ self.add_options(self.parser)
+ if add_options:
+ add_options(self.parser)
+ (self.options, self.args) = self.parser.parse_args(args)
+
+ def add_options(self, parser):
+
+ parser.add_option(
+ "-p",
+ "--profile",
+ dest="profile",
+ help="The path to the profile to operate on. "
+ "If none, creates a new profile in temp directory",
+ )
+ parser.add_option(
+ "-a",
+ "--addon",
+ dest="addons",
+ action="append",
+ default=[],
+ help="Addon paths to install. Can be a filepath, "
+ "a directory containing addons, or a url",
+ )
+ parser.add_option(
+ "--pref",
+ dest="prefs",
+ action="append",
+ default=[],
+ help="A preference to set. " "Must be a key-value pair separated by a ':'",
+ )
+ parser.add_option(
+ "--preferences",
+ dest="prefs_files",
+ action="append",
+ default=[],
+ metavar="FILE",
+ help="read preferences from a JSON or INI file. "
+ "For INI, use 'file.ini:section' to specify a particular section.",
+ )
+
+ def profile_args(self):
+ """arguments to instantiate the profile class"""
+ return dict(
+ profile=self.options.profile,
+ addons=self.options.addons,
+ preferences=self.preferences(),
+ )
+
+ def preferences(self):
+ """profile preferences"""
+
+ # object to hold preferences
+ prefs = Preferences()
+
+ # add preferences files
+ for prefs_file in self.options.prefs_files:
+ prefs.add_file(prefs_file)
+
+ # change CLI preferences into 2-tuples
+ cli_prefs = parse_key_value(self.options.prefs, separator=":")
+
+ # string preferences
+ prefs.add(cli_prefs, cast=True)
+
+ return prefs()
+
+ def profile(self, restore=False):
+ """create the profile"""
+
+ kwargs = self.profile_args()
+ kwargs["restore"] = restore
+ return self.profile_class(**kwargs)
+
+
+def cli(args=sys.argv[1:]):
+ """Handles the command line arguments for ``mozprofile`` via ``sys.argv``"""
+
+ # add a view method for this cli method only
+ def add_options(parser):
+ parser.add_option(
+ "--view",
+ dest="view",
+ action="store_true",
+ default=False,
+ help="view summary of profile following invocation",
+ )
+ parser.add_option(
+ "--firefox",
+ dest="firefox_profile",
+ action="store_true",
+ default=False,
+ help="use FirefoxProfile defaults",
+ )
+
+ # process the command line
+ cli = MozProfileCLI(args, add_options)
+
+ if cli.args:
+ cli.parser.error("Program doesn't support positional arguments.")
+
+ if cli.options.firefox_profile:
+ cli.profile_class = FirefoxProfile
+
+ # create the profile
+ profile = cli.profile()
+
+ if cli.options.view:
+ # view the profile, if specified
+ print(profile.summary())
+ return
+
+ # if no profile was passed in print the newly created profile
+ if not cli.options.profile:
+ print(profile.profile)
+
+
+if __name__ == "__main__":
+ cli()
diff --git a/testing/mozbase/mozprofile/mozprofile/diff.py b/testing/mozbase/mozprofile/mozprofile/diff.py
new file mode 100644
index 0000000000..f8376e4d7b
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/diff.py
@@ -0,0 +1,86 @@
+#!/usr/bin/env python
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+"""
+diff two profile summaries
+"""
+
+import difflib
+import optparse
+import os
+import profile
+import sys
+
+__all__ = ["diff", "diff_profiles"]
+
+
+def diff(profile1, profile2, diff_function=difflib.unified_diff):
+
+ profiles = (profile1, profile2)
+ parts = {}
+ parts_dict = {}
+ for index in (0, 1):
+ prof = profiles[index]
+
+ # first part, the path, isn't useful for diffing
+ parts[index] = prof.summary(return_parts=True)[1:]
+
+ parts_dict[index] = dict(parts[index])
+
+ # keys the first profile is missing
+ first_missing = [i for i in parts_dict[1] if i not in parts_dict[0]]
+ parts[0].extend([(i, "") for i in first_missing])
+
+ # diffs
+ retval = []
+ for key, value in parts[0]:
+ other = parts_dict[1].get(key, "")
+ value = value.strip()
+ other = other.strip()
+
+ if key == "Files":
+ # first line of files is the path; we don't care to diff that
+ value = "\n".join(value.splitlines()[1:])
+ if other:
+ other = "\n".join(other.splitlines()[1:])
+
+ value = value.splitlines()
+ other = other.splitlines()
+ section_diff = list(
+ diff_function(value, other, profile1.profile, profile2.profile)
+ )
+ if section_diff:
+ retval.append((key, "\n".join(section_diff)))
+
+ return retval
+
+
+def diff_profiles(args=sys.argv[1:]):
+
+ # parse command line
+ usage = "%prog [options] profile1 profile2"
+ parser = optparse.OptionParser(usage=usage, description=__doc__)
+ options, args = parser.parse_args(args)
+ if len(args) != 2:
+ parser.error("Must give two profile paths")
+ missing = [arg for arg in args if not os.path.exists(arg)]
+ if missing:
+ parser.error("Profile not found: %s" % (", ".join(missing)))
+
+ # get the profile differences
+ diffs = diff(*([profile.Profile(arg) for arg in args]))
+
+ # display them
+ while diffs:
+ key, value = diffs.pop(0)
+ print("[%s]:\n" % key)
+ print(value)
+ if diffs:
+ print("-" * 4)
+
+
+if __name__ == "__main__":
+ diff_profiles()
diff --git a/testing/mozbase/mozprofile/mozprofile/permissions.py b/testing/mozbase/mozprofile/mozprofile/permissions.py
new file mode 100644
index 0000000000..ffb4e5acdb
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/permissions.py
@@ -0,0 +1,335 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+"""
+add permissions to the profile
+"""
+
+import codecs
+import os
+
+from six import string_types
+from six.moves.urllib import parse
+
+__all__ = [
+ "MissingPrimaryLocationError",
+ "MultiplePrimaryLocationsError",
+ "DEFAULT_PORTS",
+ "DuplicateLocationError",
+ "BadPortLocationError",
+ "LocationsSyntaxError",
+ "Location",
+ "ServerLocations",
+ "Permissions",
+]
+
+# http://hg.mozilla.org/mozilla-central/file/b871dfb2186f/build/automation.py.in#l28
+DEFAULT_PORTS = {"http": "8888", "https": "4443", "ws": "4443", "wss": "4443"}
+
+
+class LocationError(Exception):
+ """Signifies an improperly formed location."""
+
+ def __str__(self):
+ s = "Bad location"
+ m = str(Exception.__str__(self))
+ if m:
+ s += ": %s" % m
+ return s
+
+
+class MissingPrimaryLocationError(LocationError):
+ """No primary location defined in locations file."""
+
+ def __init__(self):
+ LocationError.__init__(self, "missing primary location")
+
+
+class MultiplePrimaryLocationsError(LocationError):
+ """More than one primary location defined."""
+
+ def __init__(self):
+ LocationError.__init__(self, "multiple primary locations")
+
+
+class DuplicateLocationError(LocationError):
+ """Same location defined twice."""
+
+ def __init__(self, url):
+ LocationError.__init__(self, "duplicate location: %s" % url)
+
+
+class BadPortLocationError(LocationError):
+ """Location has invalid port value."""
+
+ def __init__(self, given_port):
+ LocationError.__init__(self, "bad value for port: %s" % given_port)
+
+
+class LocationsSyntaxError(Exception):
+ """Signifies a syntax error on a particular line in server-locations.txt."""
+
+ def __init__(self, lineno, err=None):
+ self.err = err
+ self.lineno = lineno
+
+ def __str__(self):
+ s = "Syntax error on line %s" % self.lineno
+ if self.err:
+ s += ": %s." % self.err
+ else:
+ s += "."
+ return s
+
+
+class Location(object):
+ """Represents a location line in server-locations.txt."""
+
+ attrs = ("scheme", "host", "port")
+
+ def __init__(self, scheme, host, port, options):
+ for attr in self.attrs:
+ setattr(self, attr, locals()[attr])
+ self.options = options
+ try:
+ int(self.port)
+ except ValueError:
+ raise BadPortLocationError(self.port)
+
+ def isEqual(self, location):
+ """compare scheme://host:port, but ignore options"""
+ return len(
+ [i for i in self.attrs if getattr(self, i) == getattr(location, i)]
+ ) == len(self.attrs)
+
+ __eq__ = isEqual
+
+ def __hash__(self):
+ # pylint --py3k: W1641
+ return hash(tuple(getattr(attr) for attr in self.attrs))
+
+ def url(self):
+ return "%s://%s:%s" % (self.scheme, self.host, self.port)
+
+ def __str__(self):
+ return "%s %s" % (self.url(), ",".join(self.options))
+
+
+class ServerLocations(object):
+ """Iterable collection of locations.
+ Use provided functions to add new locations, rather that manipulating
+ _locations directly, in order to check for errors and to ensure the
+ callback is called, if given.
+ """
+
+ def __init__(self, filename=None):
+ self._locations = []
+ self.hasPrimary = False
+ if filename:
+ self.read(filename)
+
+ def __iter__(self):
+ return self._locations.__iter__()
+
+ def __len__(self):
+ return len(self._locations)
+
+ def add(self, location):
+ if "primary" in location.options:
+ if self.hasPrimary:
+ raise MultiplePrimaryLocationsError()
+ self.hasPrimary = True
+
+ self._locations.append(location)
+
+ def add_host(self, host, port="80", scheme="http", options="privileged"):
+ if isinstance(options, string_types):
+ options = options.split(",")
+ self.add(Location(scheme, host, port, options))
+
+ def read(self, filename, check_for_primary=True):
+ """
+ Reads the file and adds all valid locations to the ``self._locations`` array.
+
+ :param filename: in the format of server-locations.txt_
+ :param check_for_primary: if True, a ``MissingPrimaryLocationError`` exception is raised
+ if no primary is found
+
+ .. _server-locations.txt: http://searchfox.org/mozilla-central/source/build/pgo/server-locations.txt # noqa
+
+ The only exception is that the port, if not defined, defaults to 80 or 443.
+
+ FIXME: Shouldn't this default to the protocol-appropriate port? Is
+ there any reason to have defaults at all?
+ """
+
+ locationFile = codecs.open(filename, "r", "UTF-8")
+ lineno = 0
+ new_locations = []
+
+ for line in locationFile:
+ line = line.strip()
+ lineno += 1
+
+ # check for comments and blank lines
+ if line.startswith("#") or not line:
+ continue
+
+ # split the server from the options
+ try:
+ server, options = line.rsplit(None, 1)
+ options = options.split(",")
+ except ValueError:
+ server = line
+ options = []
+
+ # parse the server url
+ if "://" not in server:
+ server = "http://" + server
+ scheme, netloc, path, query, fragment = parse.urlsplit(server)
+ # get the host and port
+ try:
+ host, port = netloc.rsplit(":", 1)
+ except ValueError:
+ host = netloc
+ port = DEFAULT_PORTS.get(scheme, "80")
+
+ try:
+ location = Location(scheme, host, port, options)
+ self.add(location)
+ except LocationError as e:
+ raise LocationsSyntaxError(lineno, e)
+
+ new_locations.append(location)
+
+ # ensure that a primary is found
+ if check_for_primary and not self.hasPrimary:
+ raise LocationsSyntaxError(lineno + 1, MissingPrimaryLocationError())
+
+
+class Permissions(object):
+ """Allows handling of permissions for ``mozprofile``"""
+
+ def __init__(self, locations=None):
+ self._locations = ServerLocations()
+ if locations:
+ if isinstance(locations, ServerLocations):
+ self._locations = locations
+ elif isinstance(locations, list):
+ for l in locations:
+ self._locations.add_host(**l)
+ elif isinstance(locations, dict):
+ self._locations.add_host(**locations)
+ elif os.path.exists(locations):
+ self._locations.read(locations)
+
+ def network_prefs(self, proxy=None):
+ """
+ take known locations and generate preferences to handle permissions and proxy
+ returns a tuple of prefs, user_prefs
+ """
+
+ prefs = []
+
+ if proxy:
+ dohServerPort = proxy.get("dohServerPort")
+ if dohServerPort is not None:
+ # make sure we don't use proxy
+ user_prefs = [("network.proxy.type", 0)]
+ # Use TRR_ONLY mode
+ user_prefs.append(("network.trr.mode", 3))
+ trrUri = "https://foo.example.com:{}/dns-query".format(dohServerPort)
+ user_prefs.append(("network.trr.uri", trrUri))
+ user_prefs.append(("network.trr.bootstrapAddr", "127.0.0.1"))
+ user_prefs.append(("network.dns.force_use_https_rr", True))
+ else:
+ user_prefs = self.pac_prefs(proxy)
+ else:
+ user_prefs = []
+
+ return prefs, user_prefs
+
+ def pac_prefs(self, user_proxy=None):
+ """
+ return preferences for Proxy Auto Config.
+ """
+ proxy = DEFAULT_PORTS.copy()
+
+ # We need to proxy every server but the primary one.
+ origins = ["'%s'" % l.url() for l in self._locations]
+ origins = ", ".join(origins)
+ proxy["origins"] = origins
+
+ for l in self._locations:
+ if "primary" in l.options:
+ proxy["remote"] = l.host
+ proxy[l.scheme] = l.port
+
+ # overwrite defaults with user specified proxy
+ if isinstance(user_proxy, dict):
+ proxy.update(user_proxy)
+
+ # TODO: this should live in a template!
+ # If you must escape things in this string with backslashes, be aware
+ # of the multiple layers of escaping at work:
+ #
+ # - Python will unescape backslashes;
+ # - Writing out the prefs will escape things via JSON serialization;
+ # - The prefs file reader will unescape backslashes;
+ # - The JS engine parser will unescape backslashes.
+ pacURL = (
+ """data:text/plain,
+var knownOrigins = (function () {
+ return [%(origins)s].reduce(function(t, h) { t[h] = true; return t; }, {})
+})();
+var uriRegex = new RegExp('^([a-z][-a-z0-9+.]*)' +
+ '://' +
+ '(?:[^/@]*@)?' +
+ '(.*?)' +
+ '(?::(\\\\d+))?/');
+var defaultPortsForScheme = {
+ 'http': 80,
+ 'ws': 80,
+ 'https': 443,
+ 'wss': 443
+};
+var originSchemesRemap = {
+ 'ws': 'http',
+ 'wss': 'https'
+};
+var proxyForScheme = {
+ 'http': 'PROXY %(remote)s:%(http)s',
+ 'https': 'PROXY %(remote)s:%(https)s',
+ 'ws': 'PROXY %(remote)s:%(ws)s',
+ 'wss': 'PROXY %(remote)s:%(wss)s'
+};
+
+function FindProxyForURL(url, host)
+{
+ var matches = uriRegex.exec(url);
+ if (!matches)
+ return 'DIRECT';
+ var originalScheme = matches[1];
+ var host = matches[2];
+ var port = matches[3];
+ if (!port && originalScheme in defaultPortsForScheme) {
+ port = defaultPortsForScheme[originalScheme];
+ }
+ var schemeForOriginChecking = originSchemesRemap[originalScheme] || originalScheme;
+
+ var origin = schemeForOriginChecking + '://' + host + ':' + port;
+ if (!(origin in knownOrigins))
+ return 'DIRECT';
+ return proxyForScheme[originalScheme] || 'DIRECT';
+}"""
+ % proxy
+ )
+ pacURL = "".join(pacURL.splitlines())
+
+ prefs = []
+ prefs.append(("network.proxy.type", 2))
+ prefs.append(("network.proxy.autoconfig_url", pacURL))
+
+ return prefs
diff --git a/testing/mozbase/mozprofile/mozprofile/prefs.py b/testing/mozbase/mozprofile/mozprofile/prefs.py
new file mode 100644
index 0000000000..6c3b1173c8
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/prefs.py
@@ -0,0 +1,263 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+user preferences
+"""
+import json
+import os
+import tokenize
+
+import mozfile
+import six
+from six import StringIO, string_types
+from six.moves.configparser import SafeConfigParser as ConfigParser
+
+if six.PY3:
+
+ def unicode(input):
+ return input
+
+
+__all__ = ("PreferencesReadError", "Preferences")
+
+
+class PreferencesReadError(Exception):
+ """read error for prefrences files"""
+
+
+class Preferences(object):
+ """assembly of preferences from various sources"""
+
+ def __init__(self, prefs=None):
+ self._prefs = []
+ if prefs:
+ self.add(prefs)
+
+ def add(self, prefs, cast=False):
+ """
+ :param prefs:
+ :param cast: whether to cast strings to value, e.g. '1' -> 1
+ """
+ # wants a list of 2-tuples
+ if isinstance(prefs, dict):
+ prefs = prefs.items()
+ if cast:
+ prefs = [(i, self.cast(j)) for i, j in prefs]
+ self._prefs += prefs
+
+ def add_file(self, path):
+ """a preferences from a file
+
+ :param path:
+ """
+ self.add(self.read(path))
+
+ def __call__(self):
+ return self._prefs
+
+ @classmethod
+ def cast(cls, value):
+ """
+ interpolate a preference from a string
+ from the command line or from e.g. an .ini file, there is no good way to denote
+ what type the preference value is, as natively it is a string
+
+ - integers will get cast to integers
+ - true/false will get cast to True/False
+ - anything enclosed in single quotes will be treated as a string
+ with the ''s removed from both sides
+ """
+
+ if not isinstance(value, string_types):
+ return value # no op
+ quote = "'"
+ if value == "true":
+ return True
+ if value == "false":
+ return False
+ try:
+ return int(value)
+ except ValueError:
+ pass
+ if value.startswith(quote) and value.endswith(quote):
+ value = value[1:-1]
+ return value
+
+ @classmethod
+ def read(cls, path):
+ """read preferences from a file"""
+
+ section = None # for .ini files
+ basename = os.path.basename(path)
+ if ":" in basename:
+ # section of INI file
+ path, section = path.rsplit(":", 1)
+
+ if not os.path.exists(path) and not mozfile.is_url(path):
+ raise PreferencesReadError("'%s' does not exist" % path)
+
+ if section:
+ try:
+ return cls.read_ini(path, section)
+ except PreferencesReadError:
+ raise
+ except Exception as e:
+ raise PreferencesReadError(str(e))
+
+ # try both JSON and .ini format
+ try:
+ return cls.read_json(path)
+ except Exception as e:
+ try:
+ return cls.read_ini(path)
+ except Exception as f:
+ for exception in e, f:
+ if isinstance(exception, PreferencesReadError):
+ raise exception
+ raise PreferencesReadError("Could not recognize format of %s" % path)
+
+ @classmethod
+ def read_ini(cls, path, section=None):
+ """read preferences from an .ini file"""
+
+ parser = ConfigParser()
+ parser.optionxform = str
+ parser.readfp(mozfile.load(path))
+
+ if section:
+ if section not in parser.sections():
+ raise PreferencesReadError("No section '%s' in %s" % (section, path))
+ retval = parser.items(section, raw=True)
+ else:
+ retval = parser.defaults().items()
+
+ # cast the preferences since .ini is just strings
+ return [(i, cls.cast(j)) for i, j in retval]
+
+ @classmethod
+ def read_json(cls, path):
+ """read preferences from a JSON blob"""
+
+ prefs = json.loads(mozfile.load(path).read())
+
+ if type(prefs) not in [list, dict]:
+ raise PreferencesReadError("Malformed preferences: %s" % path)
+ if isinstance(prefs, list):
+ if [i for i in prefs if type(i) != list or len(i) != 2]:
+ raise PreferencesReadError("Malformed preferences: %s" % path)
+ values = [i[1] for i in prefs]
+ elif isinstance(prefs, dict):
+ values = prefs.values()
+ else:
+ raise PreferencesReadError("Malformed preferences: %s" % path)
+ types = (bool, string_types, int)
+ if [i for i in values if not any([isinstance(i, j) for j in types])]:
+ raise PreferencesReadError("Only bool, string, and int values allowed")
+ return prefs
+
+ @classmethod
+ def read_prefs(cls, path, pref_setter="user_pref", interpolation=None):
+ """
+ Read preferences from (e.g.) prefs.js
+
+ :param path: The path to the preference file to read.
+ :param pref_setter: The name of the function used to set preferences
+ in the preference file.
+ :param interpolation: If provided, a dict that will be passed
+ to str.format to interpolate preference values.
+ """
+
+ marker = "##//" # magical marker
+ lines = [i.strip() for i in mozfile.load(path).readlines()]
+ _lines = []
+ multi_line_pref = None
+ for line in lines:
+ # decode bytes in case of URL processing
+ if isinstance(line, bytes):
+ line = line.decode()
+ pref_start = line.startswith(pref_setter)
+
+ # Handle preferences split over multiple lines
+ # Some lines may include brackets so do our best to ensure this
+ # is an actual expected end of function call by checking for a
+ # semi-colon as well.
+ if pref_start and not ");" in line:
+ multi_line_pref = line
+ continue
+ elif multi_line_pref:
+ multi_line_pref = multi_line_pref + line
+ if ");" in line:
+ if "//" in multi_line_pref:
+ multi_line_pref = multi_line_pref.replace("//", marker)
+ _lines.append(multi_line_pref)
+ multi_line_pref = None
+ continue
+ elif not pref_start:
+ continue
+
+ if "//" in line:
+ line = line.replace("//", marker)
+ _lines.append(line)
+ string = "\n".join(_lines)
+
+ # skip trailing comments
+ processed_tokens = []
+ f_obj = StringIO(string)
+ for token in tokenize.generate_tokens(f_obj.readline):
+ if token[0] == tokenize.COMMENT:
+ continue
+ processed_tokens.append(
+ token[:2]
+ ) # [:2] gets around http://bugs.python.org/issue9974
+ string = tokenize.untokenize(processed_tokens)
+
+ retval = []
+
+ def pref(a, b):
+ if interpolation and isinstance(b, string_types):
+ b = b.format(**interpolation)
+ retval.append((a, b))
+
+ lines = [i.strip().rstrip(";") for i in string.split("\n") if i.strip()]
+
+ _globals = {"retval": retval, "true": True, "false": False}
+ _globals[pref_setter] = pref
+ for line in lines:
+ try:
+ eval(line, _globals, {})
+ except SyntaxError:
+ print(line)
+ raise
+
+ # de-magic the marker
+ for index, (key, value) in enumerate(retval):
+ if isinstance(value, string_types) and marker in value:
+ retval[index] = (key, value.replace(marker, "//"))
+
+ return retval
+
+ @classmethod
+ def write(cls, _file, prefs, pref_string="user_pref(%s, %s);"):
+ """write preferences to a file"""
+
+ if isinstance(_file, string_types):
+ f = open(_file, "a")
+ else:
+ f = _file
+
+ if isinstance(prefs, dict):
+ # order doesn't matter
+ prefs = prefs.items()
+
+ # serialize -> JSON
+ _prefs = [(json.dumps(k), json.dumps(v)) for k, v in prefs]
+
+ # write the preferences
+ for _pref in _prefs:
+ print(unicode(pref_string % _pref), file=f)
+
+ # close the file if opened internally
+ if isinstance(_file, string_types):
+ f.close()
diff --git a/testing/mozbase/mozprofile/mozprofile/profile.py b/testing/mozbase/mozprofile/mozprofile/profile.py
new file mode 100644
index 0000000000..401715c93d
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/profile.py
@@ -0,0 +1,595 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import os
+import platform
+import tempfile
+import time
+import uuid
+from abc import ABCMeta, abstractmethod, abstractproperty
+from io import open
+from shutil import copytree
+
+import mozfile
+import six
+from six import python_2_unicode_compatible, string_types
+
+if six.PY3:
+
+ def unicode(input):
+ return input
+
+
+from .addons import AddonManager
+from .permissions import Permissions
+from .prefs import Preferences
+
+__all__ = [
+ "BaseProfile",
+ "ChromeProfile",
+ "ChromiumProfile",
+ "Profile",
+ "FirefoxProfile",
+ "ThunderbirdProfile",
+ "create_profile",
+]
+
+
+@six.add_metaclass(ABCMeta)
+class BaseProfile(object):
+ def __init__(self, profile=None, addons=None, preferences=None, restore=True):
+ """Create a new Profile.
+
+ All arguments are optional.
+
+ :param profile: Path to a profile. If not specified, a new profile
+ directory will be created.
+ :param addons: List of paths to addons which should be installed in the profile.
+ :param preferences: Dict of preferences to set in the profile.
+ :param restore: Whether or not to clean up any modifications made to this profile
+ (default True).
+ """
+ self._addons = addons or []
+
+ # Prepare additional preferences
+ if preferences:
+ if isinstance(preferences, dict):
+ # unordered
+ preferences = preferences.items()
+
+ # sanity check
+ assert not [i for i in preferences if len(i) != 2]
+ else:
+ preferences = []
+ self._preferences = preferences
+
+ # Handle profile creation
+ self.restore = restore
+ self.create_new = not profile
+ if profile:
+ # Ensure we have a full path to the profile
+ self.profile = os.path.abspath(os.path.expanduser(profile))
+ else:
+ self.profile = tempfile.mkdtemp(suffix=".mozrunner")
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, type, value, traceback):
+ self.cleanup()
+
+ def __del__(self):
+ self.cleanup()
+
+ def cleanup(self):
+ """Cleanup operations for the profile."""
+
+ if self.restore:
+ # If it's a temporary profile we have to remove it
+ if self.create_new:
+ mozfile.remove(self.profile)
+
+ @abstractmethod
+ def _reset(self):
+ pass
+
+ def reset(self):
+ """
+ reset the profile to the beginning state
+ """
+ self.cleanup()
+ self._reset()
+
+ @abstractmethod
+ def set_preferences(self, preferences, filename="user.js"):
+ pass
+
+ @abstractproperty
+ def preference_file_names(self):
+ """A tuple of file basenames expected to contain preferences."""
+
+ def merge(self, other, interpolation=None):
+ """Merges another profile into this one.
+
+ This will handle pref files matching the profile's
+ `preference_file_names` property, and any addons in the
+ other/extensions directory.
+ """
+ for basename in os.listdir(other):
+ if basename not in self.preference_file_names:
+ continue
+
+ path = os.path.join(other, basename)
+ try:
+ prefs = Preferences.read_json(path)
+ except ValueError:
+ prefs = Preferences.read_prefs(path, interpolation=interpolation)
+ self.set_preferences(prefs, filename=basename)
+
+ extension_dir = os.path.join(other, "extensions")
+ if not os.path.isdir(extension_dir):
+ return
+
+ for basename in os.listdir(extension_dir):
+ path = os.path.join(extension_dir, basename)
+
+ if self.addons.is_addon(path):
+ self._addons.append(path)
+ self.addons.install(path)
+
+ @classmethod
+ def clone(cls, path_from, path_to=None, ignore=None, **kwargs):
+ """Instantiate a temporary profile via cloning
+ - path: path of the basis to clone
+ - ignore: callable passed to shutil.copytree
+ - kwargs: arguments to the profile constructor
+ """
+ if not path_to:
+ tempdir = tempfile.mkdtemp() # need an unused temp dir name
+ mozfile.remove(tempdir) # copytree requires that dest does not exist
+ path_to = tempdir
+ copytree(path_from, path_to, ignore=ignore, ignore_dangling_symlinks=True)
+
+ c = cls(path_to, **kwargs)
+ c.create_new = True # deletes a cloned profile when restore is True
+ return c
+
+ def exists(self):
+ """returns whether the profile exists or not"""
+ return os.path.exists(self.profile)
+
+
+@python_2_unicode_compatible
+class Profile(BaseProfile):
+ """Handles all operations regarding profile.
+
+ Creating new profiles, installing add-ons, setting preferences and
+ handling cleanup.
+
+ The files associated with the profile will be removed automatically after
+ the object is garbage collected: ::
+
+ profile = Profile()
+ print profile.profile # this is the path to the created profile
+ del profile
+ # the profile path has been removed from disk
+
+ :meth:`cleanup` is called under the hood to remove the profile files. You
+ can ensure this method is called (even in the case of exception) by using
+ the profile as a context manager: ::
+
+ with Profile() as profile:
+ # do things with the profile
+ pass
+ # profile.cleanup() has been called here
+ """
+
+ preference_file_names = ("user.js", "prefs.js")
+
+ def __init__(
+ self,
+ profile=None,
+ addons=None,
+ preferences=None,
+ locations=None,
+ proxy=None,
+ restore=True,
+ whitelistpaths=None,
+ **kwargs
+ ):
+ """
+ :param profile: Path to the profile
+ :param addons: String of one or list of addons to install
+ :param preferences: Dictionary or class of preferences
+ :param locations: ServerLocations object
+ :param proxy: Setup a proxy
+ :param restore: Flag for removing all custom settings during cleanup
+ :param whitelistpaths: List of paths to pass to Firefox to allow read
+ access to from the content process sandbox.
+ """
+ super(Profile, self).__init__(
+ profile=profile,
+ addons=addons,
+ preferences=preferences,
+ restore=restore,
+ **kwargs
+ )
+
+ self._locations = locations
+ self._proxy = proxy
+ self._whitelistpaths = whitelistpaths
+
+ # Initialize all class members
+ self._reset()
+
+ def _reset(self):
+ """Internal: Initialize all class members to their default value"""
+
+ if not os.path.exists(self.profile):
+ os.makedirs(self.profile)
+
+ # Preferences files written to
+ self.written_prefs = set()
+
+ # Our magic markers
+ nonce = "%s %s" % (str(time.time()), uuid.uuid4())
+ self.delimeters = (
+ "#MozRunner Prefs Start %s" % nonce,
+ "#MozRunner Prefs End %s" % nonce,
+ )
+
+ # If sub-classes want to set default preferences
+ if hasattr(self.__class__, "preferences"):
+ self.set_preferences(self.__class__.preferences)
+ # Set additional preferences
+ self.set_preferences(self._preferences)
+
+ self.permissions = Permissions(self._locations)
+ prefs_js, user_js = self.permissions.network_prefs(self._proxy)
+
+ if self._whitelistpaths:
+ # On macOS we don't want to support a generalized read whitelist,
+ # and the macOS sandbox policy language doesn't have support for
+ # lists, so we handle these specially.
+ if platform.system() == "Darwin":
+ assert len(self._whitelistpaths) <= 2
+ if len(self._whitelistpaths) == 2:
+ prefs_js.append(
+ (
+ "security.sandbox.content.mac.testing_read_path2",
+ self._whitelistpaths[1],
+ )
+ )
+ prefs_js.append(
+ (
+ "security.sandbox.content.mac.testing_read_path1",
+ self._whitelistpaths[0],
+ )
+ )
+ else:
+ prefs_js.append(
+ (
+ "security.sandbox.content.read_path_whitelist",
+ ",".join(self._whitelistpaths),
+ )
+ )
+ self.set_preferences(prefs_js, "prefs.js")
+ self.set_preferences(user_js)
+
+ # handle add-on installation
+ self.addons = AddonManager(self.profile, restore=self.restore)
+ self.addons.install(self._addons)
+
+ def cleanup(self):
+ """Cleanup operations for the profile."""
+
+ if self.restore:
+ # If copies of those class instances exist ensure we correctly
+ # reset them all (see bug 934484)
+ self.clean_preferences()
+ if getattr(self, "addons", None) is not None:
+ self.addons.clean()
+ super(Profile, self).cleanup()
+
+ def clean_preferences(self):
+ """Removed preferences added by mozrunner."""
+ for filename in self.written_prefs:
+ if not os.path.exists(os.path.join(self.profile, filename)):
+ # file has been deleted
+ break
+ while True:
+ if not self.pop_preferences(filename):
+ break
+
+ # methods for preferences
+
+ def set_preferences(self, preferences, filename="user.js"):
+ """Adds preferences dict to profile preferences"""
+ prefs_file = os.path.join(self.profile, filename)
+ with open(prefs_file, "a") as f:
+ if not preferences:
+ return
+
+ # note what files we've touched
+ self.written_prefs.add(filename)
+
+ # opening delimeter
+ f.write(unicode("\n%s\n" % self.delimeters[0]))
+
+ Preferences.write(f, preferences)
+
+ # closing delimeter
+ f.write(unicode("%s\n" % self.delimeters[1]))
+
+ def set_persistent_preferences(self, preferences):
+ """
+ Adds preferences dict to profile preferences and save them during a
+ profile reset
+ """
+
+ # this is a dict sometimes, convert
+ if isinstance(preferences, dict):
+ preferences = preferences.items()
+
+ # add new prefs to preserve them during reset
+ for new_pref in preferences:
+ # if dupe remove item from original list
+ self._preferences = [
+ pref for pref in self._preferences if not new_pref[0] == pref[0]
+ ]
+ self._preferences.append(new_pref)
+
+ self.set_preferences(preferences, filename="user.js")
+
+ def pop_preferences(self, filename):
+ """
+ pop the last set of preferences added
+ returns True if popped
+ """
+
+ path = os.path.join(self.profile, filename)
+ with open(path, "r", encoding="utf-8") as f:
+ lines = f.read().splitlines()
+
+ def last_index(_list, value):
+ """
+ returns the last index of an item;
+ this should actually be part of python code but it isn't
+ """
+ for index in reversed(range(len(_list))):
+ if _list[index] == value:
+ return index
+
+ s = last_index(lines, self.delimeters[0])
+ e = last_index(lines, self.delimeters[1])
+
+ # ensure both markers are found
+ if s is None:
+ assert e is None, "%s found without %s" % (
+ self.delimeters[1],
+ self.delimeters[0],
+ )
+ return False # no preferences found
+ elif e is None:
+ assert s is None, "%s found without %s" % (
+ self.delimeters[0],
+ self.delimeters[1],
+ )
+
+ # ensure the markers are in the proper order
+ assert e > s, "%s found at %s, while %s found at %s" % (
+ self.delimeters[1],
+ e,
+ self.delimeters[0],
+ s,
+ )
+
+ # write the prefs
+ cleaned_prefs = "\n".join(lines[:s] + lines[e + 1 :])
+ with open(path, "w") as f:
+ f.write(cleaned_prefs)
+ return True
+
+ # methods for introspection
+
+ def summary(self, return_parts=False):
+ """
+ returns string summarizing profile information.
+ if return_parts is true, return the (Part_name, value) list
+ of tuples instead of the assembled string
+ """
+
+ parts = [("Path", self.profile)] # profile path
+
+ # directory tree
+ parts.append(("Files", "\n%s" % mozfile.tree(self.profile)))
+
+ # preferences
+ for prefs_file in ("user.js", "prefs.js"):
+ path = os.path.join(self.profile, prefs_file)
+ if os.path.exists(path):
+
+ # prefs that get their own section
+ # This is currently only 'network.proxy.autoconfig_url'
+ # but could be expanded to include others
+ section_prefs = ["network.proxy.autoconfig_url"]
+ line_length = 80
+ # buffer for 80 character display:
+ # length = 80 - len(key) - len(': ') - line_length_buffer
+ line_length_buffer = 10
+ line_length_buffer += len(": ")
+
+ def format_value(key, value):
+ if key not in section_prefs:
+ return value
+ max_length = line_length - len(key) - line_length_buffer
+ if len(value) > max_length:
+ value = "%s..." % value[:max_length]
+ return value
+
+ prefs = Preferences.read_prefs(path)
+ if prefs:
+ prefs = dict(prefs)
+ parts.append(
+ (
+ prefs_file,
+ "\n%s"
+ % (
+ "\n".join(
+ [
+ "%s: %s" % (key, format_value(key, prefs[key]))
+ for key in sorted(prefs.keys())
+ ]
+ )
+ ),
+ )
+ )
+
+ # Currently hardcorded to 'network.proxy.autoconfig_url'
+ # but could be generalized, possibly with a generalized (simple)
+ # JS-parser
+ network_proxy_autoconfig = prefs.get("network.proxy.autoconfig_url")
+ if network_proxy_autoconfig and network_proxy_autoconfig.strip():
+ network_proxy_autoconfig = network_proxy_autoconfig.strip()
+ lines = network_proxy_autoconfig.replace(
+ ";", ";\n"
+ ).splitlines()
+ lines = [line.strip() for line in lines]
+ origins_string = "var origins = ["
+ origins_end = "];"
+ if origins_string in lines[0]:
+ start = lines[0].find(origins_string)
+ end = lines[0].find(origins_end, start)
+ splitline = [
+ lines[0][:start],
+ lines[0][start : start + len(origins_string) - 1],
+ ]
+ splitline.extend(
+ lines[0][start + len(origins_string) : end]
+ .replace(",", ",\n")
+ .splitlines()
+ )
+ splitline.append(lines[0][end:])
+ lines[0:1] = [i.strip() for i in splitline]
+ parts.append(
+ (
+ "Network Proxy Autoconfig, %s" % (prefs_file),
+ "\n%s" % "\n".join(lines),
+ )
+ )
+
+ if return_parts:
+ return parts
+
+ retval = "%s\n" % (
+ "\n\n".join(["[%s]: %s" % (key, value) for key, value in parts])
+ )
+ return retval
+
+ def __str__(self):
+ return self.summary()
+
+
+class FirefoxProfile(Profile):
+ """Specialized Profile subclass for Firefox"""
+
+ preferences = {}
+
+
+class ThunderbirdProfile(Profile):
+ """Specialized Profile subclass for Thunderbird"""
+
+ preferences = {
+ "extensions.update.enabled": False,
+ "extensions.update.notifyUser": False,
+ "browser.shell.checkDefaultBrowser": False,
+ "browser.tabs.warnOnClose": False,
+ "browser.warnOnQuit": False,
+ "browser.sessionstore.resume_from_crash": False,
+ # prevents the 'new e-mail address' wizard on new profile
+ "mail.provider.enabled": False,
+ }
+
+
+class ChromiumProfile(BaseProfile):
+ preference_file_names = ("Preferences",)
+
+ class AddonManager(list):
+ def install(self, addons):
+ if isinstance(addons, string_types):
+ addons = [addons]
+ self.extend(addons)
+
+ @classmethod
+ def is_addon(self, addon):
+ # Don't include testing/profiles on Google Chrome
+ return False
+
+ def __init__(self, **kwargs):
+ super(ChromiumProfile, self).__init__(**kwargs)
+
+ if self.create_new:
+ self.profile = os.path.join(self.profile, "Default")
+ self._reset()
+
+ def _reset(self):
+ if not os.path.isdir(self.profile):
+ os.makedirs(self.profile)
+
+ if self._preferences:
+ self.set_preferences(self._preferences)
+
+ self.addons = self.AddonManager()
+ if self._addons:
+ self.addons.install(self._addons)
+
+ def set_preferences(self, preferences, filename="Preferences", **values):
+ pref_file = os.path.join(self.profile, filename)
+
+ prefs = {}
+ if os.path.isfile(pref_file):
+ with open(pref_file, "r") as fh:
+ prefs.update(json.load(fh))
+
+ prefs.update(preferences)
+ with open(pref_file, "w") as fh:
+ prefstr = json.dumps(prefs)
+ prefstr % values # interpolate prefs with values
+ if six.PY2:
+ fh.write(unicode(prefstr))
+ else:
+ fh.write(prefstr)
+
+
+class ChromeProfile(ChromiumProfile):
+ # update this if Google Chrome requires more
+ # specific profiles
+ pass
+
+
+profile_class = {
+ "chrome": ChromeProfile,
+ "chromium": ChromiumProfile,
+ "firefox": FirefoxProfile,
+ "thunderbird": ThunderbirdProfile,
+}
+
+
+def create_profile(app, **kwargs):
+ """Create a profile given an application name.
+
+ :param app: String name of the application to create a profile for, e.g 'firefox'.
+ :param kwargs: Same as the arguments for the Profile class (optional).
+ :returns: An application specific Profile instance
+ :raises: NotImplementedError
+ """
+ cls = profile_class.get(app)
+
+ if not cls:
+ raise NotImplementedError(
+ "Profiles not supported for application '{}'".format(app)
+ )
+
+ return cls(**kwargs)
diff --git a/testing/mozbase/mozprofile/mozprofile/view.py b/testing/mozbase/mozprofile/mozprofile/view.py
new file mode 100644
index 0000000000..9356623232
--- /dev/null
+++ b/testing/mozbase/mozprofile/mozprofile/view.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+"""
+script to view mozilla profiles
+"""
+import optparse
+import os
+import sys
+
+import mozprofile
+
+__all__ = ["view_profile"]
+
+
+def view_profile(args=sys.argv[1:]):
+
+ usage = "%prog [options] profile_path <...>"
+ parser = optparse.OptionParser(usage=usage, description=__doc__)
+ options, args = parser.parse_args(args)
+ if not args:
+ parser.print_usage()
+ parser.exit()
+
+ # check existence
+ missing = [i for i in args if not os.path.exists(i)]
+ if missing:
+ if len(missing) > 1:
+ missing_string = "Profiles do not exist"
+ else:
+ missing_string = "Profile does not exist"
+ parser.error("%s: %s" % (missing_string, ", ".join(missing)))
+
+ # print summary for each profile
+ while args:
+ path = args.pop(0)
+ profile = mozprofile.Profile(path)
+ print(profile.summary())
+ if args:
+ print("-" * 4)
+
+
+if __name__ == "__main__":
+ view_profile()