1
# This Source Code Form is subject to the terms of the Mozilla Public
2
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
3
# You can obtain one at http://mozilla.org/MPL/2.0/.
10
from distutils import dir_util
11
from manifestparser import ManifestParser
12
from xml.dom import minidom
14
# Needed for the AMO's rest API - https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
15
AMO_API_VERSION = "1.5"
17
class AddonManager(object):
19
Handles all operations regarding addons including: installing and cleaning addons
22
def __init__(self, profile):
24
profile - the path to the profile for which we install addons
26
self.profile = profile
28
# information needed for profile reset:
29
# https://github.com/mozilla/mozbase/blob/270a857328b130860d1b1b512e23899557a3c8f7/mozprofile/mozprofile/profile.py#L93
30
self.installed_addons = []
31
self.installed_manifests = []
33
# addons that we've installed; needed for cleanup
36
def install_addons(self, addons=None, manifests=None):
38
Installs all types of addons
39
addons - a list of addon paths to install
40
manifest - a list of addon manifests to install
44
if isinstance(addons, basestring):
46
self.installed_addons.extend(addons)
48
self.install_from_path(addon)
49
# install addon manifests
51
if isinstance(manifests, basestring):
52
manifests = [manifests]
53
for manifest in manifests:
54
self.install_from_manifest(manifest)
55
self.installed_manifests.extended(manifests)
57
def install_from_manifest(self, filepath):
59
Installs addons from a manifest
60
filepath - path to the manifest of addons to install
62
manifest = ManifestParser()
63
manifest.read(filepath)
64
addons = manifest.get()
67
if '://' in addon['path'] or os.path.exists(addon['path']):
68
self.install_from_path(addon['path'])
71
# No path specified, try to grab it off AMO
72
locale = addon.get('amo_locale', 'en_US')
74
query = 'https://services.addons.mozilla.org/' + locale + '/firefox/api/' + AMO_API_VERSION + '/'
76
query += 'addon/' + addon['amo_id'] # this query grabs information on the addon base on its id
78
query += 'search/' + addon['name'] + '/default/1' # this query grabs information on the first addon returned from a search
79
install_path = AddonManager.get_amo_install_path(query)
80
self.install_from_path(install_path)
83
def get_amo_install_path(self, query):
85
Return the addon xpi install path for the specified AMO query.
86
See: https://developer.mozilla.org/en/addons.mozilla.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API
87
for query documentation.
89
response = urllib2.urlopen(query)
90
dom = minidom.parseString(response.read())
91
for node in dom.getElementsByTagName('install')[0].childNodes:
92
if node.nodeType == node.TEXT_NODE:
96
def addon_details(cls, addon_path):
98
returns a dictionary of details about the addon
99
- addon_path : path to the addon directory
101
{'id': u'rainbow@colors.org', # id of the addon
102
'version': u'1.4', # version of the addon
103
'name': u'Rainbow', # name of the addon
104
'unpack': False } # whether to unpack the addon
107
# TODO: We don't use the unpack variable yet, but we should: bug 662683
115
def get_namespace_id(doc, url):
116
attributes = doc.documentElement.attributes
118
for i in range(attributes.length):
119
if attributes.item(i).value == url:
120
if ":" in attributes.item(i).name:
121
# If the namespace is not the default one remove 'xlmns:'
122
namespace = attributes.item(i).name.split(':')[1] + ":"
126
def get_text(element):
127
"""Retrieve the text value of a given node"""
129
for node in element.childNodes:
130
if node.nodeType == node.TEXT_NODE:
132
return ''.join(rc).strip()
134
doc = minidom.parse(os.path.join(addon_path, 'install.rdf'))
136
# Get the namespaces abbreviations
137
em = get_namespace_id(doc, "http://www.mozilla.org/2004/em-rdf#")
138
rdf = get_namespace_id(doc, "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
140
description = doc.getElementsByTagName(rdf + "Description").item(0)
141
for node in description.childNodes:
142
# Remove the namespace prefix from the tag for comparison
143
entry = node.nodeName.replace(em, "")
144
if entry in details.keys():
145
details.update({ entry: get_text(node) })
147
# turn unpack into a true/false value
148
if isinstance(details['unpack'], basestring):
149
details['unpack'] = details['unpack'].lower() == 'true'
153
def install_from_path(self, path, unpack=False):
155
Installs addon from a filepath, url
156
or directory of addons in the profile.
157
- path: url, path to .xpi, or directory of addons
158
- unpack: whether to unpack unless specified otherwise in the install.rdf
161
# if the addon is a url, download it
162
# note that this won't work with protocols urllib2 doesn't support
164
response = urllib2.urlopen(path)
165
fd, path = tempfile.mkstemp(suffix='.xpi')
166
os.write(fd, response.read())
172
# if the addon is a directory, install all addons in it
174
if not path.endswith('.xpi') and not os.path.exists(os.path.join(path, 'install.rdf')):
175
# If the path doesn't exist, then we don't really care, just return
176
if not os.path.isdir(path):
178
addons = [os.path.join(path, x) for x in os.listdir(path) if
179
os.path.isdir(os.path.join(path, x))]
185
if addon.endswith('.xpi'):
186
tmpdir = tempfile.mkdtemp(suffix = '.' + os.path.split(addon)[-1])
187
compressed_file = zipfile.ZipFile(addon, 'r')
188
for name in compressed_file.namelist():
189
if name.endswith('/'):
190
os.makedirs(os.path.join(tmpdir, name))
192
if not os.path.isdir(os.path.dirname(os.path.join(tmpdir, name))):
193
os.makedirs(os.path.dirname(os.path.join(tmpdir, name)))
194
data = compressed_file.read(name)
195
f = open(os.path.join(tmpdir, name), 'wb')
201
# determine the addon id
202
addon_details = AddonManager.addon_details(addon)
203
addon_id = addon_details.get('id')
204
assert addon_id, 'The addon id could not be found: %s' % addon
206
# copy the addon to the profile
207
extensions_path = os.path.join(self.profile, 'extensions', 'staged')
208
addon_path = os.path.join(extensions_path, addon_id)
209
if not unpack and not addon_details['unpack'] and xpifile:
210
if not os.path.exists(extensions_path):
211
os.makedirs(extensions_path)
212
shutil.copy(xpifile, addon_path + '.xpi')
214
dir_util.copy_tree(addon, addon_path, preserve_symlinks=1)
215
self._addon_dirs.append(addon_path)
217
# remove the temporary directory, if any
219
dir_util.remove_tree(tmpdir)
221
# remove temporary file, if any
225
def clean_addons(self):
226
"""Cleans up addons in the profile."""
227
for addon in self._addon_dirs:
228
if os.path.isdir(addon):
229
dir_util.remove_tree(addon)