From 757cea31357a43f51572eee7ad254e6fc41f667c Mon Sep 17 00:00:00 2001 From: NicoHood Date: Wed, 15 Jun 2022 22:29:51 +0200 Subject: [PATCH 1/2] Fix #355 url escaping --- src/icalendar/parser.py | 81 ++++++++++++++++++++++++----------------- 1 file changed, 48 insertions(+), 33 deletions(-) diff --git a/src/icalendar/parser.py b/src/icalendar/parser.py index 5344de89..305b162a 100644 --- a/src/icalendar/parser.py +++ b/src/icalendar/parser.py @@ -263,25 +263,6 @@ def from_ical(cls, st, strict=False): % (param, exc)) return result - -def escape_string(val): - # '%{:02X}'.format(i) - return val.replace(r'\,', '%2C').replace(r'\:', '%3A')\ - .replace(r'\;', '%3B').replace(r'\\', '%5C') - - -def unescape_string(val): - return val.replace('%2C', ',').replace('%3A', ':')\ - .replace('%3B', ';').replace('%5C', '\\') - - -def unescape_list_or_string(val): - if isinstance(val, list): - return [unescape_string(s) for s in val] - else: - return unescape_string(val) - - ######################################### # parsing and generation of content lines @@ -319,34 +300,68 @@ def from_parts(cls, name, params, values, sorted=True): return cls('%s:%s' % (name, values)) def parts(self): - """Split the content line up into (name, parameters, values) parts. + """ + Split the content line up into (name, parameters, values) parts. + + Example with parameter: + DESCRIPTION;ALTREP="cid:part1.0001@example.org":The Fall'98 Wild + + Example without parameters: + DESCRIPTION:The Fall'98 Wild + + https://icalendar.org/iCalendar-RFC-5545/3-2-property-parameters.html """ try: - st = escape_string(self) + st = self name_split = None value_split = None in_quotes = False + # Any character can be escaped using a backslash, e.g.: "test\:test" + quote_character = False for i, ch in enumerate(st): - if not in_quotes: - if ch in ':;' and not name_split: - name_split = i - if ch == ':' and not value_split: - value_split = i + # We can also quote using quotation marks. This ignores any output, until another quote appears. if ch == '"': in_quotes = not in_quotes - name = unescape_string(st[:name_split]) + continue + + # Ignore input, as we are currently in quotation mark quotes + if in_quotes: + continue + + # Skip quoted character + if quote_character: + quote_character = False + continue + + # The next character should be ignored + if ch == '\\': + quote_character = True + continue + + # The name ends either after the parameter or value delimiter + if ch in ':;' and not name_split: + name_split = i + + # The value starts after the value delimiter + if ch == ':' and not value_split: + value_split = i + + # Get name + name = st[:name_split] if not name: raise ValueError('Key name is required') validate_token(name) + + # Check if parameters are empty if not name_split or name_split + 1 == value_split: raise ValueError('Invalid content line') + + # Get parameters (text between ; and :) params = Parameters.from_ical(st[name_split + 1: value_split], - strict=self.strict) - params = Parameters( - (unescape_string(key), unescape_list_or_string(value)) - for key, value in compat.iteritems(params) - ) - values = unescape_string(st[value_split + 1:]) + strict=self.strict) + + # Get the value after the : + values = st[value_split + 1:] return (name, params, values) except ValueError as exc: raise ValueError( From 5a6a988bef2e1da14fb7f48263085b843e179051 Mon Sep 17 00:00:00 2001 From: Nicco Kunzmann Date: Tue, 23 Aug 2022 04:25:16 +0100 Subject: [PATCH 2/2] add test with examples see https://github.com/collective/icalendar/pull/356#issuecomment-1222626128 --- .../tests/issue_355_url_unescaping.ics | 33 +++++++++++++++++++ src/icalendar/tests/test_fixed_issues.py | 20 ++++++++--- 2 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 src/icalendar/tests/issue_355_url_unescaping.ics diff --git a/src/icalendar/tests/issue_355_url_unescaping.ics b/src/icalendar/tests/issue_355_url_unescaping.ics new file mode 100644 index 00000000..c9f413f9 --- /dev/null +++ b/src/icalendar/tests/issue_355_url_unescaping.ics @@ -0,0 +1,33 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//ical.marudot.com//iCal Event Maker +CALSCALE:GREGORIAN +BEGIN:VTIMEZONE +TZID:Europe/Berlin +LAST-MODIFIED:20201011T015911Z +TZURL:http://tzurl.org/zoneinfo-outlook/Europe/Berlin +X-LIC-LOCATION:Europe/Berlin +BEGIN:DAYLIGHT +TZNAME:CEST +TZOFFSETFROM:+0100 +TZOFFSETTO:+0200 +DTSTART:19700329T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU +END:DAYLIGHT +BEGIN:STANDARD +TZNAME:CET +TZOFFSETFROM:+0200 +TZOFFSETTO:+0100 +DTSTART:19701025T030000 +RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTAMP:20220822T164528Z +UID:1661186704812-62650@ical.marudot.com +DTSTART;TZID=Europe/Berlin:20220822T120000 +DTEND;TZID=Europe/Berlin:20220822T120000 +SUMMARY:test +DESCRIPTION:https://www.facebook.com/events/756119502186737/?acontext=%7B%22source%22%3A5%2C%22action_history%22%3A[%7B%22surface%22%3A%22page%22%2C%22mechanism%22%3A%22main_list%22%2C%22extra_data%22%3A%22%5C%22[]%5C%22%22%7D]%2C%22has_source%22%3Atrue%7D +END:VEVENT +END:VCALENDAR \ No newline at end of file diff --git a/src/icalendar/tests/test_fixed_issues.py b/src/icalendar/tests/test_fixed_issues.py index 423e71bf..0c137844 100644 --- a/src/icalendar/tests/test_fixed_issues.py +++ b/src/icalendar/tests/test_fixed_issues.py @@ -6,6 +6,8 @@ import os import pytz +HERE = os.path.dirname(__file__) + class TestIssues(unittest.TestCase): @@ -14,8 +16,7 @@ def test_issue_53(self): https://github.com/collective/icalendar/issues/53 """ - directory = os.path.dirname(__file__) - ics = open(os.path.join(directory, 'issue_53_parsing_failure.ics'), + ics = open(os.path.join(HERE, 'issue_53_parsing_failure.ics'), 'rb') cal = icalendar.Calendar.from_ical(ics.read()) ics.close() @@ -231,8 +232,7 @@ def test_issue_112(self): """Issue #112 - No timezone info on EXDATE https://github.com/collective/icalendar/issues/112 """ - directory = os.path.dirname(__file__) - path = os.path.join(directory, + path = os.path.join(HERE, 'issue_112_missing_tzinfo_on_exdate.ics') with open(path, 'rb') as ics: cal = icalendar.Calendar.from_ical(ics.read()) @@ -432,6 +432,18 @@ def test_issue_184(self): b'END:VEVENT\r\n' ) + def test_issue_356_url_escaping(self): + """Test that the URLs stay intact. + + see https://github.com/collective/icalendar/pull/356 + see https://github.com/collective/icalendar/issues/355 + """ + ics = open(os.path.join(HERE, 'issue_53_parsing_failure.ics'), 'rb') + cal = icalendar.Calendar.from_ical(ics) + event = cal.walk(name='VEVENT')[0] + URL = "https://www.facebook.com/events/756119502186737/?acontext=%7B%22source%22%3A5%2C%22action_history%22%3A[%7B%22surface%22%3A%22page%22%2C%22mechanism%22%3A%22main_list%22%2C%22extra_data%22%3A%22%5C%22[]%5C%22%22%7D]%2C%22has_source%22%3Atrue%7D" + self.assertEqual(event["DESCRIPTION"], URL) + def test_issue_237(self): """Issue #237 - Fail to parse timezone with non-ascii TZID"""