Skip to content

Commit 170bfbf

Browse files
authored
#127 Bugfix: Email Preview Exposes HTML Tags
1 parent 8897837 commit 170bfbf

File tree

6 files changed

+164
-26
lines changed

6 files changed

+164
-26
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ The `./scripts/run_tests.sh` script runs all unit tests using [py.test](http://p
1818

1919
## Versioning
2020

21-
With the virtual environment active, run `python setup.py --version` to see the current version **on the current branch**. Use `git tag` to add release tags to commits, if desired, and push the tags.
21+
With the virtual environment active, run `python setup.py --version` to see the current version **on the current branch**.
2222

23-
After merging changes in this repository, you must update notification-api to use the changes. Run `poetry update notification-utils` in an api branch, and then push the PR for approval/merge. The PR only should contain changes made to the lock file.
23+
Before merging, update the version in the `notifications_utils/version.py` file. Once merged, use `git tag` to add release tags to commits, and push the tags.
24+
25+
After merging changes in this repository, you must update notification-api to use the changes. Run `poetry update notification-utils` in an api branch, and then push the PR for approval/merge. The PR only should contain changes made to the lock file.
2426

2527
## E-mail Template Documentation
2628

notifications_utils/formatters.py

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -299,7 +299,7 @@ def get_action_links(html: str) -> list[str]:
299299
return re.findall(action_link_regex, html)
300300

301301

302-
def get_img_link() -> str:
302+
def get_action_link_image_url() -> str:
303303
"""Get action link image url for the current environment. (insert_action_link helper)"""
304304
env_map = {
305305
'production': 'prod',
@@ -322,7 +322,7 @@ def insert_action_link(html: str) -> str:
322322

323323
action_link_list = get_action_links(html)
324324

325-
img_link = get_img_link()
325+
img_link = get_action_link_image_url()
326326

327327
for item in action_link_list:
328328
# Puts the action link in a new <p> tag with appropriate styling.
@@ -365,6 +365,61 @@ def insert_action_link(html: str) -> str:
365365
return html
366366

367367

368+
def strip_parentheses_in_link_placeholders(value: str) -> str:
369+
"""
370+
Captures markdown links with placeholders in them and replaces the parentheses around the placeholders with
371+
!! at the start and ## at the end. This makes them easy to put back after the convertion to html.
372+
373+
Example Conversion:
374+
`[link text](http://example.com/((placeholder))) -> [link text](http://example.com/!!placeholder##)`
375+
376+
Args:
377+
value (str): The email body to be processed
378+
379+
Returns:
380+
str: The email body with the placeholders in markdown links with parentheses replaced with !! and ##
381+
"""
382+
markdown_link_pattern = re.compile(r'\]\((.*?\({2}.*?\){2}.*?)+?\)')
383+
384+
# find all markdown links with placeholders in them and replace the parentheses and html tags with !! and ##
385+
for item in re.finditer(markdown_link_pattern, value):
386+
link = item.group(0)
387+
# replace the opening parentheses with !!, include the opening span and mark tags if they exist
388+
modified_link = re.sub(r'((<span class=[\'\"]placeholder[\'\"]><mark>)?\(\((?![\(]))', '!!', link)
389+
# replace the closing parentheses with ##, include the closing span and mark tags if they exist
390+
modified_link = re.sub(r'(\)\)(<\/mark><\/span>)?)', '##', modified_link)
391+
392+
value = value.replace(link, modified_link)
393+
394+
return value
395+
396+
397+
def replace_symbols_with_placeholder_parens(value: str) -> str:
398+
"""
399+
Replaces the `!!` and `##` symbols with placeholder parentheses in the given string.
400+
401+
Example Output: `!!placeholder## -> ((placeholder))`
402+
403+
Args:
404+
value (str): The email body that has been converted from markdown to html
405+
406+
Returns:
407+
str: The processed string with tags replaced by placeholder parentheses.
408+
"""
409+
# pattern to find the placeholders surrounded by !! and ##
410+
placeholder_in_link_pattern = re.compile(r'(!![^()]+?##)')
411+
412+
# find all instances of !! and ## and replace them with (( and ))
413+
for item in re.finditer(placeholder_in_link_pattern, value):
414+
placeholder = item.group(0)
415+
mod_placeholder = placeholder.replace('!!', '((')
416+
mod_placeholder = mod_placeholder.replace('##', '))')
417+
418+
value = value.replace(placeholder, mod_placeholder)
419+
420+
return value
421+
422+
368423
class NotifyLetterMarkdownPreviewRenderer(mistune.Renderer):
369424

370425
def block_code(self, code, language=None):

notifications_utils/template.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,45 @@
11
import math
22
import sys
3-
from os import path
43
from datetime import datetime
4+
from html import unescape
5+
from os import path
56

67
from jinja2 import Environment, FileSystemLoader
78
from markupsafe import Markup
8-
from html import unescape
99

1010
from notifications_utils import SMS_CHAR_COUNT_LIMIT
1111
from notifications_utils.columns import Columns
1212
from notifications_utils.field import Field
1313
from notifications_utils.formatters import (
14+
add_prefix,
15+
add_trailing_newline,
16+
autolink_sms, escape_html,
1417
insert_action_link,
15-
unlink_govuk_escaped,
18+
make_quotes_smart,
1619
nl2br,
1720
nl2li,
18-
add_prefix,
19-
autolink_sms,
21+
normalise_newlines,
22+
normalise_whitespace,
2023
notify_email_markdown,
2124
notify_email_preheader_markdown,
22-
notify_plain_text_email_markdown,
2325
notify_letter_preview_markdown,
26+
notify_plain_text_email_markdown,
2427
remove_empty_lines,
25-
sms_encode,
26-
escape_html,
27-
strip_dvla_markup,
28-
strip_pipes,
28+
remove_smart_quotes_from_email_addresses,
2929
remove_whitespace_before_punctuation,
30-
make_quotes_smart,
3130
replace_hyphens_with_en_dashes,
3231
replace_hyphens_with_non_breaking_hyphens,
33-
tweak_dvla_list_markup,
32+
replace_symbols_with_placeholder_parens,
33+
sms_encode, strip_dvla_markup,
3434
strip_leading_whitespace,
35-
add_trailing_newline,
36-
normalise_newlines,
37-
normalise_whitespace,
38-
remove_smart_quotes_from_email_addresses,
35+
strip_pipes,
36+
strip_parentheses_in_link_placeholders,
3937
strip_unsupported_characters,
40-
)
38+
tweak_dvla_list_markup,
39+
unlink_govuk_escaped)
40+
from notifications_utils.sanitise_text import SanitiseSMS
4141
from notifications_utils.take import Take
4242
from notifications_utils.template_change import TemplateChange
43-
from notifications_utils.sanitise_text import SanitiseSMS
44-
4543

4644
template_env = Environment(loader=FileSystemLoader(
4745
path.join(
@@ -704,8 +702,14 @@ def get_html_email_body(
704702
strip_unsupported_characters
705703
).then(
706704
add_trailing_newline
705+
).then(
706+
# before converting to markdown, strip out the "(())" for placeholders (preview mode or test emails)
707+
strip_parentheses_in_link_placeholders
707708
).then(
708709
notify_email_markdown
710+
).then(
711+
# after converting to html link, replace !!foo## with ((foo))
712+
replace_symbols_with_placeholder_parens
709713
).then(
710714
do_nice_typography
711715
).then(

notifications_utils/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = '2.2.1'
1+
__version__ = '2.2.2'

tests/test_field_html_handling.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,16 @@
5757
'string &amp; entity',
5858
'string & entity',
5959
),
60-
])
60+
],
61+
ids=[
62+
'string with html',
63+
'string with html inside placeholder',
64+
'string with placeholder',
65+
'string with html inside conditional placeholder',
66+
'string with conditional placeholder with html after condition',
67+
'string with special character &',
68+
]
69+
)
6170
def test_field_handles_html(content, values, expected_stripped, expected_escaped, expected_passthrough):
6271
assert str(Field(content, values)) == expected_stripped
6372
assert str(Field(content, values, html='strip')) == expected_stripped

