1/* iCalToDot+SOGo.m - this file is part of SOGo 2 * 3 * Copyright (C) 2008-2017 Inverse inc. 4 * 5 * This file is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2, or (at your option) 8 * any later version. 9 * 10 * This file is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program; see the file COPYING. If not, write to 17 * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, 18 * Boston, MA 02111-1307, USA. 19 */ 20 21#import <Foundation/NSDictionary.h> 22#import <Foundation/NSValue.h> 23 24#import <NGExtensions/NSNull+misc.h> 25#import <NGExtensions/NSObject+Logs.h> 26 27#import <NGCards/NSString+NGCards.h> 28#import <NGCards/iCalCalendar.h> 29#import <NGCards/iCalDateTime.h> 30#import <NGCards/iCalPerson.h> 31#import <NGCards/iCalTimeZone.h> 32 33#import <SoObjects/SOGo/WOContext+SOGo.h> 34 35#import <SOGo/CardElement+SOGo.h> 36#import <SOGo/NSCalendarDate+SOGo.h> 37#import <SOGo/SOGoUser.h> 38#import <SOGo/SOGoUserDefaults.h> 39 40#import "iCalRepeatableEntityObject+SOGo.h" 41 42#import "iCalToDo+SOGo.h" 43 44@implementation iCalToDo (SOGoExtensions) 45 46+ (NSString *) statusForCode: (int) statusCode 47{ 48 if (statusCode == taskStatusCompleted) 49 return @"completed"; 50 else if (statusCode == taskStatusInProcess) 51 return @"in-process"; 52 else if (statusCode == taskStatusCancelled) 53 return @"cancelled"; 54 else if (statusCode == taskStatusNeedsAction) 55 return @"needs-action"; 56 57 return @""; 58} 59 60- (NSDictionary *) attributesInContext: (WOContext *) context 61{ 62 BOOL isAllDayStartDate, isAllDayDueDate; 63 NSCalendarDate *startDate, *dueDate, *completedDate; 64 NSMutableDictionary *data; 65 NSTimeZone *timeZone; 66 SOGoUserDefaults *ud; 67 68 ud = [[context activeUser] userDefaults]; 69 timeZone = [ud timeZone]; 70 71 startDate = [self startDate]; 72 isAllDayStartDate = [(iCalDateTime *) [self uniqueChildWithTag: @"dtstart"] isAllDay]; 73 if (!isAllDayStartDate) 74 [startDate setTimeZone: timeZone]; 75 76 dueDate = [self due]; 77 isAllDayDueDate = [(iCalDateTime *) [self uniqueChildWithTag: @"due"] isAllDay]; 78 if (!isAllDayDueDate) 79 [dueDate setTimeZone: timeZone]; 80 81 completedDate = [self completed]; 82 [completedDate setTimeZone: timeZone]; 83 84 data = [NSMutableDictionary dictionaryWithDictionary: [super attributesInContext: context]]; 85 86 if (startDate) 87 [data setObject: [startDate iso8601DateString] forKey: @"startDate"]; 88 if (dueDate) 89 [data setObject: [dueDate iso8601DateString] forKey: @"dueDate"]; 90 if (completedDate) 91 [data setObject: [completedDate iso8601DateString] forKey: @"completedDate"]; 92 93 if ([[self percentComplete] length]) 94 [data setObject: [NSNumber numberWithInt: [[self percentComplete] intValue]] forKey: @"percentComplete"]; 95 96 return data; 97} 98 99/** 100 * @see [iCalRepeatableEntityObject+SOGo setAttributes:inContext:] 101 * @see [iCalEntityObject+SOGo setAttributes:inContext:] 102 * @see [UIxAppointmentEditor saveAction] 103 */ 104- (void) setAttributes: (NSDictionary *) data 105 inContext: (WOContext *) context 106{ 107 BOOL isAllDayStartDate, isAllDayDueDate; 108 NSCalendarDate *startDate, *dueDate, *completedDate; 109 NSInteger percent; 110 SOGoUserDefaults *ud; 111 iCalDateTime *todoStartDate, *todoDueDate; 112 iCalTimeZone *tz; 113 id o; 114 115 [super setAttributes: data inContext: context]; 116 117 startDate = dueDate = completedDate = nil; 118 119 // Handle start date 120 isAllDayStartDate = YES; 121 o = [data objectForKey: @"startDate"]; 122 if ([o isKindOfClass: [NSString class]] && [o length]) 123 startDate = [self dateFromString: o inContext: context]; 124 125 o = [data objectForKey: @"startTime"]; 126 if ([o isKindOfClass: [NSString class]] && [o length]) 127 { 128 isAllDayStartDate = NO; 129 [self adjustDate: &startDate withTimeString: o inContext: context]; 130 } 131 132 if (startDate) 133 [self setStartDate: startDate]; 134 else 135 [self setStartDate: nil]; 136 137 // Handle due date 138 isAllDayDueDate = YES; 139 o = [data objectForKey: @"dueDate"]; 140 if ([o isKindOfClass: [NSString class]] && [o length]) 141 dueDate = [self dateFromString: o inContext: context]; 142 143 o = [data objectForKey: @"dueTime"]; 144 if ([o isKindOfClass: [NSString class]] && [o length]) 145 { 146 isAllDayDueDate = NO; 147 [self adjustDate: &dueDate withTimeString: o inContext: context]; 148 } 149 150 if (dueDate) 151 if (isAllDayDueDate) 152 [self setAllDayDue: dueDate]; 153 else 154 [self setDue: dueDate]; 155 else 156 [self setDue: nil]; 157 158 if (!startDate && !dueDate) 159 [self removeAllAlarms]; 160 161 // Handle time zone 162 todoStartDate = (iCalDateTime *)[self uniqueChildWithTag: @"dtstart"]; 163 todoDueDate = (iCalDateTime *)[self uniqueChildWithTag: @"due"]; 164 tz = [todoStartDate timeZone]; 165 if (!tz) 166 tz = [todoDueDate timeZone]; 167 168 if (isAllDayStartDate && isAllDayDueDate) 169 { 170 if (tz) 171 [[self parent] removeChild: tz]; 172 [todoStartDate setTimeZone: nil]; 173 [todoDueDate setTimeZone: nil]; 174 } 175 else 176 { 177 if (!tz) 178 { 179 ud = [[context activeUser] userDefaults]; 180 tz = [iCalTimeZone timeZoneForName: [ud timeZoneName]]; 181 } 182 if (tz) 183 { 184 [[self parent] addTimeZone: tz]; 185 if (todoStartDate) 186 { 187 if (isAllDayStartDate) 188 [todoStartDate setTimeZone: nil]; 189 else if (![todoStartDate timeZone]) 190 [todoStartDate setTimeZone: tz]; 191 } 192 if (todoDueDate) 193 { 194 if (isAllDayDueDate) 195 [todoDueDate setTimeZone: nil]; 196 else if (![todoDueDate timeZone]) 197 [todoDueDate setTimeZone: tz]; 198 } 199 } 200 } 201 202 // Handle completed date 203 o = [data objectForKey: @"completedDate"]; 204 if ([o isKindOfClass: [NSString class]] && [o length]) 205 { 206 completedDate = [self dateFromString: o inContext: context]; 207 208 o = [data objectForKey: @"completedTime"]; 209 if ([o isKindOfClass: [NSString class]] && [o length]) 210 [self adjustDate: &completedDate withTimeString: o inContext: context]; 211 } 212 else 213 [(iCalDateTime *) [self uniqueChildWithTag: @"completed"] setDateTime: nil]; 214 215 o = [self status]; 216 if ([o length]) 217 { 218 if ([o isEqualToString: @"COMPLETED"] && completedDate) 219 // As specified in RFC5545, the COMPLETED property must use UTC time 220 [self setCompleted: completedDate]; 221 } 222 223 // Percent complete 224 o = [data objectForKey: @"percentComplete"]; 225 if ([o isKindOfClass: [NSNumber class]]) 226 { 227 percent = [o intValue]; 228 if (percent >= 0 && percent <= 100) 229 [self setPercentComplete: [NSString stringWithFormat: @"%i", (int)percent]]; 230 } 231 else 232 [self setPercentComplete: @""]; 233} 234 235- (NSMutableDictionary *) quickRecordFromContent: (NSString *) theContent 236 container: (id) theContainer 237 nameInContainer: (NSString *) nameInContainer 238{ 239 NSMutableDictionary *row; 240 NSCalendarDate *startDate, *dueDate, *completed; 241 NSArray *attendees, *categories; 242 NSString *uid, *title, *location, *status; 243 NSNumber *sequence; 244 id organizer, date; 245 id participants, partmails; 246 NSMutableString *partstates; 247 unsigned i, count, code; 248 iCalAccessClass accessClass; 249 250 /* extract values */ 251 252 startDate = [self startDate]; 253 dueDate = [self due]; 254 uid = [self uid]; 255 title = [self summary]; 256 if (![title isNotNull]) 257 title = @""; 258 location = [self location]; 259 sequence = [self sequence]; 260 accessClass = [self symbolicAccessClass]; 261 completed = [self completed]; 262 status = [[self status] uppercaseString]; 263 264 attendees = [self attendees]; 265 partmails = [attendees valueForKey: @"rfc822Email"]; 266 partmails = [partmails componentsJoinedByString: @"\n"]; 267 participants = [attendees valueForKey: @"cn"]; 268 participants = [participants componentsJoinedByString: @"\n"]; 269 270 /* build row */ 271 272 row = [NSMutableDictionary dictionaryWithCapacity:8]; 273 274 [row setObject: @"vtodo" forKey: @"c_component"]; 275 276 if ([uid isNotNull]) 277 [row setObject:uid forKey: @"c_uid"]; 278 else 279 [self logWithFormat: @"WARNING: could not extract a uid from event!"]; 280 281 [row setObject:[NSNumber numberWithBool:[self isRecurrent]] 282 forKey: @"c_iscycle"]; 283 [row setObject:[NSNumber numberWithInt:[self priorityNumber]] 284 forKey: @"c_priority"]; 285 286 [row setObject: [NSNumber numberWithBool: NO] 287 forKey: @"c_isallday"]; 288 [row setObject: [NSNumber numberWithBool: NO] 289 forKey: @"c_isopaque"]; 290 291 [row setObject: title forKey: @"c_title"]; 292 if ([location isNotNull]) [row setObject: location forKey: @"c_location"]; 293 if ([sequence isNotNull]) [row setObject: sequence forKey: @"c_sequence"]; 294 295 if ([startDate isNotNull]) 296 date = [self quickRecordDateAsNumber: startDate 297 withOffset: 0 forAllDay: NO]; 298 else 299 date = [NSNull null]; 300 [row setObject: date forKey: @"c_startdate"]; 301 302 if ([dueDate isNotNull]) 303 date = [self quickRecordDateAsNumber: dueDate 304 withOffset: 0 forAllDay: NO]; 305 else 306 date = [NSNull null]; 307 [row setObject: date forKey: @"c_enddate"]; 308 309 if ([self isRecurrent]) 310 { 311 [row setObject: iCalDistantFutureNumber forKey: @"c_cycleenddate"]; 312 [row setObject: [self cycleInfo] forKey: @"c_cycleinfo"]; 313 } 314 315 if ([participants length] > 0) 316 [row setObject:participants forKey: @"c_participants"]; 317 if ([partmails length] > 0) 318 [row setObject:partmails forKey: @"c_partmails"]; 319 320 if (completed || [status isNotNull]) 321 { 322 code = 0; 323 if (completed || [status isEqualToString: @"COMPLETED"]) 324 code = taskStatusCompleted; 325 else if ([status isEqualToString: @"IN-PROCESS"]) 326 code = taskStatusInProcess; 327 else if ([status isEqualToString: @"CANCELLED"]) 328 code = taskStatusCancelled; 329 else if ([status isEqualToString: @"NEEDS-ACTION"]) 330 code = taskStatusNeedsAction; 331 [row setObject: [NSNumber numberWithInt: code] forKey: @"c_status"]; 332 } 333 else 334 { 335 /* confirmed by default */ 336 [row setObject: [NSNumber numberWithInt: 0] forKey: @"c_status"]; 337 } 338 339 [row setObject: [NSNumber numberWithUnsignedInt: accessClass] 340 forKey: @"c_classification"]; 341 342 organizer = [self organizer]; 343 if (organizer) 344 { 345 NSString *email; 346 347 email = [organizer valueForKey: @"rfc822Email"]; 348 if (email) 349 [row setObject:email forKey: @"c_orgmail"]; 350 } 351 352 /* construct partstates */ 353 count = [attendees count]; 354 partstates = [[NSMutableString alloc] initWithCapacity:count * 2]; 355 for (i = 0; i < count; i++) 356 { 357 iCalPerson *p; 358 iCalPersonPartStat stat; 359 360 p = [attendees objectAtIndex:i]; 361 stat = [p participationStatus]; 362 if(i != 0) 363 [partstates appendString: @"\n"]; 364 [partstates appendFormat: @"%d", stat]; 365 } 366 [row setObject:partstates forKey: @"c_partstates"]; 367 [partstates release]; 368 369 /* handle alarms */ 370 [self updateNextAlarmDateInRow: row forContainer: theContainer nameInContainer: nameInContainer]; 371 372 categories = [self categories]; 373 if ([categories count] > 0) 374 [row setObject: [categories componentsJoinedByString: @","] 375 forKey: @"c_category"]; 376 377 /* handle description */ 378 if ([self comment]) 379 [row setObject: [self comment] forKey: @"c_description"]; 380 else 381 [row setObject: [NSNull null] forKey: @"c_description"]; 382 383 return row; 384} 385 386- (NSTimeInterval) occurenceInterval 387{ 388 if ([self due]) 389 return [[self due] timeIntervalSinceDate: [self startDate]]; 390 else 391 // When no due date is defined, base recurrence calculation on a 60-minute duration 392 return 3600; 393} 394 395@end 396