Beispiel #1
0
def fetch_icon(webapp, file_obj=None, **kw):
    """
    Downloads a webapp icon from the location specified in the manifest.

    Returns False if icon was not able to be retrieved

    If `file_obj` is not provided it will use the file from the app's
    `current_version`.

    """
    log.info(u"[1@None] Fetching icon for webapp %s." % webapp.name)
    file_obj = file_obj or webapp.current_version and webapp.current_version.all_files[0]
    manifest = webapp.get_manifest_json(file_obj)

    if not manifest or "icons" not in manifest:
        # Set the icon type to empty.
        webapp.update(icon_type="")
        return

    try:
        biggest = max(int(size) for size in manifest["icons"])
    except ValueError:
        log.error('No icon to fetch for webapp "%s"' % webapp.name)
        return False

    icon_url = manifest["icons"][str(biggest)]
    if icon_url.startswith("data:image"):
        image_string = icon_url.split("base64,")[1]
        content = base64.decodestring(image_string)
    else:
        if webapp.is_packaged:
            # Get icons from package.
            if icon_url.startswith("/"):
                icon_url = icon_url[1:]
            try:
                zf = SafeUnzip(file_obj.file_path)
                zf.is_valid()
                content = zf.extract_path(icon_url)
            except (KeyError, forms.ValidationError):  # Not found in archive.
                log.error(u"[Webapp:%s] Icon %s not found in archive" % (webapp, icon_url))
                return False
        else:
            if not urlparse.urlparse(icon_url).scheme:
                icon_url = webapp.origin + icon_url

            try:
                response = _fetch_content(icon_url)
            except Exception, e:
                log.error(u"[Webapp:%s] Failed to fetch icon for webapp: %s" % (webapp, e))
                # Set the icon type to empty.
                webapp.update(icon_type="")
                return False

            try:
                content = get_content_and_check_size(response, settings.MAX_ICON_UPLOAD_SIZE)
            except ResponseTooLargeException:
                log.warning(u"[Webapp:%s] Icon exceeds maximum size." % webapp)
                return False
Beispiel #2
0
 def test_is_broken(self):
     zip = SafeUnzip(self.packaged_app_path('signed.zip'))
     zip.is_valid()
     sf_re = re.compile('^META\-INF/(\w+)\.sf$')
     for info in zip.info:
         if sf_re.match(info.filename):
             info.filename = 'META-INF/foo.foo'
             break
     assert not zip.is_signed()
 def test_is_broken(self):
     zip = SafeUnzip(self.packaged_app_path('signed.zip'))
     zip.is_valid()
     sf_re = re.compile('^META\-INF/(\w+)\.sf$')
     for info in zip.info:
         if sf_re.match(info.filename):
             info.filename = 'META-INF/foo.foo'
             break
     assert not zip.is_signed()
Beispiel #4
0
def fetch_icon(webapp, **kw):
    """Downloads a webapp icon from the location specified in the manifest.
    Returns False if icon was not able to be retrieved
    """
    log.info(u'[1@None] Fetching icon for webapp %s.' % webapp.name)
    manifest = webapp.get_manifest_json()
    if not manifest or not 'icons' in manifest:
        # Set the icon type to empty.
        webapp.update(icon_type='')
        return

    try:
        biggest = max(int(size) for size in manifest['icons'])
    except ValueError:
        log.error('No icon to fetch for webapp "%s"' % webapp.name)
        return False

    icon_url = manifest['icons'][str(biggest)]
    if icon_url.startswith('data:image'):
        image_string = icon_url.split('base64,')[1]
        content = base64.decodestring(image_string)
    else:
        if webapp.is_packaged:
            # Get icons from package.
            if icon_url.startswith('/'):
                icon_url = icon_url[1:]
            try:
                zf = SafeUnzip(webapp.get_latest_file().file_path)
                zf.is_valid()
                content = zf.extract_path(icon_url)
            except (KeyError, forms.ValidationError):  # Not found in archive.
                log.error(u'[Webapp:%s] Icon %s not found in archive'
                          % (webapp, icon_url))
                return False
        else:
            if not urlparse.urlparse(icon_url).scheme:
                icon_url = webapp.origin + icon_url

            try:
                response = _fetch_content(icon_url)
            except Exception, e:
                log.error(u'[Webapp:%s] Failed to fetch icon for webapp: %s'
                          % (webapp, e))
                # Set the icon type to empty.
                webapp.update(icon_type='')
                return False

            try:
                content = get_content_and_check_size(
                    response, settings.MAX_ICON_UPLOAD_SIZE)
            except ResponseTooLargeException:
                log.warning(u'[Webapp:%s] Icon exceeds maximum size.' % webapp)
                return False
Beispiel #5
0
    def manifest_contents(self):
        fp = get_file(self.fileorpath)
        if zipfile.is_zipfile(fp):
            zf = SafeUnzip(fp)
            zf.is_valid()  # Raises forms.ValidationError if problems.
            try:
                data = zf.extract_path('manifest.json')
            except KeyError:
                raise forms.ValidationError(
                    _('The file "manifest.json" was not found at the root '
                      'of the zip archive.'))
        else:
            raise forms.ValidationError(
                _('Addons need to be packaged into a valid zip archive.'))

        return self.decode_manifest(data)
