Skip to content

Commit 041f65b

Browse files
author
Jared Turner
committed
Fix iCal support for special characters (newline, comma, semicolon, backslash)
1 parent 2b58574 commit 041f65b

File tree

6 files changed

+89
-33
lines changed

6 files changed

+89
-33
lines changed

Gemfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
PATH
22
remote: .
33
specs:
4-
add_to_calendar (0.2.2)
4+
add_to_calendar (0.2.3)
55
tzinfo (>= 1.1, < 3)
66
tzinfo-data (~> 1.2020)
77

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,12 @@ cal = AddToCalendar::URLs.new(event_attributes)
104104

105105
- Offset values eg. "2020-05-13 15:31:00 **+05:00**" are ignored. It is only important that you have the correct date and time numbers set. The timezone is set directly using its own attribute `timezone`.
106106
- You must set a timezone so that when users add the event to their calendar it shows at their correct local time.
107-
- Eg. London event @ `2020-05-13 13:30:00` will save in a New Yorkers calendar as local time `2020-05-13 17:30:00`
107+
- Eg. London event @ `2020-05-13 13:30:00` will save in a New Yorker's calendar as local time `2020-05-13 17:30:00`
108108

109109
### Browser support
110110

111-
- IE11 and lower will not work for `ical_url`, `apple_url` and `outlook_url` (IE does not properly support [data-uri links](https://caniuse.com/#feat=datauri). See [#16](https://github.com/jaredlt/add_to_calendar/issues/16)).
111+
- IE11 and lower will not work for `ical_url`, `apple_url` and `outlook_url` (IE does not properly support [data-uri links](https://caniuse.com/#feat=datauri). See [#16](https://github.com/jaredlt/add_to_calendar/issues/16)).
112+
- IE11 will also not work with `Yahoo`, but this is because Yahoo only offers a simplified interface for IE11 which does not work with the add event URL.
112113

113114
### More details
114115

lib/add_to_calendar.rb

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ def outlook_com_url
9595
end
9696

9797
def ical_url
98-
# Downloads a *.ics file provided as a data:text href
99-
# Eg. data:text/calendar;charset=utf8,BEGIN:VCALENDAR%0AVERSION:2.0%0ABEGIN:VEVENT%0ADTSTART=20200610T123000Z%0ADTEND=20200610T133000Z%0ASUMMARY=Holly%27s%208th%20Birthday%21%0AURL=https%3A%2F%2Fwww.example.com%2Fevent-details%0ADESCRIPTION=Come%20join%20us%20for%20lots%20of%20fun%20%26%20cake%21\n\nhttps%3A%2F%2Fwww.example.com%2Fevent-details%0ALOCATION=Flat%204%2C%20The%20Edge%2C%2038%20Smith-Dorrien%20St%2C%20London%2C%20N1%207GU%0AUID=-https%3A%2F%2Fwww.example.com%2Fevent-details%0AEND:VEVENT%0AEND:VCALENDAR
98+
# Downloads a *.ics file provided as a data-uri
99+
# Eg. "data:text/calendar;charset=utf8,BEGIN:VCALENDAR%0AVERSION:2.0%0ABEGIN:VEVENT%0ADTSTART:20200512T123000Z%0ADTEND:20200512T160000Z%0ASUMMARY:Holly%27s%208th%20Birthday%21%0AURL:https%3A%2F%2Fwww.example.com%2Fevent-details%0ADESCRIPTION:Come%20join%20us%20for%20lots%20of%20fun%20%26%20cake%21\\n\\nhttps%3A%2F%2Fwww.example.com%2Fevent-details%0ALOCATION:Flat%204%5C%2C%20The%20Edge%5C%2C%2038%20Smith-Dorrien%20St%5C%2C%20London%5C%2C%20N1%207GU%0AUID:-https%3A%2F%2Fwww.example.com%2Fevent-details%0AEND:VEVENT%0AEND:VCALENDAR"
100100
calendar_url = "data:text/calendar;charset=utf8,BEGIN:VCALENDAR%0AVERSION:2.0%0ABEGIN:VEVENT"
101101
params = {}
102102
params[:DTSTART] = utc_datetime(start_datetime)
@@ -105,19 +105,19 @@ def ical_url
105105
else
106106
params[:DTEND] = utc_datetime(start_datetime + 60*60) # 1 hour later
107107
end
108-
params[:SUMMARY] = url_encode(title)
108+
params[:SUMMARY] = url_encode_ical(title)
109109
params[:URL] = url_encode(url) if url
110-
params[:DESCRIPTION] = url_encode_ical_description(description) if description
110+
params[:DESCRIPTION] = url_encode_ical(description) if description
111111
if add_url_to_description && url
112112
if params[:DESCRIPTION]
113113
params[:DESCRIPTION] << "\\n\\n#{url_encode(url)}"
114114
else
115115
params[:DESCRIPTION] = url_encode(url)
116116
end
117117
end
118-
params[:LOCATION] = url_encode(location) if location
118+
params[:LOCATION] = url_encode_ical(location) if location
119119
params[:UID] = "-#{url_encode(url)}" if url
120-
params[:UID] = "-#{utc_datetime(start_datetime)}-#{url_encode(title)}" unless params[:UID] # set uid based on starttime and title only if url is unavailable
120+
params[:UID] = "-#{utc_datetime(start_datetime)}-#{url_encode_ical(title)}" unless params[:UID] # set uid based on starttime and title only if url is unavailable
121121

122122
new_line = "%0A"
123123
params.each do |key, value|
@@ -239,12 +239,17 @@ def newlines_to_html_br(string)
239239
string.gsub(/(?:\n\r?|\r\n?)/, '<br>')
240240
end
241241

242-
def url_encode_ical_description(description)
243-
description.split("\n").map { |e|
244-
if e == "\n"
245-
"\\n"
242+
def url_encode_ical(string)
243+
# per https://tools.ietf.org/html/rfc5545#section-3.3.11
244+
string.gsub!("\\", "\\\\\\") # \ >> \\ --yes, really: https://stackoverflow.com/questions/6209480/how-to-replace-backslash-with-double-backslash
245+
string.gsub!(",", "\\,")
246+
string.gsub!(";", "\\;")
247+
string.gsub!("\r\n", "\n") # so can handle all newlines the same
248+
string.split("\n").map { |e|
249+
if e.empty?
250+
e
246251
else
247-
url_encode(e) if e != "\n"
252+
url_encode(e)
248253
end
249254
}.join("\\n")
250255
end

lib/add_to_calendar/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module AddToCalendar
2-
VERSION = "0.2.2"
2+
VERSION = "0.2.3"
33
end

test/add_to_calendar_test.rb

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,24 +169,74 @@ def test_newlines_convert_to_html_br
169169
def test_ical_description_url_encoded_with_newlines
170170
# final *.ics file must include `\n`
171171
# which means the string output must be `\\n` (not url encoded)
172+
# this is to ensure it works when included in data-uris
172173
# this method should:
173174
# url encode all characters except newlines \n
174175
# update all newlines \n to \\n
175-
cal = AddToCalendar::URLs.new(
176-
start_datetime: Time.new(@next_month_year,@next_month_month,@next_month_day,13,30,00,0),
177-
end_datetime: Time.new(@next_month_year,@next_month_month,@next_month_day,17,00,00,0),
178-
title: @title,
179-
timezone: @timezone
180-
)
181-
string_without_newlines = cal.send(:url_encode_ical_description, "string without newlines")
176+
event_attributes = {
177+
start_datetime: Time.new(2020,12,12,9,00,00,0), # required
178+
end_datetime: Time.new(2020,12,12,17,00,00,0),
179+
title: "Ruby Conference", # required
180+
timezone: 'America/New_York', # required
181+
location: "20 W 34th St, New York, NY 10001",
182+
url: "https://www.ruby-lang.org/en/",
183+
description: "Join us to learn\n\nall about Ruby.",
184+
add_url_to_description: true # defaults to true
185+
}
186+
cal = AddToCalendar::URLs.new(event_attributes)
187+
188+
string_without_newlines = cal.send(:url_encode_ical, "string without newlines")
182189
assert string_without_newlines == "string%20without%20newlines"
183190

184-
string_with_newline = cal.send(:url_encode_ical_description, "string with\nnewline")
191+
string_with_newline = cal.send(:url_encode_ical, "string with\nnewline")
185192
assert string_with_newline == "string%20with\\nnewline"
186193

187-
string_with_newlines = cal.send(:url_encode_ical_description, "string\nwith\n\nnewlines")
194+
string_with_newlines = cal.send(:url_encode_ical, "string\nwith\n\nnewlines")
188195
assert string_with_newlines == "string\\nwith\\n\\nnewlines"
189196

190197
end
191198

199+
def test_ical_escapes_special_characters
200+
# per https://tools.ietf.org/html/rfc5545#section-3.3.11
201+
# special characters are: BACKSLASH, COMMA, SEMICOLON, NEWLINE
202+
event_attributes = {
203+
start_datetime: Time.new(2020,12,12,9,00,00,0), # required
204+
end_datetime: Time.new(2020,12,12,17,00,00,0),
205+
title: "Ruby Conference; Rails Conference", # required
206+
timezone: 'America/New_York', # required
207+
location: "20 W 34th St, New York, NY 10001",
208+
url: "https://www.ruby-lang.org/en/",
209+
description: "Join us to learn all about Ruby \\ Rails.",
210+
add_url_to_description: true # defaults to true
211+
}
212+
cal = AddToCalendar::URLs.new(event_attributes)
213+
214+
backslash = cal.send(:url_encode_ical, "Ruby\\Rails")
215+
assert backslash == "Ruby%5C%5CRails" # url_encoded `\\`` where %5C == \
216+
217+
comma = cal.send(:url_encode_ical, "Ruby,Rails")
218+
assert comma == "Ruby%5C%2CRails" # url_encoded `\,` where %2C == ,
219+
220+
semicolon = cal.send(:url_encode_ical, "Ruby;Rails")
221+
assert semicolon == "Ruby%5C%3BRails" # url_encoded `\;` where %3B == ;
222+
end
223+
224+
def test_rn_newline_should_be_detected_converted_and_escaped
225+
# \r\n should be converted to \n so that we can also escape them to \\n
226+
event_attributes = {
227+
start_datetime: Time.new(2020,12,12,9,00,00,0), # required
228+
end_datetime: Time.new(2020,12,12,17,00,00,0),
229+
title: "Ruby Conference; Rails Conference", # required
230+
timezone: 'America/New_York', # required
231+
location: "20 W 34th St, New York, NY 10001",
232+
url: "https://www.ruby-lang.org/en/",
233+
description: "Join us to learn all about Ruby \\ Rails.",
234+
add_url_to_description: true # defaults to true
235+
}
236+
cal = AddToCalendar::URLs.new(event_attributes)
237+
238+
rn_newline = cal.send(:url_encode_ical, "rn\r\nnewline")
239+
assert rn_newline == "rn\\nnewline"
240+
end
241+
192242
end

test/urls/ical_url_test.rb

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,14 @@ def setup
2727

2828
def test_with_only_required_attributes
2929
cal = AddToCalendar::URLs.new(start_datetime: Time.new(@next_month_year,@next_month_month,@next_month_day,13,30,00,0), title: @title, timezone: @timezone)
30-
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{url_encode(cal.title)}"
30+
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{cal.send(:url_encode_ical, cal.title)}"
3131
assert cal.ical_url == @url_with_defaults_required + uid + @url_end
3232
end
3333

3434
def test_without_end_datetime
3535
# should set end as start + 1 hour
3636
cal = AddToCalendar::URLs.new(start_datetime: Time.new(@next_month_year,@next_month_month,@next_month_day,13,30,00,0), title: @title, timezone: @timezone)
37-
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{url_encode(cal.title)}"
37+
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{cal.send(:url_encode_ical, cal.title)}"
3838
assert cal.ical_url == "data:text/calendar;charset=utf8,BEGIN:VCALENDAR%0AVERSION:2.0%0ABEGIN:VEVENT" +
3939
"%0ADTSTART:#{@next_month_year}#{@next_month_month}#{@next_month_day}T123000Z" +
4040
"%0ADTEND:#{@next_month_year}#{@next_month_month}#{@next_month_day}T133000Z" +
@@ -50,7 +50,7 @@ def test_with_end_datetime
5050
title: @title,
5151
timezone: @timezone
5252
)
53-
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{url_encode(cal.title)}"
53+
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{cal.send(:url_encode_ical, cal.title)}"
5454
assert cal.ical_url == "data:text/calendar;charset=utf8,BEGIN:VCALENDAR%0AVERSION:2.0%0ABEGIN:VEVENT" +
5555
"%0ADTSTART:#{@next_month_year}#{@next_month_month}#{@next_month_day}T123000Z" +
5656
"%0ADTEND:#{@next_month_year}#{@next_month_month}#{@next_month_day}T160000Z" +
@@ -66,7 +66,7 @@ def test_with_end_datetime_after_midnight
6666
title: @title,
6767
timezone: @timezone
6868
)
69-
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{url_encode(cal.title)}"
69+
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{cal.send(:url_encode_ical, cal.title)}"
7070
assert cal.ical_url == "data:text/calendar;charset=utf8,BEGIN:VCALENDAR%0AVERSION:2.0%0ABEGIN:VEVENT" +
7171
"%0ADTSTART:#{@next_month_year}#{@next_month_month}#{@next_month_day}T123000Z" +
7272
"%0ADTEND:#{@next_month_year}#{@next_month_month}#{@next_month_day.to_i+1}T160000Z" +
@@ -77,8 +77,8 @@ def test_with_end_datetime_after_midnight
7777

7878
def test_with_location
7979
cal = AddToCalendar::URLs.new(start_datetime: Time.new(@next_month_year,@next_month_month,@next_month_day,13,30,00,0), title: @title, timezone: @timezone, location: @location)
80-
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{url_encode(cal.title)}"
81-
assert cal.ical_url == @url_with_defaults_required + "%0ALOCATION:Flat%204%2C%20The%20Edge%2C%2038%20Smith-Dorrien%20St%2C%20London%2C%20N1%207GU" + uid + @url_end
80+
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{cal.send(:url_encode_ical, cal.title)}"
81+
assert cal.ical_url == @url_with_defaults_required + "%0ALOCATION:Flat%204%5C%2C%20The%20Edge%5C%2C%2038%20Smith-Dorrien%20St%5C%2C%20London%5C%2C%20N1%207GU" + uid + @url_end
8282
end
8383

8484
def test_with_url_without_description
@@ -97,7 +97,7 @@ def test_description_with_newlines
9797
# final *.ics file must include `\n`
9898
# which means the string output must be `\\n`
9999
cal = AddToCalendar::URLs.new(start_datetime: Time.new(@next_month_year,@next_month_month,@next_month_day,13,30,00,0), title: @title, timezone: @timezone, description: "Come join us for lots of fun & cake!\n\nBring a towel!")
100-
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{url_encode(cal.title)}"
100+
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{cal.send(:url_encode_ical, cal.title)}"
101101
assert cal.ical_url == @url_with_defaults_required + "%0ADESCRIPTION:Come%20join%20us%20for%20lots%20of%20fun%20%26%20cake%21\\n\\nBring%20a%20towel%21" + uid + @url_end
102102
end
103103

@@ -108,7 +108,7 @@ def test_add_url_to_description_false_without_url
108108
timezone: @timezone,
109109
add_url_to_description: false,
110110
)
111-
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{url_encode(cal.title)}"
111+
uid = "%0AUID:-#{cal.send(:utc_datetime, cal.start_datetime)}-#{cal.send(:url_encode_ical, cal.title)}"
112112
assert cal.ical_url == @url_with_defaults_required + uid + @url_end
113113
end
114114

@@ -141,7 +141,7 @@ def test_with_all_attributes
141141
"%0ASUMMARY:Holly%27s%208th%20Birthday%21" +
142142
"%0AURL:https%3A%2F%2Fwww.example.com%2Fevent-details" +
143143
"%0ADESCRIPTION:Come%20join%20us%20for%20lots%20of%20fun%20%26%20cake%21\\n\\nhttps%3A%2F%2Fwww.example.com%2Fevent-details" +
144-
"%0ALOCATION:Flat%204%2C%20The%20Edge%2C%2038%20Smith-Dorrien%20St%2C%20London%2C%20N1%207GU" +
144+
"%0ALOCATION:Flat%204%5C%2C%20The%20Edge%5C%2C%2038%20Smith-Dorrien%20St%5C%2C%20London%5C%2C%20N1%207GU" +
145145
uid +
146146
@url_end
147147
end

0 commit comments

Comments
 (0)