]> err.no Git - sope/blob - sope-ical/NGiCal/iCalRenderer.m
bugfixes for cycle calculation, modified implementations with future extensions in...
[sope] / sope-ical / NGiCal / iCalRenderer.m
1 /*
2   Copyright (C) 2004-2005 SKYRIX Software AG
3
4   This file is part of SOPE.
5
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
9   later version.
10
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.
15
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
19   02111-1307, USA.
20 */
21
22 #include "iCalRenderer.h"
23 #include "iCalEvent.h"
24 #include "iCalPerson.h"
25 #include "common.h"
26
27 @interface NSDate(UsedPrivates)
28 - (NSString *)icalString; // declared in NGiCal
29 @end
30
31 @implementation iCalRenderer
32
33 static iCalRenderer *renderer = nil;
34
35 /* assume length of 1K - reasonable ? */
36 static unsigned DefaultICalStringCapacity = 1024;
37
38 + (id)sharedICalendarRenderer {
39   if (renderer == nil)
40     renderer = [[self alloc] init];
41   return renderer;
42 }
43
44 /* renderer */
45
46 - (void)addPreambleForAppointment:(iCalEvent *)_apt
47   toString:(NSMutableString *)s
48 {
49   [s appendString:@"BEGIN:VCALENDAR\r\nMETHOD:REQUEST\r\n"];
50   [s appendFormat:@"PRODID:NGiCal/%i.%i\r\n",
51        SOPE_MAJOR_VERSION, SOPE_MINOR_VERSION];
52   [s appendString:@"VERSION:2.0\r\n"];
53 }
54 - (void)addPostambleForAppointment:(iCalEvent *)_apt
55   toString:(NSMutableString *)s
56 {
57   [s appendString:@"END:VCALENDAR\r\n"];
58 }
59
60 - (void)addOrganizer:(iCalPerson *)p toString:(NSMutableString *)s {
61   NSString *x;
62   
63   if (![p isNotNull]) return;
64   
65   [s appendString:@"ORGANIZER;CN=\""];
66   if ((x = [p cn]))
67     [s appendString:[x iCalDQUOTESafeString]];
68   
69   [s appendString:@"\""];
70   if ((x = [p email])) {
71     [s appendString:@":"]; /* sic! */
72     [s appendString:[x iCalSafeString]];
73   }
74   [s appendString:@"\r\n"];
75 }
76
77 - (void)addAttendees:(NSArray *)persons toString:(NSMutableString *)s {
78   unsigned   i, count;
79   iCalPerson *p;
80
81   count   = [persons count];
82   for (i = 0; i < count; i++) {
83     NSString *x;
84     
85     p = [persons objectAtIndex:i];
86     [s appendString:@"ATTENDEE;"];
87     
88     if ((x = [p role])) {
89       [s appendString:@"ROLE="];
90       [s appendString:[x iCalSafeString]];
91       [s appendString:@";"];
92     }
93     
94     [s appendString:@"CN=\""];
95     if ((x = [p cn])) {
96       [s appendString:[x iCalDQUOTESafeString]];
97     }
98     [s appendString:@"\""];
99     if ([(x = [p email]) isNotNull]) {
100       [s appendString:@":"]; /* sic! */
101       [s appendString:[x iCalSafeString]];
102     }
103     [s appendString:@"\r\n"];
104   }
105 }
106
107 - (void)addVEventForAppointment:(iCalEvent *)event
108   toString:(NSMutableString *)s
109 {
110   id tmp;
111   
112   [s appendString:@"BEGIN:VEVENT\r\n"];
113   
114   [s appendString:@"SUMMARY:"];
115   [s appendString:[[event summary] iCalSafeString]];
116   [s appendString:@"\r\n"];
117   if ([[event location] length] > 0) {
118     [s appendString:@"LOCATION:"];
119     [s appendString:[[event location] iCalSafeString]];
120     [s appendString:@"\r\n"];
121   }
122   
123   if ((tmp = [event uid]) != nil) {
124     [s appendString:@"UID:"];
125     [s appendString:tmp];
126     [s appendString:@"\r\n"];
127   }
128   
129   [s appendString:@"DTSTART:"];
130   [s appendString:[[event startDate] icalString]];
131   [s appendString:@"\r\n"];
132   
133   if ([event hasEndDate]) {
134     [s appendString:@"DTEND:"];
135     [s appendString:[[event endDate] icalString]];
136     [s appendString:@"\r\n"];
137   }
138   if ([event hasDuration]) {
139     [s appendString:@"DURATION:"];
140     [s appendString:[event duration]];
141     [s appendString:@"\r\n"];
142   }
143   if ([[event priority] length] > 0) {
144     [s appendString:@"PRIORITY:"];
145     [s appendString:[event priority]];
146     [s appendString:@"\r\n"];
147   }
148   if ([[event categories] length] > 0) {
149     NSString *catString;
150     
151     catString = [event categories];
152     [s appendString:@"CATEGORIES:"];
153     [s appendString:catString];
154     [s appendString:@"\r\n"];
155   }
156   if ([[event comment] length] > 0) {
157     [s appendString:@"DESCRIPTION:"]; /* this is what iCal.app does */
158     [s appendString:[[event comment] iCalSafeString]];
159     [s appendString:@"\r\n"];
160   }
161
162   if ((tmp = [event status]) != nil) {
163     [s appendString:@"STATUS:"];
164     [s appendString:tmp];
165     [s appendString:@"\r\n"];
166   }
167
168   
169   /* what's all this? */
170   [s appendString:@"TRANSP:OPAQUE\r\n"]; /* transparency */
171   [s appendString:@"CLASS:PRIVATE\r\n"]; /* classification [like 'top secret'] */
172   
173   [self addOrganizer:[event organizer] toString:s];
174   [self addAttendees:[event attendees] toString:s];
175   
176   /* postamble */
177   [s appendString:@"END:VEVENT\r\n"];
178 }
179
180 - (BOOL)isValidAppointment:(iCalEvent *)_apt {
181   if (![_apt isNotNull])
182     return NO;
183   
184   if ([[_apt uid] length] == 0) {
185     [self logWithFormat:
186             @"WARNING: got apt without uid, rejecting iCal generation: %@", 
187             _apt];
188     return NO;
189   }
190   if ([[[_apt startDate] icalString] length] == 0) {
191     [self logWithFormat:
192             @"WARNING: got apt without start date, "
193             @"rejecting iCal generation: %@",
194             _apt];
195     return NO;
196   }
197   
198   return YES;
199 }
200
201 - (NSString *)vEventStringForEvent:(iCalEvent *)_apt {
202   NSMutableString *s;
203   
204   if (![self isValidAppointment:_apt])
205     return nil;
206   
207   s = [NSMutableString stringWithCapacity:DefaultICalStringCapacity];
208   [self addVEventForAppointment:_apt toString:s];
209   return s;
210 }
211
212 - (NSString *)iCalendarStringForEvent:(iCalEvent *)_apt {
213   NSMutableString *s;
214   
215   if (![self isValidAppointment:_apt])
216     return nil;
217   
218   s = [NSMutableString stringWithCapacity:DefaultICalStringCapacity];
219   [self addPreambleForAppointment:_apt  toString:s];
220   [self addVEventForAppointment:_apt    toString:s];
221   [self addPostambleForAppointment:_apt toString:s];
222   return s;
223 }
224
225 @end /* iCalRenderer */
226
227 @interface NSString (SOGoiCal_Private)
228 - (NSString *)iCalCleanString;
229 @end
230
231 @interface SOGoICalStringEscaper : NSObject <NGStringEscaping>
232 {
233 }
234 + (id)sharedEscaper;
235 @end
236
237 @implementation SOGoICalStringEscaper
238 + (id)sharedEscaper {
239   static id sharedInstance = nil;
240   if (!sharedInstance) {
241     sharedInstance = [[self alloc] init];
242   }
243   return sharedInstance;
244 }
245
246 - (NSString *)stringByEscapingString:(NSString *)_s {
247   unichar c;
248
249   if (!_s || [_s length] == 0)
250     return nil;
251
252   c = [_s characterAtIndex:0];
253   if (c == '\n') {
254     return @"\\n";
255   }
256   else if (c == '\r') {
257     return nil; /* effectively remove char */
258   }
259   return [NSString stringWithFormat:@"\\%@", _s];
260 }
261
262 @end
263
264 @implementation NSString (SOGoiCal)
265
266 #if 0
267 - (NSString *)iCalFoldedString {
268   /* RFC2445, 4.1 Content Lines
269   
270   The iCalendar object is organized into individual lines of text,
271   called content lines. Content lines are delimited by a line break,
272   which is a CRLF sequence (US-ASCII decimal 13, followed by US-ASCII
273                             decimal 10).
274   Lines of text SHOULD NOT be longer than 75 octets, excluding the line
275   break. Long content lines SHOULD be split into a multiple line
276   representations using a line "folding" technique. That is, a long
277   line can be split between any two characters by inserting a CRLF
278   immediately followed by a single linear white space character (i.e.,
279   SPACE, US-ASCII decimal 32 or HTAB, US-ASCII decimal 9).
280   Any sequence of CRLF followed immediately by a single linear white space
281   character is ignored (i.e., removed) when processing the content type.
282   */
283 }
284 #endif
285
286 /* strip off any characters from string which are not allowed in iCal */
287 - (NSString *)iCalCleanString {
288   static NSCharacterSet *replaceSet = nil;
289
290   if (replaceSet == nil) {
291     replaceSet = [NSCharacterSet characterSetWithCharactersInString:@"\r"];
292     [replaceSet retain];
293   }
294   
295   return [self stringByEscapingCharactersFromSet:replaceSet
296                usingStringEscaping:[SOGoICalStringEscaper sharedEscaper]];
297 }
298
299 - (NSString *)iCalDQUOTESafeString {
300   static NSCharacterSet *escapeSet = nil;
301   
302   if (escapeSet == nil) {
303     escapeSet = [NSCharacterSet characterSetWithCharactersInString:@"\\\""];
304     [escapeSet retain];
305   }
306   return [self iCalEscapedStringWithEscapeSet:escapeSet];
307 }
308
309 - (NSString *)iCalSafeString {
310   static NSCharacterSet *escapeSet = nil;
311   
312   if (escapeSet == nil) {
313     escapeSet = 
314       [[NSCharacterSet characterSetWithCharactersInString:@"\n,;\\\""] retain];
315   }
316   return [self iCalEscapedStringWithEscapeSet:escapeSet];
317 }
318
319 /* Escape unsafe characters */
320 - (NSString *)iCalEscapedStringWithEscapeSet:(NSCharacterSet *)_es {
321   NSString *s;
322
323   s = [self iCalCleanString];
324   return [s stringByEscapingCharactersFromSet:_es
325             usingStringEscaping:[SOGoICalStringEscaper sharedEscaper]];
326 }
327
328 @end /* NSString (SOGoiCal) */