Beispiel #6
0
    def manifest_contents(self):
        fp = get_file(self.fileorpath)
        if zipfile.is_zipfile(fp):
            zf = SafeUnzip(fp)
            zf.is_valid()  # Raises forms.ValidationError if problems.
            try:
                data = zf.extract_path('manifest.json')
            except KeyError:
                raise forms.ValidationError(
                    _('The file "manifest.json" was not found at the root '
                      'of the zip archive.'))
        else:
            raise forms.ValidationError(
                _('Addons need to be packaged into a valid zip archive.'))

        return self.decode_manifest(data)
Beispiel #7
0
 def validate_file(self, file_obj):
     """
     Verify that the upload is a valid zip file that contains a
     manifest.json file.
     """
     if file_obj.content_type not in self.valid_content_types:
         raise ParseError(self.errors['BAD_CONTENT_TYPE'])
     try:
         zf = SafeUnzip(file_obj)
         try:
             zf.is_valid()  # Will throw ValidationError if necessary.
         except ValidationError as e:
             raise ParseError(unicode(e))
         except (BadZipfile, IOError):
             raise ParseError(self.errors['INVALID_ZIP'])
         manifest = zf.extract_path('manifest.json')
     except KeyError:
         raise ParseError(self.errors['NO_MANIFEST'])
     return manifest
Beispiel #8
0
    def clean_upload(self):
        upload = self.cleaned_data['upload']
        errors = []

        if upload.size > self.max_size:
            errors.append({
                'type': 'error',
                'message': _('Packaged app too large for submission. Packages '
                             'must be smaller than %s.' % filesizeformat(
                                 self.max_size)),
                'tier': 1,
            })
            # Immediately raise an error, do not process the rest of the view,
            # which would read the file.
            raise self.persist_errors(errors, upload)

        manifest = None
        try:
            # Be careful to keep this as in-memory zip reading.
            safe_zip = SafeUnzip(upload, 'r')
            safe_zip.is_valid()  # Will throw ValidationError if necessary.
            manifest = safe_zip.extract_path('manifest.webapp')
        except forms.ValidationError as e:
            errors.append({
                'type': 'error',
                'message': ''.join(e.messages),
                'tier': 1,
            })
        except Exception as e:
            errors.append({
                'type': 'error',
                'message': _('Error extracting manifest from zip file.'),
                'tier': 1,
            })
        finally:
            safe_zip.close()

        origin = None
        if manifest:
            try:
                origin = WebAppParser.decode_manifest(manifest).get('origin')
            except forms.ValidationError as e:
                errors.append({
                    'type': 'error',
                    'message': ''.join(e.messages),
                    'tier': 1,
                })

        if origin:
            try:
                verify_app_domain(origin, packaged=True, exclude=self.addon)
            except forms.ValidationError, e:
                errors.append({
                    'type': 'error',
                    'message': ''.join(e.messages),
                    'tier': 1,
                })
Beispiel #9
0
 def validate_file(self, file_obj):
     """
     Verify that the upload is a valid zip file that contains a
     manifest.json file.
     """
     if file_obj.content_type not in self.valid_content_types:
         self.error('BAD_CONTENT_TYPE')
     try:
         self.zipfile = SafeUnzip(file_obj)
         try:
             # Will throw ValidationError if necessary.
             self.zipfile.is_valid()
         except ValidationError as e:
             raise ParseError(unicode(e))
         except (BadZipfile, IOError):
             self.error('INVALID_ZIP')
         manifest = self.zipfile.extract_path('manifest.json')
     except KeyError:
         self.error('NO_MANIFEST')
     return manifest
Beispiel #10
0
 def validate_file(self, file_obj):
     """
     Verify that the upload is a valid zip file that contains a
     manifest.json file.
     """
     if file_obj.content_type not in self.valid_content_types:
         self.error("BAD_CONTENT_TYPE")
     try:
         self.zipfile = SafeUnzip(file_obj)
         try:
             # Will throw ValidationError if necessary.
             self.zipfile.is_valid()
         except ValidationError as e:
             raise ParseError(unicode(e))
         except (BadZipfile, IOError):
             self.error("INVALID_ZIP")
         manifest = self.zipfile.extract_path("manifest.json")
     except KeyError:
         self.error("NO_MANIFEST")
     return manifest
