This commit is contained in:
Jeff Balogh 2011-02-22 16:49:40 -05:00
Родитель c0d1ad92f4 ae4bf67074
Коммит e917639574
8 изменённых файлов: 510 добавлений и 4 удалений

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

@ -351,3 +351,9 @@ def media(context, url):
else:
build = context['BUILD_ID_IMG']
return context['MEDIA_URL'] + utils.urlparams(url, b=build)
@register.function
@jinja2.evalcontextfunction
def attrs(ctx, *args, **kw):
return jinja2.filters.do_xmlattr(ctx, dict(*args, **kw))

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

@ -1,6 +1,7 @@
from django.contrib import admin
from .models import BlocklistApp, BlocklistItem, BlocklistPlugin
from .models import BlocklistApp, BlocklistItem, BlocklistPlugin, BlocklistGfx
admin.site.register(BlocklistApp)
admin.site.register(BlocklistItem)
admin.site.register(BlocklistPlugin)
admin.site.register(BlocklistGfx)

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

@ -1,10 +1,12 @@
from django.db import models
import redisutils
import amo.models
class BlocklistApp(amo.models.ModelBase):
blitem = models.ForeignKey('BlocklistItem')
blitem = models.ForeignKey('BlocklistItem', related_name='app')
guid = models.CharField(max_length=255, blank=True, db_index=True,
null=True)
min = models.CharField(max_length=255, blank=True, null=True)
@ -57,3 +59,25 @@ class BlocklistPlugin(amo.models.ModelBase):
def flush_urls(self):
return ['/blocklist*'] # no lang/app
class BlocklistGfx(amo.models.ModelBase):
guid = models.CharField(max_length=255, blank=True, null=True)
os = models.CharField(max_length=255, blank=True, null=True)
vendor = models.CharField(max_length=255, blank=True, null=True)
devices = models.CharField(max_length=255, blank=True, null=True)
feature = models.CharField(max_length=255, blank=True, null=True)
feature_status = models.CharField(max_length=255, blank=True, null=True)
driver_version = models.CharField(max_length=255, blank=True, null=True)
driver_version_comparator = models.CharField(max_length=255, blank=True,
null=True)
class Meta:
db_table = 'blgfxdrivers'
def __unicode__(self):
return '%s: %s : %s : %s' % (self.guid, self.os, self.vendor,
self.devices)
def flush_urls(self):
return ['/blocklist*'] # no lang/app

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

@ -0,0 +1,68 @@
<?xml version="1.0"?>
<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
{% if items %}
<emItems>
{% for guid, rows in items.items() %}
<emItem id="{{ guid }}" {{ attrs(os=rows.os) }}>
{% for row in rows.rows %}
{% if row.min or row.max or row.severity or row.apps %}
<versionRange {{ attrs(minVersion=row.min, maxVersion=row.max,
severity=row.severity) }}>
{% for app in row.apps %}
<targetApplication {{ attrs(id=app.guid) }}>
{% if app.min and app.max %}
<versionRange {{ attrs(minVersion=app.min, maxVersion=app.max) }} />
{% endif %}
</targetApplication>
{% endfor %}
</versionRange>
{% endif %}
{% endfor %}
</emItem>
{% endfor %}
</emItems>
{% endif %}
{% if plugins %}
<pluginItems>
{% for plugin in plugins %}
<pluginItem {{ attrs(os=plugin.os, xpcomabi=plugin.xpcomabi) }}>
{% if plugin.name %}<match name="name" exp="{{ plugin.name }}" />{% endif %}
{% if plugin.description %}<match name="description" exp="{{ plugin.description }}" />{% endif %}
{% if plugin.filename %}<match name="filename" exp="{{ plugin.filename }}" />{% endif %}
{% if plugin.severity or plugin.min or plugin.max %}
<versionRange {{ attrs(severity=plugin.severity) }}>
{% if apiver > 2 and plugin.min and plugin.max %}
<targetApplication id="{{ appguid }}">
<versionRange {{ attrs(minVersion=plugin.min, maxVersion=plugin.max) }} />
</targetApplication>
{% endif %}
</versionRange>
{% endif %}
</pluginItem>
{% endfor %}
</pluginItems>
{% endif %}
{% if gfxs %}
<gfxItems>
{% for gfx in gfxs %}
<gfxBlacklistEntry>
<os>{{ gfx.os }}</os>
<vendor>{{ gfx.vendor }}</vendor>
<devices>
{% for device in gfx.devices.split(' ') %}
<device>{{ device }}</device>
{% endfor %}
</devices>
<feature>{{ gfx.feature }}</feature>
<featureStatus>{{ gfx.feature_status }}</featureStatus>
<driverVersion>{{ gfx.driver_version }}</driverVersion>
<driverVersionComparator>{{ gfx.driver_version_comparator }}</driverVersionComparator>
</gfxBlacklistEntry>
{% endfor %}
</gfxItems>
{% endif %}
</blocklist>

