Path: blob/trunk/py/selenium/webdriver/firefox/firefox_profile.py
1864 views
# Licensed to the Software Freedom Conservancy (SFC) under one1# or more contributor license agreements. See the NOTICE file2# distributed with this work for additional information3# regarding copyright ownership. The SFC licenses this file4# to you under the Apache License, Version 2.0 (the5# "License"); you may not use this file except in compliance6# with the License. You may obtain a copy of the License at7#8# http://www.apache.org/licenses/LICENSE-2.09#10# Unless required by applicable law or agreed to in writing,11# software distributed under the License is distributed on an12# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY13# KIND, either express or implied. See the License for the14# specific language governing permissions and limitations15# under the License.1617import base6418import copy19import json20import os21import re22import shutil23import sys24import tempfile25import warnings26import zipfile27from io import BytesIO28from xml.dom import minidom2930from typing_extensions import deprecated3132from selenium.common.exceptions import WebDriverException3334WEBDRIVER_PREFERENCES = "webdriver_prefs.json"353637@deprecated("Addons must be added after starting the session")38class AddonFormatError(Exception):39"""Exception for not well-formed add-on manifest files."""404142class FirefoxProfile:43DEFAULT_PREFERENCES = None4445def __init__(self, profile_directory=None):46"""Initialises a new instance of a Firefox Profile.4748:args:49- profile_directory: Directory of profile that you want to use. If a50directory is passed in it will be cloned and the cloned directory51will be used by the driver when instantiated.52This defaults to None and will create a new53directory when object is created.54"""55self._desired_preferences = {}56if profile_directory:57newprof = os.path.join(tempfile.mkdtemp(), "webdriver-py-profilecopy")58shutil.copytree(59profile_directory, newprof, ignore=shutil.ignore_patterns("parent.lock", "lock", ".parentlock")60)61self._profile_dir = newprof62os.chmod(self._profile_dir, 0o755)63else:64self._profile_dir = tempfile.mkdtemp()65if not FirefoxProfile.DEFAULT_PREFERENCES:66with open(67os.path.join(os.path.dirname(__file__), WEBDRIVER_PREFERENCES), encoding="utf-8"68) as default_prefs:69FirefoxProfile.DEFAULT_PREFERENCES = json.load(default_prefs)7071self._desired_preferences = copy.deepcopy(FirefoxProfile.DEFAULT_PREFERENCES["mutable"])72for key, value in FirefoxProfile.DEFAULT_PREFERENCES["frozen"].items():73self._desired_preferences[key] = value7475# Public Methods76def set_preference(self, key, value):77"""Sets the preference that we want in the profile."""78self._desired_preferences[key] = value7980@deprecated("Addons must be added after starting the session")81def add_extension(self, extension=None):82self._install_extension(extension)8384def update_preferences(self):85"""Writes the desired user prefs to disk."""86user_prefs = os.path.join(self._profile_dir, "user.js")87if os.path.isfile(user_prefs):88os.chmod(user_prefs, 0o644)89self._read_existing_userjs(user_prefs)90with open(user_prefs, "w", encoding="utf-8") as f:91for key, value in self._desired_preferences.items():92f.write(f'user_pref("{key}", {json.dumps(value)});\n')9394# Properties9596@property97def path(self):98"""Gets the profile directory that is currently being used."""99return self._profile_dir100101@property102@deprecated("The port is stored in the Service class")103def port(self):104"""Gets the port that WebDriver is working on."""105return self._port106107@port.setter108@deprecated("The port is stored in the Service class")109def port(self, port) -> None:110"""Sets the port that WebDriver will be running on."""111if not isinstance(port, int):112raise WebDriverException("Port needs to be an integer")113try:114port = int(port)115if port < 1 or port > 65535:116raise WebDriverException("Port number must be in the range 1..65535")117except (ValueError, TypeError):118raise WebDriverException("Port needs to be an integer")119self._port = port120self.set_preference("webdriver_firefox_port", self._port)121122@property123@deprecated("Allowing untrusted certs is toggled in the Options class")124def accept_untrusted_certs(self):125return self._desired_preferences["webdriver_accept_untrusted_certs"]126127@accept_untrusted_certs.setter128@deprecated("Allowing untrusted certs is toggled in the Options class")129def accept_untrusted_certs(self, value) -> None:130if not isinstance(value, bool):131raise WebDriverException("Please pass in a Boolean to this call")132self.set_preference("webdriver_accept_untrusted_certs", value)133134@property135@deprecated("Allowing untrusted certs is toggled in the Options class")136def assume_untrusted_cert_issuer(self):137return self._desired_preferences["webdriver_assume_untrusted_issuer"]138139@assume_untrusted_cert_issuer.setter140@deprecated("Allowing untrusted certs is toggled in the Options class")141def assume_untrusted_cert_issuer(self, value) -> None:142if not isinstance(value, bool):143raise WebDriverException("Please pass in a Boolean to this call")144145self.set_preference("webdriver_assume_untrusted_issuer", value)146147@property148def encoded(self) -> str:149"""Updates preferences and creates a zipped, base64 encoded string of150profile directory."""151if self._desired_preferences:152self.update_preferences()153fp = BytesIO()154with zipfile.ZipFile(fp, "w", zipfile.ZIP_DEFLATED, strict_timestamps=False) as zipped:155path_root = len(self.path) + 1 # account for trailing slash156for base, _, files in os.walk(self.path):157for fyle in files:158filename = os.path.join(base, fyle)159zipped.write(filename, filename[path_root:])160return base64.b64encode(fp.getvalue()).decode("UTF-8")161162def _read_existing_userjs(self, userjs):163"""Reads existing preferences and adds them to desired preference164dictionary."""165pref_pattern = re.compile(r'user_pref\("(.*)",\s(.*)\)')166with open(userjs, encoding="utf-8") as f:167for usr in f:168matches = pref_pattern.search(usr)169try:170self._desired_preferences[matches.group(1)] = json.loads(matches.group(2))171except Exception:172warnings.warn(173f"(skipping) failed to json.loads existing preference: {matches.group(1) + matches.group(2)}"174)175176@deprecated("Addons must be added after starting the session")177def _install_extension(self, addon, unpack=True):178"""Installs addon from a filepath, url or directory of addons in the179profile.180181- path: url, absolute path to .xpi, or directory of addons182- unpack: whether to unpack unless specified otherwise in the install.rdf183"""184tmpdir = None185xpifile = None186if addon.endswith(".xpi"):187tmpdir = tempfile.mkdtemp(suffix="." + os.path.split(addon)[-1])188compressed_file = zipfile.ZipFile(addon, "r")189for name in compressed_file.namelist():190if name.endswith("/"):191if not os.path.isdir(os.path.join(tmpdir, name)):192os.makedirs(os.path.join(tmpdir, name))193else:194if not os.path.isdir(os.path.dirname(os.path.join(tmpdir, name))):195os.makedirs(os.path.dirname(os.path.join(tmpdir, name)))196data = compressed_file.read(name)197with open(os.path.join(tmpdir, name), "wb") as f:198f.write(data)199xpifile = addon200addon = tmpdir201202# determine the addon id203addon_details = self._addon_details(addon)204addon_id = addon_details.get("id")205assert addon_id, f"The addon id could not be found: {addon}"206207# copy the addon to the profile208extensions_dir = os.path.join(self._profile_dir, "extensions")209addon_path = os.path.join(extensions_dir, addon_id)210if not unpack and not addon_details["unpack"] and xpifile:211if not os.path.exists(extensions_dir):212os.makedirs(extensions_dir)213os.chmod(extensions_dir, 0o755)214shutil.copy(xpifile, addon_path + ".xpi")215else:216if not os.path.exists(addon_path):217shutil.copytree(addon, addon_path, symlinks=True)218219# remove the temporary directory, if any220if tmpdir:221shutil.rmtree(tmpdir)222223@deprecated("Addons must be added after starting the session")224def _addon_details(self, addon_path):225"""Returns a dictionary of details about the addon.226227:param addon_path: path to the add-on directory or XPI228229Returns::230231{232"id": "[email protected]", # id of the addon233"version": "1.4", # version of the addon234"name": "Rainbow", # name of the addon235"unpack": False,236} # whether to unpack the addon237"""238239details = {"id": None, "unpack": False, "name": None, "version": None}240241def get_namespace_id(doc, url):242attributes = doc.documentElement.attributes243namespace = ""244for i in range(attributes.length):245if attributes.item(i).value == url:246if ":" in attributes.item(i).name:247# If the namespace is not the default one remove 'xlmns:'248namespace = attributes.item(i).name.split(":")[1] + ":"249break250return namespace251252def get_text(element):253"""Retrieve the text value of a given node."""254rc = []255for node in element.childNodes:256if node.nodeType == node.TEXT_NODE:257rc.append(node.data)258return "".join(rc).strip()259260def parse_manifest_json(content):261"""Extracts the details from the contents of a WebExtensions262`manifest.json` file."""263manifest = json.loads(content)264try:265id = manifest["applications"]["gecko"]["id"]266except KeyError:267id = manifest["name"].replace(" ", "") + "@" + manifest["version"]268return {269"id": id,270"version": manifest["version"],271"name": manifest["version"],272"unpack": False,273}274275if not os.path.exists(addon_path):276raise OSError(f"Add-on path does not exist: {addon_path}")277278try:279if zipfile.is_zipfile(addon_path):280with zipfile.ZipFile(addon_path, "r") as compressed_file:281if "manifest.json" in compressed_file.namelist():282return parse_manifest_json(compressed_file.read("manifest.json"))283284manifest = compressed_file.read("install.rdf")285elif os.path.isdir(addon_path):286manifest_json_filename = os.path.join(addon_path, "manifest.json")287if os.path.exists(manifest_json_filename):288with open(manifest_json_filename, encoding="utf-8") as f:289return parse_manifest_json(f.read())290291with open(os.path.join(addon_path, "install.rdf"), encoding="utf-8") as f:292manifest = f.read()293else:294raise OSError(f"Add-on path is neither an XPI nor a directory: {addon_path}")295except (OSError, KeyError) as e:296raise AddonFormatError(str(e), sys.exc_info()[2])297298try:299doc = minidom.parseString(manifest)300301# Get the namespaces abbreviations302em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")303rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#")304305description = doc.getElementsByTagName(rdf + "Description").item(0)306if not description:307description = doc.getElementsByTagName("Description").item(0)308for node in description.childNodes:309# Remove the namespace prefix from the tag for comparison310entry = node.nodeName.replace(em, "")311if entry in details:312details.update({entry: get_text(node)})313if not details.get("id"):314for i in range(description.attributes.length):315attribute = description.attributes.item(i)316if attribute.name == em + "id":317details.update({"id": attribute.value})318except Exception as e:319raise AddonFormatError(str(e), sys.exc_info()[2])320321# turn unpack into a true/false value322if isinstance(details["unpack"], str):323details["unpack"] = details["unpack"].lower() == "true"324325# If no ID is set, the add-on is invalid326if not details.get("id"):327raise AddonFormatError("Add-on id could not be found.")328329return details330331332