-
Notifications
You must be signed in to change notification settings - Fork 530
/
tasks.py
149 lines (128 loc) Β· 6.45 KB
/
tasks.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
import json
import logging
import os
import shutil
import zipfile
from django.db.models import Q
from celeryutils import task
from lxml import etree
import amo
from versions.models import Version
from lib.crypto.packaged import sign_file
from versions.compare import version_int
log = logging.getLogger('z.task')
# Minimum Firefox version for default to compatible addons.
MIN_D2C_VERSION = '4'
# Minimum Firefox version for not default to compatible addons.
MIN_NOT_D2C_VERSION = '37'
@task
def sign_addons(addon_ids, force=False, **kw):
"""Used to sign all the versions of an addon.
This is used in the 'sign_addons' and 'process_addons --task sign_addons'
management commands.
It also bumps the version number of the file and the Version, so the
Firefox extension update mecanism picks this new signed version and
installs it.
"""
log.info('[{0}] Signing addons.'.format(len(addon_ids)))
def file_supports_firefox(version):
"""Return a Q object: files supporting at least a firefox version."""
return Q(version__apps__max__application=amo.FIREFOX.id,
version__apps__max__version_int__gte=version_int(version))
is_default_compatible = Q(binary_components=False,
strict_compatibility=False)
# We only want to sign files that are at least compatible with Firefox
# MIN_D2C_VERSION, or Firefox MIN_NOT_D2C_VERSION if they are not default
# to compatible.
# The signing feature should be supported from Firefox 40 and above, but
# we're still signing some files that are a bit older just in case.
ff_version_filter = (
(is_default_compatible & file_supports_firefox(MIN_D2C_VERSION)) |
(~is_default_compatible & file_supports_firefox(MIN_NOT_D2C_VERSION)))
for version in Version.objects.filter(addon_id__in=addon_ids,
addon__type=amo.ADDON_EXTENSION):
to_sign = version.files.filter(ff_version_filter)
if force:
to_sign = to_sign.all()
else:
to_sign = [f for f in to_sign.all() if not f.is_signed]
if not to_sign:
log.info('Not signing addon {0}, version {1} (no files or already '
'signed)'.format(version.addon, version))
continue
log.info('Signing addon {0}, version {1}'.format(version.addon,
version))
bump_version = False # Did we sign at least one file?
for file_obj in to_sign:
if not os.path.isfile(file_obj.file_path):
log.info('File {0} does not exist, skip'.format(file_obj.pk))
continue
# Save the original file, before bumping the version.
backup_path = '{0}.backup_signature'.format(file_obj.file_path)
shutil.copy(file_obj.file_path, backup_path)
try:
# Need to bump the version (modify install.rdf or package.json)
# before the file is signed.
bump_version_number(file_obj)
signed = bool(sign_file(file_obj))
if signed: # Bump the version number if at least one signed.
bump_version = True
else: # We didn't sign, so revert the version bump.
shutil.move(backup_path, file_obj.file_path)
except:
log.error('Failed signing file {0}'.format(file_obj.pk),
exc_info=True)
# Revert the version bump, restore the backup.
shutil.move(backup_path, file_obj.file_path)
# Now update the Version model, if we signed at least one file.
if bump_version:
version.update(version=_dot_one(version.version))
def bump_version_number(file_obj):
"""Add a '.1-signed' to the version number."""
# Create a new xpi with the bumped version.
bumped = '{0}.bumped'.format(file_obj.file_path)
# Copy the original XPI, with the updated install.rdf or package.json.
with zipfile.ZipFile(file_obj.file_path, 'r') as source:
file_list = source.infolist()
with zipfile.ZipFile(bumped, 'w', zipfile.ZIP_DEFLATED) as dest:
for file_ in file_list:
content = source.read(file_.filename)
if file_.filename == 'install.rdf':
content = _bump_version_in_install_rdf(content)
if file_.filename == 'package.json':
content = _bump_version_in_package_json(content)
dest.writestr(file_, content)
# Move the bumped file to the original file.
shutil.move(bumped, file_obj.file_path)
def _dot_one(version):
"""Returns the version with an appended '.1-signed' on it."""
return '{0}.1-signed'.format(version)
def _bump_version_in_install_rdf(content):
"""Add a '.1-signed' to the version number in the install.rdf provided."""
# We need to use an XML parser, and not a RDF parser, because our
# install.rdf files aren't really standard (they use default namespaces,
# don't namespace the "about" attribute... rdflib can parse them, and can
# now even serialize them, but the end result could be very different from
# the format we need.
tree = etree.fromstring(content)
# There's two different formats for the install.rdf: the "standard" one
# uses nodes for each item (like <em:version>1.2</em:version>), the other
# alternate one sets attributes on the <RDF:Description
# RDF:about="urn:mozilla:install-manifest"> element.
# Get the version node, if it's the common format, or the Description node
# that has the "em:version" attribute if it's the alternate format.
namespace = 'http://www.mozilla.org/2004/em-rdf#'
version_uri = '{{{0}}}version'.format(namespace)
for node in tree.xpath('//em:version | //*[@em:version]',
namespaces={'em': namespace}):
if node.tag == version_uri: # Common format, version is a node.
node.text = _dot_one(node.text)
else: # Alternate format, version is an attribute.
node.set(version_uri, _dot_one(node.get(version_uri)))
return etree.tostring(tree, xml_declaration=True, encoding='utf-8')
def _bump_version_in_package_json(content):
"""Add a '.1-signed' to the version number in the package.json provided."""
bumped = json.loads(content)
if 'version' in bumped:
bumped['version'] = _dot_one(bumped['version'])
return json.dumps(bumped)