diff options
Diffstat (limited to 'testing/mozbase/mozprofile')
46 files changed, 3832 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..104f6bd08c --- /dev/null +++ b/testing/mozbase/mozprofile/mozprofile/cli.py @@ -0,0 +1,203 @@ +#!/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..192b59cd9e --- /dev/null +++ b/testing/mozbase/mozprofile/mozprofile/diff.py @@ -0,0 +1,84 @@ +#!/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..b84d531f23 --- /dev/null +++ b/testing/mozbase/mozprofile/mozprofile/prefs.py @@ -0,0 +1,271 @@ +# 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 + +try: + from six.moves.configparser import SafeConfigParser as ConfigParser +except ImportError: # SafeConfigParser was removed in 3.12 + from configparser import ConfigParser +try: + ConfigParser.read_file +except AttributeError: # read_file was added in 3.2, readfp removed in 3.12 + ConfigParser.read_file = ConfigParser.readfp + +if six.PY3: + + def unicode(input): + return input + + +__all__ = ("PreferencesReadError", "Preferences") + + +class PreferencesReadError(Exception): + """read error for preferences 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.read_file(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..25e10d23bf --- /dev/null +++ b/testing/mozbase/mozprofile/mozprofile/profile.py @@ -0,0 +1,594 @@ +# 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..455751148b --- /dev/null +++ b/testing/mozbase/mozprofile/mozprofile/view.py @@ -0,0 +1,46 @@ +#!/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() diff --git a/testing/mozbase/mozprofile/setup.cfg b/testing/mozbase/mozprofile/setup.cfg new file mode 100644 index 0000000000..3c6e79cf31 --- /dev/null +++ b/testing/mozbase/mozprofile/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/testing/mozbase/mozprofile/setup.py b/testing/mozbase/mozprofile/setup.py new file mode 100644 index 0000000000..8344bd83ae --- /dev/null +++ b/testing/mozbase/mozprofile/setup.py @@ -0,0 +1,49 @@ +# 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/. + +from setuptools import setup + +PACKAGE_NAME = "mozprofile" +PACKAGE_VERSION = "2.6.1" + +deps = [ + "mozfile>=1.2", + "mozlog>=6.0", + "six>=1.13.0,<2", +] + +setup( + name=PACKAGE_NAME, + version=PACKAGE_VERSION, + description="Library to create and modify Mozilla application profiles", + long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html", + classifiers=[ + "Environment :: Console", + "Intended Audience :: Developers", + "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", + "Natural Language :: English", + "Operating System :: OS Independent", + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.5", + "Topic :: Software Development :: Libraries :: Python Modules", + ], + keywords="mozilla", + author="Mozilla Automation and Tools team", + author_email="tools@lists.mozilla.org", + url="https://wiki.mozilla.org/Auto-tools/Projects/Mozbase", + license="MPL 2.0", + packages=["mozprofile"], + include_package_data=True, + zip_safe=False, + install_requires=deps, + extras_require={"manifest": ["manifestparser >= 0.6"]}, + tests_require=["wptserve"], + entry_points=""" + # -*- Entry points: -*- + [console_scripts] + mozprofile = mozprofile:cli + view-profile = mozprofile:view_profile + diff-profiles = mozprofile:diff_profiles + """, +) diff --git a/testing/mozbase/mozprofile/tests/addon_stubs.py b/testing/mozbase/mozprofile/tests/addon_stubs.py new file mode 100644 index 0000000000..37c6305c6a --- /dev/null +++ b/testing/mozbase/mozprofile/tests/addon_stubs.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python + +import os +import tempfile +import zipfile + +import mozfile + +here = os.path.dirname(os.path.abspath(__file__)) + +# stubs is a dict of the form {'addon id': 'install manifest content'} +stubs = { + "test-addon-1@mozilla.org": "test_addon_1.rdf", + "test-addon-2@mozilla.org": "test_addon_2.rdf", + "test-addon-3@mozilla.org": "test_addon_3.rdf", + "test-addon-4@mozilla.org": "test_addon_4.rdf", + "test-addon-invalid-no-id@mozilla.org": "test_addon_invalid_no_id.rdf", + "test-addon-invalid-version@mozilla.org": "test_addon_invalid_version.rdf", + "test-addon-invalid-no-manifest@mozilla.org": None, + "test-addon-invalid-not-wellformed@mozilla.org": "test_addon_invalid_not_wellformed.rdf", + "test-addon-unpack@mozilla.org": "test_addon_unpack.rdf", +} + + +def generate_addon(addon_id, path=None, name=None, xpi=True): + """ + Method to generate a single addon. + + :param addon_id: id of an addon to generate from the stubs dictionary + :param path: path where addon and .xpi should be generated + :param name: name for the addon folder or .xpi file + :param xpi: Flag if an XPI or folder should be generated + + Returns the file-path of the addon's .xpi file + """ + + if addon_id not in stubs: + raise IOError('Requested addon stub "%s" does not exist' % addon_id) + + # Generate directory structure for addon + try: + tmpdir = path or tempfile.mkdtemp() + addon_dir = os.path.join(tmpdir, name or addon_id) + os.mkdir(addon_dir) + except IOError: + raise IOError("Could not generate directory structure for addon stub.") + + # Write install.rdf for addon + if stubs[addon_id]: + install_rdf = os.path.join(addon_dir, "install.rdf") + with open(install_rdf, "w") as f: + manifest = os.path.join(here, "install_manifests", stubs[addon_id]) + f.write(open(manifest, "r").read()) + + if not xpi: + return addon_dir + + # Generate the .xpi for the addon + xpi_file = os.path.join(tmpdir, (name or addon_id) + ".xpi") + with zipfile.ZipFile(xpi_file, "w") as x: + x.write(install_rdf, install_rdf[len(addon_dir) :]) + + # Ensure we remove the temporary folder to not install the addon twice + mozfile.rmtree(addon_dir) + + return xpi_file diff --git a/testing/mozbase/mozprofile/tests/addons/apply-css-id-via-browser-specific-settings.xpi b/testing/mozbase/mozprofile/tests/addons/apply-css-id-via-browser-specific-settings.xpi Binary files differnew file mode 100644 index 0000000000..c9ad38f63b --- /dev/null +++ b/testing/mozbase/mozprofile/tests/addons/apply-css-id-via-browser-specific-settings.xpi diff --git a/testing/mozbase/mozprofile/tests/addons/apply-css-sans-id.xpi b/testing/mozbase/mozprofile/tests/addons/apply-css-sans-id.xpi Binary files differnew file mode 100644 index 0000000000..fa721a4f76 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/addons/apply-css-sans-id.xpi diff --git a/testing/mozbase/mozprofile/tests/addons/apply-css.xpi b/testing/mozbase/mozprofile/tests/addons/apply-css.xpi Binary files differnew file mode 100644 index 0000000000..0ed64f79ac --- /dev/null +++ b/testing/mozbase/mozprofile/tests/addons/apply-css.xpi diff --git a/testing/mozbase/mozprofile/tests/addons/empty.xpi b/testing/mozbase/mozprofile/tests/addons/empty.xpi Binary files differnew file mode 100644 index 0000000000..26f28f099d --- /dev/null +++ b/testing/mozbase/mozprofile/tests/addons/empty.xpi diff --git a/testing/mozbase/mozprofile/tests/addons/empty/install.rdf b/testing/mozbase/mozprofile/tests/addons/empty/install.rdf new file mode 100644 index 0000000000..70b9e13e44 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/addons/empty/install.rdf @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>test-empty@quality.mozilla.org</em:id> + <em:version>0.1</em:version> + <em:name>Test Extension (empty)</em:name> + <em:creator>Mozilla QA</em:creator> + <em:homepageURL>http://quality.mozilla.org</em:homepageURL> + <em:type>2</em:type> + + <!-- Firefox --> + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5.*</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/testing/mozbase/mozprofile/tests/addons/invalid.xpi b/testing/mozbase/mozprofile/tests/addons/invalid.xpi Binary files differnew file mode 100644 index 0000000000..2f222c7637 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/addons/invalid.xpi diff --git a/testing/mozbase/mozprofile/tests/files/dummy-profile/.eslintrc.js b/testing/mozbase/mozprofile/tests/files/dummy-profile/.eslintrc.js new file mode 100644 index 0000000000..8d36cca287 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + globals: { + user_pref: true, + }, +}; diff --git a/testing/mozbase/mozprofile/tests/files/dummy-profile/Preferences b/testing/mozbase/mozprofile/tests/files/dummy-profile/Preferences new file mode 100644 index 0000000000..697201c687 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/Preferences @@ -0,0 +1 @@ +{"Preferences": 1} diff --git a/testing/mozbase/mozprofile/tests/files/dummy-profile/extensions/empty.xpi b/testing/mozbase/mozprofile/tests/files/dummy-profile/extensions/empty.xpi Binary files differnew file mode 100644 index 0000000000..26f28f099d --- /dev/null +++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/extensions/empty.xpi diff --git a/testing/mozbase/mozprofile/tests/files/dummy-profile/prefs.js b/testing/mozbase/mozprofile/tests/files/dummy-profile/prefs.js new file mode 100644 index 0000000000..c2e18261a8 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/prefs.js @@ -0,0 +1 @@ +user_pref("prefs.js", 1); diff --git a/testing/mozbase/mozprofile/tests/files/dummy-profile/user.js b/testing/mozbase/mozprofile/tests/files/dummy-profile/user.js new file mode 100644 index 0000000000..66752d6397 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/files/dummy-profile/user.js @@ -0,0 +1 @@ +user_pref("user.js", 1); diff --git a/testing/mozbase/mozprofile/tests/files/not_an_addon.txt b/testing/mozbase/mozprofile/tests/files/not_an_addon.txt new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/files/not_an_addon.txt diff --git a/testing/mozbase/mozprofile/tests/files/prefs_with_comments.js b/testing/mozbase/mozprofile/tests/files/prefs_with_comments.js new file mode 100644 index 0000000000..06a56f2138 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/files/prefs_with_comments.js @@ -0,0 +1,6 @@ +# A leading comment +user_pref("browser.startup.homepage", "http://planet.mozilla.org"); # A trailing comment +user_pref("zoom.minPercent", 30); +// Another leading comment +user_pref("zoom.maxPercent", 300); // Another trailing comment +user_pref("webgl.verbose", "false"); diff --git a/testing/mozbase/mozprofile/tests/files/prefs_with_interpolation.js b/testing/mozbase/mozprofile/tests/files/prefs_with_interpolation.js new file mode 100644 index 0000000000..52700df6d8 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/files/prefs_with_interpolation.js @@ -0,0 +1,5 @@ +/* globals user_pref */ +user_pref("browser.foo", "http://{server}"); +user_pref("zoom.minPercent", 30); +user_pref("webgl.verbose", "false"); +user_pref("browser.bar", "{abc}xyz"); diff --git a/testing/mozbase/mozprofile/tests/files/prefs_with_multiline.js b/testing/mozbase/mozprofile/tests/files/prefs_with_multiline.js new file mode 100644 index 0000000000..f3860f6472 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/files/prefs_with_multiline.js @@ -0,0 +1,5 @@ +/* globals user_pref */ +user_pref( + "browser.long.preference.name.that.causes.the.line.to.wrap", + "itislong" +); diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_1.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_1.rdf new file mode 100644 index 0000000000..839ea9fbd5 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_1.rdf @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>test-addon-1@mozilla.org</em:id> + <em:version>0.1</em:version> + <em:name>Test Add-on 1</em:name> + <em:creator>Mozilla</em:creator> + <em:homepageURL>http://mozilla.org</em:homepageURL> + <em:type>2</em:type> + + <!-- Firefox --> + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5.*</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_2.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_2.rdf new file mode 100644 index 0000000000..8303e862fc --- /dev/null +++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_2.rdf @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>test-addon-2@mozilla.org</em:id> + <em:version>0.2</em:version> + <em:name>Test Add-on 2</em:name> + <em:creator>Mozilla</em:creator> + <em:homepageURL>http://mozilla.org</em:homepageURL> + <em:type>2</em:type> + + <!-- Firefox --> + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5.*</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_3.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_3.rdf new file mode 100644 index 0000000000..5bd6d38043 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_3.rdf @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>test-addon-3@mozilla.org</em:id> + <em:version>0.1</em:version> + <em:name>Test Add-on 3</em:name> + <em:creator>Mozilla</em:creator> + <em:homepageURL>http://mozilla.org</em:homepageURL> + <em:type>2</em:type> + + <!-- Firefox --> + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5.*</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> + diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_4.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_4.rdf new file mode 100644 index 0000000000..e0f99d3133 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_4.rdf @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>test-addon-4@mozilla.org</em:id> + <em:version>0.1</em:version> + <em:name>Test Add-on 4</em:name> + <em:creator>Mozilla</em:creator> + <em:homepageURL>http://mozilla.org</em:homepageURL> + <em:type>2</em:type> + + <!-- Firefox --> + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5.*</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> + diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_no_id.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_no_id.rdf new file mode 100644 index 0000000000..23f60fece1 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_no_id.rdf @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <!-- Invalid because of a missing add-on id --> + <em:version>0.1</em:version> + <em:name>Test Invalid Extension (no id)</em:name> + <em:creator>Mozilla</em:creator> + <em:homepageURL>http://mozilla.org</em:homepageURL> + <em:type>2</em:type> + + <!-- Firefox --> + <em:targetApplication> + <Description> + <!-- Invalid target application string --> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5.*</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_not_wellformed.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_not_wellformed.rdf new file mode 100644 index 0000000000..690ec406cc --- /dev/null +++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_not_wellformed.rdf @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <!-- Invalid because it's not well-formed --> + <em:id>test-addon-invalid-not-wellformed@mozilla.org</em:id + <em:version>0.1</em:version> + <em:name>Test Invalid Extension (no id)</em:name> + <em:creator>Mozilla</em:creator> + <em:homepageURL>http://mozilla.org</em:homepageURL> + <em:type>2</em:type> + + <!-- Firefox --> + <em:targetApplication> + <Description> + <!-- Invalid target application string --> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5.*</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_version.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_version.rdf new file mode 100644 index 0000000000..c854bfcdb5 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_invalid_version.rdf @@ -0,0 +1,23 @@ +<?xml version="1.0"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>test-addon-invalid-version@mozilla.org</em:id> + <!-- Invalid addon version --> + <em:version>0.NOPE</em:version> + <em:name>Test Invalid Extension (invalid version)</em:name> + <em:creator>Mozilla</em:creator> + <em:homepageURL>http://mozilla.org</em:homepageURL> + <em:type>2</em:type> + + <!-- Firefox --> + <em:targetApplication> + <Description> + <!-- Invalid target application string --> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5.*</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/testing/mozbase/mozprofile/tests/install_manifests/test_addon_unpack.rdf b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_unpack.rdf new file mode 100644 index 0000000000..cc85ea560f --- /dev/null +++ b/testing/mozbase/mozprofile/tests/install_manifests/test_addon_unpack.rdf @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>test-addon-unpack@mozilla.org</em:id> + <em:version>0.1</em:version> + <em:name>Test Add-on (unpack)</em:name> + <em:creator>Mozilla</em:creator> + <em:homepageURL>http://mozilla.org</em:homepageURL> + <em:type>2</em:type> + <em:unpack>true</em:unpack> + + <!-- Firefox --> + <em:targetApplication> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5.*</em:minVersion> + <em:maxVersion>*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF> diff --git a/testing/mozbase/mozprofile/tests/manifest.toml b/testing/mozbase/mozprofile/tests/manifest.toml new file mode 100644 index 0000000000..1f5b90914a --- /dev/null +++ b/testing/mozbase/mozprofile/tests/manifest.toml @@ -0,0 +1,24 @@ +[DEFAULT] +subsuite = "mozbase" + +["test_addonid.py"] + +["test_addons.py"] + +["test_bug758250.py"] + +["test_chrome_profile.py"] + +["test_clone_cleanup.py"] + +["test_nonce.py"] + +["test_permissions.py"] + +["test_preferences.py"] + +["test_profile.py"] + +["test_profile_view.py"] + +["test_server_locations.py"] diff --git a/testing/mozbase/mozprofile/tests/test_addonid.py b/testing/mozbase/mozprofile/tests/test_addonid.py new file mode 100755 index 0000000000..4a9bef0163 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_addonid.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python + +import os + +import mozunit +import pytest +from mozprofile import addons + +here = os.path.dirname(os.path.abspath(__file__)) + + +"""Test finding the addon id in a variety of install.rdf styles""" + + +ADDON_ID_TESTS = [ + """ +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:id>winning</em:id> + <em:name>MozMill</em:name> + <em:version>2.0a</em:version> + <em:creator>Adam Christian</em:creator> + <em:description>A testing extension based on the + Windmill Testing Framework client source</em:description> + <em:unpack>true</em:unpack> + <em:targetApplication> + <!-- Firefox --> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5</em:minVersion> + <em:maxVersion>8.*</em:maxVersion> + </Description> + </em:targetApplication> + <em:targetApplication> + <!-- Thunderbird --> + <Description> + <em:id>{3550f703-e582-4d05-9a08-453d09bdfdc6}</em:id> + <em:minVersion>3.0a1pre</em:minVersion> + <em:maxVersion>3.2*</em:maxVersion> + </Description> + </em:targetApplication> + <em:targetApplication> + <!-- Sunbird --> + <Description> + <em:id>{718e30fb-e89b-41dd-9da7-e25a45638b28}</em:id> + <em:minVersion>0.6a1</em:minVersion> + <em:maxVersion>1.0pre</em:maxVersion> + </Description> + </em:targetApplication> + <em:targetApplication> + <!-- SeaMonkey --> + <Description> + <em:id>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</em:id> + <em:minVersion>2.0a1</em:minVersion> + <em:maxVersion>2.1*</em:maxVersion> + </Description> + </em:targetApplication> + <em:targetApplication> + <!-- Songbird --> + <Description> + <em:id>songbird@songbirdnest.com</em:id> + <em:minVersion>0.3pre</em:minVersion> + <em:maxVersion>1.3.0a</em:maxVersion> + </Description> + </em:targetApplication> + <em:targetApplication> + <Description> + <em:id>toolkit@mozilla.org</em:id> + <em:minVersion>1.9.1</em:minVersion> + <em:maxVersion>2.0*</em:maxVersion> + </Description> + </em:targetApplication> + </Description> +</RDF>""", + """ +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:em="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <em:targetApplication> + <!-- Firefox --> + <Description> + <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id> + <em:minVersion>3.5</em:minVersion> + <em:maxVersion>8.*</em:maxVersion> + </Description> + </em:targetApplication> + <em:id>winning</em:id> + <em:name>MozMill</em:name> + <em:version>2.0a</em:version> + <em:creator>Adam Christian</em:creator> + <em:description>A testing extension based on the + Windmill Testing Framework client source</em:description> + <em:unpack>true</em:unpack> + </Description> + </RDF>""", + """ +<RDF xmlns="http://www.mozilla.org/2004/em-rdf#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"> + <rdf:Description about="urn:mozilla:install-manifest"> + <id>winning</id> + <name>foo</name> + <version>42</version> + <description>A testing extension based on the + Windmill Testing Framework client source</description> + </rdf:Description> +</RDF>""", + """ +<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:foobar="http://www.mozilla.org/2004/em-rdf#"> + <Description about="urn:mozilla:install-manifest"> + <foobar:targetApplication> + <!-- Firefox --> + <Description> + <foobar:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</foobar:id> + <foobar:minVersion>3.5</foobar:minVersion> + <foobar:maxVersion>8.*</foobar:maxVersion> + </Description> + </foobar:targetApplication> + <foobar:id>winning</foobar:id> + <foobar:name>MozMill</foobar:name> + <foobar:version>2.0a</foobar:version> + <foobar:creator>Adam Christian</foobar:creator> + <foobar:description>A testing extension based on the + Windmill Testing Framework client source</foobar:description> + <foobar:unpack>true</foobar:unpack> + </Description> + </RDF>""", +] + + +@pytest.fixture( + params=ADDON_ID_TESTS, ids=[str(i) for i in range(0, len(ADDON_ID_TESTS))] +) +def profile(request, tmpdir): + test = request.param + path = tmpdir.mkdtemp().strpath + + with open(os.path.join(path, "install.rdf"), "w") as fh: + fh.write(test) + return path + + +def test_addonID(profile): + a = addons.AddonManager(os.path.join(profile, "profile")) + addon_id = a.addon_details(profile)["id"] + assert addon_id == "winning" + + +def test_addonID_xpi(): + a = addons.AddonManager("profile") + addon = a.addon_details(os.path.join(here, "addons", "empty.xpi")) + assert addon["id"] == "test-empty@quality.mozilla.org" + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprofile/tests/test_addons.py b/testing/mozbase/mozprofile/tests/test_addons.py new file mode 100644 index 0000000000..f6b79ce498 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_addons.py @@ -0,0 +1,373 @@ +#!/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/. + +import os +import zipfile + +import mozprofile +import mozunit +import pytest +from addon_stubs import generate_addon + +here = os.path.dirname(os.path.abspath(__file__)) + + +@pytest.fixture +def profile(): + return mozprofile.Profile() + + +@pytest.fixture +def am(profile): + return profile.addons + + +def test_install_multiple_same_source(tmpdir, am): + path = tmpdir.strpath + + # Generate installer stubs for all possible types of addons + addon_xpi = generate_addon("test-addon-1@mozilla.org", path=path) + addon_folder = generate_addon("test-addon-1@mozilla.org", path=path, xpi=False) + + # The same folder should not be installed twice + am.install([addon_folder, addon_folder]) + assert am.installed_addons == [addon_folder] + am.clean() + + # The same XPI file should not be installed twice + am.install([addon_xpi, addon_xpi]) + assert am.installed_addons == [addon_xpi] + am.clean() + + # Even if it is the same id the add-on should be installed twice, if + # specified via XPI and folder + am.install([addon_folder, addon_xpi]) + assert len(am.installed_addons) == 2 + assert addon_folder in am.installed_addons + assert addon_xpi in am.installed_addons + am.clean() + + +def test_install_webextension_from_dir(tmpdir, am): + tmpdir = tmpdir.strpath + + addon = os.path.join(here, "addons", "apply-css.xpi") + zipped = zipfile.ZipFile(addon) + try: + zipped.extractall(tmpdir) + finally: + zipped.close() + am.install(tmpdir) + assert len(am.installed_addons) == 1 + assert os.path.isdir(am.installed_addons[0]) + + +def test_install_webextension(am): + addon = os.path.join(here, "addons", "apply-css.xpi") + + am.install(addon) + assert len(am.installed_addons) == 1 + assert os.path.isfile(am.installed_addons[0]) + assert "apply-css.xpi" == os.path.basename(am.installed_addons[0]) + + details = am.addon_details(am.installed_addons[0]) + assert "test-webext@quality.mozilla.org" == details["id"] + + +def test_install_webextension_id_via_browser_specific_settings(am): + # See Bug 1572404 + addon = os.path.join( + here, "addons", "apply-css-id-via-browser-specific-settings.xpi" + ) + am.install(addon) + assert len(am.installed_addons) == 1 + assert os.path.isfile(am.installed_addons[0]) + assert "apply-css-id-via-browser-specific-settings.xpi" == os.path.basename( + am.installed_addons[0] + ) + + details = am.addon_details(am.installed_addons[0]) + assert "test-webext@quality.mozilla.org" == details["id"] + + +def test_install_webextension_sans_id(am): + addon = os.path.join(here, "addons", "apply-css-sans-id.xpi") + am.install(addon) + + assert len(am.installed_addons) == 1 + assert os.path.isfile(am.installed_addons[0]) + assert "apply-css-sans-id.xpi" == os.path.basename(am.installed_addons[0]) + + details = am.addon_details(am.installed_addons[0]) + assert "@temporary-addon" in details["id"] + + +def test_install_xpi(tmpdir, am): + tmpdir = tmpdir.strpath + + addons_to_install = [] + addons_installed = [] + + # Generate installer stubs and install them + for ext in ["test-addon-1@mozilla.org", "test-addon-2@mozilla.org"]: + temp_addon = generate_addon(ext, path=tmpdir) + addons_to_install.append(am.addon_details(temp_addon)["id"]) + am.install(temp_addon) + + # Generate a list of addons installed in the profile + addons_installed = [ + str(x[: -len(".xpi")]) + for x in os.listdir(os.path.join(am.profile, "extensions")) + ] + assert addons_to_install.sort() == addons_installed.sort() + + +def test_install_folder(tmpdir, am): + tmpdir = tmpdir.strpath + + # Generate installer stubs for all possible types of addons + addons = [] + addons.append(generate_addon("test-addon-1@mozilla.org", path=tmpdir)) + addons.append(generate_addon("test-addon-2@mozilla.org", path=tmpdir, xpi=False)) + addons.append( + generate_addon("test-addon-3@mozilla.org", path=tmpdir, name="addon-3") + ) + addons.append( + generate_addon( + "test-addon-4@mozilla.org", path=tmpdir, name="addon-4", xpi=False + ) + ) + addons.sort() + + am.install(tmpdir) + + assert am.installed_addons == addons + + +def test_install_unpack(tmpdir, am): + tmpdir = tmpdir.strpath + + # Generate installer stubs for all possible types of addons + addon_xpi = generate_addon("test-addon-unpack@mozilla.org", path=tmpdir) + addon_folder = generate_addon( + "test-addon-unpack@mozilla.org", path=tmpdir, xpi=False + ) + addon_no_unpack = generate_addon("test-addon-1@mozilla.org", path=tmpdir) + + # Test unpack flag for add-on as XPI + am.install(addon_xpi) + assert am.installed_addons == [addon_xpi] + am.clean() + + # Test unpack flag for add-on as folder + am.install(addon_folder) + assert am.installed_addons == [addon_folder] + am.clean() + + # Test forcing unpack an add-on + am.install(addon_no_unpack, unpack=True) + assert am.installed_addons == [addon_no_unpack] + am.clean() + + +def test_install_after_reset(tmpdir, profile): + tmpdir = tmpdir.strpath + am = profile.addons + + # Installing the same add-on after a reset should not cause a failure + addon = generate_addon("test-addon-1@mozilla.org", path=tmpdir, xpi=False) + + # We cannot use am because profile.reset() creates a new instance + am.install(addon) + profile.reset() + + am.install(addon) + assert am.installed_addons == [addon] + + +def test_install_backup(tmpdir, am): + tmpdir = tmpdir.strpath + + staged_path = os.path.join(am.profile, "extensions") + + # Generate installer stubs for all possible types of addons + addon_xpi = generate_addon("test-addon-1@mozilla.org", path=tmpdir) + addon_folder = generate_addon("test-addon-1@mozilla.org", path=tmpdir, xpi=False) + addon_name = generate_addon( + "test-addon-1@mozilla.org", path=tmpdir, name="test-addon-1-dupe@mozilla.org" + ) + + # Test backup of xpi files + am.install(addon_xpi) + assert am.backup_dir is None + + am.install(addon_xpi) + assert am.backup_dir is not None + assert os.listdir(am.backup_dir) == ["test-addon-1@mozilla.org.xpi"] + + am.clean() + assert os.listdir(staged_path) == ["test-addon-1@mozilla.org.xpi"] + am.clean() + + # Test backup of folders + am.install(addon_folder) + assert am.backup_dir is None + + am.install(addon_folder) + assert am.backup_dir is not None + assert os.listdir(am.backup_dir) == ["test-addon-1@mozilla.org"] + + am.clean() + assert os.listdir(staged_path) == ["test-addon-1@mozilla.org"] + am.clean() + + # Test backup of xpi files with another file name + am.install(addon_name) + assert am.backup_dir is None + + am.install(addon_xpi) + assert am.backup_dir is not None + assert os.listdir(am.backup_dir) == ["test-addon-1@mozilla.org.xpi"] + + am.clean() + assert os.listdir(staged_path) == ["test-addon-1@mozilla.org.xpi"] + am.clean() + + +def test_install_invalid_addons(tmpdir, am): + tmpdir = tmpdir.strpath + + # Generate installer stubs for all possible types of addons + addons = [] + addons.append( + generate_addon( + "test-addon-invalid-no-manifest@mozilla.org", path=tmpdir, xpi=False + ) + ) + addons.append(generate_addon("test-addon-invalid-no-id@mozilla.org", path=tmpdir)) + + am.install(tmpdir) + + assert am.installed_addons == [] + + +@pytest.mark.xfail(reason="feature not implemented as part of AddonManger") +def test_install_error(am): + """Check install raises an error with an invalid addon""" + temp_addon = generate_addon("test-addon-invalid-version@mozilla.org") + # This should raise an error here + with pytest.raises(Exception): + am.install(temp_addon) + + +def test_addon_details(tmpdir, am): + tmpdir = tmpdir.strpath + + # Generate installer stubs for a valid and invalid add-on manifest + valid_addon = generate_addon("test-addon-1@mozilla.org", path=tmpdir) + invalid_addon = generate_addon( + "test-addon-invalid-not-wellformed@mozilla.org", path=tmpdir + ) + + # Check valid add-on + details = am.addon_details(valid_addon) + assert details["id"] == "test-addon-1@mozilla.org" + assert details["name"] == "Test Add-on 1" + assert not details["unpack"] + assert details["version"] == "0.1" + + # Check invalid add-on + with pytest.raises(mozprofile.addons.AddonFormatError): + am.addon_details(invalid_addon) + + # Check invalid path + with pytest.raises(IOError): + am.addon_details("") + + # Check invalid add-on format + addon_path = os.path.join(os.path.join(here, "files"), "not_an_addon.txt") + with pytest.raises(mozprofile.addons.AddonFormatError): + am.addon_details(addon_path) + + +def test_clean_addons(am): + addon_one = generate_addon("test-addon-1@mozilla.org") + addon_two = generate_addon("test-addon-2@mozilla.org") + + am.install(addon_one) + installed_addons = [ + str(x[: -len(".xpi")]) + for x in os.listdir(os.path.join(am.profile, "extensions")) + ] + + # Create a new profile based on an existing profile + # Install an extra addon in the new profile + # Cleanup addons + duplicate_profile = mozprofile.profile.Profile(profile=am.profile, addons=addon_two) + duplicate_profile.addons.clean() + + addons_after_cleanup = [ + str(x[: -len(".xpi")]) + for x in os.listdir(os.path.join(duplicate_profile.profile, "extensions")) + ] + # New addons installed should be removed by clean_addons() + assert installed_addons == addons_after_cleanup + + +def test_noclean(tmpdir): + """test `restore=True/False` functionality""" + profile = tmpdir.mkdtemp().strpath + tmpdir = tmpdir.mkdtemp().strpath + + # empty initially + assert not bool(os.listdir(profile)) + + # make an addon + addons = [ + generate_addon("test-addon-1@mozilla.org", path=tmpdir), + os.path.join(here, "addons", "empty.xpi"), + ] + + # install it with a restore=True AddonManager + am = mozprofile.addons.AddonManager(profile, restore=True) + + for addon in addons: + am.install(addon) + + # now its there + assert os.listdir(profile) == ["extensions"] + staging_folder = os.path.join(profile, "extensions") + assert os.path.exists(staging_folder) + assert len(os.listdir(staging_folder)) == 2 + + del am + + assert os.listdir(profile) == ["extensions"] + assert os.path.exists(staging_folder) + assert os.listdir(staging_folder) == [] + + +def test_remove_addon(tmpdir, am): + tmpdir = tmpdir.strpath + + addons = [] + addons.append(generate_addon("test-addon-1@mozilla.org", path=tmpdir)) + addons.append(generate_addon("test-addon-2@mozilla.org", path=tmpdir)) + + am.install(tmpdir) + + extensions_path = os.path.join(am.profile, "extensions") + staging_path = os.path.join(extensions_path) + + for addon in am._addons: + am.remove_addon(addon) + + assert os.listdir(staging_path) == [] + assert os.listdir(extensions_path) == [] + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprofile/tests/test_bug758250.py b/testing/mozbase/mozprofile/tests/test_bug758250.py new file mode 100755 index 0000000000..526454a476 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_bug758250.py @@ -0,0 +1,45 @@ +#!/usr/bin/env python + +import os +import shutil + +import mozprofile +import mozunit + +here = os.path.dirname(os.path.abspath(__file__)) + +""" +use of --profile in mozrunner just blows away addon sources: +https://bugzilla.mozilla.org/show_bug.cgi?id=758250 +""" + + +def test_profile_addon_cleanup(tmpdir): + tmpdir = tmpdir.mkdtemp().strpath + addon = os.path.join(here, "addons", "empty") + + # sanity check: the empty addon should be here + assert os.path.exists(addon) + assert os.path.isdir(addon) + assert os.path.exists(os.path.join(addon, "install.rdf")) + + # because we are testing data loss, let's make sure we make a copy + shutil.rmtree(tmpdir) + shutil.copytree(addon, tmpdir) + assert os.path.exists(os.path.join(tmpdir, "install.rdf")) + + # make a starter profile + profile = mozprofile.FirefoxProfile() + path = profile.profile + + # make a new profile based on the old + newprofile = mozprofile.FirefoxProfile(profile=path, addons=[tmpdir]) + newprofile.cleanup() + + # the source addon *should* still exist + assert os.path.exists(tmpdir) + assert os.path.exists(os.path.join(tmpdir, "install.rdf")) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprofile/tests/test_chrome_profile.py b/testing/mozbase/mozprofile/tests/test_chrome_profile.py new file mode 100644 index 0000000000..27c07686f8 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_chrome_profile.py @@ -0,0 +1,72 @@ +# 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 mozunit +from mozprofile import ChromeProfile + + +def test_chrome_profile_pre_existing(tmpdir): + path = tmpdir.strpath + profile = ChromeProfile(profile=path) + assert not profile.create_new + assert os.path.isdir(profile.profile) + assert profile.profile == path + + +def test_chrome_profile_create_new(): + profile = ChromeProfile() + assert profile.create_new + assert os.path.isdir(profile.profile) + assert profile.profile.endswith("Default") + + +def test_chrome_preferences(tmpdir): + prefs = {"foo": "bar"} + profile = ChromeProfile(preferences=prefs) + prefs_file = os.path.join(profile.profile, "Preferences") + + assert os.path.isfile(prefs_file) + + with open(prefs_file) as fh: + assert json.load(fh) == prefs + + # test with existing prefs + prefs_file = tmpdir.join("Preferences").strpath + with open(prefs_file, "w") as fh: + json.dump({"num": "1"}, fh) + + profile = ChromeProfile(profile=tmpdir.strpath, preferences=prefs) + + def assert_prefs(): + with open(prefs_file) as fh: + data = json.load(fh) + + assert len(data) == 2 + assert data.get("foo") == "bar" + assert data.get("num") == "1" + + assert_prefs() + profile.reset() + assert_prefs() + + +def test_chrome_addons(): + addons = ["foo", "bar"] + profile = ChromeProfile(addons=addons) + + assert isinstance(profile.addons, list) + assert profile.addons == addons + + profile.addons.install("baz") + assert profile.addons == addons + ["baz"] + + profile.reset() + assert profile.addons == addons + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprofile/tests/test_clone_cleanup.py b/testing/mozbase/mozprofile/tests/test_clone_cleanup.py new file mode 100644 index 0000000000..5a2e9bbd24 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_clone_cleanup.py @@ -0,0 +1,78 @@ +#!/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/. + +import os + +import mozfile +import mozunit +import pytest +from mozprofile.profile import Profile + +""" +test cleanup logic for the clone functionality +see https://bugzilla.mozilla.org/show_bug.cgi?id=642843 +""" + + +@pytest.fixture +def profile(tmpdir): + # make a profile with one preference + path = tmpdir.mkdtemp().strpath + profile = Profile(path, preferences={"foo": "bar"}, restore=False) + user_js = os.path.join(profile.profile, "user.js") + assert os.path.exists(user_js) + return profile + + +def test_restore_true(profile): + counter = [0] + + def _feedback(dir, content): + # Called by shutil.copytree on each visited directory. + # Used here to display info. + # + # Returns the items that should be ignored by + # shutil.copytree when copying the tree, so always returns + # an empty list. + counter[0] += 1 + return [] + + # make a clone of this profile with restore=True + clone = Profile.clone(profile.profile, restore=True, ignore=_feedback) + try: + clone.cleanup() + + # clone should be deleted + assert not os.path.exists(clone.profile) + assert counter[0] > 0 + finally: + mozfile.remove(clone.profile) + + +def test_restore_false(profile): + # make a clone of this profile with restore=False + clone = Profile.clone(profile.profile, restore=False) + try: + clone.cleanup() + + # clone should still be around on the filesystem + assert os.path.exists(clone.profile) + finally: + mozfile.remove(clone.profile) + + +def test_cleanup_on_garbage_collected(profile): + clone = Profile.clone(profile.profile) + profile_dir = clone.profile + assert os.path.exists(profile_dir) + del clone + + # clone should be deleted + assert not os.path.exists(profile_dir) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprofile/tests/test_nonce.py b/testing/mozbase/mozprofile/tests/test_nonce.py new file mode 100755 index 0000000000..3e3b2ae2b8 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_nonce.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python + +""" +test nonce in prefs delimeters +see https://bugzilla.mozilla.org/show_bug.cgi?id=722804 +""" + +import os + +import mozunit +from mozprofile.prefs import Preferences +from mozprofile.profile import Profile + + +def test_nonce(tmpdir): + # make a profile with one preference + path = tmpdir.strpath + profile = Profile(path, preferences={"foo": "bar"}, restore=False) + user_js = os.path.join(profile.profile, "user.js") + assert os.path.exists(user_js) + + # ensure the preference is correct + prefs = Preferences.read_prefs(user_js) + assert dict(prefs) == {"foo": "bar"} + + del profile + + # augment the profile with a second preference + profile = Profile(path, preferences={"fleem": "baz"}, restore=True) + prefs = Preferences.read_prefs(user_js) + assert dict(prefs) == {"foo": "bar", "fleem": "baz"} + + # cleanup the profile; + # this should remove the new preferences but not the old + profile.cleanup() + prefs = Preferences.read_prefs(user_js) + assert dict(prefs) == {"foo": "bar"} + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprofile/tests/test_permissions.py b/testing/mozbase/mozprofile/tests/test_permissions.py new file mode 100755 index 0000000000..3fe688366d --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_permissions.py @@ -0,0 +1,64 @@ +#!/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/. + +import mozunit +import pytest +from mozprofile.permissions import Permissions + +LOCATIONS = """http://mochi.test:8888 primary,privileged +http://127.0.0.1:8888 privileged +""" + + +@pytest.fixture +def locations_file(tmpdir): + locations_file = tmpdir.join("locations.txt") + locations_file.write(LOCATIONS) + return locations_file.strpath + + +@pytest.fixture +def perms(tmpdir, locations_file): + return Permissions(locations_file) + + +def test_nw_prefs(perms): + prefs, user_prefs = perms.network_prefs(False) + + assert len(user_prefs) == 0 + assert len(prefs) == 0 + + prefs, user_prefs = perms.network_prefs({"http": 8888}) + assert len(user_prefs) == 2 + assert user_prefs[0] == ("network.proxy.type", 2) + assert user_prefs[1][0] == "network.proxy.autoconfig_url" + + origins_decl = ( + "var knownOrigins = (function () { return ['http://mochi.test:8888', " + "'http://127.0.0.1:8888'].reduce" + ) + assert origins_decl in user_prefs[1][1] + + proxy_check = ( + "'http': 'PROXY mochi.test:8888'", + "'https': 'PROXY mochi.test:4443'", + "'ws': 'PROXY mochi.test:4443'", + "'wss': 'PROXY mochi.test:4443'", + ) + assert all(c in user_prefs[1][1] for c in proxy_check) + + prefs, user_prefs = perms.network_prefs({"dohServerPort": 443}) + print(user_prefs) + assert len(user_prefs) == 5 + assert user_prefs[0] == ("network.proxy.type", 0) + assert user_prefs[1] == ("network.trr.mode", 3) + assert user_prefs[2] == ("network.trr.uri", "https://foo.example.com:443/dns-query") + assert user_prefs[3] == ("network.trr.bootstrapAddr", "127.0.0.1") + assert user_prefs[4] == ("network.dns.force_use_https_rr", True) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprofile/tests/test_preferences.py b/testing/mozbase/mozprofile/tests/test_preferences.py new file mode 100755 index 0000000000..6cc62546e3 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_preferences.py @@ -0,0 +1,428 @@ +#!/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/. + +import os +import shutil +import tempfile + +import mozfile +import mozunit +import pytest +from mozprofile.cli import MozProfileCLI +from mozprofile.prefs import Preferences, PreferencesReadError +from mozprofile.profile import Profile +from wptserve import server + +here = os.path.dirname(os.path.abspath(__file__)) + + +# preferences from files/prefs_with_comments.js +_prefs_with_comments = { + "browser.startup.homepage": "http://planet.mozilla.org", + "zoom.minPercent": 30, + "zoom.maxPercent": 300, + "webgl.verbose": "false", +} + + +@pytest.fixture +def run_command(): + """ + invokes mozprofile command line via the CLI factory + - args : command line arguments (equivalent of sys.argv[1:]) + """ + + def inner(*args): + # instantiate the factory + cli = MozProfileCLI(list(args)) + + # create the profile + profile = cli.profile() + + # return path to profile + return profile.profile + + return inner + + +@pytest.fixture +def compare_generated(run_command): + """ + writes out to a new profile with mozprofile command line + reads the generated preferences with prefs.py + compares the results + cleans up + """ + + def inner(prefs, commandline): + profile = run_command(*commandline) + prefs_file = os.path.join(profile, "user.js") + assert os.path.exists(prefs_file) + read = Preferences.read_prefs(prefs_file) + if isinstance(prefs, dict): + read = dict(read) + assert prefs == read + shutil.rmtree(profile) + + return inner + + +def test_basic_prefs(compare_generated): + """test setting a pref from the command line entry point""" + + _prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"} + commandline = [] + for pref, value in _prefs.items(): + commandline += ["--pref", "%s:%s" % (pref, value)] + compare_generated(_prefs, commandline) + + +def test_ordered_prefs(compare_generated): + """ensure the prefs stay in the right order""" + _prefs = [ + ("browser.startup.homepage", "http://planet.mozilla.org/"), + ("zoom.minPercent", 30), + ("zoom.maxPercent", 300), + ("webgl.verbose", "false"), + ] + commandline = [] + for pref, value in _prefs: + commandline += ["--pref", "%s:%s" % (pref, value)] + _prefs = [(i, Preferences.cast(j)) for i, j in _prefs] + compare_generated(_prefs, commandline) + + +def test_ini(compare_generated): + # write the .ini file + _ini = """[DEFAULT] +browser.startup.homepage = http://planet.mozilla.org/ + +[foo] +browser.startup.homepage = http://github.com/ +""" + try: + fd, name = tempfile.mkstemp(suffix=".ini", text=True) + os.write(fd, _ini.encode()) + os.close(fd) + commandline = ["--preferences", name] + + # test the [DEFAULT] section + _prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"} + compare_generated(_prefs, commandline) + + # test a specific section + _prefs = {"browser.startup.homepage": "http://github.com/"} + commandline[-1] = commandline[-1] + ":foo" + compare_generated(_prefs, commandline) + + finally: + # cleanup + os.remove(name) + + +def test_ini_keep_case(compare_generated): + """ + Read a preferences config file with a preference in camel-case style. + Check that the read preference name has not been lower-cased + """ + # write the .ini file + _ini = """[DEFAULT] +network.dns.disableIPv6 = True +""" + try: + fd, name = tempfile.mkstemp(suffix=".ini", text=True) + os.write(fd, _ini.encode()) + os.close(fd) + commandline = ["--preferences", name] + + # test the [DEFAULT] section + _prefs = {"network.dns.disableIPv6": "True"} + compare_generated(_prefs, commandline) + + finally: + # cleanup + os.remove(name) + + +def test_reset_should_remove_added_prefs(): + """Check that when we call reset the items we expect are updated""" + profile = Profile() + prefs_file = os.path.join(profile.profile, "user.js") + + # we shouldn't have any initial preferences + initial_prefs = Preferences.read_prefs(prefs_file) + assert not initial_prefs + initial_prefs = open(prefs_file).read().strip() + assert not initial_prefs + + # add some preferences + prefs1 = [("mr.t.quotes", "i aint getting on no plane!")] + profile.set_preferences(prefs1) + assert prefs1 == Preferences.read_prefs(prefs_file) + lines = open(prefs_file).read().strip().splitlines() + assert any(line.startswith("#MozRunner Prefs Start") for line in lines) + assert any(line.startswith("#MozRunner Prefs End") for line in lines) + + profile.reset() + assert prefs1 != Preferences.read_prefs(os.path.join(profile.profile, "user.js")) + + +def test_reset_should_keep_user_added_prefs(): + """Check that when we call reset the items we expect are updated""" + profile = Profile() + prefs_file = os.path.join(profile.profile, "user.js") + + # we shouldn't have any initial preferences + initial_prefs = Preferences.read_prefs(prefs_file) + assert not initial_prefs + initial_prefs = open(prefs_file).read().strip() + assert not initial_prefs + + # add some preferences + prefs1 = [("mr.t.quotes", "i aint getting on no plane!")] + profile.set_persistent_preferences(prefs1) + assert prefs1 == Preferences.read_prefs(prefs_file) + lines = open(prefs_file).read().strip().splitlines() + assert any(line.startswith("#MozRunner Prefs Start") for line in lines) + assert any(line.startswith("#MozRunner Prefs End") for line in lines) + + profile.reset() + assert prefs1 == Preferences.read_prefs(os.path.join(profile.profile, "user.js")) + + +def test_magic_markers(): + """ensure our magic markers are working""" + + profile = Profile() + prefs_file = os.path.join(profile.profile, "user.js") + + # we shouldn't have any initial preferences + initial_prefs = Preferences.read_prefs(prefs_file) + assert not initial_prefs + initial_prefs = open(prefs_file).read().strip() + assert not initial_prefs + + # add some preferences + prefs1 = [ + ("browser.startup.homepage", "http://planet.mozilla.org/"), + ("zoom.minPercent", 30), + ] + profile.set_preferences(prefs1) + assert prefs1 == Preferences.read_prefs(prefs_file) + lines = open(prefs_file).read().strip().splitlines() + assert bool([line for line in lines if line.startswith("#MozRunner Prefs Start")]) + assert bool([line for line in lines if line.startswith("#MozRunner Prefs End")]) + + # add some more preferences + prefs2 = [("zoom.maxPercent", 300), ("webgl.verbose", "false")] + profile.set_preferences(prefs2) + assert prefs1 + prefs2 == Preferences.read_prefs(prefs_file) + lines = open(prefs_file).read().strip().splitlines() + assert ( + len([line for line in lines if line.startswith("#MozRunner Prefs Start")]) == 2 + ) + assert len([line for line in lines if line.startswith("#MozRunner Prefs End")]) == 2 + + # now clean it up + profile.clean_preferences() + final_prefs = Preferences.read_prefs(prefs_file) + assert not final_prefs + lines = open(prefs_file).read().strip().splitlines() + assert "#MozRunner Prefs Start" not in lines + assert "#MozRunner Prefs End" not in lines + + +def test_preexisting_preferences(): + """ensure you don't clobber preexisting preferences""" + + # make a pretend profile + tempdir = tempfile.mkdtemp() + + try: + # make a user.js + contents = """ +user_pref("webgl.enabled_for_all_sites", true); +user_pref("webgl.force-enabled", true); +""" + user_js = os.path.join(tempdir, "user.js") + f = open(user_js, "w") + f.write(contents) + f.close() + + # make sure you can read it + prefs = Preferences.read_prefs(user_js) + original_prefs = [ + ("webgl.enabled_for_all_sites", True), + ("webgl.force-enabled", True), + ] + assert prefs == original_prefs + + # now read this as a profile + profile = Profile( + tempdir, preferences={"browser.download.dir": "/home/jhammel"} + ) + + # make sure the new pref is now there + new_prefs = original_prefs[:] + [("browser.download.dir", "/home/jhammel")] + prefs = Preferences.read_prefs(user_js) + assert prefs == new_prefs + + # clean up the added preferences + profile.cleanup() + del profile + + # make sure you have the original preferences + prefs = Preferences.read_prefs(user_js) + assert prefs == original_prefs + finally: + shutil.rmtree(tempdir) + + +def test_can_read_prefs_with_multiline_comments(): + """ + Ensure that multiple comments in the file header do not break reading + the prefs (https://bugzilla.mozilla.org/show_bug.cgi?id=1233534). + """ + user_js = tempfile.NamedTemporaryFile(suffix=".js", delete=False) + try: + with user_js: + user_js.write( + """ +# Mozilla User Preferences + +/* Do not edit this file. +* +* If you make changes to this file while the application is running, +* the changes will be overwritten when the application exits. +* +* To make a manual change to preferences, you can visit the URL about:config +*/ + +user_pref("webgl.enabled_for_all_sites", true); +user_pref("webgl.force-enabled", true); +""".encode() + ) + assert Preferences.read_prefs(user_js.name) == [ + ("webgl.enabled_for_all_sites", True), + ("webgl.force-enabled", True), + ] + finally: + mozfile.remove(user_js.name) + + +def test_json(compare_generated): + _prefs = {"browser.startup.homepage": "http://planet.mozilla.org/"} + json = '{"browser.startup.homepage": "http://planet.mozilla.org/"}' + + # just repr it...could use the json module but we don't need it here + with mozfile.NamedTemporaryFile(suffix=".json") as f: + f.write(json.encode()) + f.flush() + + commandline = ["--preferences", f.name] + compare_generated(_prefs, commandline) + + +def test_json_datatypes(): + # minPercent is at 30.1 to test if non-integer data raises an exception + json = """{"zoom.minPercent": 30.1, "zoom.maxPercent": 300}""" + + with mozfile.NamedTemporaryFile(suffix=".json") as f: + f.write(json.encode()) + f.flush() + + with pytest.raises(PreferencesReadError): + Preferences.read_json(f._path) + + +def test_prefs_write(): + """test that the Preferences.write() method correctly serializes preferences""" + + _prefs = { + "browser.startup.homepage": "http://planet.mozilla.org", + "zoom.minPercent": 30, + "zoom.maxPercent": 300, + } + + # make a Preferences manager with the testing preferences + preferences = Preferences(_prefs) + + # write them to a temporary location + path = None + read_prefs = None + try: + with mozfile.NamedTemporaryFile(suffix=".js", delete=False, mode="w+t") as f: + path = f.name + preferences.write(f, _prefs) + + # read them back and ensure we get what we put in + read_prefs = dict(Preferences.read_prefs(path)) + + finally: + # cleanup + if path and os.path.exists(path): + os.remove(path) + + assert read_prefs == _prefs + + +def test_read_prefs_with_comments(): + """test reading preferences from a prefs.js file that contains comments""" + + path = os.path.join(here, "files", "prefs_with_comments.js") + assert dict(Preferences.read_prefs(path)) == _prefs_with_comments + + +def test_read_prefs_with_interpolation(): + """test reading preferences from a prefs.js file whose values + require interpolation""" + + expected_prefs = { + "browser.foo": "http://server-name", + "zoom.minPercent": 30, + "webgl.verbose": "false", + "browser.bar": "somethingxyz", + } + values = {"server": "server-name", "abc": "something"} + path = os.path.join(here, "files", "prefs_with_interpolation.js") + read_prefs = Preferences.read_prefs(path, interpolation=values) + assert dict(read_prefs) == expected_prefs + + +def test_read_prefs_with_multiline(): + """test reading preferences from a prefs.js file that contains multiline prefs""" + + path = os.path.join(here, "files", "prefs_with_multiline.js") + assert dict(Preferences.read_prefs(path)) == { + "browser.long.preference.name.that.causes.the.line.to.wrap": "itislong" + } + + +def test_read_prefs_ttw(): + """test reading preferences through the web via wptserve""" + + # create a WebTestHttpd instance + docroot = os.path.join(here, "files") + host = "127.0.0.1" + port = 8888 + httpd = server.WebTestHttpd(host=host, port=port, doc_root=docroot) + + # create a preferences instance + prefs = Preferences() + + try: + # start server + httpd.start() + + # read preferences through the web + read = prefs.read_prefs("http://%s:%d/prefs_with_comments.js" % (host, port)) + assert dict(read) == _prefs_with_comments + finally: + httpd.stop() + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprofile/tests/test_profile.py b/testing/mozbase/mozprofile/tests/test_profile.py new file mode 100644 index 0000000000..afbd4c365b --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_profile.py @@ -0,0 +1,111 @@ +#!/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/. + +import os + +import mozunit +import pytest +from mozprofile import ( + BaseProfile, + ChromeProfile, + ChromiumProfile, + FirefoxProfile, + Profile, + ThunderbirdProfile, + create_profile, +) +from mozprofile.prefs import Preferences + +here = os.path.abspath(os.path.dirname(__file__)) + + +def test_with_profile_should_cleanup(): + with Profile() as profile: + assert os.path.exists(profile.profile) + + # profile is cleaned + assert not os.path.exists(profile.profile) + + +def test_with_profile_should_cleanup_even_on_exception(): + with pytest.raises(ZeroDivisionError): + # pylint --py3k W1619 + with Profile() as profile: + assert os.path.exists(profile.profile) + 1 / 0 # will raise ZeroDivisionError + + # profile is cleaned + assert not os.path.exists(profile.profile) + + +@pytest.mark.parametrize( + "app,cls", + [ + ("chrome", ChromeProfile), + ("chromium", ChromiumProfile), + ("firefox", FirefoxProfile), + ("thunderbird", ThunderbirdProfile), + ("unknown", None), + ], +) +def test_create_profile(tmpdir, app, cls): + path = tmpdir.strpath + + if cls is None: + with pytest.raises(NotImplementedError): + create_profile(app) + return + + profile = create_profile(app, profile=path) + assert isinstance(profile, BaseProfile) + assert profile.__class__ == cls + assert profile.profile == path + + +@pytest.mark.parametrize( + "cls", + [ + Profile, + ChromeProfile, + ChromiumProfile, + ], +) +def test_merge_profile(cls): + profile = cls(preferences={"foo": "bar"}) + assert profile._addons == [] + assert os.path.isfile( + os.path.join(profile.profile, profile.preference_file_names[0]) + ) + + other_profile = os.path.join(here, "files", "dummy-profile") + profile.merge(other_profile) + + # make sure to add a pref file for each preference_file_names in the dummy-profile + prefs = {} + for name in profile.preference_file_names: + path = os.path.join(profile.profile, name) + assert os.path.isfile(path) + + try: + prefs.update(Preferences.read_json(path)) + except ValueError: + prefs.update(Preferences.read_prefs(path)) + + assert "foo" in prefs + assert len(prefs) == len(profile.preference_file_names) + 1 + assert all(name in prefs for name in profile.preference_file_names) + + # for Google Chrome currently we ignore webext in profile prefs + if cls == Profile: + assert len(profile._addons) == 1 + assert profile._addons[0].endswith("empty.xpi") + assert os.path.exists(profile._addons[0]) + else: + assert len(profile._addons) == 0 + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprofile/tests/test_profile_view.py b/testing/mozbase/mozprofile/tests/test_profile_view.py new file mode 100644 index 0000000000..67ad284298 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_profile_view.py @@ -0,0 +1,76 @@ +#!/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/. + +import os +import sys + +import mozprofile +import mozunit +import pytest +from six import text_type + +here = os.path.dirname(os.path.abspath(__file__)) + + +def test_profileprint(tmpdir): + """Test the summary function.""" + keys = set(["Files", "Path"]) + + tmpdir = tmpdir.strpath + profile = mozprofile.FirefoxProfile(tmpdir) + parts = profile.summary(return_parts=True) + parts = dict(parts) + + assert parts["Path"] == tmpdir + assert set(parts.keys()) == keys + + +def test_str_cast(): + """Test casting to a string.""" + profile = mozprofile.Profile() + if sys.version_info[0] >= 3: + assert str(profile) == profile.summary() + else: + assert str(profile) == profile.summary().encode("utf-8") + + +@pytest.mark.skipif( + sys.version_info[0] >= 3, reason="no unicode() operator starting from python3" +) +def test_unicode_cast(): + """Test casting to a unicode string.""" + profile = mozprofile.Profile() + assert text_type(profile) == profile.summary() + + +def test_profile_diff(): + profile1 = mozprofile.Profile() + profile2 = mozprofile.Profile(preferences=dict(foo="bar")) + + # diff a profile against itself; no difference + assert mozprofile.diff(profile1, profile1) == [] + + # diff two profiles + diff = dict(mozprofile.diff(profile1, profile2)) + assert list(diff.keys()) == ["user.js"] + lines = [line.strip() for line in diff["user.js"].splitlines()] + assert "+foo: bar" in lines + + # diff a blank vs FirefoxProfile + ff_profile = mozprofile.FirefoxProfile() + diff = dict(mozprofile.diff(profile2, ff_profile)) + assert list(diff.keys()) == ["user.js"] + lines = [line.strip() for line in diff["user.js"].splitlines()] + assert "-foo: bar" in lines + ff_pref_lines = [ + "+%s: %s" % (key, value) + for key, value in mozprofile.FirefoxProfile.preferences.items() + ] + assert set(ff_pref_lines).issubset(lines) + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozprofile/tests/test_server_locations.py b/testing/mozbase/mozprofile/tests/test_server_locations.py new file mode 100644 index 0000000000..7eae8ac834 --- /dev/null +++ b/testing/mozbase/mozprofile/tests/test_server_locations.py @@ -0,0 +1,117 @@ +#!/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/. + +import mozunit +import pytest +from mozprofile.permissions import ( + BadPortLocationError, + LocationsSyntaxError, + MissingPrimaryLocationError, + MultiplePrimaryLocationsError, + ServerLocations, +) + +LOCATIONS = """# This is the primary location from which tests run. +# +http://mochi.test:8888 primary,privileged + +# a few test locations +http://127.0.0.1:80 privileged +http://127.0.0.1:8888 privileged +https://test:80 privileged +http://example.org:80 privileged +http://test1.example.org privileged + +""" + +LOCATIONS_NO_PRIMARY = """http://secondary.test:80 privileged +http://tertiary.test:8888 privileged +""" + +LOCATIONS_BAD_PORT = """http://mochi.test:8888 primary,privileged +http://127.0.0.1:80 privileged +http://127.0.0.1:8888 privileged +http://test:badport privileged +http://example.org:80 privileged +""" + + +def compare_location(location, scheme, host, port, options): + assert location.scheme == scheme + assert location.host == host + assert location.port == port + assert location.options == options + + +@pytest.fixture +def create_temp_file(tmpdir): + def inner(contents): + f = tmpdir.mkdtemp().join("locations.txt") + f.write(contents) + return f.strpath + + return inner + + +def test_server_locations(create_temp_file): + # write a permissions file + f = create_temp_file(LOCATIONS) + + # read the locations + locations = ServerLocations(f) + + # ensure that they're what we expect + assert len(locations) == 6 + i = iter(locations) + compare_location(next(i), "http", "mochi.test", "8888", ["primary", "privileged"]) + compare_location(next(i), "http", "127.0.0.1", "80", ["privileged"]) + compare_location(next(i), "http", "127.0.0.1", "8888", ["privileged"]) + compare_location(next(i), "https", "test", "80", ["privileged"]) + compare_location(next(i), "http", "example.org", "80", ["privileged"]) + compare_location(next(i), "http", "test1.example.org", "8888", ["privileged"]) + + locations.add_host("mozilla.org") + assert len(locations) == 7 + compare_location(next(i), "http", "mozilla.org", "80", ["privileged"]) + + # test some errors + with pytest.raises(MultiplePrimaryLocationsError): + locations.add_host("primary.test", options="primary") + + # assert we don't throw DuplicateLocationError + locations.add_host("127.0.0.1") + + with pytest.raises(BadPortLocationError): + locations.add_host("127.0.0.1", port="abc") + + # test some errors in locations file + f = create_temp_file(LOCATIONS_NO_PRIMARY) + + exc = None + try: + ServerLocations(f) + except LocationsSyntaxError as e: + exc = e + assert exc is not None + assert exc.err.__class__ == MissingPrimaryLocationError + assert exc.lineno == 3 + + # test bad port in a locations file to ensure lineno calculated + # properly. + f = create_temp_file(LOCATIONS_BAD_PORT) + + exc = None + try: + ServerLocations(f) + except LocationsSyntaxError as e: + exc = e + assert exc is not None + assert exc.err.__class__ == BadPortLocationError + assert exc.lineno == 4 + + +if __name__ == "__main__": + mozunit.main() |