forked from mozilla/zamboni
-
Notifications
You must be signed in to change notification settings - Fork 8
/
update.py
315 lines (263 loc) · 11 KB
/
update.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
from email.Utils import formatdate
from email.mime.text import MIMEText
import smtplib
import sys
from time import time
import traceback
from urlparse import parse_qsl
import MySQLdb as mysql
import sqlalchemy.pool as pool
import commonware.log
from django.core.management import setup_environ
import settings_local as settings
setup_environ(settings)
import log_settings
try:
from compare import version_int
except ImportError:
from apps.versions.compare import version_int
from utils import (get_mirror,
APP_GUIDS, PLATFORMS, VERSION_BETA,
STATUS_PUBLIC, STATUSES_PUBLIC, STATUS_BETA, STATUS_NULL,
STATUS_LITE, STATUS_LITE_AND_NOMINATED, ADDON_SLUGS_UPDATE)
good_rdf = """<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<RDF:Description about="urn:mozilla:%(type)s:%(guid)s">
<em:updates>
<RDF:Seq>
<RDF:li resource="urn:mozilla:%(type)s:%(guid)s:%(version)s"/>
</RDF:Seq>
</em:updates>
</RDF:Description>
<RDF:Description about="urn:mozilla:%(type)s:%(guid)s:%(version)s">
<em:version>%(version)s</em:version>
<em:targetApplication>
<RDF:Description>
<em:id>%(appguid)s</em:id>
<em:minVersion>%(min)s</em:minVersion>
<em:maxVersion>%(max)s</em:maxVersion>
<em:updateLink>%(url)s</em:updateLink>
%(if_update)s
%(if_hash)s
</RDF:Description>
</em:targetApplication>
</RDF:Description>
</RDF:RDF>"""
bad_rdf = """<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
</RDF:RDF>"""
no_updates_rdf = """<?xml version="1.0"?>
<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<RDF:Description about="urn:mozilla:%(type)s:%(guid)s">
<em:updates>
<RDF:Seq>
</RDF:Seq>
</em:updates>
</RDF:Description>
</RDF:RDF>"""
timing_log = commonware.log.getLogger('z.timer')
def getconn():
db = settings.SERVICES_DATABASE
return mysql.connect(host=db['HOST'], user=db['USER'],
passwd=db['PASSWORD'], db=db['NAME'])
mypool = pool.QueuePool(getconn, max_overflow=10, pool_size=5, recycle=3600)
class Update(object):
def __init__(self, data):
self.conn, self.cursor = None, None
self.data = data.copy()
self.data['row'] = {}
self.flags = {'use_version': False, 'multiple_status': False}
self.is_beta_version = False
self.version_int = 0
def is_valid(self):
# If you accessing this from unit tests, then before calling
# is valid, you can assign your own cursor.
if not self.cursor:
self.conn = mypool.connect()
self.cursor = self.conn.cursor()
data = self.data
for field in ['reqVersion', 'id', 'version', 'appID', 'appVersion']:
if field not in data:
return False
data['app_id'] = APP_GUIDS.get(data['appID'])
if not data['app_id']:
return False
sql = """SELECT id, status, addontype_id, guid FROM addons
WHERE guid = %(guid)s AND inactive = 0 LIMIT 1;"""
self.cursor.execute(sql, {'guid': self.data['id']})
result = self.cursor.fetchone()
if result is None:
return False
data['id'], data['addon_status'], data['type'], data['guid'] = result
data['version_int'] = version_int(data['appVersion'])
if 'appOS' in data:
for k, v in PLATFORMS.items():
if k in data['appOS']:
data['appOS'] = v
break
else:
data['appOS'] = None
self.is_beta_version = VERSION_BETA.search(data.get('version', ''))
return True
def get_beta(self):
data = self.data
data['status'] = STATUS_PUBLIC
if data['addon_status'] == STATUS_PUBLIC:
# Beta channel looks at the addon name to see if it's beta.
if self.is_beta_version:
# For beta look at the status of the existing files.
sql = """
SELECT versions.id, status
FROM files INNER JOIN versions
ON files.version_id = versions.id
WHERE versions.addon_id = %(id)s
AND versions.version = %(version)s LIMIT 1;"""
self.cursor.execute(sql, data)
result = self.cursor.fetchone()
# Only change the status if there are files.
if result is not None:
status = result[1]
# If it's in Beta or Public, then we should be looking
# for similar. If not, find something public.
if status in (STATUS_BETA, STATUS_PUBLIC):
data['status'] = status
else:
data.update(STATUSES_PUBLIC)
self.flags['multiple_status'] = True
elif data['addon_status'] in (STATUS_LITE, STATUS_LITE_AND_NOMINATED):
data['status'] = STATUS_LITE
else:
# Otherwise then we'll keep the update within the current version.
data['status'] = STATUS_NULL
self.flags['use_version'] = True
def get_update(self):
self.get_beta()
data = self.data
sql = """
SELECT
addons.guid as guid, addons.addontype_id as type,
addons.inactive as disabled_by_user,
applications.guid as appguid, appmin.version as min,
appmax.version as max, files.id as file_id,
files.status as file_status, files.hash,
files.filename, versions.id as version_id,
files.datestatuschanged as datestatuschanged,
versions.releasenotes, versions.version as version
FROM versions
INNER JOIN addons
ON addons.id = versions.addon_id AND addons.id = %(id)s
INNER JOIN applications_versions
ON applications_versions.version_id = versions.id
INNER JOIN applications
ON applications_versions.application_id = applications.id
AND applications.id = %(app_id)s
INNER JOIN appversions appmin
ON appmin.id = applications_versions.min
INNER JOIN appversions appmax
ON appmax.id = applications_versions.max
INNER JOIN files
ON files.version_id = versions.id AND (files.platform_id = 1
"""
if data.get('appOS'):
sql += ' OR files.platform_id = %(appOS)s'
if self.flags['use_version']:
sql += (') WHERE files.status > %(status)s AND '
'versions.version = %(version)s ')
else:
if self.flags['multiple_status']:
# Note that getting this properly escaped is a pain.
# Suggestions for improvement welcome.
sql += (') WHERE files.status in (%(STATUS_PUBLIC)s,'
'%(STATUS_LITE)s,%(STATUS_LITE_AND_NOMINATED)s)')
else:
sql += ') WHERE files.status = %(status)s '
sql += """
AND (appmin.version_int <= %(version_int)s
AND appmax.version_int >= %(version_int)s)
ORDER BY versions.id DESC LIMIT 1;
"""
self.cursor.execute(sql, data)
result = self.cursor.fetchone()
if result:
row = dict(zip([
'guid', 'type', 'disabled_by_user', 'appguid', 'min', 'max',
'file_id', 'file_status', 'hash', 'filename', 'version_id',
'datestatuschanged', 'releasenotes', 'version'],
list(result)))
row['type'] = ADDON_SLUGS_UPDATE[row['type']]
row['url'] = get_mirror(self.data['addon_status'],
self.data['id'], row)
data['row'] = row
return True
return False
def get_bad_rdf(self):
return bad_rdf
def get_rdf(self):
if self.is_valid():
if self.get_update():
rdf = self.get_good_rdf()
else:
rdf = self.get_no_updates_rdf()
else:
rdf = self.get_bad_rdf()
self.cursor.close()
if self.conn:
self.conn.close()
return rdf
def get_no_updates_rdf(self):
name = ADDON_SLUGS_UPDATE[self.data['type']]
return no_updates_rdf % ({'guid': self.data['guid'], 'type': name})
def get_good_rdf(self):
data = self.data['row']
data['if_hash'] = ''
if data['hash']:
data['if_hash'] = ('<em:updateHash>%s</em:updateHash>' %
data['hash'])
data['if_update'] = ''
if data['releasenotes']:
data['if_update'] = ('<em:updateInfoURL>%s%s%s/%%APP_LOCALE%%/'
'</em:updateInfoURL>' %
(settings.SITE_URL, '/versions/updateInfo/',
data['version_id']))
return good_rdf % data
def format_date(self, secs):
return '%s GMT' % formatdate(time() + secs)[:25]
def get_headers(self, length):
return [('Content-Type', 'text/xml'),
('Cache-Control', 'public, max-age=3600'),
('Last-Modified', self.format_date(0)),
('Expires', self.format_date(3600)),
('Content-Length', str(length))]
def mail_exception(data):
if settings.EMAIL_BACKEND != 'django.core.mail.backends.smtp.EmailBackend':
return
msg = MIMEText('%s\n\n%s' % (
'\n'.join(traceback.format_exception(*sys.exc_info())), data))
msg['Subject'] = '[Update] ERROR at /services/update'
msg['To'] = ','.join([a[1] for a in settings.ADMINS])
msg['From'] = settings.DEFAULT_FROM_EMAIL
conn = smtplib.SMTP(getattr(settings, 'EMAIL_HOST', 'localhost'),
getattr(settings, 'EMAIL_PORT', '25'))
conn.sendmail(settings.DEFAULT_FROM_EMAIL, msg['To'], msg.as_string())
conn.close()
def application(environ, start_response):
start = time()
status = '200 OK'
timing = (environ['REQUEST_METHOD'], '%s?%s' %
(environ['SCRIPT_NAME'], environ['QUERY_STRING']))
data = dict(parse_qsl(environ['QUERY_STRING']))
try:
update = Update(data)
output = update.get_rdf()
start_response(status, update.get_headers(len(output)))
except:
timing_log.info('%s "%s" (500) %.2f [ANON]' %
(timing[0], timing[1], time() - start))
mail_exception(data)
raise
timing_log.info('%s "%s" (200) %.2f [ANON]' %
(timing[0], timing[1], time() - start))
return [output]