tests/test_template_types.py

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,10 +177,78 @@ def test_pass_through_renderer():
177177
'two of the same action link',
178178
]
179179
)
180-
def test_get_html_email_body(content, values, expected):
180+
def test_get_html_email_body_with_action_links(content, values, expected):
181181
assert get_html_email_body(content, values) == expected
182182

183183

184+
@pytest.mark.parametrize(
185+
'content, expected',
186+
[
187+
(
188+
'normal placeholder formatting: ((foo))',
189+
(
190+
f'<p style="{PARAGRAPH_STYLE}">normal placeholder formatting: <span class=\'placeholder\'><mark>((foo))'
191+
'</mark></span></p>'
192+
),
193+
),
194+
(
195+
'regular markdown link: [link text](#)',
196+
(
197+
f'<p style="{PARAGRAPH_STYLE}">regular markdown link: '
198+
f'<a style="{LINK_STYLE}" href="#" target="_blank">link text</a></p>'
199+
),
200+
),
201+
(
202+
'placeholder in link text, without placeholder in link: [link ((foo))](https://test.com/)',
203+
(
204+
f'<p style="{PARAGRAPH_STYLE}">placeholder in link text, without placeholder in link: '
205+
f'<a style="{LINK_STYLE}" href="https://test.com/" target="_blank">link '
206+
'<span class=\'placeholder\'><mark>((foo))</mark></span></a></p>'
207+
),
208+
),
209+
(
210+
'no format within link, placeholder at end: [link text](https://test.com/((foo)))',
211+
(
212+
f'<p style="{PARAGRAPH_STYLE}">no format within link, placeholder at end: '
213+
f'<a style="{LINK_STYLE}" href="https://test.com/((foo))" target="_blank">link text</a></p>'
214+
)
215+
),
216+
(
217+
'no format within link, placeholder in middle: [link text](https://test.com/((foo))?xyz=123)',
218+
(
219+
f'<p style="{PARAGRAPH_STYLE}">no format within link, placeholder in middle: '
220+
f'<a style="{LINK_STYLE}" href="https://test.com/((foo))?xyz=123" target="_blank">link text</a></p>'
221+
)
222+
),
223+
(
224+
'no format in link, with only placeholder: [link text](((foo)))',
225+
(
226+
f'<p style="{PARAGRAPH_STYLE}">no format in link, with only placeholder: '
227+
f'<a style="{LINK_STYLE}" href="((foo))" target="_blank">link text</a></p>'
228+
)
229+
),
230+
(
231+
'no format within link, multiple placeholders: [link text](https://test.com/((foo))?xyz=((bar)))',
232+
(
233+
f'<p style="{PARAGRAPH_STYLE}">no format within link, multiple placeholders: '
234+
f'<a style="{LINK_STYLE}" href="https://test.com/((foo))?xyz=((bar))" target="_blank">link text</a></p>'
235+
)
236+
),
237+
],
238+
ids=[
239+
'formatting with placeholder',
240+
'no formatting with only markdown link',
241+
'formatting with placeholder in markdown link text',
242+
'formatting with placeholder in markdown link url',
243+
'formatting with placeholder in markdown link url and text around placeholder',
244+
'formatting when placeholder is markdown link url',
245+
'formatting with multiple placeholders in markdown link'
246+
]
247+
)
248+
def test_get_html_email_body_preview_with_placeholder_in_markdown_link(content, expected):
249+
assert get_html_email_body(content, template_values={}, preview_mode=True) == expected
250+
251+
184252
def test_html_email_inserts_body():
185253
assert 'the &lt;em&gt;quick&lt;/em&gt; brown fox' in str(HTMLEmailTemplate(
186254
{'content': 'the <em>quick</em> brown fox', 'subject': ''}

0 commit comments

Comments
 (0)