2 Copyright (C) 2004-2005 SKYRIX Software AG
4 This file is part of SOPE.
6 SOPE is free software; you can redistribute it and/or modify it under
7 the terms of the GNU Lesser General Public License as published by the
8 Free Software Foundation; either version 2, or (at your option) any
11 SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
12 WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
14 License for more details.
16 You should have received a copy of the GNU Lesser General Public
17 License along with SOPE; see the file COPYING. If not, write to the
18 Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
22 #include "iCalRenderer.h"
23 #include "iCalEvent.h"
24 #include "iCalPerson.h"
25 #include "iCalRecurrenceRule.h"
26 #include "NSCalendarDate+ICal.h"
29 @implementation iCalRenderer
31 static iCalRenderer *renderer = nil;
33 /* assume length of 1K - reasonable ? */
34 static unsigned DefaultICalStringCapacity = 1024;
36 + (id)sharedICalendarRenderer {
38 renderer = [[self alloc] init];
44 - (void)addPreambleForAppointment:(iCalEvent *)_apt
45 toString:(NSMutableString *)s
47 [s appendString:@"BEGIN:VCALENDAR\r\nMETHOD:REQUEST\r\n"];
48 [s appendFormat:@"PRODID:NGiCal/%i.%i\r\n",
49 SOPE_MAJOR_VERSION, SOPE_MINOR_VERSION];
50 [s appendString:@"VERSION:2.0\r\n"];
52 - (void)addPostambleForAppointment:(iCalEvent *)_apt
53 toString:(NSMutableString *)s
55 [s appendString:@"END:VCALENDAR\r\n"];
58 - (void)addOrganizer:(iCalPerson *)p toString:(NSMutableString *)s {
61 if (![p isNotNull]) return;
63 [s appendString:@"ORGANIZER;CN=\""];
65 [s appendString:[x iCalDQUOTESafeString]];
67 [s appendString:@"\""];
68 if ((x = [p email])) {
69 [s appendString:@":"]; /* sic! */
70 [s appendString:[x iCalSafeString]];
72 [s appendString:@"\r\n"];
75 - (void)addAttendees:(NSArray *)persons toString:(NSMutableString *)s {
79 count = [persons count];
80 for (i = 0; i < count; i++) {
83 p = [persons objectAtIndex:i];
84 [s appendString:@"ATTENDEE;"];
87 [s appendString:@"ROLE="];
88 [s appendString:[x iCalSafeString]];
89 [s appendString:@";"];
92 if ((x = [p partStat])) {
93 if ([p participationStatus] != iCalPersonPartStatNeedsAction) {
94 [s appendString:@"PARTSTAT="];
95 [s appendString:[x iCalSafeString]];
96 [s appendString:@";"];
100 [s appendString:@"CN=\""];
101 if ((x = [p cnWithoutQuotes])) {
102 [s appendString:[x iCalDQUOTESafeString]];
104 [s appendString:@"\""];
105 if ([(x = [p email]) isNotNull]) {
106 [s appendString:@":"]; /* sic! */
107 [s appendString:[x iCalSafeString]];
109 [s appendString:@"\r\n"];
113 - (void)addVEventForAppointment:(iCalEvent *)event
114 toString:(NSMutableString *)s
118 [s appendString:@"BEGIN:VEVENT\r\n"];
120 [s appendString:@"SUMMARY:"];
121 [s appendString:[[event summary] iCalSafeString]];
122 [s appendString:@"\r\n"];
123 if ([[event location] length] > 0) {
124 [s appendString:@"LOCATION:"];
125 [s appendString:[[event location] iCalSafeString]];
126 [s appendString:@"\r\n"];
129 if ((tmp = [event uid]) != nil) {
130 [s appendString:@"UID:"];
131 [s appendString:tmp];
132 [s appendString:@"\r\n"];
135 [s appendString:@"DTSTART:"];
136 [s appendString:[[event startDate] icalString]];
137 [s appendString:@"\r\n"];
139 if ([event hasEndDate]) {
140 [s appendString:@"DTEND:"];
141 [s appendString:[[event endDate] icalString]];
142 [s appendString:@"\r\n"];
144 if ([event hasDuration]) {
145 [s appendString:@"DURATION:"];
146 [s appendString:[event duration]];
147 [s appendString:@"\r\n"];
149 if ([[event priority] length] > 0) {
150 [s appendString:@"PRIORITY:"];
151 [s appendString:[event priority]];
152 [s appendString:@"\r\n"];
154 if ([[event categories] length] > 0) {
157 catString = [event categories];
158 [s appendString:@"CATEGORIES:"];
159 [s appendString:catString];
160 [s appendString:@"\r\n"];
162 if ([[event comment] length] > 0) {
163 [s appendString:@"DESCRIPTION:"]; /* this is what iCal.app does */
164 [s appendString:[[event comment] iCalSafeString]];
165 [s appendString:@"\r\n"];
168 if ((tmp = [event status]) != nil) {
169 [s appendString:@"STATUS:"];
170 [s appendString:tmp];
171 [s appendString:@"\r\n"];
174 if ((tmp = [event transparency]) != nil) {
175 [s appendString:@"TRANSP:"];
176 [s appendString:tmp];
177 [s appendString:@"\r\n"];
180 [s appendString:@"CLASS:"];
181 [s appendString:[event accessClass]];
182 [s appendString:@"\r\n"];
184 /* recurrence rules */
185 if ([event hasRecurrenceRules]) {
189 rules = [event recurrenceRules];
190 count = [rules count];
191 for (i = 0; i < count; i++) {
192 iCalRecurrenceRule *rule;
194 rule = [rules objectAtIndex:i];
195 [s appendString:@"RRULE:"];
196 [s appendString:[rule iCalRepresentation]];
197 [s appendString:@"\r\n"];
201 /* exception rules */
202 if ([event hasExceptionRules]) {
206 rules = [event exceptionRules];
207 count = [rules count];
208 for (i = 0; i < count; i++) {
209 iCalRecurrenceRule *rule;
211 rule = [rules objectAtIndex:i];
212 [s appendString:@"EXRULE:"];
213 [s appendString:[rule iCalRepresentation]];
214 [s appendString:@"\r\n"];
218 /* exception dates */
219 if ([event hasExceptionDates]) {
223 dates = [event exceptionDates];
224 count = [dates count];
225 [s appendString:@"EXDATE:"];
226 for (i = 0; i < count; i++) {
228 [s appendString:@","];
229 [s appendString:[[dates objectAtIndex:i] icalString]];
231 [s appendString:@"\r\n"];
234 [self addOrganizer:[event organizer] toString:s];
235 [self addAttendees:[event attendees] toString:s];
238 [s appendString:@"END:VEVENT\r\n"];
241 - (BOOL)isValidAppointment:(iCalEvent *)_apt {
242 if (![_apt isNotNull])
245 if ([[_apt uid] length] == 0) {
246 [self warnWithFormat:@"got apt without uid, rejecting iCal generation: %@",
250 if ([[[_apt startDate] icalString] length] == 0) {
251 [self warnWithFormat:@"got apt without start date, "
252 @"rejecting iCal generation: %@",
260 - (NSString *)vEventStringForEvent:(iCalEvent *)_apt {
263 if (![self isValidAppointment:_apt])
266 s = [NSMutableString stringWithCapacity:DefaultICalStringCapacity];
267 [self addVEventForAppointment:_apt toString:s];
271 - (NSString *)iCalendarStringForEvent:(iCalEvent *)_apt {
274 if (![self isValidAppointment:_apt])
277 s = [NSMutableString stringWithCapacity:DefaultICalStringCapacity];
278 [self addPreambleForAppointment:_apt toString:s];
279 [self addVEventForAppointment:_apt toString:s];
280 [self addPostambleForAppointment:_apt toString:s];
284 @end /* iCalRenderer */
286 @interface NSString (SOGoiCal_Private)
287 - (NSString *)iCalCleanString;
290 @interface SOGoICalStringEscaper : NSObject <NGStringEscaping>
296 @implementation SOGoICalStringEscaper
297 + (id)sharedEscaper {
298 static id sharedInstance = nil;
299 if (!sharedInstance) {
300 sharedInstance = [[self alloc] init];
302 return sharedInstance;
305 - (NSString *)stringByEscapingString:(NSString *)_s {
308 if (!_s || [_s length] == 0)
311 c = [_s characterAtIndex:0];
315 else if (c == '\r') {
316 return nil; /* effectively remove char */
318 return [NSString stringWithFormat:@"\\%@", _s];
323 @implementation NSString (SOGoiCal)
326 - (NSString *)iCalFoldedString {
327 /* RFC2445, 4.1 Content Lines
329 The iCalendar object is organized into individual lines of text,
330 called content lines. Content lines are delimited by a line break,
331 which is a CRLF sequence (US-ASCII decimal 13, followed by US-ASCII
333 Lines of text SHOULD NOT be longer than 75 octets, excluding the line
334 break. Long content lines SHOULD be split into a multiple line
335 representations using a line "folding" technique. That is, a long
336 line can be split between any two characters by inserting a CRLF
337 immediately followed by a single linear white space character (i.e.,
338 SPACE, US-ASCII decimal 32 or HTAB, US-ASCII decimal 9).
339 Any sequence of CRLF followed immediately by a single linear white space
340 character is ignored (i.e., removed) when processing the content type.
345 /* strip off any characters from string which are not allowed in iCal */
346 - (NSString *)iCalCleanString {
347 static NSCharacterSet *replaceSet = nil;
349 if (replaceSet == nil) {
350 replaceSet = [NSCharacterSet characterSetWithCharactersInString:@"\r"];
354 return [self stringByEscapingCharactersFromSet:replaceSet
355 usingStringEscaping:[SOGoICalStringEscaper sharedEscaper]];
358 - (NSString *)iCalDQUOTESafeString {
359 static NSCharacterSet *escapeSet = nil;
361 if (escapeSet == nil) {
362 escapeSet = [NSCharacterSet characterSetWithCharactersInString:@"\\\""];
365 return [self iCalEscapedStringWithEscapeSet:escapeSet];
368 - (NSString *)iCalSafeString {
369 static NSCharacterSet *escapeSet = nil;
371 if (escapeSet == nil) {
373 [[NSCharacterSet characterSetWithCharactersInString:@"\n,;\\\""] retain];
375 return [self iCalEscapedStringWithEscapeSet:escapeSet];
378 /* Escape unsafe characters */
379 - (NSString *)iCalEscapedStringWithEscapeSet:(NSCharacterSet *)_es {
382 s = [self iCalCleanString];
383 return [s stringByEscapingCharactersFromSet:_es
384 usingStringEscaping:[SOGoICalStringEscaper sharedEscaper]];
387 @end /* NSString (SOGoiCal) */