Bug 1377510 - Add the ability to install webextensions with mozprofile r=automatedtester
authorTarek Ziadé <tarek@mozilla.com>
Mon, 24 Jul 2017 12:08:53 +0200
changeset 420118 7b98f21c8b79286628ed6ad0cb5a84a1ec50ba19
parent 420117 d02f661a04bee7e71e6835035d479c6001aa1cf4
child 420119 23ed43f92e98b5feb2f5adcdef031f08092af858
push id7566
push usermtabara@mozilla.com
push dateWed, 02 Aug 2017 08:25:16 +0000
treeherdermozilla-beta@86913f512c3c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersautomatedtester
bugs1377510
milestone56.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1377510 - Add the ability to install webextensions with mozprofile r=automatedtester MozReview-Commit-ID: DgI7hZFKHns
testing/mozbase/mozprofile/mozprofile/addons.py
testing/mozbase/mozprofile/tests/addons/apply-css-sans-id.xpi
testing/mozbase/mozprofile/tests/addons/apply-css.xpi
testing/mozbase/mozprofile/tests/test_addons.py
--- a/testing/mozbase/mozprofile/mozprofile/addons.py
+++ b/testing/mozbase/mozprofile/mozprofile/addons.py
@@ -1,26 +1,30 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
-
+import json
 import os
 import shutil
 import sys
 import tempfile
 import urllib2
 import zipfile
+import hashlib
 from xml.dom import minidom
 
 import mozfile
 from mozlog.unstructured import getLogger
 
 # 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
 AMO_API_VERSION = "1.5"
+_SALT = os.urandom(32).encode('hex')
+_TEMPORARY_ADDON_SUFFIX = "@temporary-addon"
+
 
 # Logger for 'mozprofile.addons' module
 module_logger = getLogger(__name__)
 
 
 class AddonFormatError(Exception):
     """Exception for not well-formed add-on manifest files"""
 
@@ -228,16 +232,22 @@ class AddonManager(object):
         """
         response = urllib2.urlopen(query)
         dom = minidom.parseString(response.read())
         for node in dom.getElementsByTagName('install')[0].childNodes:
             if node.nodeType == node.TEXT_NODE:
                 return node.data
 
     @classmethod
+    def _gen_iid(cls, addon_path):
+        hash = hashlib.sha1(_SALT)
+        hash.update(addon_path)
+        return hash.hexdigest() + _TEMPORARY_ADDON_SUFFIX
+
+    @classmethod
     def addon_details(cls, addon_path):
         """
         Returns a dictionary of details about the addon.
 
         :param addon_path: path to the add-on directory or XPI
 
         Returns::
 
@@ -271,72 +281,94 @@ class AddonManager(object):
             for node in element.childNodes:
                 if node.nodeType == node.TEXT_NODE:
                     rc.append(node.data)
             return ''.join(rc).strip()
 
         if not os.path.exists(addon_path):
             raise IOError('Add-on path does not exist: %s' % addon_path)
 
+        is_webext = False
         try:
             if zipfile.is_zipfile(addon_path):
                 # Bug 944361 - We cannot use 'with' together with zipFile because
                 # it will cause an exception thrown in Python 2.6.
                 try:
                     compressed_file = zipfile.ZipFile(addon_path, 'r')
-                    manifest = compressed_file.read('install.rdf')
+                    filenames = [f.filename for f in (compressed_file).filelist]
+                    if 'install.rdf' in filenames:
+                        manifest = compressed_file.read('install.rdf')
+                    elif 'manifest.json' in filenames:
+                        is_webext = True
+                        manifest = compressed_file.read('manifest.json')
+                        manifest = json.loads(manifest)
+                    else:
+                        raise KeyError("No manifest")
                 finally:
                     compressed_file.close()
             elif os.path.isdir(addon_path):
-                with open(os.path.join(addon_path, 'install.rdf'), 'r') as f:
-                    manifest = f.read()
+                try:
+                    with open(os.path.join(addon_path, 'install.rdf')) as f:
+                        manifest = f.read()
+                except IOError:
+                    with open(os.path.join(addon_path, 'manifest.json')) as f:
+                        manifest = json.loads(f.read())
+                        is_webext = True
             else:
                 raise IOError('Add-on path is neither an XPI nor a directory: %s' % addon_path)
         except (IOError, KeyError) as e:
             raise AddonFormatError(str(e)), None, sys.exc_info()[2]
 
