author Mozilla Releng Treescript <release+treescript@mozilla.org>
Wed, 19 Jan 2022 06:57:01 +0000
changeset 604799 184fd64dd78347ea17dbd5aef4a9d8e40fcc9f7f
parent 571772 35166f2e193636dd628014891e7e16672ce08c4a
permissions -rw-r--r--
no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD ach -> 977c0eee5774e3bd19a25900142d1dca3eb2ee29 af -> 162654a20c86192e124df9766ec5ecd54143d085 an -> d10834e1c7ba839ba9c372205f4371c4d3c19791 ar -> 3be417e9326ebc9e369fc77f1fd98481a39b07cf ast -> 41dcea23a9d890ee00cb4cc3797559fe461244bc az -> 0366cdbc61ed5620510c848b85cd593d376c75f3 be -> 62bb6ed3829ec227748a86410cf038052f673b54 bg -> 19ee7b31e0becafa054013a3c8c09844638f244c bn -> 010d5f74034cb2c18fd1100e0d0888a245d52a60 bo -> 97059fd9d1a83e254bf84da12f1612062e82d271 br -> a9e3b8ba10759c8a4f7da6b7970911ded7d04ad6 brx -> 573ec21d6a1aebf6a342533bb06b77c79b6e994b bs -> 9ebb98165b36860f938bfb97c1956090e8daf920 ca -> caf5515093962f3f92c92aea68dacd4c3e520a8d ca-valencia -> d8f8751d595bf879afe5bfa8e3854ee5b6d1b977 cak -> 0bbd8b94c9c02fa5ec6a3e5fd3bf3e69b0432829 ckb -> bd043d903599db44feb371bf29d4a3d510a48a0a cs -> ae677a6bee277f507e673c3fcd2d56088b2caafb cy -> c46474099266c675d8ddc8fbcecd23a8a11a174e da -> 4595dd6f7ed91af507076eb0a95a44f4d0646404 de -> d3fca46cd495000d26fd26d3a2776b8f4dae786c dsb -> 611248df8f629860d012cbc8f3a36acfd8744b46 el -> 22d62ecd4c214d52af254d87c2da7410bd571fb2 en-CA -> 501d80026e4fdeac79e20b41cb8087c7ec3c3ba2 en-GB -> ec99571b02068fd4bf160416b5b7e35379c899a2 eo -> 6f7723c2bfbf5e3aeb1134909464bbc099c86a4f es-AR -> e4e8f59ee9824548910f3281d02c5727caa36dd2 es-CL -> 08ee034825af181840c14c2bee5a7c20d0aa59ac es-ES -> 46729ef124e5cf1f0a3e85d50c16a069602cfed2 es-MX -> 43859d642bd044060a4d2089f4b30de8bb470e43 et -> 7cc4df8eca6d3b6c4e3073ed35770034e9be79ca eu -> 863a0a1a315b55c18694e29430c272baba355b4c fa -> 60cff0755aaf290a159cb6968c4909bf9ecae2fa ff -> ec175d67f9056e5fd2cb6568ee546336efb77d5c fi -> 6e37410128f2d675c3414ba9c6341af81b4521ff fr -> ce1d665cec91d4d29160860dba2350f22f2f4df7 fy-NL -> 5e4350e39e2a7797d6bd8e1c1bb0b15252397010 ga-IE -> 2d955174d7ab6068656b2f02e4d8c689bb2399bf gd -> 49032dc335b8b5e9ecd10435f878598bffa6541a gl -> d8761e8f84efac80d6a7482931a01cecd66dd60d gn -> aa33a53b688bed9cedeb8a3eaf0466b9451d2e18 gu-IN -> d2c670d798b925090af6321cb3270fa6f806108d he -> 3385ae23e3b569ba9c0c1aca4df25277b81a108e hi-IN -> 274838883f7f1f14d5f14d5116e7df11cc404911 hr -> a01b7ce3515d0b2f05e473625148f11a264da0a2 hsb -> f137d0e6746dc23beff4745474df87c33fae353b hu -> 39e18e316a85b5aaada8c560d51317b5e28b77d8 hy-AM -> 41e938f8ebf1581d1bc76bd088fc1ac43bb83e0b hye -> 5843bc7348ba6a533afffe08eedc48c623963542 ia -> 031f226bad1d97adf265f75fcb467a18c223c606 id -> 4d4de9751872149e1c99dcfce2b7ce4835b65589 is -> 5753814cd9664948b3ac5ace293b3d46304f0cf2 it -> 3448943cfadfc5a361bd367c7e6be58f86cc61a1 ja -> c4be75596e316966ab1dc9857a9be6b0a7bc2ef9 ja-JP-mac -> 476eca379b8b21857655d6c23908b3e2a1b09aee ka -> 2d66f0c2bd7a06ac1ad6ad02a09a26ff1e60e3cb kab -> 409893635bedbe52fa8b85784844e15756d4a8fd kk -> c68316baa4a0dd041abb4e8efd557339fb0d9cb4 km -> c04f31c98e50315177467bd4e079200d3b38b835 kn -> 0bc693a82c819a27e71f41828c040e7be4b92785 ko -> 88096d30e35f344fcb61f08900340b31fca79cc7 lij -> e46553ebd09bbabad095ccc4c8b7d147f895a475 lo -> 942d94da48979eb60bcd62d825f7f9337314999f lt -> 6a447408fe67e37ee56a85d8bea523219dac52e1 ltg -> 06e42a8ee9e5211fa8812542a6bd3c22e3c540ba lv -> 0059bd3f14ced8ec1ef0f2765b655fe4ee537e91 meh -> 54cb55f92ef9d93d0481b3806b725d08035d950f mk -> c473f0b380bd14c98754cb38cdc287c5b0df99a0 mr -> b3382076ee961b51f0e2a7369c08aaf03fb8574d ms -> ec4c315ae032575bed799d8faa57dd4441e5bd9e my -> 25d516d33a6c2f8881683c5530f468e2c25ab6eb nb-NO -> 490c2d401359fd2204ad7aa806a73107f3befc27 ne-NP -> 5676e8e0b93e18fb5eeb238953c512d4f407340c nl -> 3fbbd92a2f116150e9921f13e4c9fe37d6948464 nn-NO -> 7d8c6d43ad98b2a8a0dce7ef05234b66cb818244 oc -> 8379011f2b605f1597fc61e4585520911ca58567 pa-IN -> b28bb037d4687370eebb7a892982b31bddea32b0 pl -> 3193b4b820e7c37bdb9153ab9c525f18210a501a pt-BR -> ce1ab16cc1acd0fa7ae898feeba46c6d497fe981 pt-PT -> e0910097929ee117291ede6675c6e0add124221b rm -> cdce5683cd6621c0ca31eb6b6ffd209481fa3c85 ro -> ba7fe96d6c98e01a48f442d520c39d119c7e0727 ru -> dc6d46ab977eb359c21a0f6dfe5b22e1fa60fda0 sat -> 3ab6969e38e316491894b42527f2287dbbd7d626 sc -> 14a1b41f804a89dd00d978056ebfc966a638cbb8 scn -> 6729c4bfb82228012e51e2077cf8f2ce254e8fe6 sco -> 8aed4c4ac1dd708f213d350a46617cd3faca0a09 si -> 639820754f8563d1fccf89f48c4b155db6a3ae8d sk -> 588464d4ecd69c9a12ffd0e41ccb5e4a7f16e25e sl -> e7aa581fc059a4e59ae56eb03790d0d7e4129a42 son -> 1541de0c90d66751ae72c24d4fa8105a585cf203 sq -> 05efafdd60c5a69d6d75adf8ccb97c5b07aee239 sr -> e7c7978d2fd74f4e2098253a41b099604b88d61b sv-SE -> fbeae2bbc5837470c90793f92a1cc90b7d6fd51f szl -> 000ee773591f17bcd9fc5ed5eb504000a016ce5b ta -> 2545635e99da1c3bdfc3536ba8c6a68a7498d014 te -> 0c5e4eb71c5ffe04c651f377355ead0f976960cd tg -> 8c5d8e78f165f7aef1f2597a67944cf8c101f906 th -> b6c8c65ece954839a983d48aa92a348b84867fa7 tl -> c7615e24b3453bb59a41eb017db2e04c781c8782 tr -> 08e01536e9eab024fceabe205921d815094af748 trs -> 43debe20e58afdd09ddbcd2f7dd62039c1e8c5f8 uk -> a4d58069863410d7b878690855604f86b12c300b ur -> 354acd5f98b1a03c6960231c570fedb771588d86 uz -> f8e8cc127eaf07ab77bf51c37ec494d2721e8d2e vi -> e24285029832c0cc588e8e12aa2212dff982d623 wo -> 625cc2e007db82c90ecddd39e34f1969c4088e09 xh -> 09a4bf9ceb7fe52e7fd54b2c8a15c16c6a4c56f2 zh-CN -> 5c57a47c29f1c8105d8c268617a00c54e4a8ba8c zh-TW -> f86c570fdc59c21666014d9c81f61c9ebeac0d72

