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
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, })
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
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)
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
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')
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
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)
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)
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')
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')
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