308
apps/blocklist/tests.py Normal file
Просмотреть файл

@ -0,0 +1,308 @@
from xml.dom import minidom
from django.conf import settings
from django.core.cache import cache
import redisutils
import test_utils
from nose.tools import eq_
import amo
from amo.urlresolvers import reverse
from .models import BlocklistItem, BlocklistApp, BlocklistPlugin, BlocklistGfx
base_xml = """
<?xml version="1.0"?>
<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
</blocklist>
"""
class BlocklistTest(test_utils.TestCase):
def setUp(self):
self.fx4_url = reverse('blocklist', args=[3, amo.FIREFOX.guid, '4.0'])
self.fx2_url = reverse('blocklist', args=[2, amo.FIREFOX.guid, '2.0'])
self.mobile_url = reverse('blocklist', args=[2, amo.MOBILE.guid, '.9'])
self._redis = redisutils.mock_redis()
def tearDown(self):
redisutils.reset_redis(self._redis)
def normalize(self, s):
return '\n'.join(x.strip() for x in s.split())
def eq_(self, x, y):
return eq_(self.normalize(x), self.normalize(y))
class BlocklistItemTest(BlocklistTest):
def setUp(self):
super(BlocklistItemTest, self).setUp()
self.item = BlocklistItem.objects.create(guid='guid@addon.com')
self.app = BlocklistApp.objects.create(blitem=self.item,
guid=amo.FIREFOX.guid)
def test_no_items(self):
self.item.delete()
r = self.client.get(self.fx4_url)
self.eq_(r.content, base_xml)
def test_new_cookie_per_user(self):
self.client.get(self.fx4_url)
assert settings.BLOCKLIST_COOKIE in self.client.cookies
c = self.client.cookies[settings.BLOCKLIST_COOKIE]
eq_(c['path'], '/blocklist/')
eq_(c['secure'], True)
def test_existing_user_cookie(self):
self.client.cookies[settings.BLOCKLIST_COOKIE] = 'adfadf'
self.client.get(self.fx4_url)
eq_(self.client.cookies[settings.BLOCKLIST_COOKIE].value, 'adfadf')
def test_url_params(self):
eq_(self.client.get(self.fx4_url).status_code, 200)
eq_(self.client.get(self.fx2_url).status_code, 200)
# We ignore trailing url parameters.
eq_(self.client.get(self.fx4_url + 'other/junk/').status_code, 200)
def test_app_guid(self):
# There's one item for Firefox.
r = self.client.get(self.fx4_url)
eq_(r.status_code, 200)
eq_(len(r.context['items']), 1)
# There are no items for mobile.
r = self.client.get(self.mobile_url)
eq_(r.status_code, 200)
eq_(len(r.context['items']), 0)
# Without the app constraint we see the item.
self.app.delete()
r = self.client.get(self.mobile_url)
eq_(r.status_code, 200)
eq_(len(r.context['items']), 1)
def dom(self, url):
r = self.client.get(self.fx4_url)
return minidom.parseString(r.content)
def test_item_guid(self):
items = self.dom(self.fx4_url).getElementsByTagName('emItem')
eq_(len(items), 1)
eq_(items[0].getAttribute('id'), 'guid@addon.com')
def test_item_os(self):
item = self.dom(self.fx4_url).getElementsByTagName('emItem')[0]
assert 'os' not in item.attributes.keys()
self.item.update(os='win,mac')
item = self.dom(self.fx4_url).getElementsByTagName('emItem')[0]
eq_(item.getAttribute('os'), 'win,mac')
def test_item_severity(self):
self.item.update(severity=2)
eq_(len(self.vr()), 1)
item = self.dom(self.fx4_url).getElementsByTagName('emItem')[0]
vrange = item.getElementsByTagName('versionRange')
eq_(vrange[0].getAttribute('severity'), '2')
def vr(self):
item = self.dom(self.fx4_url).getElementsByTagName('emItem')[0]
return item.getElementsByTagName('versionRange')
def test_item_version_range(self):
self.item.update(min='0.1')
eq_(len(self.vr()), 1)
eq_(self.vr()[0].attributes.keys(), ['minVersion'])
eq_(self.vr()[0].getAttribute('minVersion'), '0.1')
self.item.update(max='0.2')
eq_(self.vr()[0].attributes.keys(), ['minVersion', 'maxVersion'])
eq_(self.vr()[0].getAttribute('minVersion'), '0.1')
eq_(self.vr()[0].getAttribute('maxVersion'), '0.2')
def test_item_multiple_version_range(self):
# There should be two <versionRange>s under one <emItem>.
self.item.update(min='0.1', max='0.2')
BlocklistItem.objects.create(guid=self.item.guid, severity=3)
item = self.dom(self.fx4_url).getElementsByTagName('emItem')
eq_(len(item), 1)
vr = item[0].getElementsByTagName('versionRange')
eq_(len(vr), 2)
eq_(vr[0].getAttribute('minVersion'), '0.1')
eq_(vr[0].getAttribute('maxVersion'), '0.2')
eq_(vr[1].getAttribute('severity'), '3')
def test_item_target_app(self):
app = self.app
self.app.delete()
self.item.update(severity=2)
version_range = self.vr()[0]
eq_(version_range.getElementsByTagName('targetApplication'), [])
app.save()
version_range = self.vr()[0]
target_app = version_range.getElementsByTagName('targetApplication')
eq_(len(target_app), 1)
eq_(target_app[0].getAttribute('id'), amo.FIREFOX.guid)
app.update(min='0.1', max='*')
version_range = self.vr()[0]
target_app = version_range.getElementsByTagName('targetApplication')
eq_(target_app[0].getAttribute('id'), amo.FIREFOX.guid)
tvr = target_app[0].getElementsByTagName('versionRange')
eq_(tvr[0].getAttribute('minVersion'), '0.1')
eq_(tvr[0].getAttribute('maxVersion'), '*')
def test_item_multiple_apps(self):
# Make sure all <targetApplication>s go under the same <versionRange>.
self.app.update(min='0.1', max='0.2')
BlocklistApp.objects.create(guid=amo.FIREFOX.guid, blitem=self.item,
min='3.0', max='3.1')
version_range = self.vr()[0]
apps = version_range.getElementsByTagName('targetApplication')
eq_(len(apps), 2)
eq_(apps[0].getAttribute('id'), amo.FIREFOX.guid)
vr = apps[0].getElementsByTagName('versionRange')[0]
eq_(vr.getAttribute('minVersion'), '0.1')
eq_(vr.getAttribute('maxVersion'), '0.2')
eq_(apps[1].getAttribute('id'), amo.FIREFOX.guid)
vr = apps[1].getElementsByTagName('versionRange')[0]
eq_(vr.getAttribute('minVersion'), '3.0')
eq_(vr.getAttribute('maxVersion'), '3.1')
def test_item_empty_version_range(self):
# No version_range without an app, min, max, or severity.
self.app.delete()
self.item.update(min=None, max=None, severity=None)
eq_(len(self.vr()), 0)
def test_item_empty_target_app(self):
# No empty <targetApplication>.
self.item.update(severity=1)
self.app.delete()
eq_(self.dom(self.fx4_url).getElementsByTagName('targetApplication'),
[])
def test_item_target_empty_version_range(self):
app = self.dom(self.fx4_url).getElementsByTagName('targetApplication')
eq_(app[0].getElementsByTagName('versionRange'), [])
class BlocklistPluginTest(BlocklistTest):
def setUp(self):
super(BlocklistPluginTest, self).setUp()
self.plugin = BlocklistPlugin.objects.create(guid=amo.FIREFOX.guid)
def test_no_plugins(self):
r = self.client.get(self.mobile_url)
self.eq_(r.content, base_xml)
def dom(self, url=None):
url = url or self.fx4_url
r = self.client.get(url)
d = minidom.parseString(r.content)
return d.getElementsByTagName('pluginItem')[0]
def test_plugin_empty(self):
eq_(self.dom().attributes.keys(), [])
eq_(self.dom().getElementsByTagName('match'), [])
eq_(self.dom().getElementsByTagName('versionRange'), [])
def test_plugin_os(self):
self.plugin.update(os='win')
eq_(self.dom().attributes.keys(), ['os'])
eq_(self.dom().getAttribute('os'), 'win')
def test_plugin_xpcomabi(self):
self.plugin.update(xpcomabi='win')
eq_(self.dom().attributes.keys(), ['xpcomabi'])
eq_(self.dom().getAttribute('xpcomabi'), 'win')
def test_plugin_name(self):
self.plugin.update(name='flash')
match = self.dom().getElementsByTagName('match')
eq_(len(match), 1)
eq_(dict(match[0].attributes.items()),
{'name': 'name', 'exp': 'flash'})
def test_plugin_description(self):
self.plugin.update(description='flash')
match = self.dom().getElementsByTagName('match')
eq_(len(match), 1)
eq_(dict(match[0].attributes.items()),
{'name': 'description', 'exp': 'flash'})
def test_plugin_filename(self):
self.plugin.update(filename='flash')
match = self.dom().getElementsByTagName('match')
eq_(len(match), 1)
eq_(dict(match[0].attributes.items()),
{'name': 'filename', 'exp': 'flash'})
def test_plugin_severity(self):
self.plugin.update(severity=2)
v = self.dom().getElementsByTagName('versionRange')[0]
eq_(v.getAttribute('severity'), '2')
def test_plugin_target_app(self):
self.plugin.update(min='1', max='2')
v = self.dom().getElementsByTagName('versionRange')[0]
app = v.getElementsByTagName('targetApplication')[0]
eq_(app.getAttribute('id'), amo.FIREFOX.guid)
vr = app.getElementsByTagName('versionRange')[0]
eq_(vr.getAttribute('minVersion'), '1')
eq_(vr.getAttribute('maxVersion'), '2')
def test_plugin_apiver_lt_3(self):
self.plugin.update(severity='2')
# No min & max so the app matches.
e = self.dom(self.fx2_url).getElementsByTagName('versionRange')[0]
eq_(e.getAttribute('severity'), '2')
eq_(e.getElementsByTagName('targetApplication'), [])
# The app version is not in range.
self.plugin.update(min='3.0', max='4.0')
self.assertRaises(IndexError, self.dom, self.fx2_url)
# The app is back in range.
self.plugin.update(min='1.1')
e = self.dom(self.fx2_url).getElementsByTagName('versionRange')[0]
eq_(e.getAttribute('severity'), '2')
eq_(e.getElementsByTagName('targetApplication'), [])
class BlocklistGfxTest(BlocklistTest):
def setUp(self):
super(BlocklistGfxTest, self).setUp()
self.gfx = BlocklistGfx.objects.create(
guid=amo.FIREFOX.guid, os='os', vendor='vendor', devices='x y z',
feature='feature', feature_status='status',
driver_version='version', driver_version_comparator='compare')
def test_no_gfx(self):
r = self.client.get(self.mobile_url)
self.eq_(r.content, base_xml)
def test_gfx(self):
r = self.client.get(self.fx4_url)
dom = minidom.parseString(r.content)
gfx = dom.getElementsByTagName('gfxBlacklistEntry')[0]
find = lambda e: gfx.getElementsByTagName(e)[0].childNodes[0].wholeText
eq_(find('os'), self.gfx.os)
eq_(find('feature'), self.gfx.feature)
eq_(find('vendor'), self.gfx.vendor)
eq_(find('featureStatus'), self.gfx.feature_status)
eq_(find('driverVersion'), self.gfx.driver_version)
eq_(find('driverVersionComparator'),
self.gfx.driver_version_comparator)
devices = gfx.getElementsByTagName('devices')[0]
for device, val in zip(devices.getElementsByTagName('device'),
self.gfx.devices.split(' ')):
eq_(device.childNodes[0].wholeText, val)

