Skip to content

Commit 1225f8a

Browse files
committed
Refactored submission code to be clearer and only do mimetype extraction in one place, made the point where files are saved less obscure, fixed bytes/str issues for file read and write, fixed regex strings, fixed variable name visibility due to scope changes in py3.
- Legacy-Id: 16375
1 parent 2e92652 commit 1225f8a

7 files changed

Lines changed: 112 additions & 88 deletions

File tree

ietf/submit/forms.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,9 @@ def clean_file(self, field_name, parser_class):
108108
if not f:
109109
return f
110110

111-
parsed_info = parser_class(f).critical_parse()
112-
if parsed_info.errors:
113-
raise forms.ValidationError(parsed_info.errors)
111+
self.parsed_info = parser_class(f).critical_parse()
112+
if self.parsed_info.errors:
113+
raise forms.ValidationError(self.parsed_info.errors)
114114

115115
return f
116116

@@ -215,7 +215,7 @@ def clean(self):
215215
bytes = txt_file.read()
216216
txt_file.seek(0)
217217
try:
218-
text = bytes.decode('utf8')
218+
text = bytes.decode(self.parsed_info.charset)
219219
except UnicodeDecodeError as e:
220220
raise forms.ValidationError('Failed decoding the uploaded file: "%s"' % str(e))
221221
#

ietf/submit/mail.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from django.core.validators import ValidationError
1313
from django.contrib.sites.models import Site
1414
from django.template.loader import render_to_string
15+
from django.utils.encoding import force_text
1516

1617
import debug # pyflakes:ignore
1718

@@ -31,13 +32,13 @@ def send_submission_confirmation(request, submission, chair_notice=False):
3132
from_email = settings.IDSUBMIT_FROM_EMAIL
3233
(to_email, cc) = gather_address_lists('sub_confirmation_requested',submission=submission)
3334

34-
confirm_url = settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.confirm_submission', kwargs=dict(submission_id=submission.pk, auth_token=generate_access_token(submission.auth_key)))
35+
confirmation_url = settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.confirm_submission', kwargs=dict(submission_id=submission.pk, auth_token=generate_access_token(submission.auth_key)))
3536
status_url = settings.IDTRACKER_BASE_URL + urlreverse('ietf.submit.views.submission_status', kwargs=dict(submission_id=submission.pk, access_token=submission.access_token()))
3637

