OLD | NEW |
(Empty) | |
| 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/. |
| 4 |
| 5 import os |
| 6 import shutil |
| 7 import tempfile |
| 8 import urllib2 |
| 9 import zipfile |
| 10 from distutils import dir_util |
| 11 from manifestparser import ManifestParser |
| 12 from xml.dom import minidom |
| 13 |
| 14 # Needed for the AMO's rest API - https://developer.mozilla.org/en/addons.mozill
a.org_%28AMO%29_API_Developers%27_Guide/The_generic_AMO_API |
| 15 AMO_API_VERSION = "1.5" |
| 16 |
| 17 class AddonManager(object): |
| 18 """ |
| 19 Handles all operations regarding addons including: installing and cleaning a
ddons |
| 20 """ |
| 21 |
| 22 def __init__(self, profile): |
| 23 """ |
| 24 profile - the path to the profile for which we install addons |
| 25 """ |
| 26 self.profile = profile |
| 27 |
| 28 # information needed for profile reset: |
| 29 # https://github.com/mozilla/mozbase/blob/270a857328b130860d1b1b512e2389
9557a3c8f7/mozprofile/mozprofile/profile.py#L93 |
| 30 self.installed_addons = [] |
| 31 self.installed_manifests = [] |
| 32 |
| 33 # addons that we've installed; needed for cleanup |
| 34 self._addon_dirs = [] |
| 35 |
| 36 def install_addons(self, addons=None, manifests=None): |
| 37 """ |
| 38 Installs all types of addons |
| 39 addons - a list of addon paths to install |
| 40 manifest - a list of addon manifests to install |
| 41 """ |
| 42 # install addon paths |
| 43 if addons: |
| 44 if isinstance(addons, basestring): |
| 45 addons = [addons] |
| 46 self.installed_addons.extend(addons) |
| 47 for addon in addons: |
| 48 self.install_from_path(addon) |
| 49 # install addon manifests |
| 50 if 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) |
| 56 |
| 57 def install_from_manifest(self, filepath): |
| 58 """ |
| 59 Installs addons from a manifest |
| 60 filepath - path to the manifest of addons to install |
| 61 """ |
| 62 manifest = ManifestParser() |
| 63 manifest.read(filepath) |
| 64 addons = manifest.get() |
| 65 |
| 66 for addon in addons: |
| 67 if '://' in addon['path'] or os.path.exists(addon['path']): |
| 68 self.install_from_path(addon['path']) |
| 69 continue |
| 70 |
| 71 # No path specified, try to grab it off AMO |
| 72 locale = addon.get('amo_locale', 'en_US') |
| 73 |
| 74 query = 'https://services.addons.mozilla.org/' + locale + '/firefox/
api/' + AMO_API_VERSION + '/' |
| 75 if 'amo_id' in addon: |
| 76 query += 'addon/' + addon['amo_id'] # this query
grabs information on the addon base on its id |
| 77 else: |
| 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) |
| 81 |
| 82 @classmethod |
| 83 def get_amo_install_path(self, query): |
| 84 """ |
| 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_D
evelopers%27_Guide/The_generic_AMO_API |
| 87 for query documentation. |
| 88 """ |
| 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: |
| 93 return node.data |
| 94 |
| 95 @classmethod |
| 96 def addon_details(cls, addon_path): |
| 97 """ |
| 98 returns a dictionary of details about the addon |
| 99 - addon_path : path to the addon directory |
| 100 Returns: |
| 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 |
| 105 """ |
| 106 |
| 107 # TODO: We don't use the unpack variable yet, but we should: bug 662683 |
| 108 details = { |
| 109 'id': None, |
| 110 'unpack': False, |
| 111 'name': None, |
| 112 'version': None |
| 113 } |
| 114 |
| 115 def get_namespace_id(doc, url): |
| 116 attributes = doc.documentElement.attributes |
| 117 namespace = "" |
| 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] + ":" |
| 123 break |
| 124 return namespace |
| 125 |
| 126 def get_text(element): |
| 127 """Retrieve the text value of a given node""" |
| 128 rc = [] |
| 129 for node in element.childNodes: |
| 130 if node.nodeType == node.TEXT_NODE: |
| 131 rc.append(node.data) |
| 132 return ''.join(rc).strip() |
| 133 |
| 134 doc = minidom.parse(os.path.join(addon_path, 'install.rdf')) |
| 135 |
| 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#
") |
| 139 |
| 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) }) |
| 146 |
| 147 # turn unpack into a true/false value |
| 148 if isinstance(details['unpack'], basestring): |
| 149 details['unpack'] = details['unpack'].lower() == 'true' |
| 150 |
| 151 return details |
| 152 |
| 153 def install_from_path(self, path, unpack=False): |
| 154 """ |
| 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.rd
f |
| 159 """ |
| 160 |
| 161 # if the addon is a url, download it |
| 162 # note that this won't work with protocols urllib2 doesn't support |
| 163 if '://' in path: |
| 164 response = urllib2.urlopen(path) |
| 165 fd, path = tempfile.mkstemp(suffix='.xpi') |
| 166 os.write(fd, response.read()) |
| 167 os.close(fd) |
| 168 tmpfile = path |
| 169 else: |
| 170 tmpfile = None |
| 171 |
| 172 # if the addon is a directory, install all addons in it |
| 173 addons = [path] |
| 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): |
| 177 return |
| 178 addons = [os.path.join(path, x) for x in os.listdir(path) if |
| 179 os.path.isdir(os.path.join(path, x))] |
| 180 |
| 181 # install each addon |
| 182 for addon in addons: |
| 183 tmpdir = None |
| 184 xpifile = None |
| 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)) |
| 191 else: |
| 192 if not os.path.isdir(os.path.dirname(os.path.join(tmpdir
, name))): |
| 193 os.makedirs(os.path.dirname(os.path.join(tmpdir, nam
e))) |
| 194 data = compressed_file.read(name) |
| 195 f = open(os.path.join(tmpdir, name), 'wb') |
| 196 f.write(data) |
| 197 f.close() |
| 198 xpifile = addon |
| 199 addon = tmpdir |
| 200 |
| 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 |
| 205 |
| 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') |
| 213 else: |
| 214 dir_util.copy_tree(addon, addon_path, preserve_symlinks=1) |
| 215 self._addon_dirs.append(addon_path) |
| 216 |
| 217 # remove the temporary directory, if any |
| 218 if tmpdir: |
| 219 dir_util.remove_tree(tmpdir) |
| 220 |
| 221 # remove temporary file, if any |
| 222 if tmpfile: |
| 223 os.remove(tmpfile) |
| 224 |
| 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) |
OLD | NEW |