diff --git a/apps/amo/tests/__init__.py b/apps/amo/tests/__init__.py index 95a32ecf6b..b713b60e2f 100644 --- a/apps/amo/tests/__init__.py +++ b/apps/amo/tests/__init__.py @@ -300,6 +300,10 @@ class AMOPaths(object): path = 'mkt/webapps/tests/sample.key' return os.path.join(settings.ROOT, path) + def mozball_image(self): + return os.path.join(settings.ROOT, + 'mkt/developers/tests/addons/mozball-128.png') + def close_to_now(dt): """ diff --git a/mkt/api/authentication.py b/mkt/api/authentication.py index cb2909ef89..f357489704 100644 --- a/mkt/api/authentication.py +++ b/mkt/api/authentication.py @@ -96,25 +96,31 @@ def initialize_oauth_server_request(request): """ # Since 'Authorization' header comes through as 'HTTP_AUTHORIZATION', - # convert it back + # convert it back. auth_header = {} if 'HTTP_AUTHORIZATION' in request.META: auth_header = {'Authorization': request.META.get('HTTP_AUTHORIZATION')} url = urljoin(settings.SITE_URL, request.path) + # Note: we are only signing using the QUERY STRING. We are not signing the + # body yet. According to the spec we should be including an oauth_body_hash + # as per: + # + # http://oauth.googlecode.com/svn/spec/ext/body_hash/1.0/drafts/1/spec.html + # + # There is no support in python-oauth2 for this yet. There is an + # outstanding pull request for this: + # + # https://github.com/simplegeo/python-oauth2/pull/110 + # + # Or time to move to a better OAuth implementation. oauth_request = oauth2.Request.from_request( - request.method, url, headers=auth_header) - - if oauth_request: - oauth_server = oauth2.Server(signature_methods={ - # Supported signature methods - 'HMAC-SHA1': oauth2.SignatureMethod_HMAC_SHA1() - }) - - else: - oauth_server = None - + request.method, url, headers=auth_header, + query_string=request.META['QUERY_STRING']) + oauth_server = oauth2.Server(signature_methods={ + 'HMAC-SHA1': oauth2.SignatureMethod_HMAC_SHA1() + }) return oauth_server, oauth_request diff --git a/mkt/api/base.py b/mkt/api/base.py index 8101002529..367bbab092 100644 --- a/mkt/api/base.py +++ b/mkt/api/base.py @@ -1,13 +1,14 @@ import json +from django.core.exceptions import ObjectDoesNotExist + from tastypie import http from tastypie.bundle import Bundle -from tastypie.exceptions import ImmediateHttpResponse +from tastypie.exceptions import ImmediateHttpResponse, NotFound from tastypie.resources import ModelResource from translations.fields import PurifiedField, TranslatedField - class MarketplaceResource(ModelResource): def get_resource_uri(self, bundle_or_obj): @@ -49,3 +50,25 @@ class MarketplaceResource(ModelResource): response = http.HttpBadRequest(json.dumps({'error_message': errors}), content_type='application/json') return ImmediateHttpResponse(response=response) + + def get_object_or_404(self, cls, **filters): + """ + A wrapper around our more familiar get_object_or_404, for when we need + to get access to an object that isn't covered by get_obj. + """ + if not filters: + raise ImmediateHttpResponse(response=http.HttpNotFound()) + try: + return cls.objects.get(**filters) + except (cls.DoesNotExist, cls.MultipleObjectsReturned): + raise ImmediateHttpResponse(response=http.HttpNotFound()) + + def get_by_resource_or_404(self, request, **kwargs): + """ + A wrapper around the obj_get to just get the object. + """ + try: + obj = self.obj_get(request, **kwargs) + except ObjectDoesNotExist: + raise ImmediateHttpResponse(response=http.HttpNotFound()) + return obj diff --git a/mkt/api/forms.py b/mkt/api/forms.py index 8353847b35..cd13082d87 100644 --- a/mkt/api/forms.py +++ b/mkt/api/forms.py @@ -1,8 +1,13 @@ +import base64 +import json +import StringIO + from django import forms import happyforms from files.models import FileUpload +from mkt.developers.utils import check_upload class UploadForm(happyforms.Form): @@ -19,3 +24,41 @@ class UploadForm(happyforms.Form): self.obj = upload return uuid + + +class JSONField(forms.Field): + def to_python(self, value): + if value == '': + return None + + try: + if isinstance(value, basestring): + return json.loads(value) + except ValueError: + pass + return value + + +class PreviewJSONForm(happyforms.Form): + file = JSONField(required=True) + position = forms.IntegerField(required=True) + + def clean_file(self): + file_ = self.cleaned_data.get('file', {}) + try: + if not set(['data', 'type']).issubset(set(file_.keys())): + raise forms.ValidationError('Type and data are required.') + except AttributeError: + raise forms.ValidationError('File must be a dictionary.') + + file_obj = StringIO.StringIO(base64.b64decode(file_['data'])) + errors, hash_ = check_upload(file_obj, 'image', file_['type']) + if errors: + raise forms.ValidationError(errors) + + self.hash_ = hash_ + return file_ + + def clean(self): + self.cleaned_data['upload_hash'] = getattr(self, 'hash_', None) + return self.cleaned_data diff --git a/mkt/api/resources.py b/mkt/api/resources.py index 4553005ffe..928da531d2 100644 --- a/mkt/api/resources.py +++ b/mkt/api/resources.py @@ -5,10 +5,12 @@ from django.db import transaction import commonware.log from tastypie import http from tastypie.exceptions import ImmediateHttpResponse +from tastypie import fields from tastypie.serializers import Serializer +from tastypie.resources import ALL_WITH_RELATIONS from addons.forms import CategoryFormSet, DeviceTypeForm -from addons.models import AddonUser, Category, DeviceType +from addons.models import AddonUser, Category, DeviceType, Preview import amo from amo.decorators import write from amo.utils import no_translation @@ -19,10 +21,11 @@ from mkt.api.base import MarketplaceResource from mkt.api.forms import UploadForm from mkt.developers import tasks from mkt.developers.forms import NewManifestForm +from mkt.developers.forms import PreviewForm +from mkt.api.forms import PreviewJSONForm from mkt.webapps.models import Webapp from mkt.submit.forms import AppDetailsBasicForm - log = commonware.log.getLogger('z.api') @@ -64,7 +67,7 @@ class ValidationResource(MarketplaceResource): raise ImmediateHttpResponse(response=http.HttpNotFound()) if not OwnerAuthorization().is_authorized(request, object=obj): - raise ImmediateHttpResponse(response=http.HttpUnauthorized()) + raise ImmediateHttpResponse(response=http.HttpForbidden()) log.info('Validation retreived: %s' % obj.pk) return obj @@ -105,7 +108,7 @@ class AppResource(MarketplaceResource): if not (OwnerAuthorization() .is_authorized(request, object=form.obj)): - raise ImmediateHttpResponse(response=http.HttpUnauthorized()) + raise ImmediateHttpResponse(response=http.HttpForbidden()) plats = [Platform.objects.get(id=amo.PLATFORM_ALL.id)] @@ -119,7 +122,7 @@ class AppResource(MarketplaceResource): def obj_get(self, request=None, **kwargs): obj = super(AppResource, self).obj_get(request=request, **kwargs) if not AppOwnerAuthorization().is_authorized(request, object=obj): - raise ImmediateHttpResponse(response=http.HttpUnauthorized()) + raise ImmediateHttpResponse(response=http.HttpForbidden()) log.info('App retreived: %s' % obj.pk) return obj @@ -148,7 +151,7 @@ class AppResource(MarketplaceResource): raise ImmediateHttpResponse(response=http.HttpNotFound()) if not AppOwnerAuthorization().is_authorized(request, object=obj): - raise ImmediateHttpResponse(response=http.HttpUnauthorized()) + raise ImmediateHttpResponse(response=http.HttpForbidden()) data['slug'] = data.get('slug', obj.app_slug) data.update(self.formset(data)) @@ -191,3 +194,62 @@ class CategoryResource(MarketplaceResource): always_return_data = True resource_name = 'category' serializer = Serializer(formats=['json']) + + +class PreviewResource(MarketplaceResource): + addon = fields.ForeignKey(AppResource, 'addon') + + class Meta: + queryset = Preview.objects.all() + list_allowed_methods = ['post'] + allowed_methods = ['get', 'delete'] + always_return_data = True + fields = ['id'] + authentication = MarketplaceAuthentication() + authorization = OwnerAuthorization() + resource_name = 'preview' + filtering = {'addon': ALL_WITH_RELATIONS} + + def obj_create(self, bundle, request, **kwargs): + filters = self.build_filters(filters=request.GET.copy()) + addon = self.get_object_or_404(Webapp, + pk=filters.get('addon__exact')) + if not AppOwnerAuthorization().is_authorized(request, object=addon): + raise ImmediateHttpResponse(response=http.HttpForbidden()) + + data_form = PreviewJSONForm(bundle.data) + if not data_form.is_valid(): + raise self.form_errors(data_form) + + form = PreviewForm(data_form.cleaned_data) + if not form.is_valid(): + raise self.form_errors(form) + + form.save(addon) + bundle.obj = form.instance + log.info('Preview created: %s' % bundle.obj.pk) + return bundle + + def obj_delete(self, request, **kwargs): + obj = self.get_by_resource_or_404(request, **kwargs) + if not AppOwnerAuthorization().is_authorized(request, + object=obj.addon): + raise ImmediateHttpResponse(response=http.HttpForbidden()) + + log.info('Preview deleted: %s' % obj.pk) + return super(PreviewResource, self).obj_delete(request, **kwargs) + + def obj_get(self, request=None, **kwargs): + obj = super(PreviewResource, self).obj_get(request=request, **kwargs) + if not AppOwnerAuthorization().is_authorized(request, + object=obj.addon): + raise ImmediateHttpResponse(response=http.HttpForbidden()) + + log.info('Preview retreived: %s' % obj.pk) + return obj + + def dehydrate(self, bundle): + # Returning an image back to the user isn't useful, let's stop that. + if 'file' in bundle.data: + del bundle.data['file'] + return bundle diff --git a/mkt/api/tests/test_forms.py b/mkt/api/tests/test_forms.py new file mode 100644 index 0000000000..3b98ff9887 --- /dev/null +++ b/mkt/api/tests/test_forms.py @@ -0,0 +1,48 @@ +import base64 + +from nose.tools import eq_ + +import amo +import amo.tests +from mkt.api.forms import PreviewJSONForm + + +class TestPreviewForm(amo.tests.TestCase, amo.tests.AMOPaths): + + def setUp(self): + self.file = base64.b64encode(open(self.mozball_image(), 'r').read()) + + def test_bad_type(self): + form = PreviewJSONForm({'file': {'data': self.file, 'type': 'wtf?'}, + 'position': 1}) + assert not form.is_valid() + eq_(form.errors['file'], ['Images must be either PNG or JPG.']) + + def test_bad_file(self): + file_ = base64.b64encode(open(self.xpi_path('langpack'), 'r').read()) + form = PreviewJSONForm({'file': {'data': file_, 'type': 'image/png'}, + 'position': 1}) + assert not form.is_valid() + eq_(form.errors['file'], ['Images must be either PNG or JPG.']) + + def test_position_missing(self): + form = PreviewJSONForm({'file': {'data': self.file, + 'type': 'image/jpg'}}) + assert not form.is_valid() + eq_(form.errors['position'], ['This field is required.']) + + def test_preview(self): + form = PreviewJSONForm({'file': {'type': '', 'data': ''}, + 'position': 1}) + assert not form.is_valid() + eq_(form.errors['file'], ['Images must be either PNG or JPG.']) + + def test_not_json(self): + form = PreviewJSONForm({'file': 1, 'position': 1}) + assert not form.is_valid() + eq_(form.errors['file'], ['File must be a dictionary.']) + + def test_not_file(self): + form = PreviewJSONForm({'position': 1}) + assert not form.is_valid() + eq_(form.errors['file'], ['This field is required.']) diff --git a/mkt/api/tests/test_handlers.py b/mkt/api/tests/test_handlers.py index 23c6995960..9e9498963b 100644 --- a/mkt/api/tests/test_handlers.py +++ b/mkt/api/tests/test_handlers.py @@ -1,3 +1,4 @@ +import base64 import json import tempfile @@ -6,7 +7,7 @@ from django.conf import settings from mock import patch from nose.tools import eq_ -from addons.models import Addon, Category, DeviceType +from addons.models import Addon, AddonUser, Category, DeviceType import amo from amo.tests import AMOPaths from files.models import FileUpload @@ -92,7 +93,7 @@ class TestGetValidationHandler(ValidationHandler): obj = self.create() obj.update(user=UserProfile.objects.get(email='admin@mozilla.com')) res = self.client.get(self.get_url) - eq_(res.status_code, 401) + eq_(res.status_code, 403) def test_not_found(self): url = ('api_dispatch_detail', @@ -150,9 +151,6 @@ class CreateHandler(BaseOAuth): return FileUpload.objects.create(user=self.user, path=self.file, name=self.file, valid=True) - def get_error(self, response): - return json.loads(response.content)['error_message'] - @patch.object(settings, 'SITE_URL', 'http://api/') class TestAppCreateHandler(CreateHandler, AMOPaths): @@ -188,7 +186,7 @@ class TestAppCreateHandler(CreateHandler, AMOPaths): obj.update(user=UserProfile.objects.get(email='admin@mozilla.com')) res = self.client.post(self.list_url, data=json.dumps({'manifest': obj.uuid})) - eq_(res.status_code, 401) + eq_(res.status_code, 403) eq_(self.count(), 0) def test_create(self): @@ -225,7 +223,7 @@ class TestAppCreateHandler(CreateHandler, AMOPaths): obj = self.create_app() obj.authors.clear() res = self.client.get(self.get_url) - eq_(res.status_code, 401) + eq_(res.status_code, 403) def base_data(self): return {'support_email': 'a@a.com', @@ -299,7 +297,7 @@ class TestAppCreateHandler(CreateHandler, AMOPaths): obj = self.create_app() obj.authors.clear() res = self.client.put(self.get_url, data='{}') - eq_(res.status_code, 401) + eq_(res.status_code, 403) def test_put_not_there(self): url = ('api_dispatch_detail', {'resource_name': 'app', 'pk': 123}) @@ -356,3 +354,88 @@ class TestCategoryHandler(BaseOAuth): {'resource_name': 'category', 'pk': self.other.pk})) eq_(res.status_code, 404) + + +@patch.object(settings, 'SITE_URL', 'http://api/') +class TestPreviewHandler(BaseOAuth, AMOPaths): + fixtures = ['base/users', 'base/user_2519', 'webapps/337141-steamcube'] + + def setUp(self): + super(TestPreviewHandler, self).setUp() + self.app = Webapp.objects.get(pk=337141) + self.user = UserProfile.objects.get(pk=2519) + AddonUser.objects.create(user=self.user, addon=self.app) + self.file = base64.b64encode(open(self.mozball_image(), 'r').read()) + self.list_url = ('api_dispatch_list', {'resource_name': 'preview'}, + {'addon__exact': self.app.pk}) + self.good = {'file': {'data': self.file, 'type': 'image/jpg'}, + 'position': 1} + + def test_no_addon(self): + list_url = ('api_dispatch_list', {'resource_name': 'preview'}) + res = self.client.post(list_url, data=json.dumps(self.good)) + eq_(res.status_code, 404) + + def test_post_preview(self): + res = self.client.post(self.list_url, data=json.dumps(self.good)) + eq_(res.status_code, 201) + previews = self.app.previews + eq_(previews.count(), 1) + eq_(previews.all()[0].position, 1) + + def test_not_mine(self): + self.app.authors.clear() + res = self.client.post(self.list_url, data=json.dumps(self.good)) + eq_(res.status_code, 403) + + def test_position_missing(self): + data = {'file': {'data': self.file, 'type': 'image/jpg'}} + res = self.client.post(self.list_url, data=json.dumps(data)) + eq_(res.status_code, 400) + eq_(self.get_error(res)['position'], ['This field is required.']) + + def test_preview_missing(self): + res = self.client.post(self.list_url, data=json.dumps({})) + eq_(res.status_code, 400) + eq_(self.get_error(res)['position'], ['This field is required.']) + + def create(self): + self.client.post(self.list_url, data=json.dumps(self.good)) + self.preview = self.app.previews.all()[0] + self.get_url = ('api_dispatch_detail', + {'resource_name': 'preview', 'pk': self.preview.pk}) + + def test_delete(self): + self.create() + res = self.client.delete(self.get_url) + eq_(res.status_code, 204) + eq_(self.app.previews.count(), 0) + + def test_delete_not_mine(self): + self.create() + self.app.authors.clear() + res = self.client.delete(self.get_url) + eq_(res.status_code, 403) + + def test_delete_not_there(self): + self.get_url = ('api_dispatch_detail', + {'resource_name': 'preview', 'pk': 123}) + res = self.client.delete(self.get_url) + eq_(res.status_code, 404) + + def test_get(self): + self.create() + res = self.client.get(self.get_url) + eq_(res.status_code, 200) + + def test_get_not_mine(self): + self.create() + self.app.authors.clear() + res = self.client.get(self.get_url) + eq_(res.status_code, 403) + + def test_get_not_there(self): + self.get_url = ('api_dispatch_detail', + {'resource_name': 'preview', 'pk': 123}) + res = self.client.get(self.get_url) + eq_(res.status_code, 404) diff --git a/mkt/api/tests/test_oauth.py b/mkt/api/tests/test_oauth.py index 5dfa31b71c..5ba765130c 100644 --- a/mkt/api/tests/test_oauth.py +++ b/mkt/api/tests/test_oauth.py @@ -19,7 +19,7 @@ With headers: oauth_nonce="1008F707-37E6-4ABF-8322-C6B658771D88", oauth_version="1.0" """ -import time +import json import urllib import urlparse @@ -38,16 +38,6 @@ from amo.urlresolvers import reverse from files.models import FileUpload -def _get_args(consumer): - return dict( - oauth_consumer_key=consumer.key, - oauth_nonce=oauth.generate_nonce(), - oauth_signature_method='HMAC-SHA1', - oauth_timestamp=int(time.time()), - oauth_version='1.0', - ) - - def get_absolute_url(url): # TODO (andym): make this more standard. url[1]['api_name'] = 'apps' @@ -58,13 +48,6 @@ def get_absolute_url(url): return res -def data_keys(d): - # Form keys and values MUST be part of the signature. - # File keys MUST be part of the signature. - # But file values MUST NOT be included as part of the signature. - return dict([k, '' if isinstance(v, file) else v] for k, v in d.items()) - - class OAuthClient(Client): """ OAuthClient can do all the requests the Django test client, @@ -80,8 +63,15 @@ class OAuthClient(Client): def header(self, method, url): if not self.consumer: return None - req = oauth.Request(method=method, url=url, - parameters=_get_args(self.consumer)) + + parsed = urlparse.urlparse(url) + args = dict(urlparse.parse_qs(parsed.query)) + + req = oauth.Request.from_consumer_and_token(self.consumer, + token=None, http_method=method, + http_url=urlparse.urlunparse(parsed._replace(query='')), + parameters=args) + req.sign_request(self.signature_method, self.consumer, None) return req.to_header()['Authorization'] @@ -152,8 +142,11 @@ class BaseOAuth(TestCase): if verb in allowed: continue res = getattr(self.client, verb)(url) - assert (res.status_code in (401, 405), - '%s: %s not 401 or 405' % (verb.upper(), res.status_code)) + assert res.status_code in (401, 405), ( + '%s: %s not 401 or 405' % (verb.upper(), res.status_code)) + + def get_error(self, response): + return json.loads(response.content)['error_message'] @patch.object(settings, 'SITE_URL', 'http://api/') diff --git a/mkt/api/urls.py b/mkt/api/urls.py index 67c3680030..0e15e567fe 100644 --- a/mkt/api/urls.py +++ b/mkt/api/urls.py @@ -1,14 +1,16 @@ from django.conf.urls.defaults import include, patterns, url from tastypie.api import Api -from mkt.api.resources import AppResource, CategoryResource, ValidationResource from mkt.search.api import SearchResource +from mkt.api.resources import (AppResource, CategoryResource, + PreviewResource, ValidationResource) api = Api(api_name='apps') api.register(ValidationResource()) api.register(AppResource()) api.register(CategoryResource()) api.register(SearchResource()) +api.register(PreviewResource()) urlpatterns = patterns('', url(r'^', include(api.urls)), diff --git a/mkt/developers/utils.py b/mkt/developers/utils.py new file mode 100644 index 0000000000..8e89abc6cb --- /dev/null +++ b/mkt/developers/utils.py @@ -0,0 +1,65 @@ +import os +import uuid + +from django.conf import settings +from django.core.files.storage import default_storage as storage +from django.template.defaultfilters import filesizeformat + +from tower import ugettext as _ +import waffle + +import amo +from lib.video import library as video_library + + +def check_upload(file_obj, upload_type, content_type): + errors = [] + upload_hash = '' + is_icon = upload_type == 'icon' + is_video = (content_type in amo.VIDEO_TYPES and + waffle.switch_is_active('video-upload')) + + # By pushing the type onto the instance hash, we can easily see what + # to do with the file later. + ext = content_type.replace('/', '-') + upload_hash = '%s.%s' % (uuid.uuid4().hex, ext) + loc = os.path.join(settings.TMP_PATH, upload_type, upload_hash) + + with storage.open(loc, 'wb') as fd: + for chunk in file_obj: + fd.write(chunk) + + if is_video: + if not video_library: + errors.append(_('Video support not enabled.')) + else: + video = video_library(loc) + video.get_meta() + if not video.is_valid(): + errors.extend(video.errors) + + else: + check = amo.utils.ImageCheck(file_obj) + if (not check.is_image() or + content_type not in amo.IMG_TYPES): + if is_icon: + errors.append(_('Icons must be either PNG or JPG.')) + else: + errors.append(_('Images must be either PNG or JPG.')) + + if check.is_animated(): + if is_icon: + errors.append(_('Icons cannot be animated.')) + else: + errors.append(_('Images cannot be animated.')) + + max_size = (settings.MAX_ICON_UPLOAD_SIZE if is_icon else + settings.MAX_VIDEO_UPLOAD_SIZE if is_video else None) + + if max_size and file_obj.size > max_size: + if is_icon or is_video: + errors.append(_('Please use files smaller than %dMB.') % + filesizeformat(max_size)) + + return errors, upload_hash + diff --git a/mkt/developers/views.py b/mkt/developers/views.py index 46cd9e7b87..62e5963e03 100644 --- a/mkt/developers/views.py +++ b/mkt/developers/views.py @@ -2,10 +2,8 @@ import json import os import sys import traceback -import uuid from django import http -from django.core.files.storage import default_storage as storage from django.conf import settings from django import forms as django_forms from django.db import models, transaction @@ -26,7 +24,6 @@ import amo import amo.utils from amo import messages from amo.decorators import json_view, login_required, post_required, write -from amo.helpers import loc from amo.utils import escape_all from amo.urlresolvers import reverse from addons import forms as addon_forms @@ -34,7 +31,6 @@ from addons.decorators import can_become_premium from addons.models import Addon, AddonUser from addons.views import BaseFilter from devhub.models import AppLog -from lib.video import library as video_library from files.models import File, FileUpload from files.utils import parse_addon from market.models import AddonPaymentData, AddonPremium, Refund @@ -51,7 +47,8 @@ from mkt.developers.decorators import dev_required from mkt.developers.forms import (AppFormBasic, AppFormDetails, AppFormMedia, AppFormSupport, CurrencyForm, InappConfigForm, PaypalSetupForm, - PreviewForm, PreviewFormSet, trap_duplicate) + PreviewFormSet, trap_duplicate) +from mkt.developers.utils import check_upload from mkt.inapp_pay.models import InappConfig from mkt.webapps.tasks import update_manifests from mkt.webapps.models import Webapp @@ -943,52 +940,10 @@ def ajax_upload_media(request, upload_type): if 'upload_image' in request.FILES: upload_preview = request.FILES['upload_image'] upload_preview.seek(0) + content_type = upload_preview.content_type + errors, upload_hash = check_upload(upload_preview, upload_type, + content_type) - is_icon = upload_type == 'icon' - is_video = (upload_preview.content_type in amo.VIDEO_TYPES and - waffle.switch_is_active('video-upload')) - - # By pushing the type onto the instance hash, we can easily see what - # to do with the file later. - ext = upload_preview.content_type.replace('/', '-') - upload_hash = '%s.%s' % (uuid.uuid4().hex, ext) - loc = os.path.join(settings.TMP_PATH, upload_type, upload_hash) - - with storage.open(loc, 'wb') as fd: - for chunk in upload_preview: - fd.write(chunk) - - if is_video: - if not video_library: - errors.append(_('Video support not enabled.')) - else: - video = video_library(loc) - video.get_meta() - if not video.is_valid(): - errors.extend(video.errors) - - else: - check = amo.utils.ImageCheck(upload_preview) - if (not check.is_image() or - upload_preview.content_type not in amo.IMG_TYPES): - if is_icon: - errors.append(_('Icons must be either PNG or JPG.')) - else: - errors.append(_('Images must be either PNG or JPG.')) - - if check.is_animated(): - if is_icon: - errors.append(_('Icons cannot be animated.')) - else: - errors.append(_('Images cannot be animated.')) - - max_size = (settings.MAX_ICON_UPLOAD_SIZE if is_icon else - settings.MAX_VIDEO_UPLOAD_SIZE if is_video else None) - - if max_size and upload_preview.size > max_size: - if is_icon or is_video: - errors.append(_('Please use files smaller than %dMB.') % ( - max_size / 1024 / 1024 - 1)) else: errors.append(_('There was an error uploading your preview.'))