From d69167bcfa775251128346093c0babad6b3f42d6 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Fri, 9 Sep 2022 13:13:46 -0700 Subject: [PATCH] notification: Don't fail when formatting message strings - When a notification's message contains unexpected formatting characters such as '{}', showing the notification and consequently the entire FreedomBox web interface fails. Prevent that by make sure that that message formatting never fails. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/notification.py | 28 +++++++++++++++++++--------- plinth/tests/test_notification.py | 14 +++++++++++++- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/plinth/notification.py b/plinth/notification.py index cc733d778..8ea773c11 100644 --- a/plinth/notification.py +++ b/plinth/notification.py @@ -1,10 +1,9 @@ # SPDX-License-Identifier: AGPL-3.0-or-later -""" -Module to provide API for showing notifications. -""" +"""Module to provide API for showing notifications.""" import copy import logging +import string from django.core.exceptions import ValidationError from django.db.models import Q @@ -278,22 +277,22 @@ class Notification(models.StoredNotification): return Notification.objects.filter(*filters)[0:10] @staticmethod - def _translate(string, data=None): + def _translate(string_, data=None): """Translate a string for final display using data dict.""" - if not string: + if not string_: return None - string = gettext(string) + string_ = gettext(string_) try: - string = str(string) + string_ = str(string_) if data: - string = string.format(**data) + string_ = SafeFormatter().vformat(string_, [], data) except KeyError as error: logger.warning( 'Notification missing required key during translation: %s', error) - return string + return string_ @staticmethod def _translate_dict(data_dict, data=None): @@ -366,3 +365,14 @@ class Notification(models.StoredNotification): notes.append(note_context) return {'notifications': notes, 'max_severity': max_severity} + + +class SafeFormatter(string.Formatter): + """A string.format() handler to deal with missing arguments.""" + + def get_value(self, key, args, kwargs): + """Retrieve a given field value.""" + try: + return super().get_value(key, args, kwargs) + except (IndexError, KeyError): + return f'?{key}?' diff --git a/plinth/tests/test_notification.py b/plinth/tests/test_notification.py index 6af90bc7f..822ec25a3 100644 --- a/plinth/tests/test_notification.py +++ b/plinth/tests/test_notification.py @@ -10,7 +10,7 @@ import pytest from django.contrib.auth.models import Group, User from django.core.exceptions import ValidationError -from plinth.notification import Notification +from plinth.notification import Notification, SafeFormatter pytestmark = pytest.mark.django_db @@ -413,3 +413,15 @@ def test_display_context_body_template(note, user, load_cfg, rf): context_note = context['notifications'][0] assert context_note['body'].content == \ b'Test notification body /plinth/help/about/\n' + + +@pytest.mark.parametrize('input_, output', ( + (('', [], {}), ''), + (('{} {}', [10, 20], {}), '10 20'), + (('{1} {0} {key1}', [10, 20], dict(key1='value1')), '20 10 value1'), + (('{2} {1} {key1}', [10, 20], {}), '?2? 20 ?key1?'), +)) +def test_safe_string_formatter(input_, output): + """Test the safe string formatter.""" + formatter = SafeFormatter() + assert output == formatter.vformat(*input_)