diff --git a/CHANGELOG.md b/CHANGELOG.md index 23975fda..23da4d4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to this project will be documented in this file. ## Improvements - Add a tooltip to the 'confirmation link' label (#436) - Update API to allow specifying channels by names (#440) +- When saving a phone number, remove any invisible unicode characers ## v1.17.0 - 2020-10-14 diff --git a/hc/front/forms.py b/hc/front/forms.py index fd9f0a32..b5f33626 100644 --- a/hc/front/forms.py +++ b/hc/front/forms.py @@ -1,12 +1,12 @@ from datetime import timedelta as td import json +import re from urllib.parse import quote, urlencode from django import forms from django.forms import URLField from django.conf import settings from django.core.exceptions import ValidationError -from django.core.validators import RegexValidator from hc.front.validators import ( CronExpressionValidator, TimezoneValidator, @@ -193,18 +193,22 @@ class AddShellForm(forms.Form): return json.dumps(dict(self.cleaned_data), sort_keys=True) -phone_validator = RegexValidator( - regex="^\+\d{5,15}$", message="Invalid phone number format." -) - - class AddSmsForm(forms.Form): error_css_class = "has-error" label = forms.CharField(max_length=100, required=False) - value = forms.CharField(max_length=16, validators=[phone_validator]) + value = forms.CharField() down = forms.BooleanField(required=False, initial=True) up = forms.BooleanField(required=False, initial=True) + def clean_value(self): + v = self.cleaned_data["value"] + + stripped = v.encode("ascii", "ignore").decode("ascii") + if not re.match(r"^\+\d{5,15}$", stripped): + raise forms.ValidationError("Invalid phone number format.") + + return stripped + class ChannelNameForm(forms.Form): name = forms.CharField(max_length=100, required=False) diff --git a/hc/front/tests/test_add_sms.py b/hc/front/tests/test_add_sms.py index eaa02842..73a6fe10 100644 --- a/hc/front/tests/test_add_sms.py +++ b/hc/front/tests/test_add_sms.py @@ -37,11 +37,11 @@ class AddSmsTestCase(BaseTestCase): self.assertEqual(c.project, self.project) def test_it_rejects_bad_number(self): - form = {"value": "not a phone number address"} - - self.client.login(username="alice@example.org", password="password") - r = self.client.post(self.url, form) - self.assertContains(r, "Invalid phone number format.") + for v in ["not a phone number address", False, 15, "+123456789A"]: + form = {"value": v} + self.client.login(username="alice@example.org", password="password") + r = self.client.post(self.url, form) + self.assertContains(r, "Invalid phone number format.") def test_it_trims_whitespace(self): form = {"value": " +1234567890 "} @@ -65,3 +65,13 @@ class AddSmsTestCase(BaseTestCase): self.client.login(username="bob@example.org", password="password") r = self.client.get(self.url) self.assertEqual(r.status_code, 403) + + def test_it_strips_invisible_formatting_characters(self): + form = {"label": "My Phone", "value": "\u202c+1234567890\u202c"} + + self.client.login(username="alice@example.org", password="password") + r = self.client.post(self.url, form) + self.assertRedirects(r, self.channels_url) + + c = Channel.objects.get() + self.assertEqual(c.phone_number, "+1234567890")