]> err.no Git - sope/blob - iCalRenderer.m
c7e108afbc8d9036b24d551b6fb4de2a006f4f95
[sope] / 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 "iCalRecurrenceRule.h"
26 #include "NSCalendarDate+ICal.h"
27 #include "common.h"
28
29 @implementation iCalRenderer
30
31 static iCalRenderer *renderer = nil;
32
33 /* assume length of 1K - reasonable ? */
34 static unsigned DefaultICalStringCapacity = 1024;
35
36 + (id)sharedICalendarRenderer {
37   if (renderer == nil)
38     renderer = [[self alloc] init];
39   return renderer;
40 }
41
42 /* renderer */
43
44 - (void)addPreambleForAppointment:(iCalEvent *)_apt
45   toString:(NSMutableString *)s
46 {
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"];
51 }
52 - (void)addPostambleForAppointment:(iCalEvent *)_apt
53   toString:(NSMutableString *)s
54 {
55   [s appendString:@"END:VCALENDAR\r\n"];
56 }
57
58 - (void)addOrganizer:(iCalPerson *)p toString:(NSMutableString *)s {
59   NSString *x;
60   
61   if (![p isNotNull]) return;
62   
63   [s appendString:@"ORGANIZER;CN=\""];
64   if ((x = [p cn]))
65     [s appendString:[x iCalDQUOTESafeString]];
66   
67   [s appendString:@"\""];
68   if ((x = [p email])) {
69     [s appendString:@":"]; /* sic! */
70     [s appendString:[x iCalSafeString]];
71   }
72   [s appendString:@"\r\n"];
73 }
74
75 - (void)addAttendees:(NSArray *)persons toString:(NSMutableString *)s {
76   unsigned   i, count;
77   iCalPerson *p;
78
79   count   = [persons count];
80   for (i = 0; i < count; i++) {
81     NSString *x;
82     
83     p = [persons objectAtIndex:i];
84     [s appendString:@"ATTENDEE;"];
85     
86     if ((x = [p role])) {
87       [s appendString:@"ROLE="];
88       [s appendString:[x iCalSafeString]];
89       [s appendString:@";"];
90     }
91     
92     if ((x = [p partStat])) {
93       if ([p participationStatus] != iCalPersonPartStatNeedsAction) {
94         [s appendString:@"PARTSTAT="];
95         [s appendString:[x iCalSafeString]];
96         [s appendString:@";"];
97       }
98     }
99
100     [s appendString:@"CN=\""];
101     if ((x = [p cnWithoutQuotes])) {
102       [s appendString:[x iCalDQUOTESafeString]];
103     }
104     [s appendString:@"\""];
105     if ([(x = [p email]) isNotNull]) {
106       [s appendString:@":"]; /* sic! */
107       [s appendString:[x iCalSafeString]];
108     }
109     [s appendString:@"\r\n"];
110   }
111 }
112
113 - (void)addVEventForAppointment:(iCalEvent *)event
114   toString:(NSMutableString *)s
115 {
116   id tmp;
117   
118   [s appendString:@"BEGIN:VEVENT\r\n"];
119   
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"];
127   }
128   
129   if ((tmp = [event uid]) != nil) {
130     [s appendString:@"UID:"];
131     [s appendString:tmp];
132     [s appendString:@"\r\n"];
133   }
134   
135   [s appendString:@"DTSTART:"];
136   [s appendString:[[event startDate] icalString]];
137   [s appendString:@"\r\n"];
138   
139   if ([event hasEndDate]) {
140     [s appendString:@"DTEND:"];
141     [s appendString:[[event endDate] icalString]];
142     [s appendString:@"\r\n"];
143   }
144   if ([event hasDuration]) {
145     [s appendString:@"DURATION:"];
146     [s appendString:[event duration]];
147     [s appendString:@"\r\n"];
148   }
149   if ([[event priority] length] > 0) {
150     [s appendString:@"PRIORITY:"];
151     [s appendString:[event priority]];
152     [s appendString:@"\r\n"];
153   }
154   if ([[event categories] length] > 0) {
155     NSString *catString;
156     
157     catString = [event categories];
158     [s appendString:@"CATEGORIES:"];
159     [s appendString:catString];
160     [s appendString:@"\r\n"];
161   }
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"];
166   }
167
168   if ((tmp = [event status]) != nil) {
169     [s appendString:@"STATUS:"];
170     [s appendString:tmp];
171     [s appendString:@"\r\n"];
172   }
173
174   if ((tmp = [event transparency]) != nil) {
175     [s appendString:@"TRANSP:"];
176     [s appendString:tmp];
177     [s appendString:@"\r\n"];
178   }
179
180   [s appendString:@"CLASS:"];
181   [s appendString:[event accessClass]];
182   [s appendString:@"\r\n"];
183
184   /* recurrence rules */
185   if ([event hasRecurrenceRules]) {
186     NSArray  *rules;
187     unsigned i, count;
188     
189     rules = [event recurrenceRules];
190     count = [rules count];
191     for (i = 0; i < count; i++) {
192       iCalRecurrenceRule *rule;
193       
194       rule = [rules objectAtIndex:i];
195       [s appendString:@"RRULE:"];
196       [s appendString:[rule iCalRepresentation]];
197       [s appendString:@"\r\n"];
198     }
199   }
200
201   /* exception rules */
202   if ([event hasExceptionRules]) {
203     NSArray  *rules;
204     unsigned i, count;
205     
206     rules = [event exceptionRules];
207     count = [rules count];
208     for (i = 0; i < count; i++) {
209       iCalRecurrenceRule *rule;
210       
211       rule = [rules objectAtIndex:i];
212       [s appendString:@"EXRULE:"];
213       [s appendString:[rule iCalRepresentation]];
214       [s appendString:@"\r\n"];
215     }
216   }
217
218   /* exception dates */
219   if ([event hasExceptionDates]) {
220     NSArray *dates;
221     unsigned i, count;
222     
223     dates = [event exceptionDates];
224     count = [dates count];
225     [s appendString:@"EXDATE:"];
226     for (i = 0; i < count; i++) {
227       if (i > 0)
228         [s appendString:@","];
229       [s appendString:[[dates objectAtIndex:i] icalString]];
230     }
231     [s appendString:@"\r\n"];
232   }
233
234   [self addOrganizer:[event organizer] toString:s];
235   [self addAttendees:[event attendees] toString:s];
236   
237   /* postamble */
238   [s appendString:@"END:VEVENT\r\n"];
239 }
240
241 - (BOOL)isValidAppointment:(iCalEvent *)_apt {
242   if (![_apt isNotNull])
243     return NO;
244   
245   if ([[_apt uid] length] == 0) {
246     [self warnWithFormat:@"got apt without uid, rejecting iCal generation: %@", 
247                            _apt];
248     return NO;
249   }
250   if ([[[_apt startDate] icalString] length] == 0) {
251     [self warnWithFormat:@"got apt without start date, "
252                                @"rejecting iCal generation: %@",
253                                  _apt];
254     return NO;
255   }
256   
257   return YES;
258 }
259
260 - (NSString *)vEventStringForEvent:(iCalEvent *)_apt {
261   NSMutableString *s;
262   
263   if (![self isValidAppointment:_apt])
264     return nil;
265   
266   s = [NSMutableString stringWithCapacity:DefaultICalStringCapacity];
267   [self addVEventForAppointment:_apt toString:s];
268   return s;
269 }
270
271 - (NSString *)iCalendarStringForEvent:(iCalEvent *)_apt {
272   NSMutableString *s;
273   
274   if (![self isValidAppointment:_apt])
275     return nil;
276   
277   s = [NSMutableString stringWithCapacity:DefaultICalStringCapacity];
278   [self addPreambleForAppointment:_apt  toString:s];
279   [self addVEventForAppointment:_apt    toString:s];
280   [self addPostambleForAppointment:_apt toString:s];
281   return s;
282 }
283
284 @end /* iCalRenderer */
285
286 @interface NSString (SOGoiCal_Private)
287 - (NSString *)iCalCleanString;
288 @end
289
290 @interface SOGoICalStringEscaper : NSObject <NGStringEscaping>
291 {
292 }
293 + (id)sharedEscaper;
294 @end
295
296 @implementation SOGoICalStringEscaper
297 + (id)sharedEscaper {
298   static id sharedInstance = nil;
299   if (!sharedInstance) {
300     sharedInstance = [[self alloc] init];
301   }
302   return sharedInstance;
303 }
304
305 - (NSString *)stringByEscapingString:(NSString *)_s {
306   unichar c;
307
308   if (!_s || [_s length] == 0)
309     return nil;
310
311   c = [_s characterAtIndex:0];
312   if (c == '\n') {
313     return @"\\n";
314   }
315   else if (c == '\r') {
316     return nil; /* effectively remove char */
317   }
318   return [NSString stringWithFormat:@"\\%@", _s];
319 }
320
321 @end
322
323 @implementation NSString (SOGoiCal)
324
325 #if 0
326 - (NSString *)iCalFoldedString {
327   /* RFC2445, 4.1 Content Lines
328   
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
332                             decimal 10).
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.
341   */
342 }
343 #endif
344
345 /* strip off any characters from string which are not allowed in iCal */
346 - (NSString *)iCalCleanString {
347   static NSCharacterSet *replaceSet = nil;
348
349   if (replaceSet == nil) {
350     replaceSet = [NSCharacterSet characterSetWithCharactersInString:@"\r"];
351     [replaceSet retain];
352   }
353   
354   return [self stringByEscapingCharactersFromSet:replaceSet
355                usingStringEscaping:[SOGoICalStringEscaper sharedEscaper]];
356 }
357
358 - (NSString *)iCalDQUOTESafeString {
359   static NSCharacterSet *escapeSet = nil;
360   
361   if (escapeSet == nil) {
362     escapeSet = [NSCharacterSet characterSetWithCharactersInString:@"\\\""];
363     [escapeSet retain];
364   }
365   return [self iCalEscapedStringWithEscapeSet:escapeSet];
366 }
367
368 - (NSString *)iCalSafeString {
369   static NSCharacterSet *escapeSet = nil;
370   
371   if (escapeSet == nil) {
372     escapeSet = 
373       [[NSCharacterSet characterSetWithCharactersInString:@"\n,;\\\""] retain];
374   }
375   return [self iCalEscapedStringWithEscapeSet:escapeSet];
376 }
377
378 /* Escape unsafe characters */
379 - (NSString *)iCalEscapedStringWithEscapeSet:(NSCharacterSet *)_es {
380   NSString *s;
381
382   s = [self iCalCleanString];
383   return [s stringByEscapingCharactersFromSet:_es
384             usingStringEscaping:[SOGoICalStringEscaper sharedEscaper]];
385 }
386
387 @end /* NSString (SOGoiCal) */