3738
send_mail(request, to_email, from_email, subject, 'submit/confirm_submission.txt',
3839
{
3940
'submission': submission,
40-
'confirm_url': confirm_url,
41+
'confirmation_url': confirmation_url,
4142
'status_url': status_url,
4243
'chair_notice': chair_notice,
4344
},
@@ -172,7 +173,7 @@ def get_reply_to():
172173
address with "plus addressing" using a random string. Guaranteed to be unique"""
173174
local,domain = get_base_submission_message_address().split('@')
174175
while True:
175-
rand = base64.urlsafe_b64encode(os.urandom(12))
176+
rand = force_text(base64.urlsafe_b64encode(os.urandom(12)))
176177
address = "{}+{}@{}".format(local,rand,domain)
177178
q = Message.objects.filter(reply_to=address)
178179
if not q:

ietf/submit/parsers/base.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,23 @@ def parse_filename_extension(self):
7777

7878
def parse_file_type(self):
7979
self.fd.file.seek(0)
80-
content = self.fd.file.read()
81-
mimetype = magic.from_buffer(content, mime=True)
80+
content = self.fd.file.read(64*1024)
81+
if hasattr(magic, "open"):
82+
m = magic.open(magic.MAGIC_MIME)
83+
m.load()
84+
filetype = m.buffer(content)
85+
else:
86+
m = magic.Magic()
87+
m.cookie = magic.magic_open(magic.MAGIC_NONE | magic.MAGIC_MIME | magic.MAGIC_MIME_ENCODING)
88+
magic.magic_load(m.cookie, None)
89+
filetype = m.from_buffer(content)
90+
if ';' in filetype and 'charset=' in filetype:
91+
mimetype, charset = re.split('; *charset=', filetype)
92+
else:
93+
mimetype = re.split(';', filetype)[0]
94+
charset = 'utf-8'
8295
if not mimetype in self.mimetypes:
8396
self.parsed_info.add_error('Expected an %s file of type "%s", found one of type "%s"' % (self.ext.upper(), '" or "'.join(self.mimetypes), mimetype))
97+
self.parsed_info.mimetype = mimetype
98+
self.parsed_info.charset = charset
99+
Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Copyright The IETF Trust 2011-2019, All Rights Reserved
2+
23
import re
34

5+
import debug # pyflakes:ignore
6+
47
from ietf.submit.parsers.base import FileParser
58

69

@@ -15,58 +18,51 @@ def __init__(self, fd):
1518
# no other file parsing is recommended
1619
def critical_parse(self):
1720
super(PlainParser, self).critical_parse()
18-
self.parse_file_charset()
21+
self.check_file_charset()
1922
self.parse_name()
2023
return self.parsed_info
2124

22-
def parse_file_charset(self):
23-
import magic
24-
self.fd.file.seek(0)
25-
content = self.fd.file.read()
26-
if hasattr(magic, "open"):
27-
m = magic.open(magic.MAGIC_MIME)
28-
m.load()
29-
filetype = m.buffer(content)
30-
else:
31-
m = magic.Magic()
32-
m.cookie = magic.magic_open(magic.MAGIC_NONE | magic.MAGIC_MIME | magic.MAGIC_MIME_ENCODING)
33-
magic.magic_load(m.cookie, None)
34-
filetype = m.from_buffer(content)
35-
if not 'ascii' in filetype and not 'utf-8' in filetype:
25+
def check_file_charset(self):
26+
charset = self.parsed_info.charset
27+
if not charset in ['us-ascii', 'utf-8',]:
3628
self.parsed_info.add_error('A plain text ASCII document is required. '
3729
'Found an unexpected encoding: "%s". '
38-
'You probably have one or more non-ascii characters in your file.' % filetype
30+
'You probably have one or more non-ascii characters in your file.' % charset
3931
)
32+
if self.fd.charset and charset != self.fd.charset:
33+
self.parsed_info.add_error("Unexpected charset mismatch: upload: %s, libmagic: %s" % (self.fd.charset, charset))
34+
4035

4136
def parse_name(self):
4237
self.fd.file.seek(0)
43-
draftre = re.compile('(draft-\S+)')
44-
revisionre = re.compile('.*-(\d+)$')
38+
draftre = re.compile(r'(draft-\S+)')
39+
revisionre = re.compile(r'.*-(\d+)$')
4540
limit = 80
46-
while limit:
47-
limit -= 1
48-
line = self.fd.readline()
49-
match = draftre.search(line)
50-
if not match:
51-
continue
52-
name = match.group(1)
53-
name = re.sub('^[^\w]+', '', name)
54-
name = re.sub('[^\w]+$', '', name)
55-
name = re.sub('\.txt$', '', name)
56-
extra_chars = re.sub('[0-9a-z\-]', '', name)
57-
if extra_chars:
58-
if len(extra_chars) == 1:
59-
self.parsed_info.add_error(('The document name on the first page, "%s", contains a disallowed character with byte code: %s ' % (name.decode('utf-8','replace'), ord(extra_chars[0]))) +
60-
'(see https://www.ietf.org/id-info/guidelines.html#naming for details).')
41+
if self.parsed_info.charset in ['us-ascii', 'utf-8']:
42+
while limit:
43+
limit -= 1
44+
line = self.fd.readline().decode(self.parsed_info.charset)
45+
match = draftre.search(line)
46+
if not match:
47+
continue
48+
name = match.group(1)
49+
name = re.sub(r'^[^\w]+', '', name)
50+
name = re.sub(r'[^\w]+$', '', name)
51+
name = re.sub(r'\.txt$', '', name)
52+
extra_chars = re.sub(r'[0-9a-z\-]', '', name)
53+
if extra_chars:
54+
if len(extra_chars) == 1:
55+
self.parsed_info.add_error(('The document name on the first page, "%s", contains a disallowed character with byte code: %s ' % (name.decode('utf-8','replace'), ord(extra_chars[0]))) +
56+
'(see https://www.ietf.org/id-info/guidelines.html#naming for details).')
57+
else:
58+
self.parsed_info.add_error(('The document name on the first page, "%s", contains disallowed characters with byte codes: %s ' % (name.decode('utf-8','replace'), (', '.join([ str(ord(c)) for c in extra_chars] )))) +
59+
'(see https://www.ietf.org/id-info/guidelines.html#naming for details).')
60+
match_revision = revisionre.match(name)
61+
if match_revision:
62+
self.parsed_info.metadata.rev = match_revision.group(1)
6163
else:
62-
self.parsed_info.add_error(('The document name on the first page, "%s", contains disallowed characters with byte codes: %s ' % (name.decode('utf-8','replace'), (', '.join([ str(ord(c)) for c in extra_chars] )))) +
63-
'(see https://www.ietf.org/id-info/guidelines.html#naming for details).')
64-
match_revision = revisionre.match(name)
65-
if match_revision:
66-
self.parsed_info.metadata.rev = match_revision.group(1)
67-
else:
68-
self.parsed_info.add_error('The name found on the first page of the document does not contain a revision: "%s"' % (name.decode('utf-8','replace'),))
69-
name = re.sub('-\d+$', '', name)
70-
self.parsed_info.metadata.name = name
71-
return
72-
self.parsed_info.add_error('The first page of the document does not contain a legitimate name that start with draft-*')
64+
self.parsed_info.add_error('The name found on the first page of the document does not contain a revision: "%s"' % (name.decode('utf-8','replace'),))
65+
name = re.sub(r'-\d+$', '', name)
66+
self.parsed_info.metadata.name = name
67+
return
68+
self.parsed_info.add_error('The first page of the document does not contain a legitimate name that starts with draft-*')

ietf/submit/tests.py

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# Copyright The IETF Trust 2011-2019, All Rights Reserved
22
# -*- coding: utf-8 -*-
33

4-
54
import datetime
65
import email
76
import os
@@ -35,7 +34,7 @@
3534
from ietf.submit.mail import add_submission_email, process_response_email
3635
from ietf.utils.mail import outbox, empty_outbox
3736
from ietf.utils.models import VersionInfo
38-
from ietf.utils.test_utils import login_testing_unauthorized, unicontent, TestCase
37+
from ietf.utils.test_utils import login_testing_unauthorized, TestCase
3938
from ietf.utils.draft import Draft
4039

4140

@@ -185,17 +184,18 @@ def supply_extra_metadata(self, name, status_url, submitter_name, submitter_emai
185184

186185
return r
187186

188-
def extract_confirm_url(self, confirm_email):
189-
# dig out confirm_email link
190-
msg = confirm_email.get_payload(decode=True)
187+
def extract_confirmation_url(self, confirmation_email):
188+
# dig out confirmation_email link
189+
charset = confirmation_email.get_content_charset()
190+
msg = confirmation_email.get_payload(decode=True).decode(charset)
191191
line_start = "http"
192-
confirm_url = None
192+
confirmation_url = None
193193
for line in msg.split("\n"):
194194
if line.strip().startswith(line_start):
195-
confirm_url = line.strip()
196-
self.assertTrue(confirm_url)
195+
confirmation_url = line.strip()
196+
self.assertTrue(confirmation_url)
197197

198-
return confirm_url
198+
return confirmation_url
199199

200200
def submit_new_wg(self, formats):
201201
# submit new -> supply submitter info -> approve
@@ -420,16 +420,16 @@ def submit_existing(self, formats, change_authors=True, group_type='wg', stream_
420420
self.assertNotIn("chairs have been copied", str(confirm_email))
421421
self.assertNotIn("mars-chairs@", confirm_email["To"].lower())
422422

423-
confirm_url = self.extract_confirm_url(confirm_email)
423+
confirmation_url = self.extract_confirmation_url(confirm_email)
424424

425425
# go to confirm page
426-
r = self.client.get(confirm_url)
426+
r = self.client.get(confirmation_url)
427427
q = PyQuery(r.content)
428428
self.assertEqual(len(q('[type=submit]:contains("Confirm")')), 1)
429429

430430
# confirm
431431
mailbox_before = len(outbox)
432-
r = self.client.post(confirm_url, {'action':'confirm'})
432+
r = self.client.post(confirmation_url, {'action':'confirm'})
433433
self.assertEqual(r.status_code, 302)
434434

435435
new_docevents = draft.docevent_set.exclude(pk__in=[event.pk for event in old_docevents])
@@ -558,16 +558,16 @@ def submit_new_individual(self, formats):
558558
self.assertTrue("submitter@example.com" in confirm_email["To"])
559559
self.assertFalse("chairs have been copied" in str(confirm_email))
560560

561-
confirm_url = self.extract_confirm_url(outbox[-1])
561+
confirmation_url = self.extract_confirmation_url(outbox[-1])
562562

563563
# go to confirm page
564-
r = self.client.get(confirm_url)
564+
r = self.client.get(confirmation_url)
565565
q = PyQuery(r.content)
566566
self.assertEqual(len(q('[type=submit]:contains("Confirm")')), 1)
567567

568568
# confirm
569569
mailbox_before = len(outbox)
570-
r = self.client.post(confirm_url, {'action':'confirm'})
570+
r = self.client.post(confirmation_url, {'action':'confirm'})
571571
self.assertEqual(r.status_code, 302)
572572

573573
draft = Document.objects.get(docalias__name=name)
@@ -613,10 +613,10 @@ def test_submit_update_individual(self):
613613
status_url = r["Location"]
614614
r = self.client.get(status_url)
615615
self.assertEqual(len(outbox), mailbox_before + 1)
616-
confirm_url = self.extract_confirm_url(outbox[-1])
616+
confirmation_url = self.extract_confirmation_url(outbox[-1])
617617
self.assertFalse("chairs have been copied" in str(outbox[-1]))
618618
mailbox_before = len(outbox)
619-
r = self.client.post(confirm_url, {'action':'confirm'})
619+
r = self.client.post(confirmation_url, {'action':'confirm'})
620620
self.assertEqual(r.status_code, 302)
621621
self.assertEqual(len(outbox), mailbox_before+3)
622622
draft = Document.objects.get(docalias__name=name)
@@ -641,9 +641,9 @@ def test_submit_cancel_confirmation(self):
641641
status_url = r["Location"]
642642
r = self.client.get(status_url)
643643
self.assertEqual(len(outbox), mailbox_before + 1)
644-
confirm_url = self.extract_confirm_url(outbox[-1])
644+
confirmation_url = self.extract_confirmation_url(outbox[-1])
645645
mailbox_before = len(outbox)
646-
r = self.client.post(confirm_url, {'action':'cancel'})
646+
r = self.client.post(confirmation_url, {'action':'cancel'})
647647
self.assertEqual(r.status_code, 302)
648648
self.assertEqual(len(outbox), mailbox_before)
649649
draft = Document.objects.get(docalias__name=name)
@@ -818,7 +818,8 @@ def test_search_for_submission_and_edit_as_secretariat(self):
818818

819819
# status page as unpriviliged => no edit button
820820
r = self.client.get(unprivileged_status_url)
821-
self.assertContains(r, "submission status of %s" % name)
821+
print(r.content)
822+
self.assertContains(r, "Submission status of %s" % name)
822823
q = PyQuery(r.content)
823824
adjust_button = q('[type=submit]:contains("Adjust")')
824825
self.assertEqual(len(adjust_button), 0)
@@ -1688,7 +1689,7 @@ def test_draft_refs_identification(self):
16881689

16891690
group = None
16901691
file, __ = submission_file('draft-some-subject', '00', group, 'txt', "test_submission.txt", )
1691-
draft = Draft(file.read().decode('utf-8'), file.name)
1692+
draft = Draft(file.read(), file.name)
16921693
refs = draft.get_refs()
16931694
self.assertEqual(refs['rfc2119'], 'norm')
16941695
self.assertEqual(refs['rfc8174'], 'norm')

ietf/submit/utils.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -458,7 +458,7 @@ def ensure_person_email_info_exists(name, email, docname):
458458
person.name = name
459459
person.name_from_draft = name
460460
log.assertion('isinstance(person.name, six.text_type)')
461-
person.ascii = unidecode_name(person.name).decode('ascii')
461+
person.ascii = unidecode_name(person.name)
462462
person.save()
463463
else:
464464
person.name_from_draft = name
@@ -595,11 +595,8 @@ def expire_submission(submission, by):
595595

596596
SubmissionEvent.objects.create(submission=submission, by=by, desc="Cancelled expired submission")
597597

598-
def get_draft_meta(form):
599-
authors = []
598+
def save_files(form):
600599
file_name = {}
601-
abstract = None
602-
file_size = None
603600
for ext in list(form.fields.keys()):
604601
if not ext in form.formats:
605602
continue
@@ -612,7 +609,13 @@ def get_draft_meta(form):
612609
with open(name, 'wb+') as destination:
613610
for chunk in f.chunks():
614611
destination.write(chunk)
612+
return file_name
615613

614+
def get_draft_meta(form, saved_files):
615+
authors = []
616+
file_name = saved_files
617+
abstract = None
618+
file_size = None
616619
if form.cleaned_data['xml']:
617620
if not ('txt' in form.cleaned_data and form.cleaned_data['txt']):
618621
file_name['txt'] = os.path.join(settings.IDSUBMIT_STAGING_PATH, '%s-%s.txt' % (form.filename, form.revision))
@@ -640,7 +643,7 @@ def get_draft_meta(form):
640643
# be retrieved from the generated text file. Provide a
641644
# parsed draft object to get at that kind of information.
642645
with open(file_name['txt']) as txt_file:
643-
form.parsed_draft = Draft(txt_file.read().decode('utf8'), txt_file.name)
646+
form.parsed_draft = Draft(txt_file.read(), txt_file.name)
644647

645648
else:
646649
file_size = form.cleaned_data['txt'].size

0 commit comments

Comments
 (0)