Bug 622602 - Unicode blocks screenshot saving

This commit is contained in:
Gregory Koberger 2011-01-11 15:53:58 -08:00
Родитель 1e23cfef7f
Коммит d9550764eb
8 изменённых файлов: 223 добавлений и 168 удалений

Просмотреть файл

@ -6,6 +6,7 @@ from django.conf import settings
from django.forms.formsets import BaseFormSet, formset_factory
import happyforms
import path
from tower import ugettext as _, ungettext as ngettext
import amo
@ -217,57 +218,28 @@ def icons():
class AddonFormMedia(AddonFormBase):
icon_upload = forms.FileField(required=False)
icon_type = forms.CharField(widget=forms.RadioSelect(
renderer=IconWidgetRenderer, choices=icons()), required=False)
icon_upload_hash = forms.CharField(required=False)
class Meta:
model = Addon
fields = ('icon_upload', 'icon_type')
fields = ('icon_upload_hash', 'icon_type')
def save(self, addon, commit=True):
if 'icon_upload' in self.request.FILES:
icon = self.request.FILES['icon_upload']
icon.seek(0)
if self.cleaned_data['icon_upload_hash']:
upload_hash = self.cleaned_data['icon_upload_hash']
upload_path = path.path(settings.TMP_PATH) / 'icon' / upload_hash
dirname = addon.get_icon_dir()
tmp_destination = os.path.join(dirname, '%s-temp' % addon.id)
if not os.path.exists(dirname):
os.makedirs(dirname)
with open(tmp_destination, 'wb+') as icon_file:
for chunk in icon.read():
icon_file.write(chunk)
destination = os.path.join(dirname, '%s' % addon.id)
remove_icons(destination)
tasks.resize_icon.delay(tmp_destination, destination,
tasks.resize_icon.delay(upload_path, destination,
amo.ADDON_ICON_SIZES)
return super(AddonFormMedia, self).save(commit)
def clean_icon_upload(self):
icon = self.cleaned_data['icon_upload']
if not icon:
return
check = ImageCheck(icon)
if (not check.is_image() or
icon.content_type not in ('image/png', 'image/jpeg', 'image/jpg')):
raise forms.ValidationError(_('Icons must be either PNG or JPG.'))
if check.is_animated():
raise forms.ValidationError(_('Icons cannot be animated.'))
if icon.size > settings.MAX_ICON_UPLOAD_SIZE:
raise forms.ValidationError(
_('Please use images smaller than %dMB.' %
(settings.MAX_ICON_UPLOAD_SIZE / 1024 / 1024 - 1)))
return icon
class AddonFormDetails(AddonFormBase):
default_locale = forms.TypedChoiceField(choices=Addon.LOCALES)

Просмотреть файл

@ -497,7 +497,7 @@ class PreviewForm(happyforms.ModelForm):
if self.cleaned_data['upload_hash']:
upload_hash = self.cleaned_data['upload_hash']
settings_path = settings.PREVIEWS_PATH
upload_path = path.path(settings_path) / 'temp' / upload_hash
upload_path = path.path(settings.TMP_PATH) / 'preview' / upload_hash
tasks.resize_preview.delay(str(upload_path),
self.instance.thumbnail_path,

Просмотреть файл

@ -21,6 +21,10 @@
<div class="icon_preview" id="icon_preview_32">
<img src="{{ addon.icon_url }}">
</div>
<div class="js-hidden">
{{ form.icon_upload_hash|safe }}
</div>
<div class="edit-media-details">
{# L10n: The size of the icon #}
{{ _('32x32px') }}
@ -43,14 +47,12 @@
</ul>
<div class="invisible-upload">
<a class="button" href="#">{{ _('Upload a Custom Icon...') }}</a>
{{ form.icon_upload|safe }}
</div>
<div class="edit-media-details">
{% trans %}
PNG and JPG supported. Icons resized to 64x64 pixels if larger.
{% endtrans %}
</div>
{{ form.icon_upload.errors|safe }}
<ul class="errorlist">
<li id="edit-icon-error"></li>
</ul>

Просмотреть файл

@ -51,14 +51,17 @@
</ul>
<div class="invisible-upload">
<a class="button" href="#">{{ _('Upload a Custom Icon...') }}</a>
{{ form.icon_upload|safe }}
<input type="file" id="id_icon_upload" name="icon_upload"
data-upload-url="{{ url('devhub.addons.upload_icon', addon.slug) }}">
</div>
<div class="js-hidden">
{{ form.icon_upload_hash|safe }}
</div>
<div class="edit-media-details">
{% trans %}
PNG and JPG supported. Icons resized to 64x64 pixels if larger.
{% endtrans %}
</div>
{{ form.icon_upload.errors|safe }}
<ul class="errorlist">
<li id="edit-icon-error"></li>
</ul>

Просмотреть файл

@ -986,6 +986,10 @@ class TestEdit(test_utils.TestCase):
self.basic_url = self.get_url('basic', True)
ctx = self.client.get(self.basic_url).context['cat_form']
self.cat_initial = initial(ctx.initial_forms[0])
self.preview_upload = reverse('devhub.addons.upload_preview',
args=[self.addon.slug])
self.icon_upload = reverse('devhub.addons.upload_icon',
args=[self.addon.slug])
def tearDown(self):
reset_redis(self._redis)
@ -1448,7 +1452,6 @@ class TestEdit(test_utils.TestCase):
for k in data:
eq_(unicode(getattr(addon, k)), data[k])
def test_edit_media_preuploadedicon(self):
data = dict(icon_type='icon/appearance')
data_formset = self.formset_media(**data)
@ -1466,21 +1469,29 @@ class TestEdit(test_utils.TestCase):
img = "%s/img/amo2009/tab-mozilla.png" % settings.MEDIA_ROOT
src_image = open(img, 'rb')
data = dict(upload_image=src_image)
response = self.client.post(self.icon_upload, data)
response_json = json.loads(response.content)
addon = self.get_addon()
# Now, save the form so it gets moved properly.
data = dict(icon_type='image/png',
icon_upload=src_image)
icon_upload_hash=response_json['upload_hash'])
data_formset = self.formset_media(**data)
r = self.client.post(self.get_url('media', True), data_formset)
eq_(r.context['form'].errors, {})
addon = self.get_addon()
addon.get_icon_url(64).endswith('%s/%s-64.png' %
(settings.ADDON_ICONS_DEFAULT_URL, addon.id))
eq_('/'.join(addon.get_icon_url(64).split('/')[-3:-1]),
'addon_icon/%s' % addon.id)
eq_(data['icon_type'], 'image/png')
# Check that it was actually uploaded
dirname = addon.get_icon_dir()
dirname = os.path.join(settings.ADDON_ICONS_PATH,
'%s' % (addon.id / 1000))
dest = os.path.join(dirname, '%s-32.png' % addon.id)
assert os.path.exists(dest)
@ -1497,16 +1508,23 @@ class TestEdit(test_utils.TestCase):
img = "%s/img/amo2009/notifications/error.png" % settings.MEDIA_ROOT
src_image = open(img, 'rb')
data = dict(upload_image=src_image)
response = self.client.post(self.icon_upload, data)
response_json = json.loads(response.content)
addon = self.get_addon()
# Now, save the form so it gets moved properly.
data = dict(icon_type='image/png',
icon_upload=src_image)
icon_upload_hash=response_json['upload_hash'])
data_formset = self.formset_media(**data)
r = self.client.post(self.get_url('media', True), data_formset)
eq_(r.context['form'].errors, {})
addon = self.get_addon()
addon.get_icon_url(64).endswith('%s/%s-64.png' %
(settings.ADDON_ICONS_DEFAULT_URL, addon.id))
eq_('/'.join(addon.get_icon_url(64).split('/')[-3:-1]),
'addon_icon/%s' % addon.id)
eq_(data['icon_type'], 'image/png')
@ -1514,6 +1532,7 @@ class TestEdit(test_utils.TestCase):
dirname = os.path.join(settings.ADDON_ICONS_PATH,
'%s' % (addon.id / 1000))
dest = os.path.join(dirname, '%s-64.png' % addon.id)
assert os.path.exists(dest)
eq_(Image.open(dest).size, (48, 48))
@ -1522,13 +1541,12 @@ class TestEdit(test_utils.TestCase):
img = "%s/js/zamboni/devhub.js" % settings.MEDIA_ROOT
src_image = open(img, 'rb')
data = dict(icon_type='image/png',
icon_upload=src_image)
data_formset = self.formset_media(**data)
data = {'upload_image': src_image}
r = self.client.post(self.get_url('media', True), data_formset)
error = 'Icons must be either PNG or JPG.'
self.assertFormError(r, 'form', 'icon_upload', error)
res = self.client.post(self.preview_upload, data)
response_json = json.loads(res.content)
eq_(response_json['errors'][0], u'Icons must be either PNG or JPG.')
def setup_image_status(self):
addon = self.get_addon()
@ -1573,19 +1591,21 @@ class TestEdit(test_utils.TestCase):
def test_icon_animated(self):
filehandle = open(get_image_path('animated.png'), 'rb')
data = {'icon_type': 'image/png', 'icon_upload': filehandle}
data_formset = self.formset_media(**data)
res = self.client.post(self.get_url('media', True), data_formset)
eq_(res.context['form'].errors['icon_upload'][0],
u'Icons cannot be animated.')
data = {'upload_image': filehandle}
res = self.client.post(self.preview_upload, data)
response_json = json.loads(res.content)
eq_(response_json['errors'][0], u'Icons cannot be animated.')
def preview_add(self, amount=1):
img = "%s/img/amo2009/tab-mozilla.png" % settings.MEDIA_ROOT
src_image = open(img, 'rb')
data = dict(upload_preview=src_image)
data = dict(upload_image=src_image)
data_formset = self.formset_media(**data)
url = reverse('devhub.addons.upload_preview', args=['a3615'])
url = self.preview_upload
r = self.client.post(url, data_formset)
details = json.loads(r.content)
@ -2575,7 +2595,6 @@ class TestSubmitStep1(TestSubmitBase):
"Looks like link %r to %r is still a placeholder" %
(href, ln.text))
class TestSubmitStep2(test_utils.TestCase):
# More tests in TestCreateAddon.
fixtures = ['base/users']
@ -2770,6 +2789,10 @@ class TestSubmitStep4(TestSubmitBase):
SubmitStep.objects.create(addon_id=3615, step=5)
self.url = reverse('devhub.submit.4', args=['a3615'])
self.next_step = reverse('devhub.submit.5', args=['a3615'])
self.icon_upload = reverse('devhub.addons.upload_icon',
args=['a3615'])
self.preview_upload = reverse('devhub.addons.upload_preview',
args=['a3615'])
def tearDown(self):
settings.ADDON_ICON_URL = self.old_addon_icon_url
@ -2829,12 +2852,21 @@ class TestSubmitStep4(TestSubmitBase):
img = "%s/img/amo2009/tab-mozilla.png" % settings.MEDIA_ROOT
src_image = open(img, 'rb')
data = dict(upload_image=src_image)
response = self.client.post(self.icon_upload, data)
response_json = json.loads(response.content)
addon = self.get_addon()
# Now, save the form so it gets moved properly.
data = dict(icon_type='image/png',
icon_upload=src_image)
icon_upload_hash=response_json['upload_hash'])
data_formset = self.formset_media(**data)
self.client.post(self.url, data_formset)
r = self.client.post(self.url, data_formset)
addon = self.get_addon()
eq_('/'.join(addon.get_icon_url(64).split('/')[-3:-1]),
'addon_icon/%s' % addon.id)
@ -2853,11 +2885,18 @@ class TestSubmitStep4(TestSubmitBase):
img = "%s/img/amo2009/notifications/error.png" % settings.MEDIA_ROOT
src_image = open(img, 'rb')
data = dict(upload_image=src_image)
response = self.client.post(self.icon_upload, data)
response_json = json.loads(response.content)
addon = self.get_addon()
# Now, save the form so it gets moved properly.
data = dict(icon_type='image/png',
icon_upload=src_image)
icon_upload_hash=response_json['upload_hash'])
data_formset = self.formset_media(**data)
self.client.post(self.url, data_formset)
r = self.client.post(self.url, data_formset)
addon = self.get_addon()
eq_('/'.join(addon.get_icon_url(64).split('/')[-3:-1]),
@ -2876,20 +2915,23 @@ class TestSubmitStep4(TestSubmitBase):
def test_client_lied(self):
filehandle = open(get_image_path('non-animated.gif'), 'rb')
data = {'icon_type': 'image/png', 'icon_upload': filehandle}
data_formset = self.formset_media(**data)
res = self.client.post(self.url, data_formset)
eq_(res.context['form'].errors['icon_upload'][0],
u'Icons must be either PNG or JPG.')
data = {'upload_image': filehandle}
res = self.client.post(self.preview_upload, data)
response_json = json.loads(res.content)
eq_(response_json['errors'][0], u'Icons must be either PNG or JPG.')
def test_icon_animated(self):
filehandle = open(get_image_path('animated.png'), 'rb')
data = {'icon_type': 'image/png', 'icon_upload': filehandle}
data_formset = self.formset_media(**data)
res = self.client.post(self.url, data_formset)
eq_(res.context['form'].errors['icon_upload'][0],
u'Icons cannot be animated.')
data = {'upload_image': filehandle}
res = self.client.post(self.preview_upload, data)
response_json = json.loads(res.content)
eq_(response_json['errors'][0], u'Icons cannot be animated.')
def test_icon_non_animated(self):
filehandle = open(get_image_path('non-animated.png'), 'rb')
@ -2899,7 +2941,6 @@ class TestSubmitStep4(TestSubmitBase):
eq_(res.status_code, 302)
eq_(self.get_step().step, 5)
class TestSubmitStep5(TestSubmitBase):
"""License submission."""
@ -3117,7 +3158,6 @@ class TestResumeStep(TestSubmitBase):
self.assertRedirects(r, reverse('devhub.submit.%s' % i,
args=['a3615']))
class TestSubmitSteps(test_utils.TestCase):
fixtures = ['base/apps', 'base/users', 'base/addon_3615']

Просмотреть файл

@ -39,8 +39,10 @@ detail_patterns = patterns('',
url('^edit_(?P<section>[^/]+)(?:/(?P<editable>[^/]+))?$',
views.addons_section, name='devhub.addons.section'),
url('^upload_preview$', views.upload_preview,
name='devhub.addons.upload_preview'),
url('^upload_preview$', views.upload_image, {'upload_type': 'preview'},
name='devhub.addons.upload_preview' ),
url('^upload_icon$', views.upload_image, {'upload_type': 'icon'},
name='devhub.addons.upload_icon'),
url('^versions/$', views.version_list, name='devhub.versions'),
url('^versions/delete$', views.version_delete,

Просмотреть файл

@ -622,16 +622,18 @@ def image_status(request, addon_id, addon):
'icons': icons,
'previews': previews}
@json_view
@dev_required
def upload_preview(request, addon_id, addon):
if 'upload_preview' in request.FILES:
upload_preview = request.FILES['upload_preview']
def upload_image(request, addon_id, addon, upload_type):
errors = []
upload_hash = ''
if 'upload_image' in request.FILES:
upload_preview = request.FILES['upload_image']
upload_preview.seek(0)
upload_hash = uuid.uuid4().hex
loc = path.path(settings.PREVIEWS_PATH) / 'temp' / upload_hash
loc = path.path(settings.TMP_PATH) / upload_type / upload_hash
if not loc.dirname().exists():
loc.dirname().makedirs()
@ -639,9 +641,27 @@ def upload_preview(request, addon_id, addon):
for chunk in upload_preview:
fd.write(chunk)
return {'upload_hash': upload_hash, 'errors': False}
check = amo.utils.ImageCheck(upload_preview)
if (not check.is_image() or
upload_preview.content_type not in
('image/png', 'image/jpeg', 'image/jpg')):
errors.append(_('Icons must be either PNG or JPG.'))
if check.is_animated():
errors.append(_('Icons cannot be animated.'))
if (upload_type == 'icon' and
upload_preview.size > settings.MAX_ICON_UPLOAD_SIZE):
errors.append(_('Please use images smaller than %dMB.') %
(settings.MAX_ICON_UPLOAD_SIZE / 1024 / 1024 - 1))
else:
errors.append(_('There was an error uploading your preview.'))
if errors:
upload_hash = ''
return {'upload_hash': upload_hash, 'errors': errors}
return {'errors': [_('There was an error uploading your preview.')]}
@dev_required

Просмотреть файл

@ -214,11 +214,18 @@ function addonFormSubmit() {
var baseurl = function(){
return parent_div.find('#addon-edit-basic').attr('data-baseurl');
}
$('form', parent_div.not('#edit-addon-media')).submit(function(e){
$('form', parent_div).submit(function(e){
e.preventDefault();
var old_baseurl = baseurl();
parent_div.find(".item").removeClass("loaded").addClass("loading");
var scrollBottom = $(document).height() - $(document).scrollTop();
if(parent_div.is('#edit-addon-media')) {
if($('input[name=icon_type]:checked').val().match(/^image\//)) {
setTimeout(checkImageStatus, 1000);
}
}
$.post(parent_div.find('form').attr('action'),
$(this).serialize(), function(d) {
parent_div.html(d).each(addonFormSubmit);
@ -237,6 +244,10 @@ function addonFormSubmit() {
setTimeout(function(){ e.remove(); }, 1500);
}, 2000);
}
if(parent_div.is('#edit-addon-media')) {
hideSameSizedIcons();
}
});
});
z.refreshL10n();
@ -298,70 +309,79 @@ function create_new_preview_field() {
return last;
}
function imageUploadFile(f, url, parent_form, upload_success, upload_errors) {
var data = f.getAsBinary(),
file = {},
xhr = new XMLHttpRequest(),
output = "",
boundary = "BoUnDaRyStRiNg";
file.name = f.name || f.fileName;
file.size = f.size;
file.data = '';
file.aborted = false;
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Length", file.size);
xhr.setRequestHeader('Content-Disposition', 'file; name="upload";');
xhr.setRequestHeader("X-File-Name", file.name);
xhr.setRequestHeader("X-File-Size", file.size);
xhr.overrideMimeType('text/plain; charset=x-user-defined-binary');
xhr.setRequestHeader('Content-length', false);
xhr.setRequestHeader("Content-Type", "multipart/form-data;" +
"boundary=" + boundary);
output += "--" + boundary + "\r\n";
output += "Content-Disposition: form-data; name=\"csrfmiddlewaretoken\";";
output += "\r\n\r\n";
output += parent_form.find('input[name=csrfmiddlewaretoken]').val();
output += "\r\n";
output += "--" + boundary + "\r\n";
output += "Content-Disposition: form-data; name=\"upload_image\";";
output += " filename=\"new-upload\";\r\n";
output += "Content-Type: " + f.type;
output += "\r\n\r\n";
output += data;
output += "\r\n";
output += "--" + boundary + "--";
xhr.onreadystatechange = function(){
if (xhr.readyState == 4 && xhr.responseText &&
(xhr.status == 200 || xhr.status == 304)) {
try {
json = JSON.parse(xhr.responseText);
} catch(err) {
return false;
}
if(json.errors.length) {
upload_errors(json.errors);
} else {
upload_success(json.upload_hash);
}
}
}
xhr.sendAsBinary(output);
}
function initUploadPreview() {
$('#edit-addon-media, #submit-media').delegate('#screenshot_upload', 'change', function(e){
url = $(this).attr('data-upload-url');
// TODO(gkoberger): Make sure this works on non-Fx browsers that don't
//support multiple files
$.each($('#screenshot_upload')[0].files, function(k, f){
var form = create_new_preview_field(),
upload_success = function(upload_hash){
form.find('[name$=upload_hash]').val(upload_hash);
},
upload_errors = function(){};
var data = f.getAsBinary(),
form = create_new_preview_field(),
file = {},
xhr = new XMLHttpRequest(),
output = "",
boundary = "BoUnDaRyStRiNg";
file.name = f.name || f.fileName;
file.size = f.size;
file.data = '';
file.aborted = false;
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-Length", file.size);
xhr.setRequestHeader('Content-Disposition', 'file; name="upload";');
xhr.setRequestHeader("X-File-Name", file.name);
xhr.setRequestHeader("X-File-Size", file.size);
xhr.overrideMimeType('text/plain; charset=x-user-defined-binary');
xhr.setRequestHeader('Content-length', false);
xhr.setRequestHeader("Content-Type", "multipart/form-data;" +
"boundary=" + boundary);
output += "--" + boundary + "\r\n";
output += "Content-Disposition: form-data; name=\"csrfmiddlewaretoken\";";
parent_form = $('#screenshot_upload').closest('form')
output += "\r\n\r\n";
output += parent_form.find('input[name=csrfmiddlewaretoken]').val();
output += "\r\n";
output += "--" + boundary + "\r\n";
output += "Content-Disposition: form-data; name=\"upload_preview\";";
output += " filename=\"new-upload\";\r\n";
output += "Content-Type: " + f.type;
output += "\r\n\r\n";
output += data;
output += "\r\n";
output += "--" + boundary + "--";
xhr.onreadystatechange = function(){
if (xhr.readyState == 4 && xhr.responseText &&
(xhr.status == 200 || xhr.status == 304)) {
try {
json = JSON.parse(xhr.responseText);
} catch(err) {
return false;
}
form.find('[name$=upload_hash]').val(json.upload_hash);
}
}
xhr.sendAsBinary(output);
imageUploadFile(f, url, $(form).closest('form'), upload_success, upload_errors);
});
$('#screenshot_upload').val("");
@ -377,26 +397,10 @@ function initUploadPreview() {
}
function initUploadIcon() {
$('#edit-addon-media').delegate('form', 'submit', function(e) {
e.preventDefault();
multipartUpload($(this), function(e, xhr){
if (xhr.readyState == 4 && xhr.responseText &&
(xhr.status == 200 || xhr.status == 304)) {
$('#edit-addon-media').html(xhr.responseText);
hideSameSizedIcons();
}
});
if($('input[name=icon_type]:checked').val().match(/^image\//)) {
setTimeout(checkImageStatus, 1000);
}
});
$('#edit-addon-media, #submit-media').delegate('#icons_default a', 'click', function(e){
e.preventDefault();
$('#edit-icon-error').hide();
$('#edit-icon-error').parent().find('li').hide();
$parent = $(this).closest('li');
$('input', $parent).attr('checked', true);
@ -411,7 +415,7 @@ function initUploadIcon() {
});
$('#edit-addon, #submit-media').delegate('#id_icon_upload', 'change', function(){
$('#edit-icon-error').hide();
$('#edit-icon-error').parent().find('li').hide();
file = $('#id_icon_upload')[0].files[0];
if(file.type == 'image/jpeg' || file.type == 'image/png') {
@ -420,8 +424,20 @@ function initUploadIcon() {
$('input[name=icon_type][value='+file.type+']', $('#icons_default'))
.attr('checked', true);
$('#icons_default a.active').removeClass('active');
$('#icon_preview img').attr('src', file.getAsDataURL());
var upload_errors = function(errors){
$.each(errors, function(i, v){
$('#icon_preview').parent().find('.errorlist').append("<li>" + v + "</li>");
});
}
var upload_success = function(upload_hash){
$('#id_icon_upload_hash').val(upload_hash)
$('#icons_default a.active').removeClass('active');
$('#icon_preview img').attr('src', file.getAsDataURL());
}
imageUploadFile(file, $(this).attr('data-upload-url'), $(this).closest('form'),
upload_success, upload_errors);
} else {
error = gettext('This filetype is not supported.');
$('#edit-icon-error').text(error).show();