Beispiel #11
0
def fetch_icon(pk, file_pk=None, **kw):
    """
    Downloads a webapp icon from the location specified in the manifest.

    Returns False if icon was not able to be retrieved

    If `file_pk` is not provided it will use the file from the app's
    `current_version`.

    """
    webapp = Webapp.objects.get(pk=pk)
    log.info(u'[1@None] Fetching icon for webapp %s.' % webapp.name)
    if file_pk:
        file_obj = File.objects.get(pk=file_pk)
    else:
        file_obj = (webapp.current_version and
                    webapp.current_version.all_files[0])
    manifest = webapp.get_manifest_json(file_obj)

    if not manifest or 'icons' not in manifest:
        # Set the icon type to empty.
        webapp.update(icon_type='')
        return

    try:
        biggest = max(int(size) for size in manifest['icons'])
    except ValueError:
        log.error('No icon to fetch for webapp "%s"' % webapp.name)
        return False

    icon_url = manifest['icons'][str(biggest)]
    if icon_url.startswith('data:image'):
        image_string = icon_url.split('base64,')[1]
        content = base64.decodestring(image_string)
    else:
        if webapp.is_packaged:
            # Get icons from package.
            if icon_url.startswith('/'):
                icon_url = icon_url[1:]
            try:
                zf = SafeUnzip(private_storage.open(file_obj.file_path))
                zf.is_valid()
                content = zf.extract_path(icon_url)
            except (KeyError, forms.ValidationError):  # Not found in archive.
                log.error(u'[Webapp:%s] Icon %s not found in archive'
                          % (webapp, icon_url))
                return False
        else:
            if not urlparse.urlparse(icon_url).scheme:
                icon_url = webapp.origin + icon_url

            try:
                response = _fetch_content(icon_url)
            except Exception, e:
                log.error(u'[Webapp:%s] Failed to fetch icon for webapp: %s'
                          % (webapp, e))
                # Set the icon type to empty.
                webapp.update(icon_type='')
                return False

            try:
                content = get_content_and_check_size(
                    response, settings.MAX_ICON_UPLOAD_SIZE)
            except ResponseTooLargeException:
                log.warning(u'[Webapp:%s] Icon exceeds maximum size.' % webapp)
                return False
 def test_not_secure(self):
     zip = SafeUnzip(self.xpi_path('extension'))
     zip.is_valid()
     assert not zip.is_signed()
 def test_is_secure(self):
     zip = SafeUnzip(self.xpi_path('signed'))
     zip.is_valid()
     assert zip.is_signed()
Beispiel #14
0
 def test_unzip_fatal(self):
     zip = SafeUnzip(self.xpi_path('search.xml'))
     self.assertRaises(zipfile.BadZipfile, zip.is_valid)
 def test_extract_path(self):
     zip = SafeUnzip(self.xpi_path('langpack-localepicker'))
     assert zip.is_valid()
     assert'locale browser de' in zip.extract_path('chrome.manifest')
Beispiel #16
0
 def test_not_secure(self):
     zip = SafeUnzip(self.packaged_app_path('mozball.zip'))
     zip.is_valid()
     assert not zip.is_signed()
Beispiel #17
0
 def test_unzip_not_fatal(self):
     zip = SafeUnzip(self.manifest_path('mozball.webapp'))
     assert not zip.is_valid(fatal=False)
Beispiel #18
0
 def test_is_secure(self):
     zip = SafeUnzip(self.packaged_app_path('signed.zip'))
     zip.is_valid()
     assert zip.is_signed()
Beispiel #19
0
 def test_extract_path(self):
     zip = SafeUnzip(self.packaged_app_path('mozball.zip'))
     assert zip.is_valid()
     desc_string = '"description": "Exciting Open Web development action!"'
     assert desc_string in zip.extract_path('manifest.webapp')
Beispiel #20
0
 def test_is_broken(self):
     zip = SafeUnzip(self.xpi_path('signed'))
     zip.is_valid()
     zip.info[2].filename = 'META-INF/foo.sf'
     assert not zip.is_signed()
Beispiel #21
0
 def test_is_secure(self):
     zip = SafeUnzip(self.xpi_path('signed'))
     zip.is_valid()
     assert zip.is_signed()
Beispiel #22
0
 def test_not_secure(self):
     zip = SafeUnzip(self.xpi_path('extension'))
     zip.is_valid()
     assert not zip.is_signed()
Beispiel #23
0
 def test_extract_path(self):
     zip = SafeUnzip(self.xpi_path('langpack-localepicker'))
     assert zip.is_valid()
     assert 'locale browser de' in zip.extract_path('chrome.manifest')
Beispiel #24
0
 def test_unzip_not_fatal(self):
     zip = SafeUnzip(self.xpi_path('search.xml'))
     assert not zip.is_valid(fatal=False)
Beispiel #25
0
 def test_unzip_limit(self):
     zip = SafeUnzip(self.packaged_app_path('full-tpa.zip'))
     self.assertRaises(forms.ValidationError, zip.is_valid)
Beispiel #26
0
 def test_unzip_fatal(self):
     zip = SafeUnzip(self.manifest_path('mozball.webapp'))
     self.assertRaises(zipfile.BadZipfile, zip.is_valid)
 def test_unzip_not_fatal(self):
     zip = SafeUnzip(self.xpi_path('search.xml'))
     assert not zip.is_valid(fatal=False)
Beispiel #28
0
 def test_extract_path(self):
     zip = SafeUnzip(self.packaged_app_path('mozball.zip'))
     assert zip.is_valid()
     desc_string = '"description": "Exciting Open Web development action!"'
     assert desc_string in zip.extract_path('manifest.webapp')