# 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 __future__ import absolute_import

import six
import json
import os
import platform
import tempfile
import time
import uuid
from abc import ABCMeta, abstractmethod, abstractproperty
from shutil import copytree
from io import open

import mozfile
from six import string_types, python_2_unicode_compatible

if six.PY3:

    def unicode(input):
        return input

from .addons import AddonManager
from .permissions import Permissions
from .prefs import Preferences

__all__ = [

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]
            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))
            self.profile = tempfile.mkdtemp(suffix=".mozrunner")

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):

    def __del__(self):

    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:

    def _reset(self):

    def reset(self):
        reset the profile to the beginning state

    def set_preferences(self, preferences, filename="user.js"):

    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:

            path = os.path.join(other, basename)
                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):

        for basename in os.listdir(extension_dir):
            path = os.path.join(extension_dir, basename)

            if self.addons.is_addon(path):

    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)

        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)

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
      # profile.cleanup() has been called here

    preference_file_names = ("user.js", "prefs.js")

    def __init__(
        :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__(

        self._locations = locations
        self._proxy = proxy
        self._whitelistpaths = whitelistpaths

        # Initialize all class members

    def _reset(self):
        """Internal: Initialize all class members to their default value"""

        if not os.path.exists(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"):
        # Set additional preferences

        self.permissions = Permissions(self.profile, 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:
        self.set_preferences(prefs_js, "prefs.js")

        # handle add-on installation
        self.addons = AddonManager(self.profile, restore=self.restore)

    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)
            if getattr(self, "addons", None) is not None:
            if getattr(self, "permissions", None) is not None:
        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
            while True:
                if not self.pop_preferences(filename):

    # 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:

            # note what files we've touched

            # 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.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" % (
            return False  # no preferences found
        elif e is None:
            assert s is None, "%s found without %s" % (

        # ensure the markers are in the proper order
        assert e > s, "%s found at %s, while %s found at %s" % (

        # write the prefs
        cleaned_prefs = "\n".join(lines[:s] + lines[e + 1 :])
        with open(path, "w") as f:
        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)
                            % (
                                        "%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"
                        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 : start + len(origins_string) - 1],
                                lines[0][start + len(origins_string) : end]
                                .replace(",", ",\n")
                            lines[0:1] = [i.strip() for i in splitline]
                                "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]

        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")

    def _reset(self):
        if not os.path.isdir(self.profile):

        if self._preferences:

        self.addons = self.AddonManager()
        if 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:

        with open(pref_file, "w") as fh:
            prefstr = json.dumps(prefs)
            prefstr % values  # interpolate prefs with values
            if six.PY2:

class ChromeProfile(ChromiumProfile):
    # update this if Google Chrome requires more
    # specific profiles

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)