Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
SeleniumHQ
GitHub Repository: SeleniumHQ/Selenium
Path: blob/trunk/py/selenium/webdriver/firefox/firefox_profile.py
1864 views
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 typing_extensions import deprecated
32
33
from selenium.common.exceptions import WebDriverException
34
35
WEBDRIVER_PREFERENCES = "webdriver_prefs.json"
36
37
38
@deprecated("Addons must be added after starting the session")
39
class AddonFormatError(Exception):
40
"""Exception for not well-formed add-on manifest files."""
41
42
43
class FirefoxProfile:
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
self._desired_preferences = {}
57
if profile_directory:
58
newprof = os.path.join(tempfile.mkdtemp(), "webdriver-py-profilecopy")
59
shutil.copytree(
60
profile_directory, newprof, ignore=shutil.ignore_patterns("parent.lock", "lock", ".parentlock")
61
)
62
self._profile_dir = newprof
63
os.chmod(self._profile_dir, 0o755)
64
else:
65
self._profile_dir = tempfile.mkdtemp()
66
if not FirefoxProfile.DEFAULT_PREFERENCES:
67
with open(
68
os.path.join(os.path.dirname(__file__), WEBDRIVER_PREFERENCES), encoding="utf-8"
69
) as default_prefs:
70
FirefoxProfile.DEFAULT_PREFERENCES = json.load(default_prefs)
71
72
self._desired_preferences = copy.deepcopy(FirefoxProfile.DEFAULT_PREFERENCES["mutable"])
73
for key, value in FirefoxProfile.DEFAULT_PREFERENCES["frozen"].items():
74
self._desired_preferences[key] = value
75
76
# Public Methods
77
def set_preference(self, key, value):
78
"""Sets the preference that we want in the profile."""
79
self._desired_preferences[key] = value
80
81
@deprecated("Addons must be added after starting the session")
82
def add_extension(self, extension=None):
83
self._install_extension(extension)
84
85
def update_preferences(self):
86
"""Writes the desired user prefs to disk."""
87
user_prefs = os.path.join(self._profile_dir, "user.js")
88
if os.path.isfile(user_prefs):
89
os.chmod(user_prefs, 0o644)
90
self._read_existing_userjs(user_prefs)
91
with open(user_prefs, "w", encoding="utf-8") as f:
92
for key, value in self._desired_preferences.items():
93
f.write(f'user_pref("{key}", {json.dumps(value)});\n')
94
95
# Properties
96
97
@property
98
def path(self):
99
"""Gets the profile directory that is currently being used."""
100
return self._profile_dir
101
102
@property
103
@deprecated("The port is stored in the Service class")
104
def port(self):
105
"""Gets the port that WebDriver is working on."""
106
return self._port
107
108
@port.setter
109
@deprecated("The port is stored in the Service class")
110
def port(self, port) -> None:
111
"""Sets the port that WebDriver will be running on."""
112
if not isinstance(port, int):
113
raise WebDriverException("Port needs to be an integer")
114
try:
115
port = int(port)
116
if port < 1 or port > 65535:
117
raise WebDriverException("Port number must be in the range 1..65535")
118
except (ValueError, TypeError):
119
raise WebDriverException("Port needs to be an integer")
120
self._port = port
121
self.set_preference("webdriver_firefox_port", self._port)
122
123
@property
124
@deprecated("Allowing untrusted certs is toggled in the Options class")
125
def accept_untrusted_certs(self):
126
return self._desired_preferences["webdriver_accept_untrusted_certs"]
127
128
@accept_untrusted_certs.setter
129
@deprecated("Allowing untrusted certs is toggled in the Options class")
130
def accept_untrusted_certs(self, value) -> None:
131
if not isinstance(value, bool):
132
raise WebDriverException("Please pass in a Boolean to this call")
133
self.set_preference("webdriver_accept_untrusted_certs", value)
134
135
@property
136
@deprecated("Allowing untrusted certs is toggled in the Options class")
137
def assume_untrusted_cert_issuer(self):
138
return self._desired_preferences["webdriver_assume_untrusted_issuer"]
139
140
@assume_untrusted_cert_issuer.setter
141
@deprecated("Allowing untrusted certs is toggled in the Options class")
142
def assume_untrusted_cert_issuer(self, value) -> None:
143
if not isinstance(value, bool):
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
"""Updates preferences and creates a zipped, base64 encoded string of
151
profile directory."""
152
if self._desired_preferences:
153
self.update_preferences()
154
fp = BytesIO()
155
with zipfile.ZipFile(fp, "w", zipfile.ZIP_DEFLATED, strict_timestamps=False) as zipped:
156
path_root = len(self.path) + 1 # account for trailing slash
157
for base, _, files in os.walk(self.path):
158
for fyle in files:
159
filename = os.path.join(base, fyle)
160
zipped.write(filename, filename[path_root:])
161
return base64.b64encode(fp.getvalue()).decode("UTF-8")
162
163
def _read_existing_userjs(self, userjs):
164
"""Reads existing preferences and adds them to desired preference
165
dictionary."""
166
pref_pattern = re.compile(r'user_pref\("(.*)",\s(.*)\)')
167
with open(userjs, encoding="utf-8") as f:
168
for usr in f:
169
matches = pref_pattern.search(usr)
170
try:
171
self._desired_preferences[matches.group(1)] = json.loads(matches.group(2))
172
except Exception:
173
warnings.warn(
174
f"(skipping) failed to json.loads existing preference: {matches.group(1) + matches.group(2)}"
175
)
176
177
@deprecated("Addons must be added after starting the session")
178
def _install_extension(self, addon, unpack=True):
179
"""Installs addon from a filepath, url or directory of addons in the
180
profile.
181
182
- path: url, absolute path to .xpi, or directory of addons
183
- unpack: whether to unpack unless specified otherwise in the install.rdf
184
"""
185
tmpdir = None
186
xpifile = None
187
if addon.endswith(".xpi"):
188
tmpdir = tempfile.mkdtemp(suffix="." + os.path.split(addon)[-1])
189
compressed_file = zipfile.ZipFile(addon, "r")
190
for name in compressed_file.namelist():
191
if name.endswith("/"):
192
if not os.path.isdir(os.path.join(tmpdir, name)):
193
os.makedirs(os.path.join(tmpdir, name))
194
else:
195
if not os.path.isdir(os.path.dirname(os.path.join(tmpdir, name))):
196
os.makedirs(os.path.dirname(os.path.join(tmpdir, name)))
197
data = compressed_file.read(name)
198
with open(os.path.join(tmpdir, name), "wb") as f:
199
f.write(data)
200
xpifile = addon
201
addon = tmpdir
202
203
# determine the addon id
204
addon_details = self._addon_details(addon)
205
addon_id = addon_details.get("id")
206
assert addon_id, f"The addon id could not be found: {addon}"
207
208
# copy the addon to the profile
209
extensions_dir = os.path.join(self._profile_dir, "extensions")
210
addon_path = os.path.join(extensions_dir, addon_id)
211
if not unpack and not addon_details["unpack"] and xpifile:
212
if not os.path.exists(extensions_dir):
213
os.makedirs(extensions_dir)
214
os.chmod(extensions_dir, 0o755)
215
shutil.copy(xpifile, addon_path + ".xpi")
216
else:
217
if not os.path.exists(addon_path):
218
shutil.copytree(addon, addon_path, symlinks=True)
219
220
# remove the temporary directory, if any
221
if tmpdir:
222
shutil.rmtree(tmpdir)
223
224
@deprecated("Addons must be added after starting the session")
225
def _addon_details(self, addon_path):
226
"""Returns a dictionary of details about the addon.
227
228
:param addon_path: path to the add-on directory or XPI
229
230
Returns::
231
232
{
233
"id": "[email protected]", # id of the addon
234
"version": "1.4", # version of the addon
235
"name": "Rainbow", # name of the addon
236
"unpack": False,
237
} # whether to unpack the addon
238
"""
239
240
details = {"id": None, "unpack": False, "name": None, "version": None}
241
242
def get_namespace_id(doc, url):
243
attributes = doc.documentElement.attributes
244
namespace = ""
245
for i in range(attributes.length):
246
if attributes.item(i).value == url:
247
if ":" in attributes.item(i).name:
248
# If the namespace is not the default one remove 'xlmns:'
249
namespace = attributes.item(i).name.split(":")[1] + ":"
250
break
251
return namespace
252
253
def get_text(element):
254
"""Retrieve the text value of a given node."""
255
rc = []
256
for node in element.childNodes:
257
if node.nodeType == node.TEXT_NODE:
258
rc.append(node.data)
259
return "".join(rc).strip()
260
261
def parse_manifest_json(content):
262
"""Extracts the details from the contents of a WebExtensions
263
`manifest.json` file."""
264
manifest = json.loads(content)
265
try:
266
id = manifest["applications"]["gecko"]["id"]
267
except KeyError:
268
id = manifest["name"].replace(" ", "") + "@" + manifest["version"]
269
return {
270
"id": id,
271
"version": manifest["version"],
272
"name": manifest["version"],
273
"unpack": False,
274
}
275
276
if not os.path.exists(addon_path):
277
raise OSError(f"Add-on path does not exist: {addon_path}")
278
279
try:
280
if zipfile.is_zipfile(addon_path):
281
with zipfile.ZipFile(addon_path, "r") as compressed_file:
282
if "manifest.json" in compressed_file.namelist():
283
return parse_manifest_json(compressed_file.read("manifest.json"))
284
285
manifest = compressed_file.read("install.rdf")
286
elif os.path.isdir(addon_path):
287
manifest_json_filename = os.path.join(addon_path, "manifest.json")
288
if os.path.exists(manifest_json_filename):
289
with open(manifest_json_filename, encoding="utf-8") as f:
290
return parse_manifest_json(f.read())
291
292
with open(os.path.join(addon_path, "install.rdf"), encoding="utf-8") as f:
293
manifest = f.read()
294
else:
295
raise OSError(f"Add-on path is neither an XPI nor a directory: {addon_path}")
296
except (OSError, KeyError) as e:
297
raise AddonFormatError(str(e), sys.exc_info()[2])
298
299
try:
300
doc = minidom.parseString(manifest)
301
302
# Get the namespaces abbreviations
303
em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")
304
rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
305
306
description = doc.getElementsByTagName(rdf + "Description").item(0)
307
if not description:
308
description = doc.getElementsByTagName("Description").item(0)
309
for node in description.childNodes:
310
# Remove the namespace prefix from the tag for comparison
311
entry = node.nodeName.replace(em, "")
312
if entry in details:
313
details.update({entry: get_text(node)})
314
if not details.get("id"):
315
for i in range(description.attributes.length):
316
attribute = description.attributes.item(i)
317
if attribute.name == em + "id":
318
details.update({"id": attribute.value})
319
except Exception as e:
320
raise AddonFormatError(str(e), sys.exc_info()[2])
321
322
# turn unpack into a true/false value
323
if isinstance(details["unpack"], str):
324
details["unpack"] = details["unpack"].lower() == "true"
325
326
# If no ID is set, the add-on is invalid
327
if not details.get("id"):
328
raise AddonFormatError("Add-on id could not be found.")
329
330
return details
331
332