Beispiel #29
0
class ExtensionValidator(object):
    """
    Firefox OS Add-on validator. If validation fails, will raise an instance of
    rest_framework.exceptions.ParseError containing information about the
    error.
    """

    errors = {
        "AUTHOR_NOT_STRING": _(u"The `author` property must be a string."),
        "AUTHOR_TOO_LONG": _(u"The `author` property cannot be longer than 128 characters."),
        "AUTHOR_TOO_SHORT": _(
            u"The `author` property must be at least 1 character"
            u" long and can not consist of only whitespace characters."
        ),
        "BAD_CONTENT_TYPE": _(u"The file sent has an unsupported content-type"),
        "DESCRIPTION_NOT_STRING": _(u"The `description` property must be a string."),
        "DESCRIPTION_TOO_LONG": _(u"The `description` property cannot be " u"longer than 132 characters."),
        "ICON_INCORRECT_DIMENSIONS": _(
            u"The icon file `%(icon_path)s` is not the specified dimensions "
            u" of %(icon_size)s x %(icon_size)s as defined in the manifest."
        ),
        "ICON_DOES_NOT_EXIST": _(
            u"The icon file `%(icon_path)s` is referenced in the manifest but" u" does not exist in the ZIP file."
        ),
        "ICONS_NO_128": _(u"If defining `icons`, you must include a 128x128 variant."),
        "ICON_NOT_A_VALID_IMAGE_OR_PNG": _(u"The icon file `%s` is not a valid PNG."),
        "ICON_NOT_SQUARE": _(u"The icon file `%(icon_path)s` is not square."),
        "ICON_INVALID_SIZE": _(u"The manifest contains an invalid icon size: %(icon_size)s"),
        "INVALID_JSON": _(u"'manifest.json' in the archive is not a valid JSON" u" file."),
        "INVALID_JSON_ENCODING": _(u"'manifest.json' in the archive is not encoded in UTF-8."),
        "INVALID_ZIP": _(u"The file sent is not a valid ZIP file."),
        "NAME_MISSING": _(u"There is no `name` property in the manifest."),
        "NAME_NOT_STRING": _(u"The `name` property must be a string."),
        "NAME_TOO_LONG": _(u"The `name` property cannot be longer than 45 " u"characters."),
        "NAME_TOO_SHORT": _(
            u"The `name` property must be at least 1 character"
            u" long and can not consist of only whitespace characters."
        ),
        "NO_MANIFEST": _(u"The archive does not contain a 'manifest.json' " u"file."),
        "VERSION_MISSING": _(u"There is no `version` property in the manifest."),
        "VERSION_NOT_STRING": _(u"The `version` property must be a string."),
        "VERSION_INVALID": _(
            u"The `version` property must be a string"
            u" containing one to four dot-separated integers each between"
            u" 0 and 65535."
        ),
    }
    valid_content_types = ("application/octet-stream", "application/zip")

    def __init__(self, file_obj=None):
        self.file_obj = file_obj
        self.zipfile = None

    def error(self, key, **kwargs):
        message = self.errors[key]
        if kwargs:
            message = self.errors[key] % kwargs
        raise ParseError(detail={"key": key, "message": message, "params": kwargs})

    def validate(self):
        """
        Run the full validation suite against the uploaded file:

        * Ensure that it is a valid zip file.
        * Ensure that it contains a valid manifest.json file.
        * Validate the manifest fields against the spec.

        Return the manifest contents (as dict).
        """
        self.manifest = self.validate_file(self.file_obj)
        self.data = self.validate_json(self.manifest)
        self.validate_name(self.data)
        self.validate_description(self.data)
        self.validate_version(self.data)
        self.validate_author(self.data)
        self.validate_icons(self.data)
        return self.data

    def validate_file(self, file_obj):
        """
        Verify that the upload is a valid zip file that contains a
        manifest.json file.
        """
        if file_obj.content_type not in self.valid_content_types:
            self.error("BAD_CONTENT_TYPE")
        try:
            self.zipfile = SafeUnzip(file_obj)
            try:
                # Will throw ValidationError if necessary.
                self.zipfile.is_valid()
            except ValidationError as e:
                raise ParseError(unicode(e))
            except (BadZipfile, IOError):
                self.error("INVALID_ZIP")
            manifest = self.zipfile.extract_path("manifest.json")
        except KeyError:
            self.error("NO_MANIFEST")
        return manifest

    def validate_json(self, contents):
        """
        Verify that the enclosed manifest.json is a valid and parsable JSON
        file.
        """
        try:
            # We support only UTF-8 encoded manifests.
            decoded_data = smart_unicode(strip_bom(contents))
        except UnicodeDecodeError:
            self.error("INVALID_JSON_ENCODING")
        try:
            return json.loads(decoded_data)
        except ValueError:
            self.error("INVALID_JSON")

    def validate_name(self, manifest_json):
        """
        Ensure that the name property of the manifest exists and is a string
        between 1 and 45 characters long.

        In addition, even though it's allowed in the spec, we consider the name
        invalid (too short) if it contains only whitespace characters.

        https://developer.chrome.com/extensions/manifest/name
        """
        try:
            name = manifest_json["name"]
        except KeyError:
            self.error("NAME_MISSING")
        if not isinstance(name, basestring):
            self.error("NAME_NOT_STRING")
        if len(name.strip()) < 1:
            self.error("NAME_TOO_SHORT")
        if len(name) > 45:
            self.error("NAME_TOO_LONG")

    def validate_description(self, manifest_json):
        """
        Ensures that, if present, the description property is no longer than
        132 characters.

        https://developer.chrome.com/extensions/manifest/description
        """
        if "description" in manifest_json:
            description = manifest_json["description"]
            if not isinstance(description, basestring):
                self.error("DESCRIPTION_NOT_STRING")
            if len(description.strip()) > 132:
                self.error("DESCRIPTION_TOO_LONG")

    def validate_version(self, manifest_json):
        """
        Ensure that the version property of the manifest exists and is a string
        containing one to four dot-separated integers, each between 0 and
        65535, with no leading zeros."""
        try:
            version = manifest_json["version"]
        except KeyError:
            self.error("VERSION_MISSING")
        if not isinstance(version, basestring):
            self.error("VERSION_NOT_STRING")
        splitted = version.split(".")
        if len(splitted) > 4:
            # Too many dots.
            self.error("VERSION_INVALID")
        for version_component in splitted:
            try:
                number = int(version_component)
                if version_component.startswith("0") and number != 0:
                    # Leading zeros are forbidden.
                    self.error("VERSION_INVALID")
                if number < 0 or number > 65535:
                    # All numbers must be between 0 and 65535 inclusive.
                    self.error("VERSION_INVALID")
            except ValueError:
                # Not a valid number.
                self.error("VERSION_INVALID")

    def validate_author(self, manifest_json):
        """
        Ensures that, if present, the author property is no longer than
        128 characters.
        """
        if "author" in manifest_json:
            author = manifest_json["author"]
            # Author must not be empty/only whitespace if present, since we'll
            # use it as link text.
            if not isinstance(author, basestring):
                self.error("AUTHOR_NOT_STRING")
            if len(author.strip()) < 1:
                self.error("AUTHOR_TOO_SHORT")
            if len(author) > 128:
                self.error("AUTHOR_TOO_LONG")

    def validate_icons(self, manifest_json):
        """
        Validate the `icons` property in the manifest:

        * Ensure that, if the icons property is present, a 128px icon is
          provided.
        * Ensure that each icon size is a valid integer.
        * Ensure that each icon file is valid by calling _validate_icon_file().
        """
        icons = manifest_json.get("icons", {})
        if icons:
            if "128" not in icons:
                self.error("ICONS_NO_128")
            for icon_size, icon_path in icons.iteritems():
                try:
                    icon_size = int(icon_size)
                    if icon_size <= 0:
                        raise ValueError
                except ValueError:
                    self.error("ICON_INVALID_SIZE", icon_size=unicode(icon_size))
                self._validate_icon_file(icon_path, icon_size)

    def _validate_icon_file(self, icon_path, icon_size):
        """
        Validate a specific icon path referenced in the manifest:

        * Ensure that the file exists in the zip.
        * Ensure that it is a valid PNG file.
        * Ensure that it is square.
        * Ensure that it has the claimed dimensions.
        """
        try:
            icon_contents = self.zipfile.extract_path(icon_path.lstrip("/"))
        except KeyError:
            self.error("ICON_DOES_NOT_EXIST", icon_path=icon_path)
        try:
            if imghdr.what(None, icon_contents) != "png":
                raise IOError
            image = Image.open(StringIO(icon_contents))
        except IOError:
            self.error("ICON_NOT_A_VALID_IMAGE_OR_PNG", icon_path=icon_path)
        if image.size[0] != image.size[1]:
            self.error("ICON_NOT_SQUARE", icon_path=icon_path)
        if icon_size != image.size[0]:
            self.error("ICON_INCORRECT_DIMENSIONS", icon_path=icon_path, icon_size=icon_size)
