Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix #355 url escaping #356

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 48 additions & 33 deletions src/icalendar/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,25 +259,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

Expand Down Expand Up @@ -315,34 +296,68 @@ def from_parts(cls, name, params, values, sorted=True):
return cls(f'{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:[email protected]":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 iter(params.items())
)
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(
Expand Down
33 changes: 33 additions & 0 deletions src/icalendar/tests/issue_355_url_unescaping.ics
Original file line number Diff line number Diff line change
@@ -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:[email protected]
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
20 changes: 16 additions & 4 deletions src/icalendar/tests/test_fixed_issues.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import os
import pytz

HERE = os.path.dirname(__file__)


class TestIssues(unittest.TestCase):

Expand All @@ -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()
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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"""

Expand Down