From 328ee129588c4b096808ab4e33afedd4b2cc5833 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Thu, 24 Jul 2025 13:04:27 +0200 Subject: [PATCH] fix: set charset for text/plain responses (#9120) * fix: charset for text/plain Use settings.DEFAULT_CHARSET * fix: more charset for text/plain ALL THE PLACES --- ietf/api/views.py | 30 +++++++++-- ietf/doc/views_ballot.py | 12 ++++- ietf/iesg/views.py | 27 +++++++--- ietf/meeting/views.py | 112 +++++++++++++++++++++++++++++++-------- ietf/nomcom/views.py | 5 +- ietf/submit/views.py | 5 +- ietf/utils/decorators.py | 2 +- 7 files changed, 155 insertions(+), 38 deletions(-) diff --git a/ietf/api/views.py b/ietf/api/views.py index b4dd7f05d6b..22523b2f172 100644 --- a/ietf/api/views.py +++ b/ietf/api/views.py @@ -114,7 +114,11 @@ class ApiV2PersonExportView(DetailView, JsonExportMixin): model = Person def err(self, code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def post(self, request): querydict = request.POST.copy() @@ -152,7 +156,11 @@ def post(self, request): def api_new_meeting_registration_v2(request): '''REST API to notify the datatracker about a new meeting registration''' def _http_err(code, text): - return HttpResponse(text, status=code, content_type="text/plain") + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def _api_response(result): return JsonResponse(data={"result": result}) @@ -192,7 +200,11 @@ def _api_response(result): process_single_registration(reg_data, meeting) - return HttpResponse('Success', status=202, content_type='text/plain') + return HttpResponse( + 'Success', + status=202, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def version(request): @@ -511,7 +523,11 @@ def related_email_list(request, email): to Datatracker, via Person object """ def _http_err(code, text): - return HttpResponse(text, status=code, content_type="text/plain") + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method == "GET": try: @@ -637,7 +653,11 @@ def ingest_email_handler(request, test_mode=False): """ def _http_err(code, text): - return HttpResponse(text, status=code, content_type="text/plain") + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def _api_response(result): return JsonResponse(data={"result": result}) diff --git a/ietf/doc/views_ballot.py b/ietf/doc/views_ballot.py index 9e2a417933d..0ba340890d6 100644 --- a/ietf/doc/views_ballot.py +++ b/ietf/doc/views_ballot.py @@ -257,7 +257,11 @@ def edit_position(request, name, ballot_id): @csrf_exempt def api_set_position(request): def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method == 'POST': ad = request.user.person name = request.POST.get('doc') @@ -290,7 +294,11 @@ def err(code, text): addrs, frm, subject, body = build_position_email(ad, doc, pos) send_mail_text(request, addrs.to, frm, subject, body, cc=addrs.cc) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def build_position_email(balloter, doc, pos): diff --git a/ietf/iesg/views.py b/ietf/iesg/views.py index f1fe77f7633..7b9f489b442 100644 --- a/ietf/iesg/views.py +++ b/ietf/iesg/views.py @@ -221,7 +221,7 @@ def agenda_txt(request, date=None): "date": data["date"], "sections": sorted(data["sections"].items(), key=lambda x:[int(p) for p in x[0].split('.')]), "domain": Site.objects.get_current().domain, - }, content_type="text/plain; charset=%s"%settings.DEFAULT_CHARSET) + }, content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}") @role_required('Area Director', 'Secretariat') def agenda_moderator_package(request, date=None): @@ -277,14 +277,23 @@ def leaf_section(num, section): @role_required('Area Director', 'Secretariat') def agenda_package(request, date=None): data = agenda_data(date) - return render(request, "iesg/agenda_package.txt", { + return render( + request, + "iesg/agenda_package.txt", + { "date": data["date"], "sections": sorted(data["sections"].items()), "roll_call": data["sections"]["1.1"]["text"], "minutes": data["sections"]["1.3"]["text"], - "management_items": [(num, section) for num, section in data["sections"].items() if "6" < num < "7"], + "management_items": [ + (num, section) + for num, section in data["sections"].items() + if "6" < num < "7" + ], "domain": Site.objects.get_current().domain, - }, content_type='text/plain') + }, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def agenda_documents_txt(request): @@ -315,7 +324,10 @@ def agenda_documents_txt(request): d.rev, ) rows.append("\t".join(row)) - return HttpResponse("\n".join(rows), content_type='text/plain') + return HttpResponse( + "\n".join(rows), + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) class RescheduleForm(forms.Form): telechat_date = forms.TypedChoiceField(coerce=lambda x: datetime.datetime.strptime(x, '%Y-%m-%d').date(), empty_value=None, required=False) @@ -610,4 +622,7 @@ def telechat_agenda_content_manage(request): @role_required("Secretariat", "IAB Chair", "Area Director") def telechat_agenda_content_view(request, section): content = get_object_or_404(TelechatAgendaContent, section__slug=section, section__used=True) - return HttpResponse(content=content.text, content_type="text/plain; charset=utf-8") + return HttpResponse( + content=content.text, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) diff --git a/ietf/meeting/views.py b/ietf/meeting/views.py index 8bd70a3733f..85eda5a8f4e 100644 --- a/ietf/meeting/views.py +++ b/ietf/meeting/views.py @@ -2760,7 +2760,10 @@ def upload_session_bluesheets(request, session_id, num): ota = session.official_timeslotassignment() sess_time = ota and ota.timeslot.time if not sess_time: - return HttpResponseGone("Cannot receive uploads for an unscheduled session. Please check the session ID.", content_type="text/plain") + return HttpResponseGone( + "Cannot receive uploads for an unscheduled session. Please check the session ID.", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) save_error = save_bluesheet(request, session, file, encoding=form.file_encoding[file.name]) @@ -2821,7 +2824,7 @@ def upload_session_minutes(request, session_id, num): except SessionNotScheduledError: return HttpResponseGone( "Cannot receive uploads for an unscheduled session. Please check the session ID.", - content_type="text/plain", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", ) except SaveMaterialsError as err: form.add_error(None, str(err)) @@ -2880,7 +2883,7 @@ def upload_session_narrativeminutes(request, session_id, num): except SessionNotScheduledError: return HttpResponseGone( "Cannot receive uploads for an unscheduled session. Please check the session ID.", - content_type="text/plain", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", ) except SaveMaterialsError as err: form.add_error(None, str(err)) @@ -2978,7 +2981,10 @@ def upload_session_agenda(request, session_id, num): ota = session.official_timeslotassignment() sess_time = ota and ota.timeslot.time if not sess_time: - return HttpResponseGone("Cannot receive uploads for an unscheduled session. Please check the session ID.", content_type="text/plain") + return HttpResponseGone( + "Cannot receive uploads for an unscheduled session. Please check the session ID.", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if session.meeting.type_id=='ietf': name = 'agenda-%s-%s' % (session.meeting.number, session.group.acronym) @@ -4282,11 +4288,17 @@ def api_set_meetecho_recording_name(request): name: the name to use for the recording at meetecho player """ def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != "POST": return HttpResponseNotAllowed( - content="Method not allowed", content_type="text/plain", permitted_methods=('POST',) + content="Method not allowed", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + permitted_methods=('POST',), ) session_id = request.POST.get('session_id', None) @@ -4306,7 +4318,11 @@ def err(code, text): session.meetecho_recording_name = name session.save() - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) @require_api_key @role_required('Recording Manager') @@ -4320,11 +4336,17 @@ def api_set_session_video_url(request): url: The recording url (on YouTube, or whatever) """ def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != 'POST': return HttpResponseNotAllowed( - content="Method not allowed", content_type="text/plain", permitted_methods=('POST',) + content="Method not allowed", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + permitted_methods=('POST',), ) # Temporary: fall back to deprecated interface if we have old-style parameters. @@ -4363,7 +4385,11 @@ def err(code, text): time = session.official_timeslotassignment().timeslot.time title = 'Video recording for %s on %s at %s' % (session.group.acronym, time.date(), time.time()) create_recording(session, incoming_url, title=title, user=request.user.person) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def deprecated_api_set_session_video_url(request): @@ -4372,7 +4398,11 @@ def deprecated_api_set_session_video_url(request): Uses meeting/group/item to identify session. """ def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method == 'POST': # parameters: # apikey: the poster's personal API key @@ -4426,7 +4456,11 @@ def err(code, text): else: return err(405, "Method not allowed") - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) @require_api_key @@ -4478,7 +4512,11 @@ def api_add_session_attendees(request): ) def err(code, text): - return HttpResponse(text, status=code, content_type="text/plain") + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != "POST": return err(405, "Method not allowed") @@ -4531,7 +4569,11 @@ def err(code, text): if save_error: return err(400, save_error) - return HttpResponse("Done", status=200, content_type="text/plain") + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) @require_api_key @@ -4539,7 +4581,11 @@ def err(code, text): @csrf_exempt def api_upload_chatlog(request): def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != 'POST': return err(405, "Method not allowed") apidata_post = request.POST.get('apidata') @@ -4572,14 +4618,22 @@ def err(code, text): write_doc_for_session(session, 'chatlog', filename, json.dumps(apidata['chatlog'])) e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) doc.save_with_history([e]) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) @require_api_key @role_required('Recording Manager') @csrf_exempt def api_upload_polls(request): def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != 'POST': return err(405, "Method not allowed") apidata_post = request.POST.get('apidata') @@ -4612,7 +4666,11 @@ def err(code, text): write_doc_for_session(session, 'polls', filename, json.dumps(apidata['polls'])) e = NewRevisionDocEvent.objects.create(doc=doc, rev=doc.rev, by=request.user.person, type='new_revision', desc='New revision available: %s'%doc.rev) doc.save_with_history([e]) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) @require_api_key @role_required('Recording Manager', 'Secretariat') @@ -4627,11 +4685,17 @@ def api_upload_bluesheet(request): [{'name': 'Name', 'affiliation': 'Organization', }, ...] """ def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse( + text, + status=code, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) if request.method != 'POST': return HttpResponseNotAllowed( - content="Method not allowed", content_type="text/plain", permitted_methods=('POST',) + content="Method not allowed", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + permitted_methods=('POST',), ) session_id = request.POST.get('session_id', None) @@ -4666,7 +4730,11 @@ def err(code, text): save_err = save_bluesheet(request, session, file) if save_err: return err(400, save_err) - return HttpResponse("Done", status=200, content_type='text/plain') + return HttpResponse( + "Done", + status=200, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) def important_dates(request, num=None, output_format=None): @@ -5090,7 +5158,7 @@ def import_session_minutes(request, session_id, num): except SessionNotScheduledError: return HttpResponseGone( "Cannot import minutes for an unscheduled session. Please check the session ID.", - content_type="text/plain", + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", ) except SaveMaterialsError as err: form.add_error(None, str(err)) diff --git a/ietf/nomcom/views.py b/ietf/nomcom/views.py index d34126b1e7f..c04e13f92b7 100644 --- a/ietf/nomcom/views.py +++ b/ietf/nomcom/views.py @@ -1017,7 +1017,10 @@ def view_feedback_nominee(request, year, nominee_id): 'positions': ','.join([str(p) for p in feedback.positions.all()]), }, request=request) - response = HttpResponse(response, content_type='text/plain') + response = HttpResponse( + response, + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) response['Content-Disposition'] = f'attachment; filename="{fn}"' return response elif submit == 'reclassify': diff --git a/ietf/submit/views.py b/ietf/submit/views.py index 2b9b55c00e2..043b6130168 100644 --- a/ietf/submit/views.py +++ b/ietf/submit/views.py @@ -755,4 +755,7 @@ def get_submission_or_404(submission_id, access_token=None): def async_poke_test(request): result = poke.delay() - return HttpResponse(f'Poked {result}', content_type='text/plain') + return HttpResponse( + f'Poked {result}', + content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}", + ) diff --git a/ietf/utils/decorators.py b/ietf/utils/decorators.py index 56c28c4b193..5e94dda91d6 100644 --- a/ietf/utils/decorators.py +++ b/ietf/utils/decorators.py @@ -48,7 +48,7 @@ def require_api_key(f): @wraps(f) def _wrapper(request, *args, **kwargs): def err(code, text): - return HttpResponse(text, status=code, content_type='text/plain') + return HttpResponse(text, status=code, content_type=f"text/plain; charset={settings.DEFAULT_CHARSET}") # Check method and get hash if request.method == 'POST': hash = request.POST.get('apikey')