Beispiel #30
0
 def test_is_secure(self):
     zip = SafeUnzip(self.packaged_app_path('signed.zip'))
     zip.is_valid()
     assert zip.is_signed()
 def test_is_broken(self):
     zip = SafeUnzip(self.xpi_path('signed'))
     zip.is_valid()
     zip.info[2].filename = 'META-INF/foo.sf'
     assert not zip.is_signed()
Beispiel #32
0
 def test_unzip_not_fatal(self):
     zip = SafeUnzip(self.manifest_path('mozball.webapp'))
     assert not zip.is_valid(fatal=False)
Beispiel #33
0
 def test_unzip_limit(self):
     zip = SafeUnzip(self.xpi_path('langpack-localepicker'))
     self.assertRaises(forms.ValidationError, zip.is_valid)
Beispiel #34
0
 def test_not_secure(self):
     zip = SafeUnzip(self.packaged_app_path('mozball.zip'))
     zip.is_valid()
     assert not zip.is_signed()
Beispiel #35
0
class ExtensionValidator(object):
    """
    Firefox OS Add-on validator. If validation fails, will raise an instance of
    rest_framework.exceptions.ParseError containing information about the
    error.
    """
    errors = {
        'AUTHOR_NOT_STRING':
        _(u'The `author` property must be a string.'),
        'AUTHOR_TOO_LONG':
        _(u'The `author` property cannot be longer than 128 characters.'),
        'AUTHOR_TOO_SHORT':
        _(u'The `author` property must be at least 1 character'
          u' long and can not consist of only whitespace characters.'),
        'BAD_CONTENT_TYPE':
        _(u'The file sent has an unsupported content-type'),
        'DESCRIPTION_NOT_STRING':
        _(u'The `description` property must be a string.'),
        'DESCRIPTION_TOO_LONG':
        _(u'The `description` property cannot be '
          u'longer than 132 characters.'),
        'ICON_INCORRECT_DIMENSIONS':
        _(u'The icon file `%(icon_path)s` is not the specified dimensions '
          u' of %(icon_size)s x %(icon_size)s as defined in the manifest.'),
        'ICON_DOES_NOT_EXIST':
        _(u'The icon file `%(icon_path)s` is referenced in the manifest but'
          u' does not exist in the ZIP file.'),
        'ICONS_NO_128':
        _(u'If defining `icons`, you must include a 128x128 variant.'),
        'ICON_NOT_A_VALID_IMAGE_OR_PNG':
        _(u'The icon file `%s` is not a valid PNG.'),
        'ICON_NOT_SQUARE':
        _(u'The icon file `%(icon_path)s` is not square.'),
        'ICON_INVALID_SIZE':
        _(u'The manifest contains an invalid icon size: %(icon_size)s'),
        'INVALID_JSON':
        _(u"'manifest.json' in the archive is not a valid JSON"
          u" file."),
        'INVALID_JSON_ENCODING':
        _(u"'manifest.json' in the archive is not encoded in UTF-8."),
        'INVALID_ZIP':
        _(u'The file sent is not a valid ZIP file.'),
        'NAME_MISSING':
        _(u'There is no `name` property in the manifest.'),
        'NAME_NOT_STRING':
        _(u'The `name` property must be a string.'),
        'NAME_TOO_LONG':
        _(u'The `name` property cannot be longer than 45 '
          u'characters.'),
        'NAME_TOO_SHORT':
        _(u'The `name` property must be at least 1 character'
          u' long and can not consist of only whitespace characters.'),
        'NO_MANIFEST':
        _(u"The archive does not contain a 'manifest.json' "
          u"file."),
        'VERSION_MISSING':
        _(u'There is no `version` property in the manifest.'),
        'VERSION_NOT_STRING':
        _(u'The `version` property must be a string.'),
        'VERSION_INVALID':
        _(u'The `version` property must be a string'
          u' containing one to four dot-separated integers each between'
          u' 0 and 65535.'),
    }
    valid_content_types = (
        'application/octet-stream',
        'application/zip',
    )

    def __init__(self, file_obj=None):
        self.file_obj = file_obj
        self.zipfile = None

    def error(self, key, **kwargs):
        message = self.errors[key]
        if kwargs:
            message = self.errors[key] % kwargs
        raise ParseError(detail={
            'key': key,
            'message': message,
            'params': kwargs,
        })

    def validate(self):
        """
        Run the full validation suite against the uploaded file:

        * Ensure that it is a valid zip file.
        * Ensure that it contains a valid manifest.json file.
        * Validate the manifest fields against the spec.

        Return the manifest contents (as dict).
        """
        self.manifest = self.validate_file(self.file_obj)
        self.data = self.validate_json(self.manifest)
        self.validate_name(self.data)
        self.validate_description(self.data)
        self.validate_version(self.data)
        self.validate_author(self.data)
        self.validate_icons(self.data)
        return self.data

    def validate_file(self, file_obj):
        """
        Verify that the upload is a valid zip file that contains a
        manifest.json file.
        """
        if file_obj.content_type not in self.valid_content_types:
            self.error('BAD_CONTENT_TYPE')
        try:
            self.zipfile = SafeUnzip(file_obj)
            try:
                # Will throw ValidationError if necessary.
                self.zipfile.is_valid()
            except ValidationError as e:
                raise ParseError(unicode(e))
            except (BadZipfile, IOError):
                self.error('INVALID_ZIP')
            manifest = self.zipfile.extract_path('manifest.json')
        except KeyError:
            self.error('NO_MANIFEST')
        return manifest

    def validate_json(self, contents):
        """
        Verify that the enclosed manifest.json is a valid and parsable JSON
        file.
        """
        try:
            # We support only UTF-8 encoded manifests.
            decoded_data = smart_unicode(strip_bom(contents))
        except UnicodeDecodeError:
            self.error('INVALID_JSON_ENCODING')
        try:
            return json.loads(decoded_data)
        except ValueError:
            self.error('INVALID_JSON')

    def validate_name(self, manifest_json):
        """
        Ensure that the name property of the manifest exists and is a string
        between 1 and 45 characters long.

        In addition, even though it's allowed in the spec, we consider the name
        invalid (too short) if it contains only whitespace characters.

        https://developer.chrome.com/extensions/manifest/name
        """
        try:
            name = manifest_json['name']
        except KeyError:
            self.error('NAME_MISSING')
        if not isinstance(name, basestring):
            self.error('NAME_NOT_STRING')
        if len(name.strip()) < 1:
            self.error('NAME_TOO_SHORT')
        if len(name) > 45:
            self.error('NAME_TOO_LONG')

    def validate_description(self, manifest_json):
        """
        Ensures that, if present, the description property is no longer than
        132 characters.

        https://developer.chrome.com/extensions/manifest/description
        """
        if 'description' in manifest_json:
            description = manifest_json['description']
            if not isinstance(description, basestring):
                self.error('DESCRIPTION_NOT_STRING')
            if len(description.strip()) > 132:
                self.error('DESCRIPTION_TOO_LONG')

    def validate_version(self, manifest_json):
        """
        Ensure that the version property of the manifest exists and is a string
        containing one to four dot-separated integers, each between 0 and
        65535, with no leading zeros."""
        try:
            version = manifest_json['version']
        except KeyError:
            self.error('VERSION_MISSING')
        if not isinstance(version, basestring):
            self.error('VERSION_NOT_STRING')
        splitted = version.split('.')
        if len(splitted) > 4:
            # Too many dots.
            self.error('VERSION_INVALID')
        for version_component in splitted:
            try:
                number = int(version_component)
                if version_component.startswith('0') and number != 0:
                    # Leading zeros are forbidden.
                    self.error('VERSION_INVALID')
                if number < 0 or number > 65535:
                    # All numbers must be between 0 and 65535 inclusive.
                    self.error('VERSION_INVALID')
            except ValueError:
                # Not a valid number.
                self.error('VERSION_INVALID')

    def validate_author(self, manifest_json):
        """
        Ensures that, if present, the author property is no longer than
        128 characters.
        """
        if 'author' in manifest_json:
            author = manifest_json['author']
            # Author must not be empty/only whitespace if present, since we'll
            # use it as link text.
            if not isinstance(author, basestring):
                self.error('AUTHOR_NOT_STRING')
            if len(author.strip()) < 1:
                self.error('AUTHOR_TOO_SHORT')
            if len(author) > 128:
                self.error('AUTHOR_TOO_LONG')

    def validate_icons(self, manifest_json):
        """
        Validate the `icons` property in the manifest:

        * Ensure that, if the icons property is present, a 128px icon is
          provided.
        * Ensure that each icon size is a valid integer.
        * Ensure that each icon file is valid by calling _validate_icon_file().
        """
        icons = manifest_json.get('icons', {})
        if icons:
            if '128' not in icons:
                self.error('ICONS_NO_128')
            for icon_size, icon_path in icons.iteritems():
                try:
                    icon_size = int(icon_size)
                    if icon_size <= 0:
                        raise ValueError
                except ValueError:
                    self.error('ICON_INVALID_SIZE',
                               icon_size=unicode(icon_size))
                self._validate_icon_file(icon_path, icon_size)

    def _validate_icon_file(self, icon_path, icon_size):
        """
        Validate a specific icon path referenced in the manifest:

        * Ensure that the file exists in the zip.
        * Ensure that it is a valid PNG file.
        * Ensure that it is square.
        * Ensure that it has the claimed dimensions.
        """
        try:
            icon_contents = self.zipfile.extract_path(icon_path.lstrip('/'))
        except KeyError:
            self.error('ICON_DOES_NOT_EXIST', icon_path=icon_path)
        try:
            if imghdr.what(None, icon_contents) != 'png':
                raise IOError
            image = Image.open(StringIO(icon_contents))
        except IOError:
            self.error('ICON_NOT_A_VALID_IMAGE_OR_PNG', icon_path=icon_path)
        if image.size[0] != image.size[1]:
            self.error('ICON_NOT_SQUARE', icon_path=icon_path)
        if icon_size != image.size[0]:
            self.error('ICON_INCORRECT_DIMENSIONS',
                       icon_path=icon_path,
                       icon_size=icon_size)
