2013-03-31 06:09:25 +04:00
|
|
|
import base64
|
|
|
|
import json
|
|
|
|
import os
|
2013-04-03 02:39:39 +04:00
|
|
|
import posixpath
|
2013-03-31 06:09:25 +04:00
|
|
|
import re
|
|
|
|
from time import time
|
2013-05-20 21:35:27 +04:00
|
|
|
from wsgiref.handlers import format_date_time
|
2013-03-31 06:09:25 +04:00
|
|
|
|
|
|
|
from constants import base
|
|
|
|
from utils import log_configure, log_exception, mypool
|
2014-08-05 13:20:50 +04:00
|
|
|
from amo.helpers import user_media_path
|
2013-03-31 06:09:25 +04:00
|
|
|
|
2013-05-20 21:35:27 +04:00
|
|
|
from services.utils import settings
|
2013-03-31 06:09:25 +04:00
|
|
|
|
|
|
|
# Configure the log.
|
|
|
|
log_configure()
|
|
|
|
|
2013-05-20 21:35:27 +04:00
|
|
|
# This has to be imported after the settings (utils).
|
|
|
|
from django_statsd.clients import statsd
|
|
|
|
|
2013-03-31 06:09:25 +04:00
|
|
|
|
|
|
|
class ThemeUpdate(object):
|
|
|
|
|
|
|
|
def __init__(self, locale, id_, qs=None):
|
|
|
|
self.conn, self.cursor = None, None
|
2013-04-29 00:33:08 +04:00
|
|
|
self.from_gp = qs == 'src=gp'
|
2013-03-31 06:09:25 +04:00
|
|
|
self.data = {
|
|
|
|
'locale': locale,
|
|
|
|
'id': id_,
|
|
|
|
# If we came from getpersonas.com, then look up by `persona_id`.
|
|
|
|
# Otherwise, look up `addon_id`.
|
2013-04-29 00:33:08 +04:00
|
|
|
'primary_key': 'persona_id' if self.from_gp else 'addon_id',
|
2013-03-31 06:09:25 +04:00
|
|
|
'atype': base.ADDON_PERSONA,
|
|
|
|
'row': {}
|
|
|
|
}
|
|
|
|
if not self.cursor:
|
|
|
|
self.conn = mypool.connect()
|
|
|
|
self.cursor = self.conn.cursor()
|
|
|
|
|
|
|
|
def base64_icon(self, addon_id):
|
2013-04-03 02:39:39 +04:00
|
|
|
path = self.image_path('icon.jpg')
|
2014-06-13 22:44:40 +04:00
|
|
|
if not os.path.isfile(path):
|
|
|
|
return ''
|
|
|
|
|
2013-03-31 06:09:25 +04:00
|
|
|
try:
|
|
|
|
with open(path, 'r') as f:
|
|
|
|
return base64.b64encode(f.read())
|
|
|
|
except IOError, e:
|
|
|
|
if len(e.args) == 1:
|
|
|
|
log_exception('I/O error: {0}'.format(e[0]))
|
|
|
|
else:
|
|
|
|
log_exception('I/O error({0}): {1}'.format(e[0], e[1]))
|
|
|
|
return ''
|
|
|
|
|
|
|
|
def get_headers(self, length):
|
|
|
|
return [('Cache-Control', 'public, max-age=3600'),
|
|
|
|
('Content-Length', str(length)),
|
|
|
|
('Content-Type', 'application/json'),
|
|
|
|
('Expires', format_date_time(time() + 3600)),
|
|
|
|
('Last-Modified', format_date_time(time()))]
|
|
|
|
|
|
|
|
def get_update(self):
|
|
|
|
"""
|
|
|
|
TODO:
|
|
|
|
|
|
|
|
* When themes have versions let's not use
|
|
|
|
`personas.approve` as a `modified` timestamp. Either set this
|
|
|
|
during theme approval, or let's keep a hash of the header and
|
|
|
|
footer.
|
|
|
|
|
|
|
|
* Do a join on `addons_users` to get the actual correct user.
|
|
|
|
We're cheating and setting `personas.display_username` during
|
|
|
|
submission/management heh. But `personas.author` and
|
|
|
|
`personas.display_username` are not what we want.
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
sql = """
|
2013-07-09 01:40:26 +04:00
|
|
|
SELECT p.persona_id, a.id, a.slug, v.version,
|
2013-03-31 06:09:25 +04:00
|
|
|
t_name.localized_string AS name,
|
|
|
|
t_desc.localized_string AS description,
|
|
|
|
p.display_username, p.header,
|
|
|
|
p.footer, p.accentcolor, p.textcolor,
|
|
|
|
UNIX_TIMESTAMP(a.modified) AS modified
|
|
|
|
FROM addons AS a
|
|
|
|
LEFT JOIN personas AS p ON p.addon_id=a.id
|
2013-07-09 01:40:26 +04:00
|
|
|
LEFT JOIN versions AS v ON a.current_version=v.id
|
2013-03-31 06:09:25 +04:00
|
|
|
LEFT JOIN translations AS t_name
|
|
|
|
ON t_name.id=a.name AND t_name.locale=%(locale)s
|
|
|
|
LEFT JOIN translations AS t_desc
|
|
|
|
ON t_desc.id=a.summary AND t_desc.locale=%(locale)s
|
|
|
|
WHERE p.{primary_key}=%(id)s AND
|
2013-04-03 02:39:39 +04:00
|
|
|
a.addontype_id=%(atype)s AND a.status=4 AND a.inactive=0
|
2013-03-31 06:09:25 +04:00
|
|
|
""".format(primary_key=self.data['primary_key'])
|
|
|
|
|
|
|
|
self.cursor.execute(sql, self.data)
|
|
|
|
row = self.cursor.fetchone()
|
|
|
|
|
|
|
|
row_to_dict = lambda row: dict(zip((
|
2013-07-09 01:40:26 +04:00
|
|
|
'persona_id', 'addon_id', 'slug', 'current_version', 'name',
|
|
|
|
'description', 'username', 'header', 'footer', 'accentcolor',
|
|
|
|
'textcolor', 'modified'),
|
2013-03-31 06:09:25 +04:00
|
|
|
list(row)))
|
|
|
|
|
|
|
|
if row:
|
|
|
|
self.data['row'] = row_to_dict(row)
|
|
|
|
|
|
|
|
# Fall back to `en-US` if the name was null for our locale.
|
|
|
|
# TODO: Write smarter SQL and don't rerun the whole query.
|
|
|
|
if not self.data['row']['name']:
|
|
|
|
self.data['locale'] = 'en-US'
|
|
|
|
self.cursor.execute(sql, self.data)
|
|
|
|
row = self.cursor.fetchone()
|
|
|
|
if row:
|
|
|
|
self.data['row'] = row_to_dict(row)
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
# TODO: Cache on row['modified']
|
|
|
|
def get_json(self):
|
|
|
|
if not self.get_update():
|
|
|
|
# Persona not found.
|
|
|
|
return
|
|
|
|
|
|
|
|
row = self.data['row']
|
|
|
|
accent = row.get('accentcolor')
|
|
|
|
text = row.get('textcolor')
|
2013-04-29 00:33:08 +04:00
|
|
|
|
|
|
|
id_ = str(row[self.data['primary_key']])
|
|
|
|
|
|
|
|
data = {
|
|
|
|
'id': id_,
|
2013-03-31 06:09:25 +04:00
|
|
|
'name': row.get('name'),
|
|
|
|
'description': row.get('description'),
|
|
|
|
# TODO: Change this to be `addons_users.user.username`.
|
|
|
|
'author': row.get('username'),
|
|
|
|
# TODO: Change this to be `addons_users.user.display_name`.
|
|
|
|
'username': row.get('username'),
|
|
|
|
'headerURL': self.image_url(row['header']),
|
|
|
|
'footerURL': self.image_url(row['footer']),
|
2013-04-05 11:51:29 +04:00
|
|
|
'detailURL': self.locale_url(settings.SITE_URL,
|
|
|
|
'/addon/%s/' % row['slug']),
|
2013-03-31 06:09:25 +04:00
|
|
|
'previewURL': self.image_url('preview.png'),
|
|
|
|
'iconURL': self.image_url('icon.png'),
|
|
|
|
'dataurl': self.base64_icon(row['addon_id']),
|
|
|
|
'accentcolor': '#%s' % accent if accent else None,
|
|
|
|
'textcolor': '#%s' % text if text else None,
|
2013-04-05 11:51:29 +04:00
|
|
|
'updateURL': self.locale_url(settings.VAMO_URL,
|
2013-04-29 00:33:08 +04:00
|
|
|
'/themes/update-check/' + id_),
|
2013-04-25 20:17:50 +04:00
|
|
|
# 04-25-2013: Bumped for GP migration so we get new `updateURL`s.
|
2013-07-09 01:40:26 +04:00
|
|
|
'version': row.get('current_version', 0)
|
2013-04-29 00:33:08 +04:00
|
|
|
}
|
|
|
|
|
|
|
|
# If this theme was originally installed from getpersonas.com,
|
|
|
|
# we have to use the `<persona_id>?src=gp` version of the `updateURL`.
|
|
|
|
if self.from_gp:
|
|
|
|
data['updateURL'] += '?src=gp'
|
|
|
|
|
|
|
|
return json.dumps(data)
|
2013-03-31 06:09:25 +04:00
|
|
|
|
2013-04-03 02:39:39 +04:00
|
|
|
def image_path(self, filename):
|
|
|
|
row = self.data['row']
|
|
|
|
|
|
|
|
# Special cased for non-AMO-uploaded themes imported from getpersonas.
|
|
|
|
if row['persona_id'] != 0:
|
|
|
|
if filename == 'preview.png':
|
|
|
|
filename = 'preview.jpg'
|
|
|
|
elif filename == 'icon.png':
|
|
|
|
filename = 'preview_small.jpg'
|
|
|
|
|
2014-08-05 13:20:50 +04:00
|
|
|
return os.path.join(user_media_path('addons'), str(row['addon_id']),
|
2013-04-03 02:39:39 +04:00
|
|
|
filename)
|
|
|
|
|
2013-03-31 06:09:25 +04:00
|
|
|
def image_url(self, filename):
|
|
|
|
row = self.data['row']
|
|
|
|
|
|
|
|
# Special cased for non-AMO-uploaded themes imported from getpersonas.
|
|
|
|
if row['persona_id'] != 0:
|
|
|
|
if filename == 'preview.png':
|
|
|
|
filename = 'preview.jpg'
|
|
|
|
elif filename == 'icon.png':
|
|
|
|
filename = 'preview_small.jpg'
|
|
|
|
|
2014-08-06 19:25:44 +04:00
|
|
|
image_url = posixpath.join(user_media_path('addons'),
|
2013-04-03 02:39:39 +04:00
|
|
|
str(row['addon_id']), filename or '')
|
2013-04-03 06:19:10 +04:00
|
|
|
modified = int(row['modified']) if row['modified'] else 0
|
|
|
|
return '%s?%s' % (image_url, modified)
|
2013-03-31 06:09:25 +04:00
|
|
|
|
2013-04-05 11:51:29 +04:00
|
|
|
def locale_url(self, domain, url):
|
|
|
|
return '%s/%s%s' % (domain, self.data.get('locale', 'en-US'), url)
|
2013-03-31 06:09:25 +04:00
|
|
|
|
|
|
|
|
|
|
|
url_re = re.compile('(?P<locale>.+)?/themes/update-check/(?P<id>\d+)$')
|
|
|
|
|
|
|
|
|
|
|
|
def application(environ, start_response):
|
|
|
|
"""
|
|
|
|
Developing locally?
|
|
|
|
|
|
|
|
gunicorn -b 0.0.0.0:7000 -w 12 -k sync -t 90 --max-requests 5000 \
|
|
|
|
-n gunicorn-theme_update services.wsgi.theme_update:application
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
status = '200 OK'
|
|
|
|
with statsd.timer('services.theme_update'):
|
|
|
|
data = environ['wsgi.input'].read()
|
|
|
|
try:
|
|
|
|
locale, id_ = url_re.match(environ['PATH_INFO']).groups()
|
|
|
|
locale = (locale or 'en-US').lstrip('/')
|
|
|
|
id_ = int(id_)
|
|
|
|
except AttributeError: # URL path incorrect.
|
|
|
|
start_response('404 Not Found', [])
|
|
|
|
return ['']
|
|
|
|
|
|
|
|
try:
|
|
|
|
update = ThemeUpdate(locale, id_, environ.get('QUERY_STRING'))
|
|
|
|
output = update.get_json()
|
|
|
|
if not output:
|
|
|
|
start_response('404 Not Found', [])
|
|
|
|
return ['']
|
|
|
|
start_response(status, update.get_headers(len(output)))
|
|
|
|
except:
|
|
|
|
log_exception(data)
|
|
|
|
raise
|
|
|
|
|
|
|
|
return [output]
|