/* Copyright (C) 2004 SKYRIX Software AG This file is part of OpenGroupware.org. OGo is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2, or (at your option) any later version. OGo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with OGo; see the file COPYING. If not, write to the Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ // $Id$ #include /* TODO: CLEAN UP */ @class NSString; @class iCalPerson; @class SOGoAppointment; @interface UIxAppointmentEditor : UIxComponent { NSString *iCalString; NSString *errorText; id item; /* individual values */ NSCalendarDate *startDate; NSCalendarDate *endDate; NSString *title; NSString *location; NSString *comment; NSArray *participants; /* array of iCalPerson's */ NSArray *resources; /* array of iCalPerson's */ NSString *priority; NSArray *categories; NSString *accessClass; BOOL isPrivate; /* default: NO */ BOOL checkForConflicts; /* default: NO */ } - (NSString *)iCalStringTemplate; - (NSString *)iCalString; - (void)setIsPrivate:(BOOL)_yn; - (void)setAccessClass:(NSString *)_class; - (void)setCheckForConflicts:(BOOL)_checkForConflicts; - (BOOL)checkForConflicts; - (NSString *)_completeURIForMethod:(NSString *)_method; - (NSArray *)getICalPersonsFromFormValues:(NSArray *)_values treatAsResource:(BOOL)_isResource; - (NSString *)iCalParticipantsAndResourcesStringFromQueryParameters; - (NSString *)iCalParticipantsStringFromQueryParameters; - (NSString *)iCalResourcesStringFromQueryParameters; - (NSString *)iCalStringFromQueryParameter:(NSString *)_qp format:(NSString *)_format; - (NSString *)iCalOrganizerString; - (id)acceptOrDeclineAction:(BOOL)_accept; @end #include "common.h" #include #include #include #include #include #include #include #include "iCalPerson+UIx.h" #include "UIxComponent+Agenor.h" @interface NSDate(UsedPrivates) - (NSString *)icalString; // TODO: this is in NGiCal @end @implementation UIxAppointmentEditor - (id)init { self = [super init]; if(self) { [self setIsPrivate:NO]; [self setCheckForConflicts:NO]; } return self; } - (void)dealloc { [self->iCalString release]; [self->errorText release]; [self->item release]; [self->startDate release]; [self->endDate release]; [self->title release]; [self->location release]; [self->comment release]; [self->participants release]; [self->resources release]; [self->priority release]; [self->categories release]; [self->accessClass release]; [super dealloc]; } /* accessors */ - (void)setItem:(id)_item { ASSIGN(self->item, _item); } - (id)item { return self->item; } - (void)setErrorText:(NSString *)_txt { ASSIGNCOPY(self->errorText, _txt); } - (NSString *)errorText { return self->errorText; } - (BOOL)hasErrorText { return [self->errorText length] > 0 ? YES : NO; } - (NSFormatter *)titleDateFormatter { SOGoDateFormatter *fmt; fmt = [[[SOGoDateFormatter alloc] initWithLocale:[self locale]] autorelease]; [fmt setFullWeekdayNameAndDetails]; return fmt; } - (void)setAptStartDate:(NSCalendarDate *)_date { ASSIGN(self->startDate, _date); } - (NSCalendarDate *)aptStartDate { return self->startDate; } - (void)setAptEndDate:(NSCalendarDate *)_date { ASSIGN(self->endDate, _date); } - (NSCalendarDate *)aptEndDate { return self->endDate; } - (void)setTitle:(NSString *)_value { ASSIGNCOPY(self->title, _value); } - (NSString *)title { return self->title; } - (void)setLocation:(NSString *)_value { ASSIGNCOPY(self->location, _value); } - (NSString *)location { return self->location; } - (void)setComment:(NSString *)_value { ASSIGNCOPY(self->comment, _value); } - (NSString *)comment { return self->comment; } - (void)setParticipants:(NSArray *)_parts { ASSIGN(self->participants, _parts); } - (NSArray *)participants { return self->participants; } - (void)setResources:(NSArray *)_res { ASSIGN(self->resources, _res); } - (NSArray *)resources { return self->resources; } /* priorities */ - (NSArray *)priorities { /* 0 == undefined 5 == normal 1 == high */ static NSArray *priorities = nil; if (!priorities) priorities = [[NSArray arrayWithObjects:@"0", @"5", @"1", nil] retain]; return priorities; } - (NSString *)itemPriorityText { NSString *key; key = [NSString stringWithFormat:@"prio_%@", self->item]; return [self labelForKey:key]; } - (void)setPriority:(NSString *)_priority { ASSIGN(self->priority, _priority); } - (NSString *)priority { return self->priority; } /* categories */ - (NSArray *)categoryItems { // TODO: make this configurable? /* Tasks categories will be modified as follow : – by default (a simple logo or no logo at all), – appointment, – outside, – meeting, – holidays, – phone. */ static NSArray *categoryItems = nil; if (!categoryItems) { categoryItems = [[NSArray arrayWithObjects:@"APPOINTMENT", @"NOT IN OFFICE", @"MEETING", @"HOLIDAY", @"PHONE CALL", nil] retain]; } return categoryItems; } - (NSString *)itemCategoryText { return [self labelForKey:self->item]; } - (void)setCategories:(NSArray *)_categories { ASSIGN(self->categories, _categories); } - (NSArray *)categories { return self->categories; } /* class */ #if 0 - (NSArray *)accessClassItems { static NSArray classItems = nil; if (!classItems) { return [[NSArray arrayWithObjects:@"PUBLIC", @"PRIVATE", nil] retain]; } return classItems; } #endif - (void)setAccessClass:(NSString *)_class { ASSIGN(self->accessClass, _class); } - (NSString *)accessClass { return self->accessClass; } - (void)setIsPrivate:(BOOL)_yn { if (_yn) [self setAccessClass:@"PRIVATE"]; else [self setAccessClass:@"PUBLIC"]; self->isPrivate = _yn; } - (BOOL)isPrivate { return self->isPrivate; } - (void)setCheckForConflicts:(BOOL)_checkForConflicts { self->checkForConflicts = _checkForConflicts; } - (BOOL)checkForConflicts { return self->checkForConflicts; } /* transparency */ - (NSString *)transparency { return @"TRANSPARENT"; } /* iCal */ - (void)setICalString:(NSString *)_s { ASSIGNCOPY(self->iCalString, _s); } - (NSString *)iCalString { return self->iCalString; } - (NSString *)iCalStringTemplate { static NSString *iCalStringTemplate = \ @"BEGIN:VCALENDAR\r\n" @"METHOD:REQUEST\r\n" @"PRODID:OpenGroupware.org SOGo 0.9\r\n" @"VERSION:2.0\r\n" @"BEGIN:VEVENT\r\n" @"UID:%@\r\n" @"CLASS:PUBLIC\r\n" @"STATUS:CONFIRMED\r\n" /* confirmed by default */ @"DTSTAMP:%@\r\n" @"DTSTART:%@\r\n" @"DTEND:%@\r\n" @"TRANSP:%@\r\n" @"SEQUENCE:1\r\n" @"PRIORITY:5\r\n" @"%@" /* organizer */ @"%@" /* participants and resources */ @"END:VEVENT\r\n" @"END:VCALENDAR"; NSCalendarDate *lStartDate, *lEndDate; NSString *template, *s; unsigned minutes; s = [self queryParameterForKey:@"dur"]; if(s && [s length] > 0) { minutes = [s intValue]; } else { minutes = 60; } lStartDate = [self selectedDate]; lEndDate = [lStartDate dateByAddingYears:0 months:0 days:0 hours:0 minutes:minutes seconds:0]; s = [self iCalParticipantsAndResourcesStringFromQueryParameters]; template = [NSString stringWithFormat:iCalStringTemplate, [[self clientObject] nameInContainer], [[NSCalendarDate date] icalString], [lStartDate icalString], [lEndDate icalString], [self transparency], [self iCalOrganizerString], s]; return template; } - (NSString *)iCalParticipantsAndResourcesStringFromQueryParameters { NSString *s; s = [self iCalParticipantsStringFromQueryParameters]; return [s stringByAppendingString: [self iCalResourcesStringFromQueryParameters]]; } - (NSString *)iCalParticipantsStringFromQueryParameters { static NSString *iCalParticipantString = \ @"ATTENDEE;ROLE=REQ-PARTICIPANT;CN=\"%@\":mailto:%@\r\n"; return [self iCalStringFromQueryParameter:@"ps" format:iCalParticipantString]; } - (NSString *)iCalResourcesStringFromQueryParameters { static NSString *iCalResourceString = \ @"ATTENDEE;ROLE=NON-PARTICIPANT;CN=\"%@\":mailto:%@\r\n"; return [self iCalStringFromQueryParameter:@"rs" format:iCalResourceString]; } - (NSString *)iCalStringFromQueryParameter:(NSString *)_qp format:(NSString *)_format { AgenorUserManager *um; NSMutableString *iCalRep; NSString *s; um = [AgenorUserManager sharedUserManager]; iCalRep = (NSMutableString *)[NSMutableString string]; s = [self queryParameterForKey:_qp]; if(s && [s length] > 0) { NSArray *es; unsigned i, count; es = [s componentsSeparatedByString:@","]; count = [es count]; for(i = 0; i < count; i++) { NSString *email, *cn; email = [es objectAtIndex:i]; cn = [um getCNForUID:[um getUIDForEmail:email]]; [iCalRep appendFormat:_format, cn, email]; } } return iCalRep; } - (NSString *)iCalOrganizerString { static NSString *fmt = @"ORGANIZER;CN=\"%@\":mailto:%@\r\n"; return [NSString stringWithFormat:fmt, [self cnForUser], [self emailForUser]]; } #if 0 - (iCalPerson *)getOrganizer { iCalPerson *p; NSString *emailProp; emailProp = [@"mailto:" stringByAppendingString:[self emailForUser]]; p = [[[iCalPerson alloc] init] autorelease]; [p setEmail:emailProp]; [p setCn:[self cnForUser]]; return p; } #endif /* helper */ - (NSString *)_completeURIForMethod:(NSString *)_method { NSString *uri; NSRange r; uri = [[[self context] request] uri]; /* first: identify query parameters */ r = [uri rangeOfString:@"?" options:NSBackwardsSearch]; if (r.length > 0) uri = [uri substringToIndex:r.location]; /* next: append trailing slash */ if (![uri hasSuffix:@"/"]) uri = [uri stringByAppendingString:@"/"]; /* next: append method */ uri = [uri stringByAppendingString:_method]; /* next: append query parameters */ return [self completeHrefForMethod:uri]; } /* new */ - (id)newAction { /* This method creates a unique ID and redirects to the "edit" method on the new ID. It is actually a folder method and should be defined on the folder. Note: 'clientObject' is the SOGoAppointmentFolder! Update: remember that there are group folders as well. */ NSString *uri, *objectId, *method; objectId = [NSClassFromString(@"SOGoAppointmentFolder") globallyUniqueObjectId]; if ([objectId length] == 0) { return [NSException exceptionWithHTTPStatus:500 /* Internal Error */ reason:@"could not create a unique ID"]; } method = [NSString stringWithFormat:@"Calendar/%@/edit", objectId]; method = [[self userFolderPath] stringByAppendingPathComponent:method]; /* add all current calendarUIDs as default participants */ if ([[self clientObject] respondsToSelector:@selector(calendarUIDs)]) { AgenorUserManager *um; NSArray *uids; NSMutableArray *emails; NSString *ps; unsigned i, count; um = [AgenorUserManager sharedUserManager]; uids = [[self clientObject] calendarUIDs]; count = [uids count]; emails = [NSMutableArray arrayWithCapacity:count]; for (i = 0; i < count; i++) { NSString *email; email = [um getEmailForUID:[uids objectAtIndex:i]]; if (email) [emails addObject:email]; } ps = [emails componentsJoinedByString:@","]; [self setQueryParameter:ps forKey:@"ps"]; } uri = [self completeHrefForMethod:method]; return [self redirectToLocation:uri]; } /* save */ /* returned dates are in GMT */ - (NSCalendarDate *)_dateFromString:(NSString *)_str { NSCalendarDate *date; date = [NSCalendarDate dateWithString:_str calendarFormat:@"%Y-%m-%d %H:%M %Z"]; [date setTimeZone:[self backendTimeZone]]; return date; } - (NSArray *)getICalPersonsFromFormValues:(NSArray *)_values treatAsResource:(BOOL)_isResource { unsigned i, count; NSMutableArray *result; count = [_values count]; result = [[NSMutableArray alloc] initWithCapacity:count]; for (i = 0; i < count; i++) { NSString *pString, *email, *cn; NSRange r; iCalPerson *p; pString = [_values objectAtIndex:i]; if ([pString length] == 0) continue; /* delimiter between email and cn */ r = [pString rangeOfString:@";"]; if (r.length > 0) { email = [pString substringToIndex:r.location]; cn = (r.location + 1 < [pString length]) ? [pString substringFromIndex:r.location + 1] : nil; } else { email = pString; cn = nil; } if (cn == nil) { /* fallback */ AgenorUserManager *um = [AgenorUserManager sharedUserManager]; cn = [um getCNForUID:[um getUIDForEmail:email]]; } p = [[iCalPerson alloc] init]; [p setEmail:[@"mailto:" stringByAppendingString:email]]; if ([cn isNotNull]) [p setCn:cn]; /* see RFC2445, sect. 4.2.16 for details */ [p setRole:_isResource ? @"NON-PARTICIPANT" : @"REQ-PARTICIPANT"]; [result addObject:p]; [p release]; } return [result autorelease]; } - (BOOL)isWriteableClientObject { return [[self clientObject] respondsToSelector:@selector(saveContentString:)]; } - (void)loadValuesFromAppointment:(SOGoAppointment *)_appointment { NSString *s; if ((self->startDate = [[_appointment startDate] copy]) == nil) self->startDate = [[[NSCalendarDate date] hour:11 minute:0] copy]; if ((self->endDate = [[_appointment endDate] copy]) == nil) { self->endDate = [[self->startDate hour:[self->startDate hourOfDay] + 1 minute:0] copy]; } [self->startDate setTimeZone:[self viewTimeZone]]; [self->endDate setTimeZone:[self viewTimeZone]]; self->title = [[_appointment summary] copy]; self->location = [[_appointment location] copy]; self->comment = [[_appointment comment] copy]; self->priority = [[_appointment priority] copy]; self->categories = [[_appointment categories] retain]; self->participants = [[_appointment participants] retain]; self->resources = [[_appointment resources] retain]; s = [_appointment accessClass]; if(!s || [s isEqualToString:@"PUBLIC"]) [self setIsPrivate:NO]; else [self setIsPrivate:YES]; /* we're possibly loosing information here */ } - (void)saveValuesIntoAppointment:(SOGoAppointment *)_appointment { /* merge in form values */ NSArray *attendees, *lResources; [_appointment setStartDate:[self aptStartDate]]; [_appointment setEndDate:[self aptEndDate]]; [_appointment setSummary:[self title]]; [_appointment setLocation:[self location]]; [_appointment setComment:[self comment]]; [_appointment setPriority:[self priority]]; [_appointment setCategories:[self categories]]; [_appointment setAccessClass:[self accessClass]]; [_appointment setTransparency:[self transparency]]; attendees = [self participants]; lResources = [self resources]; if ([lResources count] > 0) { attendees = ([attendees count] > 0) ? [attendees arrayByAddingObjectsFromArray:lResources] : lResources; } [_appointment setAttendees:attendees]; #if 0 [_appointment setOrganizer:[self getOrganizer]]; #endif } - (void)loadValuesFromICalString:(NSString *)_ical { SOGoAppointment *apt; apt = [[SOGoAppointment alloc] initWithICalString:_ical]; [self loadValuesFromAppointment:apt]; [apt release]; } /* contact editor compatibility */ - (void)setContentString:(NSString *)_s { [self setICalString:_s]; } - (NSString *)contentStringTemplate { return [self iCalStringTemplate]; } - (void)loadValuesFromContentString:(NSString *)_s { [self loadValuesFromICalString:_s]; } /* access */ - (BOOL)isMyApt { NSString *owner; owner = [[self clientObject] ownerInContext:[self context]]; if (!owner) return YES; return [owner isEqualToString:[[self user] login]]; } - (BOOL)canAccessApt { return [self isMyApt]; } - (BOOL)canEditApt { return [self isMyApt]; } /* conflict management */ - (BOOL)containsConflict:(SOGoAppointment *)_apt { NSArray *attendees, *uids; SOGoAppointmentFolder *groupCalendar; NSArray *infos; NSArray *ranges; id folder; [self logWithFormat:@"search from %@ to %@", [_apt startDate], [_apt endDate]]; folder = [[self clientObject] container]; attendees = [_apt attendees]; uids = [folder uidsFromICalPersons:attendees]; if ([uids count] == 0) { [self logWithFormat:@"Note: no UIDs selected."]; return NO; } groupCalendar = [folder lookupGroupCalendarFolderForUIDs:uids inContext:[self context]]; [self debugWithFormat:@"group calendar: %@", groupCalendar]; if (![groupCalendar respondsToSelector:@selector(fetchFreebusyInfosFrom:to:)]) { [self errorWithFormat:@"invalid folder to run freebusy query on!"]; return NO; } infos = [groupCalendar fetchFreebusyInfosFrom:[_apt startDate] to:[_apt endDate]]; [self debugWithFormat:@" process: %d events", [infos count]]; ranges = [infos arrayByCreatingDateRangesFromObjectsWithStartDateKey:@"startDate" andEndDateKey:@"endDate"]; ranges = [ranges arrayByCompactingContainedDateRanges]; [self debugWithFormat:@" blocked ranges: %@", ranges]; return [ranges count] != 0 ? YES : NO; } /* actions */ - (BOOL)shouldTakeValuesFromRequest:(WORequest *)_rq inContext:(WOContext*)_c{ return YES; } - (id)testAction { /* for testing only */ WORequest *req; SOGoAppointment *apt; NSString *content; req = [[self context] request]; apt = [[SOGoAppointment alloc] initWithICalString:[self iCalString]]; [self saveValuesIntoAppointment:apt]; content = [apt iCalString]; [self logWithFormat:@"%s -- iCal:\n%@", __PRETTY_FUNCTION__, content]; [apt release]; return self; } - (id)defaultAction { NSString *ical; /* load iCalendar file */ // TODO: can't we use [clientObject contentAsString]? ical = [[self clientObject] valueForKey:@"iCalString"]; if ([ical length] == 0) /* a new appointment */ ical = [self contentStringTemplate]; [self setContentString:ical]; [self loadValuesFromContentString:ical]; return self; } - (id)saveAction { SOGoAppointment *apt; iCalPerson *p; NSString *content; NSException *ex; if (![self isWriteableClientObject]) { /* return 400 == Bad Request */ return [NSException exceptionWithHTTPStatus:400 reason:@"method cannot be invoked on " @"the specified object"]; } apt = [[SOGoAppointment alloc] initWithICalString:[self iCalString]]; if (apt == nil) { NSString *s; s = [self labelForKey:@"Invalid iCal data!"]; [self setErrorText:s]; return self; } [self saveValuesIntoAppointment:apt]; p = [apt findParticipantWithEmail:[self emailForUser]]; if (p) { [p setParticipationStatus:iCalPersonPartStatAccepted]; } if ([self checkForConflicts]) { if ([self containsConflict:apt]) { NSString *s; s = [self labelForKey:@"Conflicts found!"]; [self setErrorText:s]; [apt release]; return self; } } content = [apt iCalString]; [apt release]; apt = nil; if (content == nil) { NSString *s; s = [self labelForKey:@"Could not create iCal data!"]; [self setErrorText:s]; return self; } ex = [[self clientObject] saveContentString:content]; if (ex != nil) { [self setErrorText:[ex reason]]; return self; } return [self redirectToLocation:[self _completeURIForMethod:@".."]]; } - (id)acceptAction { return [self acceptOrDeclineAction:YES]; } - (id)declineAction { return [self acceptOrDeclineAction:NO]; } - (id)acceptOrDeclineAction:(BOOL)_accept { SOGoAppointment *apt; iCalPerson *p; NSString *iCal, *content; NSException *ex; if (![self isWriteableClientObject]) { /* 400 == Bad Request */ return [NSException exceptionWithHTTPStatus:400 reason:@"method cannot be invoked on " @"the specified object"]; } iCal = [[self clientObject] valueForKey:@"iCalString"]; apt = [[SOGoAppointment alloc] initWithICalString:iCal]; if (apt == nil) { /* 500 == Internal Server Error */ return [NSException exceptionWithHTTPStatus:500 reason:@"unable to parse appointment"]; } p = [apt findParticipantWithEmail:[self emailForUser]]; if (!p) { /* 404 == Not found */ return [NSException exceptionWithHTTPStatus:404 reason:@"user does not participate in this " @"appointment"]; } if(_accept) [p setParticipationStatus:iCalPersonPartStatAccepted]; else [p setParticipationStatus:iCalPersonPartStatDeclined]; content = [apt iCalString]; [apt release]; if (content == nil) { /* 500 == Internal Server Error */ return [NSException exceptionWithHTTPStatus:500 reason:@"Could not create iCalendar data ..."]; } ex = [[self clientObject] saveContentString:content]; if (ex != nil) { /* 500 == Internal Server Error */ return [NSException exceptionWithHTTPStatus:500 reason:[ex reason]]; } return [self redirectToLocation:[self _completeURIForMethod:@".."]]; } @end /* UIxAppointmentEditor */