Beispiel #36
0
 def test_unzip_total_file_size_limit(self):
     # There are no files over 100 kb in that zip, but the total is over.
     zip = SafeUnzip(self.packaged_app_path('full-tpa.zip'))
     self.assertRaises(forms.ValidationError, zip.is_valid)
Beispiel #37
0
class ExtensionValidator(object):
    """
    Firefox OS Add-on validator. If validation fails, will raise an instance of
    rest_framework.exceptions.ParseError containing information about the
    error.
    """
    errors = {
        'AUTHOR_NOT_STRING': _(u'The `author` property must be a string.'),
        'AUTHOR_TOO_LONG': _(
            u'The `author` property cannot be longer than 128 characters.'),
        'AUTHOR_TOO_SHORT': _(
            u'The `author` property must be at least 1 character'
            u' long and can not consist of only whitespace characters.'),
        'BAD_CONTENT_TYPE': _(
            u'The file sent has an unsupported content-type'),
        'DESCRIPTION_NOT_STRING': _(
            u'The `description` property must be a string.'),
        'DESCRIPTION_TOO_LONG': _(
            u'The `description` property cannot be '
            u'longer than 132 characters.'),
        'ICONS_DO_NOT_EXIST': _(u'Not all specified icons exist.'),
        'ICONS_NO_128': _(
            u'If defining `icons`, you must include a 128x128 variant.'),
        'ICONS_INVALID_FORMAT': _(u'Only PNG icons are permitted.'),
        'INVALID_JSON': _(
            u"'manifest.json' in the archive is not a valid JSON"
            u" file."),
        'INVALID_JSON_ENCODING': _(
            u"'manifest.json' in the archive is not encoded in UTF-8."),
        'INVALID_ZIP': _(u'The file sent is not a valid ZIP file.'),
        'NAME_MISSING': _(u'There is no `name` property in the manifest.'),
        'NAME_NOT_STRING': _(u'The `name` property must be a string.'),
        'NAME_TOO_LONG': _(
            u'The `name` property cannot be longer than 45 '
            u'characters.'),
        'NAME_TOO_SHORT': _(
            u'The `name` property must be at least 1 character'
            u' long and can not consist of only whitespace characters.'),
        'NO_MANIFEST': _(
            u"The archive does not contain a 'manifest.json' "
            u"file."),
        'VERSION_MISSING': _(
            u'There is no `version` property in the manifest.'),
        'VERSION_NOT_STRING': _(u'The `version` property must be a string.'),
        'VERSION_INVALID': _(
            u'The `version` property must be a string'
            u' containing one to four dot-separated integers each between'
            u' 0 and 65535.'),
    }
    valid_content_types = (
        'application/octet-stream',
        'application/zip',
    )

    def __init__(self, file_obj=None):
        self.file_obj = file_obj
        self.zipfile = None

    def error(self, error_key):
        raise ParseError(detail={
            'key': error_key,
            'message': self.errors[error_key],
        })

    def validate(self):
        """
        Run the full validation suite against the uploaded file:

        * Ensure that it is a valid zip file.
        * Ensure that it contains a valid manifest.json file.
        * Validate the manifest fields against the spec.

        Return the manifest contents (as dict).
        """
        self.manifest = self.validate_file(self.file_obj)
        self.data = self.validate_json(self.manifest)
        self.validate_name(self.data)
        self.validate_description(self.data)
        self.validate_version(self.data)
        self.validate_author(self.data)
        self.validate_icons(self.data)
        # self.validate_icon_files(self.data)
        return self.data

    def validate_file(self, file_obj):
        """
        Verify that the upload is a valid zip file that contains a
        manifest.json file.
        """
        if file_obj.content_type not in self.valid_content_types:
            self.error('BAD_CONTENT_TYPE')
        try:
            self.zipfile = SafeUnzip(file_obj)
            try:
                # Will throw ValidationError if necessary.
                self.zipfile.is_valid()
            except ValidationError as e:
                raise ParseError(unicode(e))
            except (BadZipfile, IOError):
                self.error('INVALID_ZIP')
            manifest = self.zipfile.extract_path('manifest.json')
        except KeyError:
            self.error('NO_MANIFEST')
        return manifest

    def validate_json(self, contents):
        """
        Verify that the enclosed manifest.json is a valid and parsable JSON
        file.
        """
        try:
            # We support only UTF-8 encoded manifests.
            decoded_data = smart_unicode(strip_bom(contents))
        except UnicodeDecodeError:
            self.error('INVALID_JSON_ENCODING')
        try:
            return json.loads(decoded_data)
        except ValueError:
            self.error('INVALID_JSON')

    def validate_name(self, manifest_json):
        """
        Ensure that the name property of the manifest exists and is a string
        between 1 and 45 characters long.

        In addition, even though it's allowed in the spec, we consider the name
        invalid (too short) if it contains only whitespace characters.

        https://developer.chrome.com/extensions/manifest/name
        """
        try:
            name = manifest_json['name']
        except KeyError:
            self.error('NAME_MISSING')
        if not isinstance(name, basestring):
            self.error('NAME_NOT_STRING')
        if len(name.strip()) < 1:
            self.error('NAME_TOO_SHORT')
        if len(name) > 45:
            self.error('NAME_TOO_LONG')

    def validate_description(self, manifest_json):
        """
        Ensures that, if present, the description property is no longer than
        132 characters.

        https://developer.chrome.com/extensions/manifest/description
        """
        if 'description' in manifest_json:
            description = manifest_json['description']
            if not isinstance(description, basestring):
                self.error('DESCRIPTION_NOT_STRING')
            if len(description.strip()) > 132:
                self.error('DESCRIPTION_TOO_LONG')

    def validate_version(self, manifest_json):
        """
        Ensure that the version property of the manifest exists and is a string
        containing one to four dot-separated integers, each between 0 and
        65535, with no leading zeros."""
        try:
            version = manifest_json['version']
        except KeyError:
            self.error('VERSION_MISSING')
        if not isinstance(version, basestring):
            self.error('VERSION_NOT_STRING')
        splitted = version.split('.')
        if len(splitted) > 4:
            # Too many dots.
            self.error('VERSION_INVALID')
        for version_component in splitted:
            try:
                number = int(version_component)
                if version_component.startswith('0') and number != 0:
                    # Leading zeros are forbidden.
                    self.error('VERSION_INVALID')
                if number < 0 or number > 65535:
                    # All numbers must be between 0 and 65535 inclusive.
                    self.error('VERSION_INVALID')
            except ValueError:
                # Not a valid number.
                self.error('VERSION_INVALID')

    def validate_author(self, manifest_json):
        """
        Ensures that, if present, the author property is no longer than
        128 characters.
        """
        if 'author' in manifest_json:
            author = manifest_json['author']
            # Author must not be empty/only whitespace if present, since we'll
            # use it as link text.
            if not isinstance(author, basestring):
                self.error('AUTHOR_NOT_STRING')
            if len(author.strip()) < 1:
                self.error('AUTHOR_TOO_SHORT')
            if len(author) > 128:
                self.error('AUTHOR_TOO_LONG')

    def validate_icons(self, manifest_json):
        """
        Validate the `icons` property in the manifest:

        * Ensure that, if the icons property is present, a 128px icon is
          provided.
        * Ensure that each icon referenced has a .png extension.
        """
        icons = manifest_json.get('icons', {})
        if icons:
            if '128' not in icons:
                self.error('ICONS_NO_128')
            for size, path in icons.iteritems():
                if not path.endswith('.png'):
                    self.error('ICONS_INVALID_FORMAT')

    def validate_icon_files(self, manifest_json):
        """
        In turn, validate each icon file referenced by the `icons` property of
        the manifest:

        * Ensure that the file exists in the zip.
        * Ensure that it is a valid PNG file.
        * Ensure that it has the claimed dimensions.
        * Ensure that it is square.
        """
        icons = manifest_json.get('icons', {})
        return icons