diff --git a/ChangeLog b/ChangeLog index acfa980ab..1f711462f 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,109 @@ 2007-11-18 Wolfgang Sourdeau + * UI/Scheduler/UIxTaskEditor.m ([UIxTaskEditor -saveAction]): + invoke saveComponent:. + + * UI/Scheduler/UIxComponentEditor.m ([UIxComponentEditor -hasOrganizer]) + ([UIxComponentEditor -organizerName]): new template accessor + related to displaying the event's organizer. + ([-containsConflict:_component]): removed method. + ([UIxComponentEditor -takeValuesFromRequest:_rqinContext:_ctx]): + set RSVP to "TRUE" on each attendee. + + * UI/Scheduler/UIxAppointmentEditor.m ([UIxAppointmentEditor + -saveAction]): invoke saveComponent:. + + * UI/MailPartViewers/UIxMailPartICalViewer.m + ([UIxMailPartICalViewer -isLoggedInUserTheOrganizer]): make use of + the -userIsOrganizer: category method. + ([-isLoggedInUserAnAttendee]): make use of -userIsParticipant:. + ([UIxMailPartICalViewer -hasSenderStatusChanged]): new template + accessor that determines whether the "Update" button should be + displayed. + + * UI/MailPartViewers/UIxMailPartICalActions.m + ([UIxMailPartICalActions -deleteFromCalendarAction]): implemented + action. + ([UIxMailPartICalActions -updateUserStatusAction]): implemented + action. + + * UI/Common/UIxPageFrame.m ([UIxPageFrame + -setCssFiles:newCSSFiles]): new accessor that enables the + sub-templates to specify extra CSS files to load. + + * SoObjects/SOGo/SOGoUser.m ([SOGoUser + -homeFolderInContext:context]): cache the home folder of the user + object instead of the current user. + + * SoObjects/SOGo/SOGoGCSFolder.m ([SOGoGCSFolder + -deleteEntriesWithIds:ids]): invokes the "prepareDelete" optional + method if the child object implements it. + + * SoObjects/SOGo/SOGoContentObject.m ([-setContentString:]) + removed method. + + * SoObjects/SOGo/LDAPSource.m ([LDAPSource + -setBaseDN:newBaseDNIDField:newIDFieldCNField:newCNFieldUIDField:newUIDFieldmailFields:newMailFieldsandBindFields:newBindFields]): + take a new "mailFields" parameter defining an array of fields + where to look at when searching the user's emails. It defaults to + the standard "mail" LDAP field. + + * SoObjects/Appointments/SOGoAptMailICalReply.[hm]: new + SoComponent implementing a template for ITIP replies. + + * SoObjects/Appointments/iCalPerson+SOGo.m ([iCalPerson + -mailAddress]): new method that returns a properly formatted email + address for the specified person entry. + ([iCalPerson -uid]): new method that tests whether the user is + known to the system and if so, returns its user id. + + * SoObjects/Appointments/iCalPerson+SOGo.[hm]: new category module. + + * SoObjects/Appointments/iCalEventChanges+SOGo.m + ([iCalEventChanges -sequenceShouldBeIncreased]): determine whether + the changes involved need a sequence inscrease, based on the + RFC2446 (ITIP). + + * SoObjects/Appointments/iCalEventChanges+SOGo.[hm]: new category + module. + + * SoObjects/Appointments/iCalEvent+SOGo.m ([iCalEvent + -isStillRelevant]): new overriden method determining the relevance + of the current event based on its end date. + + * SoObjects/Appointments/iCalEvent+SOGo.[hm]: new category module. + + * SoObjects/Appointments/iCalEntityObject+SOGo.m + ([iCalEntityObject -attendeeUIDs]): new category methods that + returns an array containing the uids of the system-know attendees. + ([iCalEntityObject -isStillRelevant]): new template method. + ([iCalEntityObject -itipEntryWithMethod:method]): clone the + current entry calendar with the specified ITIP method. + ([iCalEntityObject -attendeesWithoutUser:user]): returns an array + of attendees while making sure the specified user is not listed. + + * SoObjects/Appointments/SOGoCalendarComponent.m + ([SOGoCalendarComponent -calendar:create:secure]): new name for + -calendar:. Added a "secure" parameter that specifies whether a + stripped calendar instance is needed or not. Also, we no longer + cache the content to simplify handling of new data. + ([SOGoCalendarComponent -component:create:secure]): same as above. + ([SOGoCalendarComponent + -sendEMailUsingTemplateNamed:_pageNameforOldObject:_oldObjectandNewObject:_newObjecttoAttendees:_attendees]): + test whether the component is "still relevant" before sending an + email... + ([SOGoCalendarComponent -sendResponseToOrganizer]): new method for + sending ITIP replies. + ([SOGoCalendarComponent -getUIDsForICalPerson:iCalPerson]): + removed method. Replaced with -[iCalPerson uid] category method. + + * SoObjects/Appointments/SOGoAppointmentObject.[hm]: rewrote + class. No longer override saveContentString:, + saveContentString:baseSequence:, .... Implemented the + saveComponent: and the prepareDelete methods instead. Those + methods are called only from the web methods. This avoids the + risks related to email sending and changes propagation. + * UI/Common/UIxTabItem.m: removed useless class module. * UI/Common/UIxTabView.[hm]: removed useless class module. diff --git a/SoObjects/Appointments/GNUmakefile b/SoObjects/Appointments/GNUmakefile index 99a7b1659..8924bd4be 100644 --- a/SoObjects/Appointments/GNUmakefile +++ b/SoObjects/Appointments/GNUmakefile @@ -10,6 +10,9 @@ Appointments_OBJC_FILES = \ Product.m \ NSArray+Appointments.m \ iCalEntityObject+SOGo.m \ + iCalEvent+SOGo.m \ + iCalEventChanges+SOGo.m \ + iCalPerson+SOGo.m \ \ SOGoCalendarComponent.m \ SOGoAppointmentObject.m \ @@ -24,6 +27,7 @@ Appointments_OBJC_FILES = \ SOGoAptMailUpdate.m \ SOGoAptMailRemoval.m \ SOGoAptMailDeletion.m \ + SOGoAptMailICalReply.m \ Appointments_RESOURCE_FILES += \ Version \ @@ -31,14 +35,17 @@ Appointments_RESOURCE_FILES += \ Appointments_COMPONENTS += \ SOGoAptMailEnglishInvitation.wo \ + SOGoAptMailEnglishICalReply.wo \ SOGoAptMailEnglishUpdate.wo \ SOGoAptMailEnglishRemoval.wo \ SOGoAptMailEnglishDeletion.wo \ SOGoAptMailFrenchInvitation.wo \ + SOGoAptMailFrenchICalReply.wo \ SOGoAptMailFrenchUpdate.wo \ SOGoAptMailFrenchRemoval.wo \ SOGoAptMailFrenchDeletion.wo \ SOGoAptMailGermanInvitation.wo \ + SOGoAptMailGermanICalReply.wo \ SOGoAptMailGermanUpdate.wo \ SOGoAptMailGermanRemoval.wo \ SOGoAptMailGermanDeletion.wo \ diff --git a/SoObjects/Appointments/SOGoAppointmentObject.h b/SoObjects/Appointments/SOGoAppointmentObject.h index 8262970f2..7b3b23795 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.h +++ b/SoObjects/Appointments/SOGoAppointmentObject.h @@ -46,12 +46,14 @@ @interface SOGoAppointmentObject : SOGoCalendarComponent +- (NSException *) changeParticipationStatus: (NSString *) _status; + /* "iCal multifolder saves" */ -- (NSException *) saveContentString: (NSString *) _iCal - baseSequence: (int) _v; -- (NSException *) deleteWithBaseSequence: (int) _v; -- (NSException *) saveContentString: (NSString *) _iCalString; +// - (NSException *) saveContentString: (NSString *) _iCal +// baseSequence: (int) _v; +// - (NSException *) deleteWithBaseSequence: (int) _v; +// - (NSException *) saveContentString: (NSString *) _iCalString; @end diff --git a/SoObjects/Appointments/SOGoAppointmentObject.m b/SoObjects/Appointments/SOGoAppointmentObject.m index 12f97b38a..58de668a1 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.m +++ b/SoObjects/Appointments/SOGoAppointmentObject.m @@ -22,7 +22,7 @@ #import #import -#import +#import #import #import #import @@ -30,14 +30,19 @@ #import #import +#import #import #import #import #import +#import #import #import "NSArray+Appointments.h" #import "SOGoAppointmentFolder.h" +#import "iCalEventChanges+SOGo.h" +#import "iCalEntityObject+SOGo.h" +#import "iCalPerson+SOGo.h" #import "SOGoAppointmentObject.h" @@ -48,387 +53,325 @@ return @"vevent"; } -/* iCal handling */ -- (NSArray *) attendeeUIDsFromAppointment: (iCalEvent *) _apt +- (SOGoAppointmentObject *) _lookupEvent: (NSString *) eventUID + forUID: (NSString *) uid +{ + SOGoAppointmentFolder *folder; + SOGoAppointmentObject *object; + NSString *possibleName; + + folder = [container lookupCalendarFolderForUID: uid]; + object = [folder lookupName: nameInContainer + inContext: context acquire: NO]; + if ([object isKindOfClass: [NSException class]]) + { + possibleName = [folder resourceNameForEventUID: eventUID]; + if (possibleName) + { + object = [folder lookupName: nameInContainer + inContext: context acquire: NO]; + if ([object isKindOfClass: [NSException class]]) + object = nil; + } + } + + if (!object) + object = [SOGoAppointmentObject objectWithName: nameInContainer + inContainer: folder]; + + return object; +} + +- (void) _addOrUpdateEvent: (iCalEvent *) event + forUID: (NSString *) uid +{ + SOGoAppointmentObject *object; + NSString *iCalString, *userLogin; + + userLogin = [[context activeUser] login]; + if (![uid isEqualToString: userLogin]) + { + object = [self _lookupEvent: [event uid] forUID: uid]; + iCalString = [[event parent] versitString]; + [object saveContentString: iCalString]; + } +} + +- (void) _removeEventFromUID: (NSString *) uid +{ + SOGoAppointmentFolder *folder; + SOGoAppointmentObject *object; + NSString *userLogin; + + userLogin = [[context activeUser] login]; + if (![uid isEqualToString: userLogin]) + { + folder = [container lookupCalendarFolderForUID: uid]; + object = [folder lookupName: nameInContainer + inContext: context acquire: NO]; + if (![object isKindOfClass: [NSException class]]) + [object delete]; + } +} + +- (void) _handleRemovedUsers: (NSArray *) attendees +{ + NSEnumerator *enumerator; + iCalPerson *currentAttendee; + NSString *currentUID; + + enumerator = [attendees objectEnumerator]; + while ((currentAttendee = [enumerator nextObject])) + { + currentUID = [currentAttendee uid]; + if (currentUID) + [self _removeEventFromUID: currentUID]; + } +} + +- (void) _requireResponseFromAttendees: (NSArray *) attendees +{ + NSEnumerator *enumerator; + iCalPerson *currentAttendee; + + enumerator = [attendees objectEnumerator]; + while ((currentAttendee = [enumerator nextObject])) + { + [currentAttendee setRsvp: @"TRUE"]; + [currentAttendee setParticipationStatus: iCalPersonPartStatNeedsAction]; + } +} + +- (void) _handleSequenceUpdateInEvent: (iCalEvent *) newEvent + ignoringAttendees: (NSArray *) attendees + fromOldEvent: (iCalEvent *) oldEvent +{ + NSMutableArray *updateAttendees, *updateUIDs; + NSEnumerator *enumerator; + iCalPerson *currentAttendee; + NSString *currentUID; + + updateAttendees = [NSMutableArray arrayWithArray: [newEvent attendees]]; + [updateAttendees removeObjectsInArray: attendees]; + + updateUIDs = [NSMutableArray arrayWithCapacity: [updateAttendees count]]; + enumerator = [updateAttendees objectEnumerator]; + while ((currentAttendee = [enumerator nextObject])) + { + currentUID = [currentAttendee uid]; + if (currentUID) + [self _addOrUpdateEvent: newEvent + forUID: currentUID]; + } + + [self sendEMailUsingTemplateNamed: @"Update" + forOldObject: oldEvent + andNewObject: [newEvent itipEntryWithMethod: @"request"] + toAttendees: updateAttendees]; +} + +- (void) _handleAddedUsers: (NSArray *) attendees + fromEvent: (iCalEvent *) newEvent +{ + NSEnumerator *enumerator; + iCalPerson *currentAttendee; + NSString *currentUID; + + enumerator = [attendees objectEnumerator]; + while ((currentAttendee = [enumerator nextObject])) + { + currentUID = [currentAttendee uid]; + if (currentUID) + [self _addOrUpdateEvent: newEvent + forUID: currentUID]; + } +} + +- (void) _handleUpdatedEvent: (iCalEvent *) newEvent + fromOldEvent: (iCalEvent *) oldEvent { - LDAPUserManager *um; - NSMutableArray *uids; NSArray *attendees; - unsigned i, count; - NSString *email, *uid; - - if (![_apt isNotNull]) - return nil; - - if ((attendees = [_apt attendees]) == nil) - return nil; - count = [attendees count]; - uids = [NSMutableArray arrayWithCapacity:count + 1]; - - um = [LDAPUserManager sharedUserManager]; - - /* add organizer */ - - email = [[_apt organizer] rfc822Email]; - if ([email isNotNull]) { - uid = [um getUIDForEmail: email]; - if ([uid isNotNull]) { - [uids addObject:uid]; - } - else - [self logWithFormat:@"Note: got no uid for organizer: '%@'", email]; - } - - /* add attendees */ - - for (i = 0; i < count; i++) - { - iCalPerson *person; - - person = [attendees objectAtIndex:i]; - email = [person rfc822Email]; - if (![email isNotNull]) continue; - - uid = [um getUIDForEmail:email]; - if (![uid isNotNull]) { - [self logWithFormat:@"Note: got no uid for email: '%@'", email]; - continue; - } - if (![uids containsObject:uid]) - [uids addObject:uid]; - } - - return uids; -} - -/* store in all the other folders */ - -- (NSException *) saveContentString: (NSString *) _iCal - inUIDs: (NSArray *) _uids -{ - NSEnumerator *e; - id folder; - NSException *allErrors = nil; - NSException *error; - SOGoAppointmentObject *apt; - - e = [[container lookupCalendarFoldersForUIDs:_uids inContext: context] - objectEnumerator]; - while ((folder = [e nextObject])) - { - apt = [SOGoAppointmentObject objectWithName: nameInContainer - inContainer: folder]; - error = [apt primarySaveContentString:_iCal]; - if (error) - { - [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder]; - // TODO: make compound - allErrors = error; - } - } - - return allErrors; -} - -- (NSException *) deleteInUIDs: (NSArray *) _uids -{ - NSEnumerator *e; - id folder; - NSException *allErrors = nil; - NSException *error; - SOGoAppointmentObject *apt; - - e = [[container lookupCalendarFoldersForUIDs:_uids inContext: context] - objectEnumerator]; - while ((folder = [e nextObject])) - { - apt = [folder lookupName: [self nameInContainer] - inContext: context - acquire:NO]; - if ([apt isKindOfClass: [NSException class]]) { - [self logWithFormat: @"%@", [(NSException *) apt reason]]; - continue; - } - - if ((error = [apt primaryDelete]) != nil) { - [self logWithFormat:@"Note: failed to delete in folder: %@", folder]; - // TODO: make compound - allErrors = error; - } - } - - return allErrors; -} - -/* "iCal multifolder saves" */ -- (BOOL) _aptIsStillRelevant: (iCalEvent *) appointment -{ - NSCalendarDate *now; - - now = [NSCalendarDate calendarDate]; - - return ([[appointment endDate] earlierDate: now] == now); -} - -- (NSException *) saveContentString: (NSString *) _iCal - baseSequence: (int) _v -{ - /* - Note: we need to delete in all participants folders and send iMIP messages - for all external accounts. - - Steps: - - fetch stored content - - parse old content - - check if sequence matches (or if 0=ignore) - - extract old attendee list + organizer (make unique) - - parse new content (ensure that sequence is increased!) - - extract new attendee list + organizer (make unique) - - make a diff => new, same, removed - - write to new, same - - delete in removed folders - - send iMIP mail for all folders not found - */ - LDAPUserManager *um; - iCalEvent *oldApt, *newApt; iCalEventChanges *changes; - iCalPerson *organizer; - NSString *oldContent, *uid; - NSArray *uids, *props; - NSMutableArray *attendees, *storeUIDs, *removedUIDs; - NSException *storeError, *delError; - BOOL updateForcesReconsider; - - if ([[context request] handledByDefaultHandler]) + + changes = [newEvent getChangesRelativeToEvent: oldEvent]; + attendees = [changes deletedAttendees]; + if ([attendees count]) { - updateForcesReconsider = NO; + [self _handleRemovedUsers: attendees]; + [self sendEMailUsingTemplateNamed: @"Deletion" + forOldObject: oldEvent + andNewObject: [newEvent itipEntryWithMethod: @"cancel"] + toAttendees: attendees]; + } - if ([_iCal length] == 0) - return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */ - reason: @"got no iCalendar content to store!"]; + attendees = [changes insertedAttendees]; + if ([changes sequenceShouldBeIncreased]) + { + [newEvent increaseSequence]; + [self _requireResponseFromAttendees: [newEvent attendees]]; + [self _handleSequenceUpdateInEvent: newEvent + ignoringAttendees: attendees + fromOldEvent: oldEvent]; + } + else + [self _requireResponseFromAttendees: attendees]; - um = [LDAPUserManager sharedUserManager]; + if ([attendees count]) + { + [self _handleAddedUsers: attendees fromEvent: newEvent]; + [self sendEMailUsingTemplateNamed: @"Invitation" + forOldObject: oldEvent + andNewObject: [newEvent itipEntryWithMethod: @"request"] + toAttendees: attendees]; + } +} - /* handle old content */ - - oldContent = [self contentAsString]; /* if nil, this is a new appointment */ - if ([oldContent length] == 0) - { - /* new appointment */ - [self debugWithFormat:@"saving new appointment: %@", _iCal]; - oldApt = nil; - } +- (void) saveComponent: (iCalEvent *) newEvent +{ + iCalEvent *oldEvent; + NSArray *attendees; + + [[newEvent parent] setMethod: @""]; + if ([newEvent userIsOrganizer: [context activeUser]]) + { + oldEvent = [self component: NO secure: NO]; + if (oldEvent) + [self _handleUpdatedEvent: newEvent fromOldEvent: oldEvent]; else - oldApt = (iCalEvent *) [self component: NO]; - - /* compare sequence if requested */ - if (_v != 0) { - // TODO - } - - /* handle new content */ - - newApt = (iCalEvent *) [self component: NO]; - if (!newApt) - return [NSException exceptionWithHTTPStatus: 400 /* Bad Request */ - reason: @"could not parse iCalendar content!"]; - - /* diff */ - - changes = [iCalEventChanges changesFromEvent: oldApt toEvent: newApt]; - uids = [self getUIDsForICalPersons: [changes deletedAttendees]]; - removedUIDs = [NSMutableArray arrayWithArray: uids]; - - uids = [self getUIDsForICalPersons: [newApt attendees]]; - storeUIDs = [NSMutableArray arrayWithArray: uids]; - props = [changes updatedProperties]; - - /* detect whether sequence has to be increased */ - if ([changes hasChanges]) - [newApt increaseSequence]; - - /* preserve organizer */ - - organizer = [newApt organizer]; - uid = [self getUIDForICalPerson: organizer]; - if (!uid) - uid = [self ownerInContext: nil]; - if (uid) { - [storeUIDs addObjectUniquely: uid]; - [removedUIDs removeObject: uid]; - } - - /* organizer might have changed completely */ - - if (oldApt && ([props containsObject: @"organizer"])) - { - uid = [self getUIDForICalPerson: [oldApt organizer]]; - if (uid && ![storeUIDs containsObject: uid]) - [removedUIDs addObjectUniquely: uid]; - } - - [self debugWithFormat: @"UID ops:\n store: %@\n remove: %@", - storeUIDs, removedUIDs]; - - /* if time did change, all participants have to re-decide ... - * ... exception from that rule: the organizer - */ - - if (oldApt - && ([props containsObject: @"startDate"] - || [props containsObject: @"endDate"] - || [props containsObject: @"duration"])) - { - NSArray *ps; - unsigned i, count; - - ps = [newApt attendees]; - count = [ps count]; - for (i = 0; i < count; i++) { - iCalPerson *p; - - p = [ps objectAtIndex:i]; - if (![p hasSameEmailAddress:organizer]) - [p setParticipationStatus:iCalPersonPartStatNeedsAction]; - } - _iCal = [[newApt parent] versitString]; - updateForcesReconsider = YES; - } - - /* perform storing */ - - storeError = [self saveContentString: _iCal inUIDs: storeUIDs]; - delError = [self deleteInUIDs: removedUIDs]; - - // TODO: make compound - if (storeError != nil) return storeError; - if (delError != nil) return delError; - - /* email notifications */ - if ([self sendEMailNotifications] - && [self _aptIsStillRelevant: newApt]) - { - iCalEvent *requestApt; - - requestApt = [newApt copy]; - [(iCalCalendar *) [requestApt parent] setMethod: @"request"]; - attendees - = [NSMutableArray arrayWithArray: [changes insertedAttendees]]; - [attendees removePerson: organizer]; - [self sendEMailUsingTemplateNamed: @"Invitation" - forOldObject: nil - andNewObject: requestApt - toAttendees: attendees]; - [requestApt release]; - - if (updateForcesReconsider) - { - iCalEvent *updatedApt; - - updatedApt = [newApt copy]; - [(iCalCalendar *) [updatedApt parent] setMethod: @"request"]; - attendees = [NSMutableArray arrayWithArray:[newApt attendees]]; - [attendees removeObjectsInArray:[changes insertedAttendees]]; - [attendees removePerson:organizer]; - [self sendEMailUsingTemplateNamed: @"Update" - forOldObject: oldApt - andNewObject: updatedApt - toAttendees: attendees]; - [updatedApt release]; - } - - attendees - = [NSMutableArray arrayWithArray: [changes deletedAttendees]]; - [attendees removePerson: organizer]; + attendees = [newEvent attendeesWithoutUser: [context activeUser]]; if ([attendees count]) { - iCalEvent *cancelledApt; - - cancelledApt = [newApt copy]; - [(iCalCalendar *) [cancelledApt parent] setMethod: @"cancel"]; - [self sendEMailUsingTemplateNamed: @"Removal" + [self _handleAddedUsers: attendees fromEvent: newEvent]; + [self sendEMailUsingTemplateNamed: @"Invitation" forOldObject: nil - andNewObject: cancelledApt + andNewObject: [newEvent itipEntryWithMethod: @"request"] toAttendees: attendees]; - [cancelledApt release]; } + + if (![[newEvent attendees] count]) + [[newEvent uniqueChildWithTag: @"organizer"] setValue: 0 + to: @""]; } } - else - [self primarySaveContentString: _iCal]; - return nil; + [super saveComponent: newEvent]; } -- (NSException *) deleteWithBaseSequence: (int)_v +- (NSException *) _updateAttendee: (iCalPerson *) attendee + forEventUID: (NSString *) eventUID + withSequence: (NSNumber *) sequence + forUID: (NSString *) uid { - /* - Note: We need to delete in all participants folders and send iMIP messages - for all external accounts. - Delete is basically identical to save with all attendees and the - organizer being deleted. - - Steps: - - fetch stored content - - parse old content - - check if sequence matches (or if 0=ignore) - - extract old attendee list + organizer (make unique) - - delete in removed folders - - send iMIP mail for all folders not found - */ - iCalEvent *apt; - NSMutableArray *attendees, *removedUIDs; + SOGoAppointmentObject *eventObject; + iCalEvent *event; + iCalPerson *otherAttendee; + NSString *iCalString; NSException *error; - if ([[context request] handledByDefaultHandler]) + error = nil; + + eventObject = [self _lookupEvent: eventUID forUID: uid]; + if (![eventObject isNew]) { - /* load existing content */ - apt = (iCalEvent *) [self component: NO]; - - /* compare sequence if requested */ - -// if (_v != 0) { -// // TODO -// } - - removedUIDs = [NSMutableArray arrayWithArray: - [self attendeeUIDsFromAppointment: apt]]; - if (![removedUIDs containsObject: owner]) - [removedUIDs addObject: owner]; - - if ([self sendEMailNotifications] - && [self _aptIsStillRelevant: apt]) + event = [eventObject component: NO secure: NO]; + if ([[event sequence] compare: sequence] + == NSOrderedSame) { - /* send notification email to attendees excluding organizer */ - attendees = [NSMutableArray arrayWithArray: [apt attendees]]; - [attendees removePerson: [apt organizer]]; - - /* flag appointment as being cancelled */ - [(iCalCalendar *) [apt parent] setMethod: @"cancel"]; - [apt increaseSequence]; - - /* remove all attendees to signal complete removal */ - [apt removeAllAttendees]; - - /* send notification email */ - [self sendEMailUsingTemplateNamed: @"Deletion" - forOldObject: nil - andNewObject: apt - toAttendees: attendees]; + otherAttendee = [event findParticipant: [context activeUser]]; + [otherAttendee setPartStat: [attendee partStat]]; + iCalString = [[event parent] versitString]; + error = [eventObject saveContentString: iCalString]; } - - error = [self deleteInUIDs: removedUIDs]; } - else - error = [self primaryDelete]; return error; } -- (NSException *) saveContentString: (NSString *) _iCalString +- (NSException *) _handleAttendee: (iCalPerson *) attendee + statusChange: (NSString *) newStatus + inEvent: (iCalEvent *) event { - return [self saveContentString: _iCalString baseSequence: 0]; + NSString *newContent, *currentStatus, *organizerUID; + NSException *ex; + + ex = nil; + + currentStatus = [attendee partStat]; + if ([currentStatus caseInsensitiveCompare: newStatus] + != NSOrderedSame) + { + [attendee setPartStat: newStatus]; + newContent = [[event parent] versitString]; + ex = [self saveContentString: newContent]; + if (!(ex || [event userIsOrganizer: [context activeUser]])) + { + if ([[attendee rsvp] isEqualToString: @"true"]) + [self sendResponseToOrganizer]; + organizerUID = [[event organizer] uid]; + if (organizerUID) + ex = [self _updateAttendee: attendee + forEventUID: [event uid] + withSequence: [event sequence] + forUID: organizerUID]; + } + } + + return ex; +} + +- (NSException *) changeParticipationStatus: (NSString *) _status +{ + iCalEvent *event; + iCalPerson *attendee; + NSException *ex; + + ex = nil; + + event = [self component: NO secure: NO]; + if (event) + { + attendee = [event findParticipant: [context activeUser]]; + if (attendee) + ex = [self _handleAttendee: attendee statusChange: _status + inEvent: event]; + else + ex = [NSException exceptionWithHTTPStatus: 404 /* Not Found */ + reason: @"user does not participate in this " + @"calendar event"]; + } + else + ex = [NSException exceptionWithHTTPStatus: 500 /* Server Error */ + reason: @"unable to parse event record"]; + + return ex; +} + +- (void) prepareDelete +{ + iCalEvent *event; + SOGoUser *currentUser; + NSArray *attendees; + + if ([[context request] handledByDefaultHandler]) + { + currentUser = [context activeUser]; + event = [self component: NO secure: NO]; + if ([event userIsOrganizer: currentUser]) + { + attendees = [event attendeesWithoutUser: currentUser]; + if ([attendees count]) + { + [self _handleRemovedUsers: attendees]; + [self sendEMailUsingTemplateNamed: @"Deletion" + forOldObject: nil + andNewObject: [event itipEntryWithMethod: @"cancel"] + toAttendees: attendees]; + } + } + else if ([event userIsParticipant: currentUser]) + [self changeParticipationStatus: @"DECLINED"]; + } } /* message type */ diff --git a/SoObjects/Appointments/SOGoAptMailEnglishICalReply.wo/SOGoAptMailEnglishICalReply.html b/SoObjects/Appointments/SOGoAptMailEnglishICalReply.wo/SOGoAptMailEnglishICalReply.html new file mode 100644 index 000000000..c861c2714 --- /dev/null +++ b/SoObjects/Appointments/SOGoAptMailEnglishICalReply.wo/SOGoAptMailEnglishICalReply.html @@ -0,0 +1,4 @@ +<#IsSubject>Re: rendez-vous le <#AptStartDate/> à <#AptStartTime/> +<#IsBody> +<#Attendee/> has <#HasAccepted>accepted<#HasDeclined>declined your invitation. + diff --git a/SoObjects/Appointments/SOGoAptMailEnglishICalReply.wo/SOGoAptMailEnglishICalReply.wod b/SoObjects/Appointments/SOGoAptMailEnglishICalReply.wo/SOGoAptMailEnglishICalReply.wod new file mode 100644 index 000000000..71a954616 --- /dev/null +++ b/SoObjects/Appointments/SOGoAptMailEnglishICalReply.wo/SOGoAptMailEnglishICalReply.wod @@ -0,0 +1,33 @@ +AptStartDate: WOString { + value = startDate; + dateformat = "%d/%m/%y"; + escapeHTML = NO; +} + +AptStartTime: WOString { + value = startDate; + dateformat = "%H:%M"; + escapeHTML = NO; +} + +Attendee: WOString { + value = attendee.cnWithoutQuotes; + escapeHTML = NO; +} + +HasAccepted: WOConditional { + condition = hasAccepted; +} + +HasDeclined: WOConditional { + condition = hasDeclined; +} + +IsSubject: WOConditional { + condition = isSubject; +} + +IsBody: WOConditional { + condition = isSubject; + negate = YES; +} diff --git a/SoObjects/Appointments/SOGoAptMailFrenchICalReply.wo/SOGoAptMailFrenchICalReply.html b/SoObjects/Appointments/SOGoAptMailFrenchICalReply.wo/SOGoAptMailFrenchICalReply.html new file mode 100644 index 000000000..b12b49933 --- /dev/null +++ b/SoObjects/Appointments/SOGoAptMailFrenchICalReply.wo/SOGoAptMailFrenchICalReply.html @@ -0,0 +1,4 @@ +<#IsSubject>Re: rendez-vous le <#AptStartDate/> à <#AptStartTime/> +<#IsBody> +<#Attendee/> a <#HasAccepted>accepté<#HasDeclined>décliné votre invitation. + diff --git a/SoObjects/Appointments/SOGoAptMailFrenchICalReply.wo/SOGoAptMailFrenchICalReply.wod b/SoObjects/Appointments/SOGoAptMailFrenchICalReply.wo/SOGoAptMailFrenchICalReply.wod new file mode 100644 index 000000000..71a954616 --- /dev/null +++ b/SoObjects/Appointments/SOGoAptMailFrenchICalReply.wo/SOGoAptMailFrenchICalReply.wod @@ -0,0 +1,33 @@ +AptStartDate: WOString { + value = startDate; + dateformat = "%d/%m/%y"; + escapeHTML = NO; +} + +AptStartTime: WOString { + value = startDate; + dateformat = "%H:%M"; + escapeHTML = NO; +} + +Attendee: WOString { + value = attendee.cnWithoutQuotes; + escapeHTML = NO; +} + +HasAccepted: WOConditional { + condition = hasAccepted; +} + +HasDeclined: WOConditional { + condition = hasDeclined; +} + +IsSubject: WOConditional { + condition = isSubject; +} + +IsBody: WOConditional { + condition = isSubject; + negate = YES; +} diff --git a/SoObjects/Appointments/SOGoAptMailGermanICalReply.wo/SOGoAptMailGermanICalReply.html b/SoObjects/Appointments/SOGoAptMailGermanICalReply.wo/SOGoAptMailGermanICalReply.html new file mode 100644 index 000000000..c861c2714 --- /dev/null +++ b/SoObjects/Appointments/SOGoAptMailGermanICalReply.wo/SOGoAptMailGermanICalReply.html @@ -0,0 +1,4 @@ +<#IsSubject>Re: rendez-vous le <#AptStartDate/> à <#AptStartTime/> +<#IsBody> +<#Attendee/> has <#HasAccepted>accepted<#HasDeclined>declined your invitation. + diff --git a/SoObjects/Appointments/SOGoAptMailGermanICalReply.wo/SOGoAptMailGermanICalReply.wod b/SoObjects/Appointments/SOGoAptMailGermanICalReply.wo/SOGoAptMailGermanICalReply.wod new file mode 100644 index 000000000..71a954616 --- /dev/null +++ b/SoObjects/Appointments/SOGoAptMailGermanICalReply.wo/SOGoAptMailGermanICalReply.wod @@ -0,0 +1,33 @@ +AptStartDate: WOString { + value = startDate; + dateformat = "%d/%m/%y"; + escapeHTML = NO; +} + +AptStartTime: WOString { + value = startDate; + dateformat = "%H:%M"; + escapeHTML = NO; +} + +Attendee: WOString { + value = attendee.cnWithoutQuotes; + escapeHTML = NO; +} + +HasAccepted: WOConditional { + condition = hasAccepted; +} + +HasDeclined: WOConditional { + condition = hasDeclined; +} + +IsSubject: WOConditional { + condition = isSubject; +} + +IsBody: WOConditional { + condition = isSubject; + negate = YES; +} diff --git a/SoObjects/Appointments/SOGoAptMailICalReply.h b/SoObjects/Appointments/SOGoAptMailICalReply.h new file mode 100644 index 000000000..3b5308951 --- /dev/null +++ b/SoObjects/Appointments/SOGoAptMailICalReply.h @@ -0,0 +1,59 @@ +/* SOGoAptMailICalReply.h - this file is part of SOGo + * + * Copyright (C) 2007 Inverse groupe conseil + * + * Author: Wolfgang Sourdeau + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef SOGOAPTMAILICALREPLY_H +#define SOGOAPTMAILICALREPLY_H + +#import + +@class NSString; +@class NSCalendarDate; + +@class iCalPerson; +@class iCalEntityObject; + +/* + * NOTE: We inherit from SoComponent in order to get the correct + * resourceManager required for this product + */ +@interface SOGoAptMailICalReply : SoComponent +{ + iCalEntityObject *apt; + iCalPerson *attendee; + NSString *homePageURL; + BOOL isSubject; +} + +- (void) setApt: (iCalEntityObject *) newApt; +- (iCalEntityObject *) apt; + +- (void) setAttendee: (iCalPerson *) newAttendee; +- (iCalPerson *) attendee; + +/* Content Generation */ + +- (NSString *) getSubject; +- (NSString *) getBody; + +@end + +#endif /* SOGOAPTMAILICALREPLY_H */ diff --git a/SoObjects/Appointments/SOGoAptMailICalReply.m b/SoObjects/Appointments/SOGoAptMailICalReply.m new file mode 100644 index 000000000..02a366e76 --- /dev/null +++ b/SoObjects/Appointments/SOGoAptMailICalReply.m @@ -0,0 +1,178 @@ +/* SOGoAptMailICalReply - this file is part of SOGo + * + * Copyright (C) 2007 Inverse groupe conseil + * + * Author: Wolfgang Sourdeau + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#import +#import +#import + +#import +#import +#import +#import + +#import +#import + +#import +#import + +#import "SOGoAptMailICalReply.h" + +@interface SOGoAptMailICalReply (PrivateAPI) + +- (BOOL) isSubject; + +@end + +@implementation SOGoAptMailICalReply + +static NSCharacterSet *wsSet = nil; + ++ (void) initialize +{ + static BOOL didInit = NO; + + if (!didInit) + { + didInit = YES; + wsSet = [[NSCharacterSet whitespaceAndNewlineCharacterSet] retain]; + } +} + +- (id) init +{ + if ((self = [super init])) + { + apt = nil; + attendee = nil; + } + + return self; +} + +- (void) dealloc +{ + [apt release]; + [attendee release]; + [super dealloc]; +} + +- (void) setApt: (iCalEntityObject *) newApt +{ + ASSIGN (apt, newApt); +} + +- (iCalEntityObject *) apt +{ + return apt; +} + +- (void) setAttendee: (iCalPerson *) newAttendee +{ + ASSIGN (attendee, newAttendee); +} + +- (iCalPerson *) attendee +{ + return attendee; +} + +- (BOOL) hasAccepted +{ + NSString *partStat; + + partStat = [[attendee partStat] lowercaseString]; + + return [partStat isEqualToString: @"accepted"]; +} + +- (BOOL) hasDeclined +{ + NSString *partStat; + + partStat = [[attendee partStat] lowercaseString]; + + return [partStat isEqualToString: @"declined"]; +} + +- (NSCalendarDate *) startDate +{ + NSCalendarDate *date; + SOGoUser *user; + + date = [apt startDate]; + user = [[self context] activeUser]; + [date setTimeZone: [user timeZone]]; + + return date; +} + +- (BOOL) isSubject +{ + return isSubject; +} + +/* Generate Response */ + +- (NSString *) getSubject +{ + NSString *subject; + + isSubject = YES; + subject = [[[self generateResponse] contentAsString] + stringByTrimmingCharactersInSet: wsSet]; + if (!subject) + { + [self errorWithFormat:@"Failed to properly generate subject! Please check " + @"template for component '%@'!", + [self name]]; + subject = @"ERROR: missing subject!"; + } + + return [subject asQPSubjectString: @"utf-8"]; +} + +- (NSString *) getBody +{ + isSubject = NO; + return [[self generateResponse] contentAsString]; +} + +@end + +@interface SOGoAptMailEnglishICalReply : SOGoAptMailICalReply +@end + +@implementation SOGoAptMailEnglishICalReply +@end + +@interface SOGoAptMailFrenchICalReply : SOGoAptMailICalReply +@end + +@implementation SOGoAptMailFrenchICalReply +@end + +@interface SOGoAptMailGermanICalReply : SOGoAptMailICalReply +@end + +@implementation SOGoAptMailGermanICalReply +@end diff --git a/SoObjects/Appointments/SOGoCalendarComponent.h b/SoObjects/Appointments/SOGoCalendarComponent.h index 4bb746a3a..a0973e08f 100644 --- a/SoObjects/Appointments/SOGoCalendarComponent.h +++ b/SoObjects/Appointments/SOGoCalendarComponent.h @@ -35,21 +35,19 @@ @class SOGoUser; @interface SOGoCalendarComponent : SOGoContentObject -{ - iCalCalendar *calendar; - NSString *calContent; -} - (NSString *) componentTag; -- (iCalCalendar *) calendar: (BOOL) create; -- (iCalRepeatableEntityObject *) component: (BOOL) create; -- (NSException *) primarySaveContentString: (NSString *) _iCalString; -- (NSException *) primaryDelete; +- (iCalCalendar *) calendar: (BOOL) create + secure: (BOOL) secure; +- (id) component: (BOOL) create secure: (BOOL) secure; -- (NSException *) delete; +// - (NSException *) primarySaveContentString: (NSString *) _iCalString; +// - (NSException *) primaryDelete; -- (NSException *) changeParticipationStatus: (NSString *) _status; +// - (NSException *) delete; + +- (void) saveComponent: (iCalRepeatableEntityObject *) newObject; /* mail notifications */ - (BOOL) sendEMailNotifications; @@ -64,7 +62,6 @@ - (iCalPerson *) findParticipantWithUID: (NSString *) uid; - (iCalPerson *) iCalPersonWithUID: (NSString *) uid; -- (NSString *) getUIDForICalPerson: (iCalPerson *) person; - (NSArray *) getUIDsForICalPersons: (NSArray *) iCalPersons; @end diff --git a/SoObjects/Appointments/SOGoCalendarComponent.m b/SoObjects/Appointments/SOGoCalendarComponent.m index 72cb73c3b..102749871 100644 --- a/SoObjects/Appointments/SOGoCalendarComponent.m +++ b/SoObjects/Appointments/SOGoCalendarComponent.m @@ -30,6 +30,7 @@ #import #import #import +#import #import #import #import @@ -42,10 +43,13 @@ #import #import #import +#import #import +#import "SOGoAptMailICalReply.h" #import "SOGoAptMailNotification.h" #import "iCalEntityObject+SOGo.h" +#import "iCalPerson+SOGo.h" #import "SOGoCalendarComponent.h" static BOOL sendEMailNotifications = NO; @@ -67,26 +71,6 @@ static BOOL sendEMailNotifications = NO; } } -- (id) init -{ - if ((self = [super init])) - { - calendar = nil; - calContent = nil; - } - - return self; -} - -- (void) dealloc -{ - if (calendar) - [calendar release]; - if (calContent) - [calContent release]; - [super dealloc]; -} - - (NSString *) davContentType { return @"text/calendar"; @@ -111,16 +95,15 @@ static BOOL sendEMailNotifications = NO; [component removeAllAlarms]; } -- (NSString *) contentAsString +- (NSString *) secureContentAsString { iCalCalendar *tmpCalendar; iCalRepeatableEntityObject *tmpComponent; // NSArray *roles; // NSString *uid; SoSecurityManager *sm; + NSString *iCalString; - if (!calContent) - { // uid = [[context activeUser] login]; // roles = [self aclsForUser: uid]; // if ([roles containsObject: SOGoCalendarRole_Organizer] @@ -139,113 +122,76 @@ static BOOL sendEMailNotifications = NO; // else // calContent = nil; - sm = [SoSecurityManager sharedSecurityManager]; - if (![sm validatePermission: SOGoCalendarPerm_ViewAllComponent - onObject: self inContext: context]) - calContent = content; - else if (![sm validatePermission: SOGoCalendarPerm_ViewDAndT - onObject: self inContext: context]) + sm = [SoSecurityManager sharedSecurityManager]; + if (![sm validatePermission: SOGoCalendarPerm_ViewAllComponent + onObject: self inContext: context]) + iCalString = content; + else if (![sm validatePermission: SOGoCalendarPerm_ViewDAndT + onObject: self inContext: context]) + { + tmpCalendar = [[self calendar: NO secure: NO] copy]; + tmpComponent = (iCalRepeatableEntityObject *) + [tmpCalendar firstChildWithTag: [self componentTag]]; + [self _filterComponent: tmpComponent]; + iCalString = [tmpCalendar versitString]; + [tmpCalendar release]; + } + else + iCalString = nil; + + return iCalString; +} + +- (iCalCalendar *) calendar: (BOOL) create secure: (BOOL) secure +{ + NSString *componentTag; + CardGroup *newComponent; + iCalCalendar *calendar; + NSString *iCalString; + + if (secure) + iCalString = [self secureContentAsString]; + else + iCalString = content; + + if ([iCalString length] > 0) + calendar = [iCalCalendar parseSingleFromSource: iCalString]; + else + { + if (create) { - tmpCalendar = [[self calendar: NO] copy]; - tmpComponent = (iCalRepeatableEntityObject *) - [tmpCalendar firstChildWithTag: [self componentTag]]; - [self _filterComponent: tmpComponent]; - calContent = [tmpCalendar versitString]; - [tmpCalendar release]; + calendar = [iCalCalendar groupWithTag: @"vcalendar"]; + [calendar setVersion: @"2.0"]; + [calendar setProdID: @"-//Inverse groupe conseil//SOGo 0.9//EN"]; + componentTag = [[self componentTag] uppercaseString]; + newComponent = [[calendar classForTag: componentTag] + groupWithTag: componentTag]; + [calendar addChild: newComponent]; } else - calContent = nil; - - [calContent retain]; - } - - return calContent; -} - -- (void) setContentString: (NSString *) newContent -{ - [super setContentString: newContent]; - ASSIGN (calendar, nil); - ASSIGN (calContent, nil); -} - -// - (NSException *) saveContentString: (NSString *) contentString -// baseVersion: (unsigned int) baseVersion -// { -// NSException *result; - -// result = [super saveContentString: contentString -// baseVersion: baseVersion]; -// if (!result && calContent) -// { -// [calContent release]; -// calContent = nil; -// } - -// return result; -// } - -- (iCalCalendar *) calendar: (BOOL) create -{ - NSString *iCalString, *componentTag; - CardGroup *newComponent; - - if (!calendar) - { - iCalString = [super contentAsString]; - if ([iCalString length] > 0) - calendar = [iCalCalendar parseSingleFromSource: iCalString]; - else - { - if (create) - { - calendar = [iCalCalendar groupWithTag: @"vcalendar"]; - [calendar setVersion: @"2.0"]; - [calendar setProdID: @"-//Inverse groupe conseil//SOGo 0.9//EN"]; - componentTag = [[self componentTag] uppercaseString]; - newComponent = [[calendar classForTag: componentTag] - groupWithTag: componentTag]; - [calendar addChild: newComponent]; - } - } - if (calendar) - [calendar retain]; + calendar = nil; } return calendar; } -- (iCalRepeatableEntityObject *) component: (BOOL) create +- (id) component: (BOOL) create secure: (BOOL) secure { - return - (iCalRepeatableEntityObject *) [[self calendar: create] - firstChildWithTag: [self componentTag]]; + return [[self calendar: create secure: secure] + firstChildWithTag: [self componentTag]]; +} + +- (void) saveComponent: (iCalRepeatableEntityObject *) newObject +{ + NSString *newiCalString; + + newiCalString = [[newObject parent] versitString]; + + [self saveContentString: newiCalString]; } /* raw saving */ -- (NSException *) primarySaveContentString: (NSString *) _iCalString -{ - return [super saveContentString: _iCalString]; -} - -- (NSException *) primaryDelete -{ - return [super delete]; -} - -- (NSException *) deleteWithBaseSequence: (int) a -{ - [self subclassResponsibility: _cmd]; - - return nil; -} - -- (NSException *) delete -{ - return [self deleteWithBaseSequence: 0]; -} - /* EMail Notifications */ - (NSString *) homePageURLForPerson: (iCalPerson *) _person { @@ -262,58 +208,13 @@ static BOOL sendEMailNotifications = NO; baseURL = @"http://localhost/"; [self warnWithFormat:@"Unable to create baseURL from context!"]; } - uid = [[LDAPUserManager sharedUserManager] - getUIDForEmail: [_person rfc822Email]]; + uid = [_person uid]; return ((uid) ? [NSString stringWithFormat:@"%@%@", baseURL, uid] : nil); } -- (NSException *) changeParticipationStatus: (NSString *) _status -{ - iCalRepeatableEntityObject *component; - iCalPerson *person; - NSString *newContent; - NSException *ex; - - ex = nil; - - component = [self component: NO]; - if (component) - { - person = [self findParticipantWithUID: owner]; - if (person) - { - // TODO: send iMIP reply mails? - [person setPartStat: _status]; - newContent = [[component parent] versitString]; - if (newContent) - { - ex = [self saveContentString: newContent]; - if (ex) - // TODO: why is the exception wrapped? - /* Server Error */ - ex = [NSException exceptionWithHTTPStatus: 500 - reason: [ex reason]]; - } - else - ex - = [NSException exceptionWithHTTPStatus: 500 /* Server Error */ - reason: @"Could not generate iCalendar data ..."]; - } - else - ex = [NSException exceptionWithHTTPStatus: 404 /* Not Found */ - reason: @"user does not participate in this " - @"calendar component"]; - } - else - ex = [NSException exceptionWithHTTPStatus: 500 /* Server Error */ - reason: @"unable to parse component record"]; - - return ex; -} - - (BOOL) sendEMailNotifications { return sendEMailNotifications; @@ -335,7 +236,7 @@ static BOOL sendEMailNotifications = NO; { NSString *pageName; iCalPerson *organizer; - NSString *cn, *email, *sender, *iCalString; + NSString *email, *sender, *iCalString; WOApplication *app; unsigned i, count; iCalPerson *attendee; @@ -347,115 +248,196 @@ static BOOL sendEMailNotifications = NO; NGMimeBodyPart *bodyPart; NGMimeMultipartBody *body; - if ([_attendees count]) + if (sendEMailNotifications + && [_newObject isStillRelevant]) { - /* sender */ - - organizer = [_newObject organizer]; - cn = [organizer cnWithoutQuotes]; - if (cn) - sender = [NSString stringWithFormat:@"%@ <%@>", - cn, - [organizer rfc822Email]]; - else - sender = [organizer rfc822Email]; - - /* generate iCalString once */ - iCalString = [[_newObject parent] versitString]; - - /* get WOApplication instance */ - app = [WOApplication application]; - - /* generate dynamic message content */ - count = [_attendees count]; - for (i = 0; i < count; i++) - { - attendee = [_attendees objectAtIndex:i]; + if (count) + { + /* sender */ + organizer = [_newObject organizer]; + sender = [organizer mailAddress]; - /* construct recipient */ - cn = [attendee cn]; - email = [attendee rfc822Email]; - if (cn) - recipient = [NSString stringWithFormat: @"%@ <%@>", - cn, email]; - else - recipient = email; + NSLog (@"sending '%@' from %@", + [(iCalCalendar *) [_newObject parent] method], organizer); - language = [[context activeUser] language]; + /* generate iCalString once */ + iCalString = [[_newObject parent] versitString]; + + /* get WOApplication instance */ + app = [WOApplication application]; + + /* generate dynamic message content */ + + for (i = 0; i < count; i++) + { + attendee = [_attendees objectAtIndex: i]; + if (![[attendee uid] isEqualToString: owner]) + { + /* construct recipient */ + recipient = [attendee mailAddress]; + email = [attendee rfc822Email]; + + NSLog (@"recipient: %@", recipient); + language = [[context activeUser] language]; #warning this could be optimized in a class hierarchy common with the \ - SOGoObject acl notification mechanism - /* create page name */ - // TODO: select user's default language? - pageName = [NSString stringWithFormat: @"SOGoAptMail%@%@", - language, - _pageName]; - /* construct message content */ - p = [app pageWithName: pageName inContext: context]; - [p setNewApt: _newObject]; - [p setOldApt: _oldObject]; - [p setHomePageURL: [self homePageURLForPerson: attendee]]; - [p setViewTZ: [self timeZoneForUser: email]]; - subject = [p getSubject]; - text = [p getBody]; + SOGoObject acl notification mechanism + /* create page name */ + pageName = [NSString stringWithFormat: @"SOGoAptMail%@%@", + language, _pageName]; + /* construct message content */ + p = [app pageWithName: pageName inContext: context]; + [p setNewApt: _newObject]; + [p setOldApt: _oldObject]; + [p setHomePageURL: [self homePageURLForPerson: attendee]]; + [p setViewTZ: [self timeZoneForUser: email]]; + subject = [p getSubject]; + text = [p getBody]; - /* construct message */ - headerMap = [NGMutableHashMap hashMapWithCapacity: 5]; + /* construct message */ + headerMap = [NGMutableHashMap hashMapWithCapacity: 5]; - /* NOTE: multipart/alternative seems like the correct choice but - * unfortunately Thunderbird doesn't offer the rich content alternative - * at all. Mail.app shows the rich content alternative _only_ - * so we'll stick with multipart/mixed for the time being. - */ - [headerMap setObject: @"multipart/mixed" forKey: @"content-type"]; - [headerMap setObject: sender forKey: @"From"]; - [headerMap setObject: recipient forKey: @"To"]; - mailDate = [[NSCalendarDate date] rfc822DateString]; - [headerMap setObject: mailDate forKey: @"date"]; - [headerMap setObject: subject forKey: @"Subject"]; - msg = [NGMimeMessage messageWithHeader: headerMap]; + /* NOTE: multipart/alternative seems like the correct choice but + * unfortunately Thunderbird doesn't offer the rich content alternative + * at all. Mail.app shows the rich content alternative _only_ + * so we'll stick with multipart/mixed for the time being. + */ + [headerMap setObject: @"multipart/mixed" forKey: @"content-type"]; + [headerMap setObject: sender forKey: @"From"]; + [headerMap setObject: recipient forKey: @"To"]; + mailDate = [[NSCalendarDate date] rfc822DateString]; + [headerMap setObject: mailDate forKey: @"date"]; + [headerMap setObject: subject forKey: @"Subject"]; + msg = [NGMimeMessage messageWithHeader: headerMap]; - /* multipart body */ - body = [[NGMimeMultipartBody alloc] initWithPart: msg]; + /* multipart body */ + body = [[NGMimeMultipartBody alloc] initWithPart: msg]; + + /* text part */ + headerMap = [NGMutableHashMap hashMapWithCapacity: 1]; + [headerMap setObject: @"text/plain; charset=utf-8" + forKey: @"content-type"]; + bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap]; + [bodyPart setBody: [text dataUsingEncoding: NSUTF8StringEncoding]]; + + /* attach text part to multipart body */ + [body addBodyPart: bodyPart]; - /* text part */ - headerMap = [NGMutableHashMap hashMapWithCapacity: 1]; - [headerMap setObject: @"text/plain; charset=utf-8" - forKey: @"content-type"]; - bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap]; - [bodyPart setBody: [text dataUsingEncoding: NSUTF8StringEncoding]]; + /* calendar part */ + header = [NSString stringWithFormat: @"text/calendar; method=%@;" + @" charset=utf-8", + [(iCalCalendar *) [_newObject parent] method]]; + headerMap = [NGMutableHashMap hashMapWithCapacity: 1]; + [headerMap setObject:header forKey: @"content-type"]; + bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap]; + [bodyPart setBody: [iCalString dataUsingEncoding: NSUTF8StringEncoding]]; - /* attach text part to multipart body */ - [body addBodyPart: bodyPart]; + /* attach calendar part to multipart body */ + [body addBodyPart: bodyPart]; - /* calendar part */ - header = [NSString stringWithFormat: @"text/calendar; method=%@;" - @" charset=utf-8", - [(iCalCalendar *) [_newObject parent] method]]; - headerMap = [NGMutableHashMap hashMapWithCapacity: 1]; - [headerMap setObject:header forKey: @"content-type"]; - bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap]; - [bodyPart setBody: [iCalString dataUsingEncoding: NSUTF8StringEncoding]]; + /* attach multipart body to message */ + [msg setBody: body]; + [body release]; - /* attach calendar part to multipart body */ - [body addBodyPart: bodyPart]; - - /* attach multipart body to message */ - [msg setBody: body]; - [body release]; - - /* send the damn thing */ - [[SOGoMailer sharedMailer] - sendMimePart: msg - toRecipients: [NSArray arrayWithObject: email] - sender: [organizer rfc822Email]]; - } + /* send the damn thing */ + [[SOGoMailer sharedMailer] + sendMimePart: msg + toRecipients: [NSArray arrayWithObject: email] + sender: [organizer rfc822Email]]; + } + } + } } } - (void) sendResponseToOrganizer { -#warning THIS IS A STUB + NSString *pageName, *language, *mailDate, *email; + WOApplication *app; + iCalPerson *organizer, *attendee; + NSString *iCalString; + iCalEvent *event; + SOGoAptMailICalReply *p; + NGMutableHashMap *headerMap; + NGMimeMessage *msg; + NGMimeBodyPart *bodyPart; + NGMimeMultipartBody *body; + NSData *bodyData; + + event = [[self component: NO secure: NO] itipEntryWithMethod: @"reply"]; + if (![event userIsOrganizer: [context activeUser]]) + { + organizer = [event organizer]; + attendee = [event findParticipant: [context activeUser]]; + [event setAttendees: [NSArray arrayWithObject: attendee]]; + + /* get WOApplication instance */ + app = [WOApplication application]; + + language = [[context activeUser] language]; + /* create page name */ + pageName + = [NSString stringWithFormat: @"SOGoAptMail%@ICalReply", language]; + /* construct message content */ + p = [app pageWithName: pageName inContext: context]; + [p setApt: event]; + [p setAttendee: attendee]; + + /* construct message */ + headerMap = [NGMutableHashMap hashMapWithCapacity: 5]; + + /* NOTE: multipart/alternative seems like the correct choice but + * unfortunately Thunderbird doesn't offer the rich content alternative + * at all. Mail.app shows the rich content alternative _only_ + * so we'll stick with multipart/mixed for the time being. + */ + [headerMap setObject: @"multipart/mixed" forKey: @"content-type"]; + [headerMap setObject: [attendee mailAddress] forKey: @"From"]; + [headerMap setObject: [organizer mailAddress] forKey: @"To"]; + mailDate = [[NSCalendarDate date] rfc822DateString]; + [headerMap setObject: mailDate forKey: @"date"]; + [headerMap setObject: [p getSubject] forKey: @"Subject"]; + msg = [NGMimeMessage messageWithHeader: headerMap]; + + NSLog (@"sending 'REPLY' from %@ to %@", + [attendee mailAddress], [organizer mailAddress]); + + /* multipart body */ + body = [[NGMimeMultipartBody alloc] initWithPart: msg]; + + /* text part */ + headerMap = [NGMutableHashMap hashMapWithCapacity: 1]; + [headerMap setObject: @"text/plain; charset=utf-8" + forKey: @"content-type"]; + bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap]; + bodyData = [[p getBody] dataUsingEncoding: NSUTF8StringEncoding]; + [bodyPart setBody: bodyData]; + + /* attach text part to multipart body */ + [body addBodyPart: bodyPart]; + + /* calendar part */ + headerMap = [NGMutableHashMap hashMapWithCapacity: 1]; + [headerMap setObject: @"text/calendar; method=REPLY; charset=utf-8" + forKey: @"content-type"]; + bodyPart = [NGMimeBodyPart bodyPartWithHeader: headerMap]; + iCalString = [[event parent] versitString]; + [bodyPart setBody: [iCalString dataUsingEncoding: NSUTF8StringEncoding]]; + + /* attach calendar part to multipart body */ + [body addBodyPart: bodyPart]; + + /* attach multipart body to message */ + [msg setBody: body]; + [body release]; + + /* send the damn thing */ + email = [organizer rfc822Email]; + [[SOGoMailer sharedMailer] + sendMimePart: msg + toRecipients: [NSArray arrayWithObject: email] + sender: [attendee rfc822Email]]; + } } // - (BOOL) isOrganizerOrOwner: (SOGoUser *) user @@ -481,7 +463,7 @@ static BOOL sendEMailNotifications = NO; SOGoUser *user; user = [SOGoUser userWithLogin: uid roles: nil]; - component = [self component: NO]; + component = [self component: NO secure: NO]; return [component findParticipant: user]; } @@ -503,31 +485,20 @@ static BOOL sendEMailNotifications = NO; return person; } -- (NSString *) getUIDForICalPerson: (iCalPerson *) person -{ - LDAPUserManager *um; - - um = [LDAPUserManager sharedUserManager]; - - return [um getUIDForEmail: [person rfc822Email]]; -} - - (NSArray *) getUIDsForICalPersons: (NSArray *) iCalPersons { iCalPerson *currentPerson; NSEnumerator *persons; NSMutableArray *uids; NSString *uid; - LDAPUserManager *um; uids = [NSMutableArray array]; - um = [LDAPUserManager sharedUserManager]; persons = [iCalPersons objectEnumerator]; currentPerson = [persons nextObject]; while (currentPerson) { - uid = [um getUIDForEmail: [currentPerson rfc822Email]]; + uid = [currentPerson uid]; if (uid) [uids addObject: uid]; currentPerson = [persons nextObject]; @@ -591,7 +562,7 @@ static BOOL sendEMailNotifications = NO; if ([superAcls count] > 0) [roles addObjectsFromArray: superAcls]; - component = [self component: NO]; + component = [self component: NO secure: NO]; ownerRole = [self _roleOfOwner: component]; if ([owner isEqualToString: uid]) [roles addObject: ownerRole]; diff --git a/SoObjects/Appointments/SOGoTaskObject.h b/SoObjects/Appointments/SOGoTaskObject.h index da442022b..c369e6fb1 100644 --- a/SoObjects/Appointments/SOGoTaskObject.h +++ b/SoObjects/Appointments/SOGoTaskObject.h @@ -44,13 +44,6 @@ @interface SOGoTaskObject : SOGoCalendarComponent -/* "iCal multifolder saves" */ - -- (NSException *) saveContentString: (NSString *) _iCal - baseSequence: (int) _v; -- (NSException *) deleteWithBaseSequence: (int) _v; -- (NSException *) saveContentString: (NSString *) _iCalString; - @end #endif /* __Appointmentss_SOGoTaskObject_H__ */ diff --git a/SoObjects/Appointments/SOGoTaskObject.m b/SoObjects/Appointments/SOGoTaskObject.m index 5c628fa66..16bbb6254 100644 --- a/SoObjects/Appointments/SOGoTaskObject.m +++ b/SoObjects/Appointments/SOGoTaskObject.m @@ -38,415 +38,17 @@ #import "SOGoTaskObject.h" -@interface SOGoTaskObject (PrivateAPI) - -- (NSString *) homePageURLForPerson: (iCalPerson *) _person; - -@end - @implementation SOGoTaskObject -static NSString *mailTemplateDefaultLanguage = nil; - -+ (void)initialize { - NSUserDefaults *ud; - static BOOL didInit = NO; - - if (didInit) return; - didInit = YES; - - ud = [NSUserDefaults standardUserDefaults]; - mailTemplateDefaultLanguage = [[ud stringForKey:@"SOGoDefaultLanguage"] - retain]; - if (!mailTemplateDefaultLanguage) - mailTemplateDefaultLanguage = @"French"; -} - - (NSString *) componentTag { return @"vtodo"; } -/* iCal handling */ - -- (NSArray *)attendeeUIDsFromTask:(iCalToDo *)_task { - LDAPUserManager *um; - NSMutableArray *uids; - NSArray *attendees; - unsigned i, count; - NSString *email, *uid; - - if (![_task isNotNull]) - return nil; - - if ((attendees = [_task attendees]) == nil) - return nil; - count = [attendees count]; - uids = [NSMutableArray arrayWithCapacity:count + 1]; - - um = [LDAPUserManager sharedUserManager]; - - /* add organizer */ - - email = [[_task organizer] rfc822Email]; - if ([email isNotNull]) { - uid = [um getUIDForEmail:email]; - if ([uid isNotNull]) { - [uids addObject:uid]; - } - else - [self logWithFormat:@"Note: got no uid for organizer: '%@'", email]; - } - - /* add attendees */ - - for (i = 0; i < count; i++) { - iCalPerson *person; - - person = [attendees objectAtIndex:i]; - email = [person rfc822Email]; - if (![email isNotNull]) continue; - - uid = [um getUIDForEmail:email]; - if (![uid isNotNull]) { - [self logWithFormat:@"Note: got no uid for email: '%@'", email]; - continue; - } - if (![uids containsObject:uid]) - [uids addObject:uid]; - } - - return uids; -} - -/* store in all the other folders */ - -- (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids { - NSEnumerator *e; - id folder; - NSException *allErrors = nil; - - e = [[container lookupCalendarFoldersForUIDs: _uids inContext: context] - objectEnumerator]; - while ((folder = [e nextObject]) != nil) { - NSException *error; - SOGoTaskObject *task; - - if (![folder isNotNull]) /* no folder was found for given UID */ - continue; - - task = [folder lookupName:[self nameInContainer] inContext: context - acquire:NO]; - if ([task isKindOfClass: [NSException class]]) - { - [self logWithFormat:@"Note: an exception occured finding '%@' in folder: %@", - [self nameInContainer], folder]; - [self logWithFormat:@"the exception reason was: %@", - [(NSException *) task reason]]; - continue; - } - - if (![task isNotNull]) { - [self logWithFormat:@"Note: did not find '%@' in folder: %@", - [self nameInContainer], folder]; - continue; - } - - if ((error = [task primarySaveContentString:_iCal]) != nil) { - [self logWithFormat:@"Note: failed to save iCal in folder: %@", folder]; - // TODO: make compound - allErrors = error; - } - } - return allErrors; -} -// - (NSException *)deleteInUIDs:(NSArray *)_uids { -// NSEnumerator *e; -// id folder; -// NSException *allErrors = nil; - -// e = [[container lookupCalendarFoldersForUIDs: _uids inContext: context] -// objectEnumerator]; -// while ((folder = [e nextObject])) { -// NSException *error; -// SOGoTaskObject *task; - -// task = [folder lookupName: [self nameInContainer] -// inContext: context -// acquire: NO]; -// if (![task isNotNull]) { -// [self logWithFormat:@"Note: did not find '%@' in folder: %@", -// [self nameInContainer], folder]; -// continue; -// } -// if ([task isKindOfClass: [NSException class]]) { -// [self logWithFormat:@"Exception: %@", [(NSException *) task reason]]; -// continue; -// } - -// if ((error = [task primaryDelete]) != nil) { -// [self logWithFormat:@"Note: failed to delete in folder: %@", folder]; -// // TODO: make compound -// allErrors = error; -// } -// } -// return allErrors; -// } - -/* "iCal multifolder saves" */ - -- (NSException *) saveContentString: (NSString *) _iCal - baseSequence: (int) _v -{ - /* - Note: we need to delete in all participants folders and send iMIP messages - for all external accounts. - - Steps: - - fetch stored content - - parse old content - - check if sequence matches (or if 0=ignore) - - extract old attendee list + organizer (make unique) - - parse new content (ensure that sequence is increased!) - - extract new attendee list + organizer (make unique) - - make a diff => new, same, removed - - write to new, same - - delete in removed folders - - send iMIP mail for all folders not found - */ -// LDAPUserManager *um; -// iCalCalendar *calendar; -// iCalToDo *oldApt, *newApt; -// // iCalToDoChanges *changes; -// iCalPerson *organizer; -// NSString *oldContent, *uid; -// NSArray *uids, *props; -// NSMutableArray *attendees, *storeUIDs, *removedUIDs; - NSException *storeError, *delError; -// BOOL updateForcesReconsider; - -// updateForcesReconsider = NO; - -// if ([_iCal length] == 0) { -// return [NSException exceptionWithHTTPStatus:400 /* Bad Request */ -// reason:@"got no iCalendar content to store!"]; -// } - -// um = [LDAPUserManager sharedUserManager]; - -// /* handle old content */ - -// oldContent = [self contentAsString]; /* if nil, this is a new task */ -// if ([oldContent length] == 0) -// { -// /* new task */ -// [self debugWithFormat:@"saving new task: %@", _iCal]; -// oldApt = nil; -// } -// else -// { -// calendar = [iCalCalendar parseSingleFromSource: oldContent]; -// oldApt = [self firstTaskFromCalendar: calendar]; -// } - -// /* compare sequence if requested */ - -// if (_v != 0) { -// // TODO -// } - - -// /* handle new content */ - -// calendar = [iCalCalendar parseSingleFromSource: _iCal]; -// newApt = [self firstTaskFromCalendar: calendar]; -// if (newApt == nil) { -// return [NSException exceptionWithHTTPStatus:400 /* Bad Request */ -// reason:@"could not parse iCalendar content!"]; -// } - -// /* diff */ - -// changes = [iCalToDoChanges changesFromEvent: oldApt -// toEvent: newApt]; - -// uids = [um getUIDsForICalPersons:[changes deletedAttendees] -// applyStrictMapping:NO]; -// removedUIDs = [NSMutableArray arrayWithArray:uids]; - -// uids = [um getUIDsForICalPersons:[newApt attendees] -// applyStrictMapping:NO]; -// storeUIDs = [NSMutableArray arrayWithArray:uids]; -// props = [changes updatedProperties]; - -// /* detect whether sequence has to be increased */ -// if ([changes hasChanges]) -// [newApt increaseSequence]; - -// /* preserve organizer */ - -// organizer = [newApt organizer]; -// uid = [um getUIDForICalPerson:organizer]; -// if (uid) { -// if (![storeUIDs containsObject:uid]) -// [storeUIDs addObject:uid]; -// [removedUIDs removeObject:uid]; -// } - -// /* organizer might have changed completely */ - -// if (oldApt && ([props containsObject: @"organizer"])) { -// uid = [um getUIDForICalPerson:[oldApt organizer]]; -// if (uid) { -// if (![storeUIDs containsObject:uid]) { -// if (![removedUIDs containsObject:uid]) { -// [removedUIDs addObject:uid]; -// } -// } -// } -// } - -// [self debugWithFormat:@"UID ops:\n store: %@\n remove: %@", -// storeUIDs, removedUIDs]; - -// /* if time did change, all participants have to re-decide ... -// * ... exception from that rule: the organizer -// */ - -// if (oldApt != nil && -// ([props containsObject:@"startDate"] || -// [props containsObject:@"endDate"] || -// [props containsObject:@"duration"])) -// { -// NSArray *ps; -// unsigned i, count; - -// ps = [newApt attendees]; -// count = [ps count]; -// for (i = 0; i < count; i++) { -// iCalPerson *p; - -// p = [ps objectAtIndex:i]; -// if (![p hasSameEmailAddress:organizer]) -// [p setParticipationStatus:iCalPersonPartStatNeedsAction]; -// } -// _iCal = [[newApt parent] versitString]; -// updateForcesReconsider = YES; -// } - -// /* perform storing */ - - storeError = [self primarySaveContentString: _iCal]; - -// storeError = [self saveContentString:_iCal inUIDs:storeUIDs]; -// delError = [self deleteInUIDs:removedUIDs]; - - // TODO: make compound - if (storeError != nil) return storeError; -// if (delError != nil) return delError; - - /* email notifications */ -// if ([self sendEMailNotifications]) -// { -// attendees = [NSMutableArray arrayWithArray:[changes insertedAttendees]]; -// [attendees removePerson:organizer]; -// [self sendInvitationEMailForTask:newApt -// toAttendees:attendees]; - -// if (updateForcesReconsider) { -// attendees = [NSMutableArray arrayWithArray:[newApt attendees]]; -// [attendees removeObjectsInArray:[changes insertedAttendees]]; -// [attendees removePerson:organizer]; -// [self sendEMailUsingTemplateNamed: @"Update" -// forOldObject: oldApt -// andNewObject: newApt -// toAttendees: attendees]; -// } - -// attendees = [NSMutableArray arrayWithArray:[changes deletedAttendees]]; -// [attendees removePerson: organizer]; -// if ([attendees count]) { -// iCalToDo *cancelledApt; - -// cancelledApt = [newApt copy]; -// [(iCalCalendar *) [cancelledApt parent] setMethod: @"cancel"]; -// [self sendEMailUsingTemplateNamed: @"Removal" -// forOldObject: nil -// andNewObject: cancelledApt -// toAttendees: attendees]; -// [cancelledApt release]; -// } -// } - - return nil; -} - -- (NSException *)deleteWithBaseSequence:(int)_v { - /* - Note: We need to delete in all participants folders and send iMIP messages - for all external accounts. - Delete is basically identical to save with all attendees and the - organizer being deleted. - - Steps: - - fetch stored content - - parse old content - - check if sequence matches (or if 0=ignore) - - extract old attendee list + organizer (make unique) - - delete in removed folders - - send iMIP mail for all folders not found - */ -// iCalToDo *task; -// NSArray *removedUIDs; -// NSMutableArray *attendees; - - [self primaryDelete]; - - return nil; -// /* load existing content */ - -// task = (iCalToDo *) [self component: NO]; - -// /* compare sequence if requested */ - -// if (_v != 0) { -// // TODO -// } - -// removedUIDs = [self attendeeUIDsFromTask:task]; - -// if ([self sendEMailNotifications]) -// { -// /* send notification email to attendees excluding organizer */ -// attendees = [NSMutableArray arrayWithArray:[task attendees]]; -// [attendees removePerson:[task organizer]]; - -// /* flag task as being cancelled */ -// [(iCalCalendar *) [task parent] setMethod: @"cancel"]; -// [task increaseSequence]; - -// /* remove all attendees to signal complete removal */ -// [task removeAllAttendees]; - -// /* send notification email */ -// [self sendEMailUsingTemplateNamed: @"Deletion" -// forOldObject: nil -// andNewObject: task -// toAttendees: attendees]; -// } - -// /* perform */ - -// return [self deleteInUIDs:removedUIDs]; -} - -- (NSException *)saveContentString:(NSString *)_iCalString { - return [self saveContentString:_iCalString baseSequence:0]; -} - /* message type */ -- (NSString *)outlookMessageClass { +- (NSString *) outlookMessageClass +{ return @"IPM.Task"; } diff --git a/SoObjects/Appointments/iCalEntityObject+SOGo.h b/SoObjects/Appointments/iCalEntityObject+SOGo.h index 7f8702b9e..f26ae5a1a 100644 --- a/SoObjects/Appointments/iCalEntityObject+SOGo.h +++ b/SoObjects/Appointments/iCalEntityObject+SOGo.h @@ -25,11 +25,20 @@ #import +@class SOGoUser; + @interface iCalEntityObject (SOGoExtensions) - (BOOL) userIsParticipant: (SOGoUser *) user; - (BOOL) userIsOrganizer: (SOGoUser *) user; +- (NSArray *) attendeeUIDs; +- (BOOL) isStillRelevant; + +- (id) itipEntryWithMethod: (NSString *) method; + +- (NSArray *) attendeesWithoutUser: (SOGoUser *) user; + @end #endif /* ICALENTITYOBJECT_SOGO_H */ diff --git a/SoObjects/Appointments/iCalEntityObject+SOGo.m b/SoObjects/Appointments/iCalEntityObject+SOGo.m index 3a4411161..83cd25a58 100644 --- a/SoObjects/Appointments/iCalEntityObject+SOGo.m +++ b/SoObjects/Appointments/iCalEntityObject+SOGo.m @@ -23,11 +23,14 @@ #import #import +#import #import #import #import +#import "iCalPerson+SOGo.h" + #import "iCalEntityObject+SOGo.h" @implementation iCalEntityObject (SOGoExtensions) @@ -52,6 +55,7 @@ return isParticipant; } +#warning user could be a delegate, we will need to handle that someday - (BOOL) userIsOrganizer: (SOGoUser *) user { NSString *orgMail; @@ -61,4 +65,66 @@ return [user hasEmail: orgMail]; } +- (NSArray *) attendeeUIDs +{ + NSEnumerator *attendees; + NSString *uid; + iCalPerson *currentAttendee; + NSMutableArray *uids; + + uids = [NSMutableArray array]; + + attendees = [[self attendees] objectEnumerator]; + while ((currentAttendee = [attendees nextObject])) + { + uid = [currentAttendee uid]; + if (uid) + [uids addObject: uid]; + } + + return uids; +} + +#warning this method should be implemented in a category of iCalToDo +- (BOOL) isStillRelevant +{ + [self subclassResponsibility: _cmd]; + return NO; +} + +- (id) itipEntryWithMethod: (NSString *) method +{ + iCalCalendar *newCalendar; + iCalEntityObject *newEntry; + + newCalendar = [parent mutableCopy]; + [newCalendar autorelease]; + [newCalendar setMethod: method]; + newEntry = (iCalEntityObject *) [newCalendar firstChildWithTag: tag]; + + return newEntry; +} + +- (NSArray *) attendeesWithoutUser: (SOGoUser *) user +{ + NSMutableArray *newAttendees; + NSArray *oldAttendees; + unsigned int count, max; + iCalPerson *currentAttendee; + NSString *userID; + + userID = [user login]; + oldAttendees = [self attendees]; + max = [oldAttendees count]; + newAttendees = [NSMutableArray arrayWithCapacity: max]; + for (count = 0; count < max; count++) + { + currentAttendee = [oldAttendees objectAtIndex: count]; + if (![[currentAttendee uid] isEqualToString: userID]) + [newAttendees addObject: currentAttendee]; + } + + return newAttendees; +} + @end diff --git a/SoObjects/Appointments/iCalEvent+SOGo.h b/SoObjects/Appointments/iCalEvent+SOGo.h new file mode 100644 index 000000000..8250b23b0 --- /dev/null +++ b/SoObjects/Appointments/iCalEvent+SOGo.h @@ -0,0 +1,35 @@ +/* iCalEvent+SOGo.h - this file is part of SOGo + * + * Copyright (C) 2007 Inverse groupe conseil + * + * Author: Wolfgang Sourdeau + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef ICALEVENT_SOGO_H +#define ICALEVENT_SOGO_H + +#import + +@interface iCalEvent (SOGoExtensions) + +- (BOOL) isStillRelevant; + +@end + + +#endif /* ICALEVENT_SOGO_H */ diff --git a/SoObjects/Appointments/iCalEvent+SOGo.m b/SoObjects/Appointments/iCalEvent+SOGo.m new file mode 100644 index 000000000..f795d3f78 --- /dev/null +++ b/SoObjects/Appointments/iCalEvent+SOGo.m @@ -0,0 +1,38 @@ +/* iCalEvent+SOGo.m - this file is part of SOGo + * + * Copyright (C) 2007 Inverse groupe conseil + * + * Author: Wolfgang Sourdeau + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#import + +#import "iCalEvent+SOGo.h" + +@implementation iCalEvent (SOGoExtensions) + +- (BOOL) isStillRelevant +{ + NSCalendarDate *now; + + now = [NSCalendarDate calendarDate]; + + return ([[self endDate] earlierDate: now] == now); +} + +@end diff --git a/SoObjects/Appointments/iCalEventChanges+SOGo.h b/SoObjects/Appointments/iCalEventChanges+SOGo.h new file mode 100644 index 000000000..1c4ed55fb --- /dev/null +++ b/SoObjects/Appointments/iCalEventChanges+SOGo.h @@ -0,0 +1,34 @@ +/* iCalEventChanges+SOGo.h - this file is part of SOGo + * + * Copyright (C) 2007 Inverse groupe conseil + * + * Author: Wolfgang Sourdeau + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef ICALEVENTCHANGES_SOGO_H +#define ICALEVENTCHANGES_SOGO_H + +#import + +@interface iCalEventChanges (SOGoExtensions) + +- (BOOL) sequenceShouldBeIncreased; + +@end + +#endif /* ICALEVENTCHANGES_SOGO_H */ diff --git a/SoObjects/Appointments/iCalEventChanges+SOGo.m b/SoObjects/Appointments/iCalEventChanges+SOGo.m new file mode 100644 index 000000000..b1e25c605 --- /dev/null +++ b/SoObjects/Appointments/iCalEventChanges+SOGo.m @@ -0,0 +1,52 @@ +/* iCalEventChanges+SOGo.m - this file is part of SOGo + * + * Copyright (C) 2007 Inverse groupe conseil + * + * Author: Wolfgang Sourdeau + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#import +#import +#import + +#import "iCalEventChanges+SOGo.h" + +@implementation iCalEventChanges (SOGoExtensions) + +- (BOOL) sequenceShouldBeIncreased +{ + NSString *properties[] = {@"organizer", @"startDate", @"endDate", /* vtask: + @"due" */ + @"rdate", @"rrule", @"exdate", @"exrule", + @"status", @"location", nil}; + NSString **currentProperty; + BOOL updateRequired; + + updateRequired = NO; + + currentProperty = properties; + while (!updateRequired && *currentProperty) + if ([updatedProperties containsObject: *currentProperty]) + updateRequired = YES; + else + currentProperty++; + + return updateRequired; +} + +@end diff --git a/SoObjects/Appointments/iCalPerson+SOGo.h b/SoObjects/Appointments/iCalPerson+SOGo.h new file mode 100644 index 000000000..06312acd2 --- /dev/null +++ b/SoObjects/Appointments/iCalPerson+SOGo.h @@ -0,0 +1,39 @@ +/* iCalPerson+SOGo.h - this file is part of SOGo + * + * Copyright (C) 2007 Inverse groupe conseil + * + * Author: Wolfgang Sourdeau + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef ICALPERSON_SOGO_H +#define ICALPERSON_SOGO_H + +#import + +#import + +@class NSString; + +@interface iCalPerson (SOGoExtension) + +- (NSString *) mailAddress; +- (NSString *) uid; + +@end + +#endif /* ICALPERSON_SOGO_H */ diff --git a/SoObjects/Appointments/iCalPerson+SOGo.m b/SoObjects/Appointments/iCalPerson+SOGo.m new file mode 100644 index 000000000..53d828c41 --- /dev/null +++ b/SoObjects/Appointments/iCalPerson+SOGo.m @@ -0,0 +1,53 @@ +/* iCalPerson+SOGo.m - this file is part of SOGo + * + * Copyright (C) 2007 Inverse groupe conseil + * + * Author: Wolfgang Sourdeau + * + * This file is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This file 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. If not, write to + * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#import + +#import "iCalPerson+SOGo.h" + +static LDAPUserManager *um = nil; + +@implementation iCalPerson (SOGoExtension) + +- (NSString *) mailAddress +{ + NSString *cn, *email, *mailAddress; + + cn = [self cnWithoutQuotes]; + email = [self rfc822Email]; + if ([cn length]) + mailAddress = [NSString stringWithFormat:@"%@ <%@>", cn, email]; + else + mailAddress = email; + + return mailAddress; +} + +- (NSString *) uid +{ + if (!um) + um = [LDAPUserManager sharedUserManager]; + + return [um getUIDForEmail: [self rfc822Email]]; +} + +@end diff --git a/SoObjects/SOGo/LDAPSource.h b/SoObjects/SOGo/LDAPSource.h index 201e16b8a..c2edec3de 100644 --- a/SoObjects/SOGo/LDAPSource.h +++ b/SoObjects/SOGo/LDAPSource.h @@ -41,6 +41,7 @@ NSString *IDField; /* the first part of a user DN */ NSString *CNField; NSString *UIDField; + NSArray *mailFields; NSString *bindFields; NGLdapConnection *ldapConnection; @@ -59,6 +60,7 @@ IDField: (NSString *) newIDField CNField: (NSString *) newCNField UIDField: (NSString *) newUIDField + mailFields: (NSArray *) newMailFields andBindFields: (NSString *) newBindFields; - (BOOL) checkLogin: (NSString *) login diff --git a/SoObjects/SOGo/LDAPSource.m b/SoObjects/SOGo/LDAPSource.m index 28475ad18..b2cd7ad32 100644 --- a/SoObjects/SOGo/LDAPSource.m +++ b/SoObjects/SOGo/LDAPSource.m @@ -146,6 +146,8 @@ static int sizeLimit; IDField = @"cn"; /* the first part of a user DN */ CNField = @"cn"; UIDField = @"uid"; + mailFields = [NSArray arrayWithObject: @"mail"]; + [mailFields retain]; bindFields = nil; ldapConnection = nil; @@ -164,6 +166,7 @@ static int sizeLimit; [IDField release]; [CNField release]; [UIDField release]; + [mailFields release]; [bindFields release]; [ldapConnection release]; [sourceID release]; @@ -183,7 +186,8 @@ static int sizeLimit; [self setBaseDN: [udSource objectForKey: @"baseDN"] IDField: [udSource objectForKey: @"IDFieldName"] CNField: [udSource objectForKey: @"CNFieldName"] - UIDField: [udSource objectForKey: @"UIDFieldName"] + UIDField: [udSource objectForKey: @"UIDFieldName"] + mailFields: [udSource objectForKey: @"MailFieldNames"] andBindFields: [udSource objectForKey: @"bindFields"]]; return self; @@ -205,6 +209,7 @@ static int sizeLimit; IDField: (NSString *) newIDField CNField: (NSString *) newCNField UIDField: (NSString *) newUIDField + mailFields: (NSArray *) newMailFields andBindFields: (NSString *) newBindFields { ASSIGN (baseDN, newBaseDN); @@ -214,6 +219,8 @@ static int sizeLimit; ASSIGN (CNField, newCNField); if (UIDField) ASSIGN (UIDField, newUIDField); + if (newMailFields) + ASSIGN (mailFields, newMailFields); if (newBindFields) ASSIGN (bindFields, newBindFields); } @@ -346,8 +353,6 @@ static int sizeLimit; - (NSArray *) _searchAttributes { - NSArray *attrs; - if (!searchAttributes) { searchAttributes = [NSMutableArray new]; @@ -355,15 +360,10 @@ static int sizeLimit; [searchAttributes addObject: CNField]; if (UIDField) [searchAttributes addObject: UIDField]; + [searchAttributes addObjectsFromArray: mailFields]; [searchAttributes addObjectsFromArray: commonSearchFields]; } - // We also include our MailFieldNames in the search - if ((attrs = [[[LDAPUserManager sharedUserManager] metadataForSourceID: sourceID] objectForKey: @"MailFieldNames"])) - { - [searchAttributes addObjectsFromArray: attrs]; - } - return searchAttributes; } @@ -397,6 +397,25 @@ static int sizeLimit; return ids; } +- (void) _fillEmailsOfEntry: (NGLdapEntry *) ldapEntry + intoContactEntry: (NSMutableDictionary *) contactEntry +{ + NSEnumerator *emailFields; + NSString *currentFieldName, *value; + NSMutableArray *emails; + + emails = [NSMutableArray new]; + emailFields = [mailFields objectEnumerator]; + while ((currentFieldName = [emailFields nextObject])) + { + value = [[ldapEntry attributeWithName: currentFieldName] stringValueAtIndex: 0]; + if (value) + [emails addObject: value]; + } + [emails autorelease]; + [contactEntry setObject: emails forKey: @"c_emails"]; +} + - (NSDictionary *) _convertLDAPEntryToContact: (NGLdapEntry *) ldapEntry { NSMutableDictionary *contactEntry; @@ -426,6 +445,7 @@ static int sizeLimit; if (!value) value = @""; [contactEntry setObject: value forKey: @"c_cn"]; + [self _fillEmailsOfEntry: ldapEntry intoContactEntry: contactEntry]; return contactEntry; } diff --git a/SoObjects/SOGo/LDAPUserManager.m b/SoObjects/SOGo/LDAPUserManager.m index be9ed79dc..6dcdd3afa 100644 --- a/SoObjects/SOGo/LDAPUserManager.m +++ b/SoObjects/SOGo/LDAPUserManager.m @@ -27,6 +27,7 @@ #import #import +#import "NSArray+Utilities.h" #import "LDAPSource.h" #import "LDAPUserManager.h" @@ -289,9 +290,7 @@ static NSString *defaultMailDomain = nil; emails = [contact objectForKey: @"emails"]; uid = [contact objectForKey: @"c_uid"]; systemEmail = [NSString stringWithFormat: @"%@@%@", uid, defaultMailDomain]; - if ([emails containsObject: systemEmail]) - [emails removeObject: systemEmail]; - [emails addObject: systemEmail]; + [emails addObjectUniquely: systemEmail]; [contact setObject: [emails objectAtIndex: 0] forKey: @"c_email"]; } @@ -302,8 +301,8 @@ static NSString *defaultMailDomain = nil; NSDictionary *userEntry; NSEnumerator *ldapSources; LDAPSource *currentSource; - NSString *cn, *email, *c_uid; - NSArray *attrs; + NSString *cn, *c_uid; + NSArray *c_emails; emails = [NSMutableArray array]; cn = nil; @@ -320,18 +319,9 @@ static NSString *defaultMailDomain = nil; cn = [userEntry objectForKey: @"c_cn"]; if (!c_uid) c_uid = [userEntry objectForKey: @"c_uid"]; - - if ((attrs = [[sourcesMetadata objectForKey: [currentSource sourceID]] objectForKey: @"MailFieldNames"])) - { - int i; - - for (i = 0; i < [attrs count]; i++) - { - email = [userEntry objectForKey: [attrs objectAtIndex: i]]; - if (email && ![emails containsObject: email]) - [emails addObject: email]; - } - } + c_emails = [userEntry objectForKey: @"c_emails"]; + if ([c_emails count]) + [emails addObjectsFromArray: c_emails]; } currentSource = [ldapSources nextObject]; } diff --git a/SoObjects/SOGo/SOGoContentObject.h b/SoObjects/SOGo/SOGoContentObject.h index 4fc2c7a8b..807b98745 100644 --- a/SoObjects/SOGo/SOGoContentObject.h +++ b/SoObjects/SOGo/SOGoContentObject.h @@ -48,7 +48,6 @@ /* content */ - (BOOL) isNew; -- (void) setContentString: (NSString *) newContent; - (NSString *) contentAsString; - (NSException *) saveContentString: (NSString *) _str baseVersion: (unsigned int) _baseVersion; @@ -65,4 +64,10 @@ @end +@interface SOGoContentObject (OptionalMethods) + +- (void) prepareDelete; + +@end + #endif /* __SOGo_SOGoContentObject_H__ */ diff --git a/SoObjects/SOGo/SOGoContentObject.m b/SoObjects/SOGo/SOGoContentObject.m index 7e43dbbbb..e45b458f7 100644 --- a/SoObjects/SOGo/SOGoContentObject.m +++ b/SoObjects/SOGo/SOGoContentObject.m @@ -138,11 +138,6 @@ return content; } -- (void) setContentString: (NSString *) newContent -{ - ASSIGN (content, newContent); -} - - (NSException *) saveContentString: (NSString *) newContent baseVersion: (unsigned int) newBaseVersion { @@ -152,11 +147,12 @@ ex = nil; - [self setContentString: newContent]; + ASSIGN (content, newContent); + folder = [container ocsFolder]; if (folder) { - ex = [folder writeContent: content toName: nameInContainer + ex = [folder writeContent: newContent toName: nameInContainer baseVersion: newBaseVersion]; if (ex) [self errorWithFormat:@"write failed: %@", ex]; diff --git a/SoObjects/SOGo/SOGoGCSFolder.m b/SoObjects/SOGo/SOGoGCSFolder.m index 5b938f091..9322f247f 100644 --- a/SoObjects/SOGo/SOGoGCSFolder.m +++ b/SoObjects/SOGo/SOGoGCSFolder.m @@ -350,7 +350,11 @@ static NSString *defaultUserID = @""; deleteObject = [self lookupName: currentID inContext: context acquire: NO]; if (![deleteObject isKindOfClass: [NSException class]]) - [deleteObject delete]; + { + if ([deleteObject respondsToSelector: @selector (prepareDelete)]) + [deleteObject prepareDelete]; + [deleteObject delete]; + } } } diff --git a/SoObjects/SOGo/SOGoUser.h b/SoObjects/SOGo/SOGoUser.h index 10b9f0fd4..64e1f8a03 100644 --- a/SoObjects/SOGo/SOGoUser.h +++ b/SoObjects/SOGo/SOGoUser.h @@ -34,17 +34,19 @@ context.activeUser */ -@class NSString; @class NSArray; @class NSDictionary; +@class NSString; +@class NSTimeZone; @class NSURL; @class NSUserDefaults; -@class NSTimeZone; + @class WOContext; + @class SOGoAppointmentFolder; @class SOGoAppointmentFolders; @class SOGoDateFormatter; -@class WOContext; +@class SOGoUserFolder; extern NSString *SOGoWeekStartHideWeekNumbers; extern NSString *SOGoWeekStartJanuary1; @@ -68,6 +70,7 @@ extern NSString *SOGoWeekStartFirstFullWeek; NSTimeZone *userTimeZone; SOGoDateFormatter *dateFormatter; NSMutableArray *mailAccounts; + SOGoUserFolder *homeFolder; } + (NSString *) language; @@ -119,7 +122,7 @@ extern NSString *SOGoWeekStartFirstFullWeek; /* folders */ -- (id) homeFolderInContext: (id) _ctx; +- (SOGoUserFolder *) homeFolderInContext: (id) context; - (SOGoAppointmentFolders *) calendarsFolderInContext: (WOContext *) context; - (SOGoAppointmentFolder *) diff --git a/SoObjects/SOGo/SOGoUser.m b/SoObjects/SOGo/SOGoUser.m index a8ebbc40f..95f32ac96 100644 --- a/SoObjects/SOGo/SOGoUser.m +++ b/SoObjects/SOGo/SOGoUser.m @@ -142,6 +142,7 @@ NSString *SOGoWeekStartFirstFullWeek = @"FirstFullWeek"; language = nil; currentPassword = nil; dateFormatter = nil; + homeFolder = nil; } return self; @@ -176,6 +177,7 @@ NSString *SOGoWeekStartFirstFullWeek = @"FirstFullWeek"; [allEmails release]; [language release]; [dateFormatter release]; + [homeFolder release]; [super dealloc]; } @@ -528,27 +530,17 @@ NSString *SOGoWeekStartFirstFullWeek = @"FirstFullWeek"; // TODO: those methods should check whether the traversal stack in the context // already contains proper folders to improve caching behaviour -- (id) homeFolderInContext: (id) _ctx +- (SOGoUserFolder *) homeFolderInContext: (id) context { - /* Note: watch out for cyclic references */ - // TODO: maybe we should add an [activeUser reset] method to SOPE - id folder; - - folder = [(WOContext *)_ctx objectForKey:@"ActiveUserHomeFolder"]; - if (folder != nil) - return [folder isNotNull] ? folder : nil; - - folder = [[WOApplication application] lookupName: [self login] - inContext: _ctx - acquire: NO]; - if ([folder isKindOfClass:[NSException class]]) - return folder; - - [(WOContext *)_ctx setObject: ((folder) - ? folder - : (id)[NSNull null]) - forKey: @"ActiveUserHomeFolder"]; - return folder; + if (!homeFolder) + { + homeFolder = [[WOApplication application] lookupName: [self login] + inContext: context + acquire: NO]; + [homeFolder retain]; + } + + return homeFolder; } - (SOGoAppointmentFolders *) calendarsFolderInContext: (WOContext *) context diff --git a/UI/Common/GNUmakefile b/UI/Common/GNUmakefile index 873df3e60..d9530e530 100644 --- a/UI/Common/GNUmakefile +++ b/UI/Common/GNUmakefile @@ -11,16 +11,12 @@ CommonUI_LANGUAGES = English French German CommonUI_OBJC_FILES += \ CommonUIProduct.m \ UIxPageFrame.m \ - UIxPrintPageFrame.m \ - UIxAppNavView.m \ \ UIxAclEditor.m \ UIxObjectActions.m \ UIxFolderActions.m \ UIxParentFolderActions.m \ UIxElemBuilder.m \ - UIxTabView.m \ - UIxTabItem.m \ UIxUserRightsEditor.m \ \ UIxToolbar.m \ diff --git a/UI/Common/UIxPageFrame.h b/UI/Common/UIxPageFrame.h index 1c3094a56..d87126f23 100644 --- a/UI/Common/UIxPageFrame.h +++ b/UI/Common/UIxPageFrame.h @@ -40,6 +40,7 @@ NSString *toolbar; id item; BOOL isPopup; + NSMutableArray *additionalCSSFiles; NSMutableArray *additionalJSFiles; } diff --git a/UI/Common/UIxPageFrame.m b/UI/Common/UIxPageFrame.m index efda984ef..9aaa5df61 100644 --- a/UI/Common/UIxPageFrame.m +++ b/UI/Common/UIxPageFrame.m @@ -241,6 +241,29 @@ return ([[self productJavaScriptURL] length] > 0); } +- (void) setCssFiles: (NSString *) newCSSFiles +{ + NSEnumerator *cssFiles; + NSString *currentFile, *filename; + + [additionalCSSFiles release]; + additionalCSSFiles = [NSMutableArray new]; + + cssFiles + = [[newCSSFiles componentsSeparatedByString: @","] objectEnumerator]; + while ((currentFile = [cssFiles nextObject])) + { + filename = [self urlForResourceFilename: + [currentFile stringByTrimmingSpaces]]; + [additionalCSSFiles addObject: filename]; + } +} + +- (NSArray *) additionalCSSFiles +{ + return additionalCSSFiles; +} + - (void) setJsFiles: (NSString *) newJSFiles { NSEnumerator *jsFiles; diff --git a/UI/MailPartViewers/English.lproj/Localizable.strings b/UI/MailPartViewers/English.lproj/Localizable.strings index 5fadd32d1..78f7a101c 100644 --- a/UI/MailPartViewers/English.lproj/Localizable.strings +++ b/UI/MailPartViewers/English.lproj/Localizable.strings @@ -19,6 +19,7 @@ Attendees = "Attendees"; request_info = "invites you to participate in a meeting."; "Add to calendar" = "Add to calendar"; "Delete from calendar" = "Delete from calendar"; +"Update status" = "Update status"; Accept = "Accept"; Decline = "Decline"; Tentative = "Tentative"; diff --git a/UI/MailPartViewers/French.lproj/Localizable.strings b/UI/MailPartViewers/French.lproj/Localizable.strings index b5db9cecc..e12683523 100644 --- a/UI/MailPartViewers/French.lproj/Localizable.strings +++ b/UI/MailPartViewers/French.lproj/Localizable.strings @@ -19,6 +19,7 @@ Attendees = "Invités"; request_info = "vous invite à une réunion."; "Add to calendar" = "Ajouter à l'agenda"; "Delete from calendar" = "Effacer de l'agenda"; +"Update status" = "Intégrer les modifications"; Accept = "Accepter"; Decline = "Decliner"; Tentative = "Tentative"; diff --git a/UI/MailPartViewers/German.lproj/Localizable.strings b/UI/MailPartViewers/German.lproj/Localizable.strings index b96425ebc..0dbc78445 100644 --- a/UI/MailPartViewers/German.lproj/Localizable.strings +++ b/UI/MailPartViewers/German.lproj/Localizable.strings @@ -19,6 +19,7 @@ Attendees = "Attendees"; request_info = "invites you to participate in a meeting."; "Add to calendar" = "Add to calendar"; "Delete from calendar" = "Delete from calendar"; +"Update status" = "Update status"; Accept = "Accept"; Decline = "Decline"; Tentative = "Tentative"; diff --git a/UI/MailPartViewers/UIxMailPartICalActions.m b/UI/MailPartViewers/UIxMailPartICalActions.m index c4c8cb78b..e51f518cf 100644 --- a/UI/MailPartViewers/UIxMailPartICalActions.m +++ b/UI/MailPartViewers/UIxMailPartICalActions.m @@ -26,12 +26,17 @@ #import #import +#import #import #import +#import + +#import #import #import +#import #import #import #import @@ -58,12 +63,12 @@ } - (SOGoAppointmentObject *) _eventObjectWithUID: (NSString *) uid + forUser: (SOGoUser *) user { SOGoAppointmentFolder *personalFolder; SOGoAppointmentObject *eventObject; - personalFolder - = [[context activeUser] personalCalendarFolderInContext: context]; + personalFolder = [user personalCalendarFolderInContext: context]; eventObject = [personalFolder lookupName: uid inContext: context acquire: NO]; if (![eventObject isKindOfClass: [SOGoAppointmentObject class]]) @@ -73,6 +78,11 @@ return eventObject; } +- (SOGoAppointmentObject *) _eventObjectWithUID: (NSString *) uid +{ + return [self _eventObjectWithUID: uid forUser: [context activeUser]]; +} + - (iCalEvent *) _setupChosenEventAndEventObject: (SOGoAppointmentObject **) eventObject { @@ -86,7 +96,7 @@ chosenEvent = emailEvent; else { - calendarEvent = (iCalEvent *) [*eventObject component: NO]; + calendarEvent = (iCalEvent *) [*eventObject component: NO secure: NO]; if ([calendarEvent compare: emailEvent] == NSOrderedAscending) chosenEvent = emailEvent; else @@ -99,14 +109,42 @@ return chosenEvent; } +#warning this is code copied from SOGoAppointmentObject... +- (void) _updateAttendee: (iCalPerson *) attendee + withSequence: (NSNumber *) sequence + andCalUID: (NSString *) calUID + forUID: (NSString *) uid +{ + SOGoAppointmentObject *eventObject; + iCalEvent *event; + iCalPerson *otherAttendee; + NSString *iCalString; + + eventObject = [self _eventObjectWithUID: calUID + forUser: [SOGoUser userWithLogin: uid roles: nil]]; + if (![eventObject isNew]) + { + event = [eventObject component: NO secure: NO]; + if ([[event sequence] compare: sequence] + == NSOrderedSame) + { + otherAttendee + = [event findParticipantWithEmail: [attendee rfc822Email]]; + [otherAttendee setPartStat: [attendee partStat]]; + iCalString = [[event parent] versitString]; + [eventObject saveContentString: iCalString]; + } + } +} + - (WOResponse *) _changePartStatusAction: (NSString *) newStatus { WOResponse *response; SOGoAppointmentObject *eventObject; iCalEvent *chosenEvent; iCalPerson *user; - iCalCalendar *calendar; - NSString *rsvp, *method; + iCalCalendar *emailCalendar, *calendar; + NSString *rsvp, *method, *organizerUID; chosenEvent = [self _setupChosenEventAndEventObject: &eventObject]; if (chosenEvent) @@ -114,7 +152,8 @@ user = [chosenEvent findParticipant: [context activeUser]]; [user setPartStat: newStatus]; calendar = [chosenEvent parent]; - method = [[calendar method] lowercaseString]; + emailCalendar = [[self _emailEvent] parent]; + method = [[emailCalendar method] lowercaseString]; if ([method isEqualToString: @"request"]) { [calendar setMethod: @""]; @@ -123,8 +162,12 @@ else rsvp = nil; [eventObject saveContentString: [calendar versitString]]; - if (rsvp && [rsvp isEqualToString: @"true"]) + if ([rsvp isEqualToString: @"true"]) [eventObject sendResponseToOrganizer]; + organizerUID = [[chosenEvent organizer] uid]; + if (organizerUID) + [self _updateAttendee: user withSequence: [chosenEvent sequence] + andCalUID: [chosenEvent uid] forUID: organizerUID]; response = [self responseWith204]; } else @@ -146,6 +189,97 @@ return [self _changePartStatusAction: @"DECLINED"]; } +- (WOResponse *) deleteFromCalendarAction +{ + iCalEvent *emailEvent; + SOGoAppointmentObject *eventObject; + WOResponse *response; + + emailEvent = [self _emailEvent]; + if (emailEvent) + { + eventObject = [self _eventObjectWithUID: [emailEvent uid]]; + response = [self responseWith204]; + } + else + { + response = [context response]; + [response setStatus: 409]; + } + + return response; +} + +- (iCalPerson *) _emailParticipantWithEvent: (iCalEvent *) event +{ + NSString *emailFrom; + SOGoMailObject *mailObject; + NGImap4EnvelopeAddress *address; + + mailObject = [[self clientObject] mailObject]; + address = [[mailObject fromEnvelopeAddresses] objectAtIndex: 0]; + emailFrom = [address baseEMail]; + + return [event findParticipantWithEmail: emailFrom]; +} + +- (BOOL) _updateParticipantStatusInEvent: (iCalEvent *) calendarEvent + fromEvent: (iCalEvent *) emailEvent + inObject: (SOGoAppointmentObject *) eventObject +{ + iCalPerson *calendarParticipant, *mailParticipant; + NSString *partStat; + BOOL result; + + calendarParticipant = [self _emailParticipantWithEvent: calendarEvent]; + mailParticipant = [self _emailParticipantWithEvent: emailEvent]; + if (calendarParticipant && mailParticipant) + { + result = YES; + partStat = [mailParticipant partStat]; + if ([partStat caseInsensitiveCompare: [calendarParticipant partStat]] + != NSOrderedSame) + { + [calendarParticipant setPartStat: [partStat uppercaseString]]; + [eventObject saveComponent: calendarEvent]; + } + } + else + result = NO; + + return result; +} + +- (WOResponse *) updateUserStatusAction +{ + iCalEvent *emailEvent, *calendarEvent; + SOGoAppointmentObject *eventObject; + WOResponse *response; + + response = nil; + + emailEvent = [self _emailEvent]; + if (emailEvent) + { + eventObject = [self _eventObjectWithUID: [emailEvent uid]]; + calendarEvent = [eventObject component: NO secure: NO]; + if (([[emailEvent sequence] compare: [calendarEvent sequence]] + != NSOrderedDescending) + && ([self _updateParticipantStatusInEvent: calendarEvent + fromEvent: emailEvent + inObject: eventObject])) + response = [self responseWith204]; + } + + if (!response) + { + response = [context response]; + [response setStatus: 409]; + } + + return response; +} + // - (WOResponse *) markTentativeAction // { // return [self _changePartStatusAction: @"TENTATIVE"]; diff --git a/UI/MailPartViewers/UIxMailPartICalViewer.h b/UI/MailPartViewers/UIxMailPartICalViewer.h index 98d75abe4..0728008bc 100644 --- a/UI/MailPartViewers/UIxMailPartICalViewer.h +++ b/UI/MailPartViewers/UIxMailPartICalViewer.h @@ -24,10 +24,12 @@ #import "UIxMailPartViewer.h" -@class SOGoDateFormatter; @class iCalEvent; @class iCalCalendar; +@class SOGoAppointmentObject; +@class SOGoDateFormatter; + @interface UIxMailPartICalViewer : UIxMailPartViewer { iCalCalendar *inCalendar; @@ -35,7 +37,7 @@ id attendee; SOGoDateFormatter *dateFormatter; id item; - id storedEventObject; + SOGoAppointmentObject *storedEventObject; iCalEvent *storedEvent; } diff --git a/UI/MailPartViewers/UIxMailPartICalViewer.m b/UI/MailPartViewers/UIxMailPartICalViewer.m index 56df0e45e..b30a88174 100644 --- a/UI/MailPartViewers/UIxMailPartICalViewer.m +++ b/UI/MailPartViewers/UIxMailPartICalViewer.m @@ -28,7 +28,6 @@ #import #import -#import #import #import @@ -40,9 +39,11 @@ #import #import +#import #import #import #import +#import #import "UIxMailPartICalViewer.h" @@ -109,35 +110,32 @@ - (BOOL) couldParseCalendar { - return [[self inCalendar] isNotNull]; + return (([self inCalendar])); } - (iCalEvent *) inEvent { NSArray *events; - if (inEvent) - return [inEvent isNotNull] ? inEvent : nil; - - events = [[self inCalendar] events]; - if ([events count] > 0) { - inEvent = [[events objectAtIndex:0] retain]; - return inEvent; - } - else { - inEvent = [[NSNull null] retain]; - return nil; - } + if (!inEvent) + { + events = [[self inCalendar] events]; + if ([events count] > 0) + inEvent = [[events objectAtIndex:0] retain]; + } + + return inEvent; } /* formatters */ - (SOGoDateFormatter *) dateFormatter { - if (dateFormatter == nil) { - dateFormatter = [[context activeUser] dateFormatterInContext: context]; - [dateFormatter retain]; - } + if (!dateFormatter) + { + dateFormatter = [[context activeUser] dateFormatterInContext: context]; + [dateFormatter retain]; + } return dateFormatter; } @@ -146,7 +144,7 @@ - (void) setAttendee: (id) _attendee { - ASSIGN(attendee, _attendee); + ASSIGN (attendee, _attendee); } - (id) attendee @@ -220,7 +218,7 @@ /* calendar folder support */ -- (id) calendarFolder +- (SOGoAppointmentFolder *) calendarFolder { /* return scheduling calendar of currently logged-in user */ SOGoUser *user; @@ -234,49 +232,50 @@ return [folder lookupName: @"personal" inContext: context acquire: NO]; } -- (id) storedEventObject +- (SOGoAppointmentObject *) storedEventObject { /* lookup object in the users Calendar */ - id calendar; + SOGoAppointmentFolder *calendar; + NSString *filename; - if (storedEventObject) - return [storedEventObject isNotNull] ? storedEventObject : nil; - - calendar = [self calendarFolder]; - if ([calendar isKindOfClass:[NSException class]]) { - [self errorWithFormat:@"Did not find Calendar folder: %@", calendar]; - } - else { - NSString *filename; - - filename = [calendar resourceNameForEventUID:[[self inEvent] uid]]; - if (filename) { - // TODO: When we get an exception, this might be an auth issue meaning - // that the UID indeed exists but that the user has no access to - // the object. - // Of course this is quite unusual for the private calendar though. - id tmp; - - tmp = [calendar lookupName:filename inContext:[self context] acquire:NO]; - if ([tmp isNotNull] && ![tmp isKindOfClass:[NSException class]]) - storedEventObject = [tmp retain]; + if (!storedEventObject) + { + calendar = [self calendarFolder]; + if ([calendar isKindOfClass: [NSException class]]) + [self errorWithFormat:@"Did not find Calendar folder: %@", calendar]; + else + { + filename = [calendar resourceNameForEventUID:[[self inEvent] uid]]; + if (filename) + { + storedEventObject = [calendar lookupName: filename + inContext: [self context] + acquire: NO]; + if ([storedEventObject isKindOfClass: [NSException class]]) + storedEventObject = nil; + else + [storedEventObject retain]; + } + } } - } - - if (storedEventObject == nil) - storedEventObject = [[NSNull null] retain]; return storedEventObject; } - (BOOL) isEventStoredInCalendar { - return [[self storedEventObject] isNotNull]; + return (([self storedEventObject])); } - (iCalEvent *) storedEvent { - return (iCalEvent *) [(SOGoAppointmentObject *)[self storedEventObject] component: NO]; + if (!storedEvent) + { + storedEvent = [[self storedEventObject] component: NO secure: NO]; + [storedEvent retain]; + } + + return storedEvent; } /* organizer tracking */ @@ -294,34 +293,24 @@ { iCalEvent *authorativeEvent; - if ([[self storedEvent] compare: [self inEvent]] - == NSOrderedAscending) + [self storedEvent]; + if (!storedEvent + || ([storedEvent compare: [self inEvent]] == NSOrderedAscending)) authorativeEvent = inEvent; else - authorativeEvent = storedEventObject; + authorativeEvent = [self storedEvent]; return authorativeEvent; } - (BOOL) isLoggedInUserTheOrganizer { - iCalPerson *organizer; - - organizer = [[self authorativeEvent] organizer]; - - return [[context activeUser] hasEmail: [organizer rfc822Email]]; + return [[self authorativeEvent] userIsOrganizer: [context activeUser]]; } - (BOOL) isLoggedInUserAnAttendee { - NSString *loginEMail; - - if ((loginEMail = [self loggedInUserEMail]) == nil) { - [self warnWithFormat:@"Could not determine email of logged in user?"]; - return NO; - } - - return [[self authorativeEvent] isParticipant:loginEMail]; + return [[self authorativeEvent] userIsParticipant: [context activeUser]]; } /* derived fields */ @@ -405,7 +394,34 @@ - (BOOL) isReplySenderAnAttendee { - return [[self storedReplyAttendee] isNotNull]; + return (([self storedReplyAttendee])); +} + +- (iCalPerson *) _emailParticipantWithEvent: (iCalEvent *) event +{ + NSString *emailFrom; + SOGoMailObject *mailObject; + NGImap4EnvelopeAddress *address; + + mailObject = [[self clientObject] mailObject]; + address = [[mailObject fromEnvelopeAddresses] objectAtIndex: 0]; + emailFrom = [address baseEMail]; + + return [event findParticipantWithEmail: emailFrom]; +} + +- (BOOL) hasSenderStatusChanged +{ + iCalPerson *emailParticipant, *calendarParticipant; + + [self inEvent]; + [self storedEvent]; + emailParticipant = [self _emailParticipantWithEvent: inEvent]; + calendarParticipant = [self _emailParticipantWithEvent: storedEvent]; + + return ([[emailParticipant partStat] + caseInsensitiveCompare: [calendarParticipant partStat]] + != NSOrderedSame); } @end /* UIxMailPartICalViewer */ diff --git a/UI/MailPartViewers/product.plist b/UI/MailPartViewers/product.plist index 31e8b466b..ddf40447a 100644 --- a/UI/MailPartViewers/product.plist +++ b/UI/MailPartViewers/product.plist @@ -20,6 +20,11 @@ actionClass = "UIxMailPartICalActions"; actionName = "decline"; }; + updateUserStatus = { + protectedBy = "View"; + actionClass = "UIxMailPartICalActions"; + actionName = "updateUserStatus"; + }; /* tentative = { protectedBy = "View"; actionClass = "UIxMailPartICalAction"; @@ -29,12 +34,12 @@ protectedBy = "View"; actionClass = "UIxMailPartICalAction"; actionName = "addToCalendar"; - }; + }; */ deleteFromCalendar = { protectedBy = "View"; actionClass = "UIxMailPartICalAction"; actionName = "deleteFromCalendar"; - }; */ + }; }; }; }; diff --git a/UI/Scheduler/UIxAppointmentEditor.m b/UI/Scheduler/UIxAppointmentEditor.m index 444e345bb..be23c9363 100644 --- a/UI/Scheduler/UIxAppointmentEditor.m +++ b/UI/Scheduler/UIxAppointmentEditor.m @@ -264,7 +264,7 @@ unsigned int minutes; iCalRecurrenceRule *rule; - event = (iCalEvent *) [[self clientObject] component: NO]; + event = (iCalEvent *) [[self clientObject] component: NO secure: YES]; if (event) { startDate = [event startDate]; @@ -352,16 +352,7 @@ - (id ) saveAction { - SOGoAppointmentObject *clientObject; - NSString *iCalString; - - clientObject = [self clientObject]; - NSLog(@"saveAction, clientObject = %@", clientObject); - - iCalString = [[clientObject calendar: NO] versitString]; - - NSLog(@"saveAction, iCalString = %@", iCalString); - [clientObject saveContentString: iCalString]; + [[self clientObject] saveComponent: event]; return [self jsCloseWithRefreshMethod: @"refreshEventsAndDisplay()"]; } @@ -385,7 +376,7 @@ iCalRecurrenceRule *rule; clientObject = [self clientObject]; - event = (iCalEvent *) [clientObject component: YES]; + event = (iCalEvent *) [clientObject component: YES secure: NO]; [super takeValuesFromRequest: _rq inContext: _ctx]; @@ -443,23 +434,18 @@ // TODO: add tentatively -- (id) acceptOrDeclineAction: (BOOL) accept +- (id) acceptAction { - [[self clientObject] changeParticipationStatus: (accept - ? @"ACCEPTED" - : @"DECLINED")]; + [[self clientObject] changeParticipationStatus: @"ACCEPTED"]; return self; } -- (id) acceptAction -{ - return [self acceptOrDeclineAction: YES]; -} - - (id) declineAction { - return [self acceptOrDeclineAction: NO]; + [[self clientObject] changeParticipationStatus: @"DECLINED"]; + + return self; } @end diff --git a/UI/Scheduler/UIxComponentEditor.m b/UI/Scheduler/UIxComponentEditor.m index 2cc2db436..04e4ee1af 100644 --- a/UI/Scheduler/UIxComponentEditor.m +++ b/UI/Scheduler/UIxComponentEditor.m @@ -41,6 +41,7 @@ #import #import +#import #import #import #import @@ -245,6 +246,16 @@ return url; } +- (BOOL) hasOrganizer +{ + return (![organizer isVoid]); +} + +- (NSString *) organizerName +{ + return [organizer mailAddress]; +} + - (void) setAttendeesNames: (NSString *) newAttendeesNames { ASSIGN (attendeesNames, newAttendeesNames); @@ -694,29 +705,8 @@ respondsToSelector: @selector(saveContentString:)]; } -- (BOOL) containsConflict: (id) _component -{ - [self subclassResponsibility: _cmd]; - - return NO; -} - /* access */ -#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 - - (BOOL) isMyComponent { return ([[context activeUser] hasEmail: [organizer rfc822Email]]); @@ -845,6 +835,7 @@ [currentAttendee setCn: [names objectAtIndex: count]]; [currentAttendee setEmail: currentEmail]; [currentAttendee setRole: @"REQ-PARTICIPANT"]; + [currentAttendee setRsvp: @"TRUE"]; [currentAttendee setParticipationStatus: iCalPersonPartStatNeedsAction]; } diff --git a/UI/Scheduler/UIxTaskEditor.m b/UI/Scheduler/UIxTaskEditor.m index 3f6054829..41c39fc99 100644 --- a/UI/Scheduler/UIxTaskEditor.m +++ b/UI/Scheduler/UIxTaskEditor.m @@ -282,7 +282,7 @@ NSString *duration; unsigned int minutes; - todo = (iCalToDo *) [[self clientObject] component: NO]; + todo = (iCalToDo *) [[self clientObject] component: NO secure: YES]; if (todo) { startDate = [todo startDate]; @@ -345,16 +345,23 @@ - (id ) saveAction { - SOGoTaskObject *clientObject; - NSString *iCalString; - - clientObject = [self clientObject]; - iCalString = [[clientObject calendar: NO] versitString]; - [clientObject saveContentString: iCalString]; + [[self clientObject] saveComponent: todo]; return [self jsCloseWithRefreshMethod: @"refreshTasks()"]; } +// - (id ) saveAction +// { +// SOGoTaskObject *clientObject; +// NSString *iCalString; + +// clientObject = [self clientObject]; +// iCalString = [[clientObject calendar: NO secure: NO] versitString]; +// [clientObject saveContentString: iCalString]; + +// return [self jsCloseWithRefreshMethod: @"refreshTasks()"]; +// } + - (BOOL) shouldTakeValuesFromRequest: (WORequest *) request inContext: (WOContext*) context { @@ -372,7 +379,7 @@ SOGoTaskObject *clientObject; clientObject = [self clientObject]; - todo = (iCalToDo *) [clientObject component: YES]; + todo = (iCalToDo *) [clientObject component: YES secure: NO]; [super takeValuesFromRequest: _rq inContext: _ctx]; @@ -428,7 +435,7 @@ NSString *newStatus, *iCalString; clientObject = [self clientObject]; - todo = (iCalToDo *) [clientObject component: NO]; + todo = (iCalToDo *) [clientObject component: NO secure: NO]; if (todo) { newStatus = [self queryParameterForKey: @"status"]; @@ -441,7 +448,7 @@ [todo setStatus: @"IN-PROCESS"]; } - iCalString = [[clientObject calendar: NO] versitString]; + iCalString = [[clientObject calendar: NO secure: NO] versitString]; [clientObject saveContentString: iCalString]; } diff --git a/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox b/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox index 87b072dab..948ee3a67 100644 --- a/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox +++ b/UI/Templates/MailPartViewers/UIxMailPartICalViewer.wox @@ -9,6 +9,9 @@ > + +
Parsing Error @@ -33,23 +36,24 @@ - + -

- - - - - - | - -

+ +

+ + + + + | + +

+

@@ -70,20 +74,20 @@ - +

-

- -

- +

+ +

+

@@ -96,7 +100,7 @@ - + @@ -113,13 +117,13 @@ - +

- +

@@ -145,7 +149,6 @@ -->
-
diff --git a/UI/Templates/SchedulerUI/UIxComponentEditor.wox b/UI/Templates/SchedulerUI/UIxComponentEditor.wox index ec1610904..5a4d70428 100644 --- a/UI/Templates/SchedulerUI/UIxComponentEditor.wox +++ b/UI/Templates/SchedulerUI/UIxComponentEditor.wox @@ -11,6 +11,7 @@ const:popup="YES" title="name" var:toolbar="toolbar" + const:cssFiles="UIxComponentEditor.css" const:jsFiles="skycalendar.js,UIxComponentEditor.js">