91
apps/blocklist/views.py Normal file
Просмотреть файл

@ -0,0 +1,91 @@
import collections
from datetime import datetime, timedelta
import uuid
from django.core.cache import cache
from django.conf import settings
from django.db.models import Q, signals as db_signals
import jingo
import redisutils
from amo.utils import sorted_groupby
from versions.compare import version_int
from .models import BlocklistItem, BlocklistPlugin, BlocklistGfx, BlocklistApp
App = collections.namedtuple('App', 'guid min max')
def blocklist(request, apiver, app, appver):
key = 'blocklist:%s:%s:%s'% (apiver, app, appver)
response = cache.get(key)
if response is None:
response = _blocklist(request, apiver, app, appver)
cache.set(key, response, 60 * 60)
# This gets cleared with the clear_blocklist signal handler.
redisutils.connections['master'].sadd('blocklist:keys', key)
if settings.BLOCKLIST_COOKIE not in request.COOKIES:
response.set_cookie(settings.BLOCKLIST_COOKIE, uuid.uuid4(),
expires=datetime.now() + timedelta(days=5 * 365),
path='/blocklist/', secure=True)
return response
def _blocklist(request, apiver, app, appver):
apiver = int(apiver)
items = get_items(apiver, app, appver)
plugins = get_plugins(apiver, app, appver)
gfxs = BlocklistGfx.objects.filter(Q(guid__isnull=True) | Q(guid=app))
return jingo.render(request, 'blocklist/blocklist.xml',
dict(items=items, plugins=plugins, gfxs=gfxs,
apiver=apiver, appguid=app, appver=appver))
def clear_blocklist(*args, **kw):
# Something in the blocklist changed; invalidate all responses.
keys = redisutils.connections['master'].smembers('blocklist:keys')
cache.delete_many(keys)
for m in BlocklistItem, BlocklistPlugin, BlocklistGfx, BlocklistApp:
db_signals.post_save.connect(clear_blocklist, sender=m,
dispatch_uid='save_%s' % m)
db_signals.post_delete.connect(clear_blocklist, sender=m,
dispatch_uid='delete_%s' % m)
def get_items(apiver, app, appver):
# Collapse multiple blocklist items (different version ranges) into one
# item and collapse each item's apps.
addons = (BlocklistItem.uncached
.filter(Q(app__guid__isnull=True) | Q(app__guid=app))
.extra(select={'app_guid': 'blapps.guid',
'app_min': 'blapps.min',
'app_max': 'blapps.max'}))
items = {}
for guid, rows in sorted_groupby(addons, 'guid'):
rr = []
for id, rs in sorted_groupby(list(rows), 'id'):
rs = list(rs)
rr.append(rs[0])
rs[0].apps = [App(r.app_guid, r.app_min, r.app_max)
for r in rs if r.app_guid]
os = [r.os for r in rr if r.os]
items[guid] = {'rows': rr, 'os': os and os[0] or None}
return items
def get_plugins(apiver, app, appver):
# API versions < 3 ignore targetApplication entries for plugins so only
# block the plugin if the appver is within the block range.
plugins = BlocklistPlugin.uncached.filter(
Q(guid__isnull=True) | Q(guid=app))
if apiver < 3:
def between(ver, min, max):
if not (min and max):
return True
return version_int(min) < ver < version_int(max)
app_version = version_int(appver)
plugins = [p for p in plugins if between(app_version, p.min, p.max)]
return plugins

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

