"Fossies" - the Fresh Open Source Software Archive

Member "selenium-selenium-4.8.1/py/selenium/webdriver/firefox/firefox_profile.py" (17 Feb 2023, 14364 Bytes) of package /linux/www/selenium-selenium-4.8.1.tar.gz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) Python source code syntax highlighting (style: standard) with prefixed line numbers. Alternatively you can here view or download the uninterpreted source code file. For more information about "firefox_profile.py" see the Fossies "Dox" file reference documentation and the last Fossies "Diffs" side-by-side code changes report: 4.7.0_vs_4.8.0.

    1 # Licensed to the Software Freedom Conservancy (SFC) under one
    2 # or more contributor license agreements.  See the NOTICE file
    3 # distributed with this work for additional information
    4 # regarding copyright ownership.  The SFC licenses this file
    5 # to you under the Apache License, Version 2.0 (the
    6 # "License"); you may not use this file except in compliance
    7 # with the License.  You may obtain a copy of the License at
    8 #
    9 #   http://www.apache.org/licenses/LICENSE-2.0
   10 #
   11 # Unless required by applicable law or agreed to in writing,
   12 # software distributed under the License is distributed on an
   13 # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
   14 # KIND, either express or implied.  See the License for the
   15 # specific language governing permissions and limitations
   16 # under the License.
   17 
   18 import base64
   19 import copy
   20 import json
   21 import os
   22 import re
   23 import shutil
   24 import sys
   25 import tempfile
   26 import warnings
   27 import zipfile
   28 from io import BytesIO
   29 from xml.dom import minidom
   30 
   31 from selenium.common.exceptions import WebDriverException
   32 
   33 WEBDRIVER_EXT = "webdriver.xpi"
   34 WEBDRIVER_PREFERENCES = "webdriver_prefs.json"
   35 EXTENSION_NAME = "fxdriver@googlecode.com"
   36 
   37 
   38 class AddonFormatError(Exception):
   39     """Exception for not well-formed add-on manifest files."""
   40 
   41 
   42 class FirefoxProfile:
   43     ANONYMOUS_PROFILE_NAME = "WEBDRIVER_ANONYMOUS_PROFILE"
   44     DEFAULT_PREFERENCES = None
   45 
   46     def __init__(self, profile_directory=None):
   47         """Initialises a new instance of a Firefox Profile.
   48 
   49         :args:
   50          - profile_directory: Directory of profile that you want to use. If a
   51            directory is passed in it will be cloned and the cloned directory
   52            will be used by the driver when instantiated.
   53            This defaults to None and will create a new
   54            directory when object is created.
   55         """
   56         warnings.warn(
   57             "firefox_profile has been deprecated, please use an Options object", DeprecationWarning, stacklevel=2
   58         )
   59         if not FirefoxProfile.DEFAULT_PREFERENCES:
   60             with open(
   61                 os.path.join(os.path.dirname(__file__), WEBDRIVER_PREFERENCES), encoding="utf-8"
   62             ) as default_prefs:
   63                 FirefoxProfile.DEFAULT_PREFERENCES = json.load(default_prefs)
   64 
   65         self.default_preferences = copy.deepcopy(FirefoxProfile.DEFAULT_PREFERENCES["mutable"])
   66         self.profile_dir = profile_directory
   67         self.tempfolder = None
   68         if not self.profile_dir:
   69             self.profile_dir = self._create_tempfolder()
   70         else:
   71             self.tempfolder = tempfile.mkdtemp()
   72             newprof = os.path.join(self.tempfolder, "webdriver-py-profilecopy")
   73             shutil.copytree(
   74                 self.profile_dir, newprof, ignore=shutil.ignore_patterns("parent.lock", "lock", ".parentlock")
   75             )
   76             self.profile_dir = newprof
   77             os.chmod(self.profile_dir, 0o755)
   78             self._read_existing_userjs(os.path.join(self.profile_dir, "user.js"))
   79         self.extensionsDir = os.path.join(self.profile_dir, "extensions")
   80         self.userPrefs = os.path.join(self.profile_dir, "user.js")
   81         if os.path.isfile(self.userPrefs):
   82             os.chmod(self.userPrefs, 0o644)
   83 
   84     # Public Methods
   85     def set_preference(self, key, value):
   86         """sets the preference that we want in the profile."""
   87         self.default_preferences[key] = value
   88 
   89     def add_extension(self, extension=WEBDRIVER_EXT):
   90         self._install_extension(extension)
   91 
   92     def update_preferences(self):
   93         for key, value in FirefoxProfile.DEFAULT_PREFERENCES["frozen"].items():
   94             # Do not update key that is being set by the user using
   95             # set_preference as users are unaware of the freeze properties
   96             # and it leads to an inconsistent behavior
   97             if key not in self.default_preferences:
   98                 self.default_preferences[key] = value
   99         self._write_user_prefs(self.default_preferences)
  100 
  101     # Properties
  102 
  103     @property
  104     def path(self):
  105         """Gets the profile directory that is currently being used."""
  106         return self.profile_dir
  107 
  108     @property
  109     def port(self):
  110         """Gets the port that WebDriver is working on."""
  111         return self._port
  112 
  113     @port.setter
  114     def port(self, port) -> None:
  115         """Sets the port that WebDriver will be running on."""
  116         if not isinstance(port, int):
  117             raise WebDriverException("Port needs to be an integer")
  118         try:
  119             port = int(port)
  120             if port < 1 or port > 65535:
  121                 raise WebDriverException("Port number must be in the range 1..65535")
  122         except (ValueError, TypeError):
  123             raise WebDriverException("Port needs to be an integer")
  124         self._port = port
  125         self.set_preference("webdriver_firefox_port", self._port)
  126 
  127     @property
  128     def accept_untrusted_certs(self):
  129         return self.default_preferences["webdriver_accept_untrusted_certs"]
  130 
  131     @accept_untrusted_certs.setter
  132     def accept_untrusted_certs(self, value) -> None:
  133         if value not in [True, False]:
  134             raise WebDriverException("Please pass in a Boolean to this call")
  135         self.set_preference("webdriver_accept_untrusted_certs", value)
  136 
  137     @property
  138     def assume_untrusted_cert_issuer(self):
  139         return self.default_preferences["webdriver_assume_untrusted_issuer"]
  140 
  141     @assume_untrusted_cert_issuer.setter
  142     def assume_untrusted_cert_issuer(self, value) -> None:
  143         if value not in [True, False]:
  144             raise WebDriverException("Please pass in a Boolean to this call")
  145 
  146         self.set_preference("webdriver_assume_untrusted_issuer", value)
  147 
  148     @property
  149     def encoded(self) -> str:
  150         """A zipped, base64 encoded string of profile directory for use with
  151         remote WebDriver JSON wire protocol."""
  152         self.update_preferences()
  153         fp = BytesIO()
  154         with zipfile.ZipFile(fp, "w", zipfile.ZIP_DEFLATED) as zipped:
  155             path_root = len(self.path) + 1  # account for trailing slash
  156             for base, _, files in os.walk(self.path):
  157                 for fyle in files:
  158                     filename = os.path.join(base, fyle)
  159                     zipped.write(filename, filename[path_root:])
  160         return base64.b64encode(fp.getvalue()).decode("UTF-8")
  161 
  162     def _create_tempfolder(self):
  163         """Creates a temp folder to store User.js and the extension."""
  164         return tempfile.mkdtemp()
  165 
  166     def _write_user_prefs(self, user_prefs):
  167         """writes the current user prefs dictionary to disk."""
  168         with open(self.userPrefs, "w", encoding="utf-8") as f:
  169             for key, value in user_prefs.items():
  170                 f.write(f'user_pref("{key}", {json.dumps(value)});\n')
  171 
  172     def _read_existing_userjs(self, userjs):
  173         pref_pattern = re.compile(r'user_pref\("(.*)",\s(.*)\)')
  174         try:
  175             with open(userjs, encoding="utf-8") as f:
  176                 for usr in f:
  177                     matches = pref_pattern.search(usr)
  178                     try:
  179                         self.default_preferences[matches.group(1)] = json.loads(matches.group(2))
  180                     except Exception:
  181                         warnings.warn(
  182                             f"(skipping) failed to json.loads existing preference: {matches.group(1) + matches.group(2)}"
  183                         )
  184         except Exception:
  185             # The profile given hasn't had any changes made, i.e no users.js
  186             pass
  187 
  188     def _install_extension(self, addon, unpack=True):
  189         """Installs addon from a filepath, url or directory of addons in the
  190         profile.
  191 
  192         - path: url, absolute path to .xpi, or directory of addons
  193         - unpack: whether to unpack unless specified otherwise in the install.rdf
  194         """
  195         if addon == WEBDRIVER_EXT:
  196             addon = os.path.join(os.path.dirname(__file__), WEBDRIVER_EXT)
  197 
  198         tmpdir = None
  199         xpifile = None
  200         if addon.endswith(".xpi"):
  201             tmpdir = tempfile.mkdtemp(suffix="." + os.path.split(addon)[-1])
  202             compressed_file = zipfile.ZipFile(addon, "r")
  203             for name in compressed_file.namelist():
  204                 if name.endswith("/"):
  205                     if not os.path.isdir(os.path.join(tmpdir, name)):
  206                         os.makedirs(os.path.join(tmpdir, name))
  207                 else:
  208                     if not os.path.isdir(os.path.dirname(os.path.join(tmpdir, name))):
  209                         os.makedirs(os.path.dirname(os.path.join(tmpdir, name)))
  210                     data = compressed_file.read(name)
  211                     with open(os.path.join(tmpdir, name), "wb") as f:
  212                         f.write(data)
  213             xpifile = addon
  214             addon = tmpdir
  215 
  216         # determine the addon id
  217         addon_details = self._addon_details(addon)
  218         addon_id = addon_details.get("id")
  219         assert addon_id, f"The addon id could not be found: {addon}"
  220 
  221         # copy the addon to the profile
  222         addon_path = os.path.join(self.extensionsDir, addon_id)
  223         if not unpack and not addon_details["unpack"] and xpifile:
  224             if not os.path.exists(self.extensionsDir):
  225                 os.makedirs(self.extensionsDir)
  226                 os.chmod(self.extensionsDir, 0o755)
  227             shutil.copy(xpifile, addon_path + ".xpi")
  228         else:
  229             if not os.path.exists(addon_path):
  230                 shutil.copytree(addon, addon_path, symlinks=True)
  231 
  232         # remove the temporary directory, if any
  233         if tmpdir:
  234             shutil.rmtree(tmpdir)
  235 
  236     def _addon_details(self, addon_path):
  237         """Returns a dictionary of details about the addon.
  238 
  239         :param addon_path: path to the add-on directory or XPI
  240 
  241         Returns::
  242 
  243             {'id':      u'rainbow@colors.org', # id of the addon
  244              'version': u'1.4',                # version of the addon
  245              'name':    u'Rainbow',            # name of the addon
  246              'unpack':  False }                # whether to unpack the addon
  247         """
  248 
  249         details = {"id": None, "unpack": False, "name": None, "version": None}
  250 
  251         def get_namespace_id(doc, url):
  252             attributes = doc.documentElement.attributes
  253             namespace = ""
  254             for i in range(attributes.length):
  255                 if attributes.item(i).value == url:
  256                     if ":" in attributes.item(i).name:
  257                         # If the namespace is not the default one remove 'xlmns:'
  258                         namespace = attributes.item(i).name.split(":")[1] + ":"
  259                         break
  260             return namespace
  261 
  262         def get_text(element):
  263             """Retrieve the text value of a given node."""
  264             rc = []
  265             for node in element.childNodes:
  266                 if node.nodeType == node.TEXT_NODE:
  267                     rc.append(node.data)
  268             return "".join(rc).strip()
  269 
  270         def parse_manifest_json(content):
  271             """Extracts the details from the contents of a WebExtensions
  272             `manifest.json` file."""
  273             manifest = json.loads(content)
  274             try:
  275                 id = manifest["applications"]["gecko"]["id"]
  276             except KeyError:
  277                 id = manifest["name"].replace(" ", "") + "@" + manifest["version"]
  278             return {
  279                 "id": id,
  280                 "version": manifest["version"],
  281                 "name": manifest["version"],
  282                 "unpack": False,
  283             }
  284 
  285         if not os.path.exists(addon_path):
  286             raise OSError(f"Add-on path does not exist: {addon_path}")
  287 
  288         try:
  289             if zipfile.is_zipfile(addon_path):
  290                 # Bug 944361 - We cannot use 'with' together with zipFile because
  291                 # it will cause an exception thrown in Python 2.6.
  292                 # TODO: use with statement when Python 2.x is no longer supported
  293                 try:
  294                     compressed_file = zipfile.ZipFile(addon_path, "r")
  295                     if "manifest.json" in compressed_file.namelist():
  296                         return parse_manifest_json(compressed_file.read("manifest.json"))
  297 
  298                     manifest = compressed_file.read("install.rdf")
  299                 finally:
  300                     compressed_file.close()
  301             elif os.path.isdir(addon_path):
  302                 manifest_json_filename = os.path.join(addon_path, "manifest.json")
  303                 if os.path.exists(manifest_json_filename):
  304                     with open(manifest_json_filename, encoding="utf-8") as f:
  305                         return parse_manifest_json(f.read())
  306 
  307                 with open(os.path.join(addon_path, "install.rdf"), encoding="utf-8") as f:
  308                     manifest = f.read()
  309             else:
  310                 raise OSError(f"Add-on path is neither an XPI nor a directory: {addon_path}")
  311         except (OSError, KeyError) as e:
  312             raise AddonFormatError(str(e), sys.exc_info()[2])
  313 
  314         try:
  315             doc = minidom.parseString(manifest)
  316 
  317             # Get the namespaces abbreviations
  318             em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")
  319             rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
  320 
  321             description = doc.getElementsByTagName(rdf + "Description").item(0)
  322             if not description:
  323                 description = doc.getElementsByTagName("Description").item(0)
  324             for node in description.childNodes:
  325                 # Remove the namespace prefix from the tag for comparison
  326                 entry = node.nodeName.replace(em, "")
  327                 if entry in details:
  328                     details.update({entry: get_text(node)})
  329             if not details.get("id"):
  330                 for i in range(description.attributes.length):
  331                     attribute = description.attributes.item(i)
  332                     if attribute.name == em + "id":
  333                         details.update({"id": attribute.value})
  334         except Exception as e:
  335             raise AddonFormatError(str(e), sys.exc_info()[2])
  336 
  337         # turn unpack into a true/false value
  338         if isinstance(details["unpack"], str):
  339             details["unpack"] = details["unpack"].lower() == "true"
  340 
  341         # If no ID is set, the add-on is invalid
  342         if not details.get("id"):
  343             raise AddonFormatError("Add-on id could not be found.")
  344 
  345         return details