diff options
Diffstat (limited to 'testing/mozbase/mozprofile/mozprofile')
-rw-r--r-- | testing/mozbase/mozprofile/mozprofile/__init__.py | 20 | ||||
-rw-r--r-- | testing/mozbase/mozprofile/mozprofile/addons.py | 354 | ||||
-rwxr-xr-x | testing/mozbase/mozprofile/mozprofile/cli.py | 204 | ||||
-rw-r--r-- | testing/mozbase/mozprofile/mozprofile/diff.py | 86 | ||||
-rw-r--r-- | testing/mozbase/mozprofile/mozprofile/permissions.py | 335 | ||||
-rw-r--r-- | testing/mozbase/mozprofile/mozprofile/prefs.py | 263 | ||||
-rw-r--r-- | testing/mozbase/mozprofile/mozprofile/profile.py | 595 | ||||
-rw-r--r-- | testing/mozbase/mozprofile/mozprofile/view.py | 47 |
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() |