-        try:
-            doc = minidom.parseString(manifest)
-
-            # Get the namespaces abbreviations
-            em = get_namespace_id(doc, 'http://www.mozilla.org/2004/em-rdf#')
-            rdf = get_namespace_id(doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')
+        if is_webext:
+            details['version'] = manifest['version']
+            details['name'] = manifest['name']
+            try:
+                details['id'] = manifest['applications']['gecko']['id']
+            except KeyError:
+                details['id'] = cls._gen_iid(addon_path)
+            details['unpack'] = False
+        else:
+            try:
+                doc = minidom.parseString(manifest)
 
-            description = doc.getElementsByTagName(rdf + 'Description').item(0)
-            for entry, value in description.attributes.items():
-                # Remove the namespace prefix from the tag for comparison
-                entry = entry.replace(em, "")
-                if entry in details.keys():
-                    details.update({entry: value})
-            for node in description.childNodes:
-                # Remove the namespace prefix from the tag for comparison
-                entry = node.nodeName.replace(em, "")
-                if entry in details.keys():
-                    details.update({entry: get_text(node)})
-        except Exception as e:
-            raise AddonFormatError(str(e)), None, sys.exc_info()[2]
+                # Get the namespaces abbreviations
+                em = get_namespace_id(doc, 'http://www.mozilla.org/2004/em-rdf#')
+                rdf = get_namespace_id(doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#')
+
+                description = doc.getElementsByTagName(rdf + 'Description').item(0)
+                for entry, value in description.attributes.items():
+                    # Remove the namespace prefix from the tag for comparison
+                    entry = entry.replace(em, "")
+                    if entry in details.keys():
+                        details.update({entry: value})
+                for node in description.childNodes:
+                    # Remove the namespace prefix from the tag for comparison
+                    entry = node.nodeName.replace(em, "")
+                    if entry in details.keys():
+                        details.update({entry: get_text(node)})
+            except Exception as e:
+                raise AddonFormatError(str(e)), None, sys.exc_info()[2]
 
         # turn unpack into a true/false value
         if isinstance(details['unpack'], basestring):
             details['unpack'] = details['unpack'].lower() == 'true'
 
         # If no ID is set, the add-on is invalid
-        if details.get('id') is None:
+        if details.get('id') is None and not is_webext:
             raise AddonFormatError('Add-on id could not be found.')
 
         return details
 
     def install_from_path(self, path, unpack=False):
         """
         Installs addon from a filepath, url or directory of addons in the profile.
 
         :param path: url, path to .xpi, or directory of addons
         :param unpack: whether to unpack unless specified otherwise in the install.rdf
         """
-
         # if the addon is a URL, download it
         # note that this won't work with protocols urllib2 doesn't support
         if mozfile.is_url(path):
             path = self.download(path)
             self.downloaded_addons.append(path)
 
         addons = [path]
 
new file mode 100644
index 0000000000000000000000000000000000000000..fa721a4f76bad44da79286edb596050f7cc27cde
GIT binary patch
literal 3371
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
new file mode 100644
index 0000000000000000000000000000000000000000..0ed64f79ac9e2d0ea2459fc6d6f9f95d593e8b3b
GIT binary patch
literal 3412
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
bfZtUH+Em##ND$jb8kljZG0qBtY(#$p+lgxr
--- a/testing/mozbase/mozprofile/tests/test_addons.py
+++ b/testing/mozbase/mozprofile/tests/test_addons.py
@@ -4,16 +4,17 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this file,
 # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 import os
 import shutil
 import tempfile
 import unittest
 import urllib2
+import zipfile
 
 import mozunit
 
 from manifestparser import ManifestParser
 import mozfile
 import mozhttpd
 import mozlog.unstructured as mozlog
 import mozprofile
@@ -108,16 +109,55 @@ class TestAddonsManager(unittest.TestCas
         # Download from an invalid URL
         addon = 'not_existent.xpi'
         self.assertRaises(ValueError,
                           self.am.download, addon, self.tmpdir)
         self.assertEqual(os.listdir(self.tmpdir), [])
 
         server.stop()
 
+    def test_install_webextension_from_dir(self):
+        addon = os.path.join(here, 'addons', 'apply-css.xpi')
+        zipped = zipfile.ZipFile(addon)
+        try:
+            zipped.extractall(self.tmpdir)
+        finally:
+            zipped.close()
+        self.am.install_from_path(self.tmpdir)
+        self.assertEqual(len(self.am.installed_addons), 1)
+        self.assertTrue(os.path.isdir(self.am.installed_addons[0]))
+
+    def test_install_webextension(self):
+        server = mozhttpd.MozHttpd(docroot=os.path.join(here, 'addons'))
+        server.start()
+        try:
+            addon = server.get_url() + 'apply-css.xpi'
+            self.am.install_from_path(addon)
+        finally:
+            server.stop()
+
+        self.assertEqual(len(self.am.downloaded_addons), 1)
+        self.assertTrue(os.path.isfile(self.am.downloaded_addons[0]))
+        self.assertIn('test-webext@quality.mozilla.org.xpi',
+                      os.path.basename(self.am.downloaded_addons[0]))
+
+    def test_install_webextension_sans_id(self):
+        server = mozhttpd.MozHttpd(docroot=os.path.join(here, 'addons'))
+        server.start()
+        try:
+            addon = server.get_url() + 'apply-css-sans-id.xpi'
+            self.am.install_from_path(addon)
+        finally:
+            server.stop()
+
+        self.assertEqual(len(self.am.downloaded_addons), 1)
+        self.assertTrue(os.path.isfile(self.am.downloaded_addons[0]))
+        self.assertIn('temporary-addon.xpi',
+                      os.path.basename(self.am.downloaded_addons[0]))
+
     def test_install_from_path_xpi(self):
         addons_to_install = []
         addons_installed = []
 
         # Generate installer stubs and install them
         for ext in ['test-addon-1@mozilla.org', 'test-addon-2@mozilla.org']:
             temp_addon = generate_addon(ext, path=self.tmpdir)
             addons_to_install.append(self.am.addon_details(temp_addon)['id'])