From c09a5fb62f3030ebbc8dcca5974fc170e8d0a972 Mon Sep 17 00:00:00 2001 From: Andrew Williamson Date: Tue, 30 Oct 2018 16:56:33 +0000 Subject: [PATCH] serve previous theme background image as json via devhub endpoint (#9847) --- src/olympia/devhub/tests/test_utils.py | 16 ++++---- src/olympia/devhub/tests/test_views.py | 37 +++++++++++++++++ src/olympia/devhub/urls.py | 3 ++ src/olympia/devhub/utils.py | 11 ----- src/olympia/devhub/views.py | 19 ++++++++- src/olympia/files/tests/test_utils_.py | 57 ++++++++++++++++++++++++++ src/olympia/files/utils.py | 42 ++++++++++++++++++- static/js/zamboni/static_theme.js | 19 +++++++-- 8 files changed, 177 insertions(+), 27 deletions(-) diff --git a/src/olympia/devhub/tests/test_utils.py b/src/olympia/devhub/tests/test_utils.py index da7f4b990c..5c232ff651 100644 --- a/src/olympia/devhub/tests/test_utils.py +++ b/src/olympia/devhub/tests/test_utils.py @@ -431,14 +431,14 @@ def test_extract_theme_properties(): copy_stored_file(zip_file, addon.current_version.all_files[0].file_path) result = utils.extract_theme_properties( addon, addon.current_version.channel) - assert result['colors'] == { - "accentcolor": "#adb09f", - "textcolor": "#000" - } - assert result['images'] == { - "headerURL": '%s%s//%s/%s/%s' % ( - settings.MEDIA_URL, 'addons', text_type(addon.id), - text_type(addon.current_version.id), 'weta.png') + assert result == { + "colors": { + "accentcolor": "#adb09f", + "textcolor": "#000" + }, + "images": { + "headerURL": "weta.png" + } } diff --git a/src/olympia/devhub/tests/test_views.py b/src/olympia/devhub/tests/test_views.py index bc7edc983e..7dad3e5882 100644 --- a/src/olympia/devhub/tests/test_views.py +++ b/src/olympia/devhub/tests/test_views.py @@ -24,6 +24,7 @@ from olympia import amo, core from olympia.activity.models import ActivityLog from olympia.addons.models import ( Addon, AddonCategory, AddonFeatureCompatibility, AddonUser) +from olympia.amo.storage_utils import copy_stored_file from olympia.amo.templatetags.jinja_helpers import ( format_date, url as url_reverse) from olympia.amo.tests import ( @@ -1755,3 +1756,39 @@ def test_get_next_version_number(): version_factory(addon=addon, version='36.0').delete() assert addon.current_version.version == '34.45.0a1pre' assert get_next_version_number(addon) == '37.0' + + +class TestThemeBackgroundImage(TestCase): + + def setUp(self): + user = user_factory(email='regular@mozilla.com') + assert self.client.login(email='regular@mozilla.com') + self.addon = addon_factory(users=[user]) + self.url = reverse( + 'devhub.submit.version.previous_background', + args=[self.addon.slug, 'listed']) + + def test_wrong_user(self): + user_factory(email='irregular@mozilla.com') + assert self.client.login(email='irregular@mozilla.com') + response = self.client.post(self.url, follow=True) + assert response.status_code == 403 + + def test_no_header_image(self): + response = self.client.post(self.url, follow=True) + assert response.status_code == 200 + data = json.loads(response.content) + assert data == {} + + def test_header_image(self): + destination = self.addon.current_version.all_files[0].current_file_path + zip_file = os.path.join( + settings.ROOT, 'src/olympia/devhub/tests/addons/static_theme.zip') + copy_stored_file(zip_file, destination) + response = self.client.post(self.url, follow=True) + assert response.status_code == 200 + data = json.loads(response.content) + assert data + assert len(data.items()) == 1 + assert 'weta.png' in data + assert len(data['weta.png']) == 168596 # base64-encoded size diff --git a/src/olympia/devhub/urls.py b/src/olympia/devhub/urls.py index f2b87621de..b3c33d64a9 100644 --- a/src/olympia/devhub/urls.py +++ b/src/olympia/devhub/urls.py @@ -83,6 +83,9 @@ detail_patterns = [ url('^versions/submit/wizard-(?Plisted|unlisted)$', views.submit_version_theme_wizard, name='devhub.submit.version.wizard'), + url('^versions/submit/wizard-(?Plisted|unlisted)/background$', + views.theme_background_image, + name='devhub.submit.version.previous_background'), url('^file/(?P[^/]+)/validation$', views.file_validation, name='devhub.file_validation'), diff --git a/src/olympia/devhub/utils.py b/src/olympia/devhub/utils.py index 8552373cfc..1c367144fe 100644 --- a/src/olympia/devhub/utils.py +++ b/src/olympia/devhub/utils.py @@ -13,7 +13,6 @@ import olympia.core.logger from olympia import amo, core from olympia.addons.models import Addon -from olympia.amo.templatetags.jinja_helpers import user_media_url from olympia.amo.urlresolvers import linkify_escape from olympia.files.models import File, FileUpload from olympia.files.utils import parse_addon, parse_xpi @@ -376,16 +375,6 @@ def extract_theme_properties(addon, channel): theme_props['colors'] = dict( process_color_value(prop, color) for prop, color in theme_props.get('colors', {}).items()) - # replace headerURL with path to existing background - if 'images' in theme_props: - if 'theme_frame' in theme_props['images']: - header_url = theme_props['images'].pop('theme_frame') - if 'headerURL' in theme_props['images']: - header_url = theme_props['images'].pop('headerURL') - if header_url: - theme_props['images']['headerURL'] = '/'.join(( - user_media_url('addons'), text_type(addon.id), - text_type(version.id), header_url)) return theme_props diff --git a/src/olympia/devhub/views.py b/src/olympia/devhub/views.py index 6ffdb07365..6b32507771 100644 --- a/src/olympia/devhub/views.py +++ b/src/olympia/devhub/views.py @@ -1,7 +1,7 @@ import datetime import os import time - +from base64 import b64encode from uuid import UUID, uuid4 from django import forms as django_forms, http @@ -46,7 +46,7 @@ from olympia.devhub.utils import ( get_addon_akismet_reports, wizard_unsupported_properties, extract_theme_properties) from olympia.files.models import File, FileUpload, FileValidation -from olympia.files.utils import parse_addon +from olympia.files.utils import get_background_images, parse_addon from olympia.lib.crypto.packaged import sign_file from olympia.reviewers.forms import PublicWhiteboardForm from olympia.reviewers.models import Whiteboard @@ -1783,3 +1783,18 @@ def send_key_revoked_email(to_email, key): from_email=settings.DEFAULT_FROM_EMAIL, recipient_list=[to_email], ) + + +@dev_required +@json_view +def theme_background_image(request, addon_id, addon, channel): + channel_id = amo.CHANNEL_CHOICES_LOOKUP[channel] + version = addon.find_latest_version(channel_id) + if not version or not version.all_files: + return {} + backgrounds = get_background_images( + version.all_files[0], theme_data=None, header_only=True) + backgrounds = { + name: b64encode(background) + for name, background in backgrounds.items()} + return backgrounds diff --git a/src/olympia/files/tests/test_utils_.py b/src/olympia/files/tests/test_utils_.py index 5b4f3f9509..eabc2d61e0 100644 --- a/src/olympia/files/tests/test_utils_.py +++ b/src/olympia/files/tests/test_utils_.py @@ -1020,3 +1020,60 @@ class TestExtractHeaderImg(TestCase): assert default_storage.size(additional_file_1) == 42 assert default_storage.exists(additional_file_2) assert default_storage.size(additional_file_2) == 93371 + + +class TestGetBackgroundImages(TestCase): + file_obj = os.path.join( + settings.ROOT, 'src/olympia/devhub/tests/addons/static_theme.zip') + + def test_get_background_images(self): + data = {'images': {'headerURL': 'weta.png'}} + + images = utils.get_background_images(self.file_obj, data) + assert 'weta.png' in images + assert len(images.items()) == 1 + assert len(images['weta.png']) == 126447 + + def test_get_background_images_no_theme_data_provided(self): + images = utils.get_background_images(self.file_obj, theme_data=None) + assert 'weta.png' in images + assert len(images.items()) == 1 + assert len(images['weta.png']) == 126447 + + def test_get_background_images_missing(self): + data = {'images': {'headerURL': 'missing_file.png'}} + + images = utils.get_background_images(self.file_obj, data) + assert not images + + def test_get_background_images_not_image(self): + self.file_obj = os.path.join( + settings.ROOT, + 'src/olympia/devhub/tests/addons/static_theme_non_image.zip') + data = {'images': {'headerURL': 'not_an_image.js'}} + + images = utils.get_background_images(self.file_obj, data) + assert not images + + def test_get_background_images_with_additional_imgs(self): + self.file_obj = os.path.join( + settings.ROOT, + 'src/olympia/devhub/tests/addons/static_theme_tiled.zip') + data = {'images': { + 'headerURL': 'empty.png', + 'additional_backgrounds': [ + 'transparent.gif', 'missing_&_ignored.png', + 'weta_for_tiling.png'] + }} + + images = utils.get_background_images(self.file_obj, data) + assert len(images.items()) == 3 + assert len(images['empty.png']) == 332 + assert len(images['transparent.gif']) == 42 + assert len(images['weta_for_tiling.png']) == 93371 + + # And again but only with the header image + images = utils.get_background_images( + self.file_obj, data, header_only=True) + assert len(images.items()) == 1 + assert len(images['empty.png']) == 332 diff --git a/src/olympia/files/utils.py b/src/olympia/files/utils.py index 311533e005..df3578e72b 100644 --- a/src/olympia/files/utils.py +++ b/src/olympia/files/utils.py @@ -530,6 +530,9 @@ class ManifestJSONExtractor(object): if self.certinfo is not None: data.update(self.certinfo.parse()) + if self.type == amo.ADDON_STATICTHEME: + data['theme'] = self.get('theme', {}) + if not minimal: data.update({ 'is_restart_required': False, @@ -546,8 +549,6 @@ class ManifestJSONExtractor(object): 'permissions': self.get('permissions', []), 'content_scripts': self.get('content_scripts', []), }) - elif self.type == amo.ADDON_STATICTHEME: - data['theme'] = self.get('theme', {}) elif self.type == amo.ADDON_DICT: data['target_locale'] = self.target_locale() return data @@ -1266,6 +1267,43 @@ def extract_header_img(file_obj, theme_data, dest_path): log.debug(ioerror) +def get_background_images(file_obj, theme_data, header_only=False): + """Extract static theme header image from `file_obj` and return in dict.""" + xpi = get_filepath(file_obj) + if not theme_data: + # we might already have theme_data, but otherwise get it from the xpi. + try: + parsed_data = parse_xpi(xpi, minimal=True) + theme_data = parsed_data.get('theme', {}) + except forms.ValidationError: + # If we can't parse the existing manifest safely return. + return {} + images_dict = theme_data.get('images', {}) + # Get the reference in the manifest. theme_frame is the Chrome variant. + header_url = images_dict.get( + 'headerURL', images_dict.get('theme_frame')) + # And any additional backgrounds too. + additional_urls = ( + images_dict.get('additional_backgrounds', []) if not header_only + else []) + image_urls = [header_url] + additional_urls + images = {} + try: + with zipfile.ZipFile(xpi, 'r') as source: + for url in image_urls: + _, file_ext = os.path.splitext(text_type(url).lower()) + if file_ext not in amo.THEME_BACKGROUND_EXTS: + # Just extract image files. + continue + try: + images[url] = source.read(url) + except KeyError: + pass + except IOError as ioerror: + log.debug(ioerror) + return images + + @contextlib.contextmanager def atomic_lock(lock_dir, lock_name, lifetime=60): """A atomic, NFS safe implementation of a file lock. diff --git a/static/js/zamboni/static_theme.js b/static/js/zamboni/static_theme.js index d022802975..7df4117c4e 100644 --- a/static/js/zamboni/static_theme.js +++ b/static/js/zamboni/static_theme.js @@ -53,17 +53,28 @@ $(document).ready(function() { $svg_img.attr('preserveAspectRatio', 'xMaxYMin '+ meetOrSlice); }); + function b64toBlob(data) { + var b64str = atob(data); + var counter = b64str.length; + var u8arr = new Uint8Array(counter); + while(counter--){ + u8arr[counter] = b64str.charCodeAt(counter); + } + return new Blob([u8arr]); + } + $wizard.find('#theme-header').each(function(index, element) { var img_src = $(element).data('existing-header'); // If we already have a preview from a selected file don't overwrite it. if (getFile() || !img_src) return; var xhr = new XMLHttpRequest(); - xhr.open("GET", img_src); - xhr.responseType = "blob"; + xhr.open("GET", window.location.href + "/background"); + xhr.responseType = "json"; // load the image as a blob so we can treat it as a File xhr.onload = function() { - preLoadBlob = xhr.response; - preLoadBlob.name = img_src.split('/').slice(-1)[0]; + jsonResponse = xhr.response; + preLoadBlob = b64toBlob(jsonResponse[img_src]); + preLoadBlob.name = img_src; $wizard.find('input[type="file"]').trigger('change'); }; xhr.send();