@ -165,12 +165,12 @@ ADMIN_MEDIA_PREFIX = '/admin-media/'
# paths that don't require an app prefix
SUPPORTED_NONAPPS = ('admin', 'developers', 'editors', 'img',
'jsi18n', 'localizers', 'media', 'robots.txt',
'statistics', 'services', 'update')
'statistics', 'services', 'update', 'blocklist')
DEFAULT_APP = 'firefox'
# paths that don't require a locale prefix
SUPPORTED_NONLOCALES = ('img', 'media', 'robots.txt', 'services', 'downloads',
'update')
'update', 'blocklist')
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'r#%9w^o_80)7f%!_ir5zx$tu3mupw9u%&s!)-_q%gy7i+fhx#)'
@ -798,3 +798,5 @@ DEFAULT_SUGGESTED_CONTRIBUTION = 5
# Path to `ps`.
PS_BIN = '/bin/ps'
BLOCKLIST_COOKIE = 'BLOCKLIST_v1'

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

@ -5,6 +5,7 @@ from django.shortcuts import redirect
from django.views.i18n import javascript_catalog
from django.views.decorators.cache import cache_page
import blocklist.views
import versions.urls
admin.autodiscover()
@ -16,6 +17,11 @@ urlpatterns = patterns('',
# Discovery pane is first for undetectable efficiency wins.
('^discovery/', include('discovery.urls')),
# There are many more params but we only care about these three. The end is
# not anchored on purpose!
url('^blocklist/(?P<apiver>\d+)/(?P<app>[^/]+)/(?P<appver>[^/]+)/',
blocklist.views.blocklist, name='blocklist'),
# Add-ons.
('', include('addons.urls')),