From 12135039e8259dcfde7c4be1fdb1af787e06fe87 Mon Sep 17 00:00:00 2001 From: wolfgang Date: Fri, 14 Sep 2007 22:04:17 +0000 Subject: [PATCH] git-svn-id: http://svn.opengroupware.org/SOGo/inverse/trunk@1173 d1b88da0-ebda-0310-925b-ed51d893ca5b --- ChangeLog | 76 +++++ NEWS | 2 + .../English.lproj/Localizable.strings | 1 + .../French.lproj/Localizable.strings | 1 + SoObjects/Appointments/GNUmakefile | 5 +- .../German.lproj/Localizable.strings | 1 + .../Appointments/SOGoAppointmentFolder.h | 4 +- .../Appointments/SOGoAppointmentFolder.m | 204 +++++------- .../Appointments/SOGoAppointmentObject.h | 7 - .../Appointments/SOGoAppointmentObject.m | 20 +- SoObjects/Appointments/SOGoTaskObject.h | 7 - SoObjects/Appointments/SOGoTaskObject.m | 16 +- SoObjects/Appointments/product.plist | 3 + .../English.lproj/Localizable.strings | 1 + .../Contacts/French.lproj/Localizable.strings | 1 + .../Contacts/German.lproj/Localizable.strings | 1 + SoObjects/Contacts/SOGoContactFolder.h | 10 - SoObjects/Contacts/SOGoContactFolders.h | 22 +- SoObjects/Contacts/SOGoContactFolders.m | 211 +------------ SoObjects/Contacts/SOGoContactGCSFolder.h | 3 - SoObjects/Contacts/SOGoContactGCSFolder.m | 51 +-- SoObjects/Contacts/SOGoContactLDAPFolder.h | 9 +- SoObjects/Contacts/SOGoContactLDAPFolder.m | 19 +- SoObjects/Contacts/product.plist | 7 +- SoObjects/SOGo/GNUmakefile | 6 +- SoObjects/SOGo/NSDictionary+Utilities.h | 2 + SoObjects/SOGo/NSDictionary+Utilities.m | 20 ++ SoObjects/SOGo/SOGoContentObject.m | 6 +- SoObjects/SOGo/SOGoFolder.h | 12 +- SoObjects/SOGo/SOGoFolder.m | 180 ++++++++--- SoObjects/SOGo/SOGoObject.h | 9 +- SoObjects/SOGo/SOGoObject.m | 92 +++++- SoObjects/SOGo/SOGoParentFolder.h | 51 +++ SoObjects/SOGo/SOGoParentFolder.m | 285 +++++++++++++++++ SoObjects/SOGo/SOGoUserFolder.m | 18 +- UI/Common/English.lproj/Localizable.strings | 1 + UI/Common/French.lproj/Localizable.strings | 1 + UI/Common/GNUmakefile | 1 + UI/Common/German.lproj/Localizable.strings | 1 + UI/Common/UIxFolderActions.h | 2 +- UI/Common/UIxFolderActions.m | 129 ++++---- UI/Common/WODirectAction+SOGo.h | 2 + UI/Common/WODirectAction+SOGo.m | 19 +- UI/Common/product.plist | 199 ++++++------ UI/Contacts/English.lproj/Localizable.strings | 2 + UI/Contacts/French.lproj/Localizable.strings | 2 + UI/Contacts/German.lproj/Localizable.strings | 2 + .../Toolbars/SOGoContactFolder.toolbar | 15 +- UI/Contacts/UIxContactEditor.m | 4 +- UI/Contacts/UIxContactFoldersView.m | 152 ++++----- UI/Contacts/UIxContactsListView.m | 19 -- UI/Contacts/UIxContactsListViewContainer.h | 12 +- UI/Contacts/UIxContactsListViewContainer.m | 68 +--- UI/Contacts/product.plist | 10 - UI/MailerUI/English.lproj/Localizable.strings | 4 +- UI/MailerUI/Toolbars/SOGoDraftObject.toolbar | 12 +- UI/MailerUI/Toolbars/SOGoMailObject.toolbar | 42 ++- UI/MailerUI/UIxMailAccountActions.m | 3 +- UI/MailerUI/UIxMailActions.m | 45 ++- UI/MailerUI/UIxMailEditor.m | 5 +- UI/MailerUI/UIxMailFolderActions.m | 37 +-- UI/MailerUI/UIxMailListView.m | 85 ++--- UI/MailerUI/UIxMailSourceView.m | 5 +- UI/MailerUI/UIxMailView.m | 55 ++-- UI/MailerUI/product.plist | 20 +- UI/MainUI/SOGoRootPage.m | 105 ++----- UI/MainUI/product.plist | 8 + UI/PreferencesUI/UIxPreferences.m | 3 +- UI/SOGoUI/UIxComponent.h | 18 +- UI/SOGoUI/UIxComponent.m | 12 +- .../English.lproj/Localizable.strings | 7 +- UI/Scheduler/French.lproj/Localizable.strings | 7 +- UI/Scheduler/GNUmakefile | 2 +- UI/Scheduler/German.lproj/Localizable.strings | 7 +- ...toolbar => SOGoAppointmentFolders.toolbar} | 27 +- UI/Scheduler/UIxAppointmentEditor.m | 10 +- UI/Scheduler/UIxCalListingActions.m | 85 ++--- UI/Scheduler/UIxCalMainView.m | 10 + UI/Scheduler/UIxCalendarSelector.h | 20 +- UI/Scheduler/UIxCalendarSelector.m | 137 ++++---- UI/Scheduler/UIxComponentEditor.m | 12 +- UI/Scheduler/UIxTaskEditor.m | 10 +- UI/Scheduler/product.plist | 53 +++- .../UIxContactsListViewContainer.wox | 9 +- UI/Templates/MailerUI/UIxMailListView.wox | 1 - UI/Templates/SchedulerUI/UIxCalMainView.wox | 3 + .../SchedulerUI/UIxCalendarSelector.wox | 22 +- UI/Templates/UIxPageFrame.wox | 2 +- UI/Templates/UIxToolbar.wox | 6 +- UI/WebServerResources/ContactsUI.js | 142 +++++---- UI/WebServerResources/MailerUI.js | 20 +- UI/WebServerResources/SOGoRootPage.css | 10 +- UI/WebServerResources/SchedulerUI.js | 296 +++++++++++------- UI/WebServerResources/UIxAclEditor.js | 3 +- UI/WebServerResources/UIxAppointmentEditor.js | 16 +- UI/WebServerResources/UIxComponentEditor.js | 2 +- UI/WebServerResources/UIxContactEditor.js | 4 +- .../UIxContactsUserFolders.js | 2 +- UI/WebServerResources/UIxMailEditor.js | 3 +- UI/WebServerResources/UIxTaskEditor.js | 10 +- UI/WebServerResources/add-user-calendar.png | Bin 0 -> 1168 bytes UI/WebServerResources/generic.js | 103 ++++-- UI/WebServerResources/lori-login.jpg | Bin 11089 -> 7861 bytes 103 files changed, 1929 insertions(+), 1575 deletions(-) create mode 100644 SoObjects/Appointments/English.lproj/Localizable.strings create mode 100644 SoObjects/Appointments/French.lproj/Localizable.strings create mode 100644 SoObjects/Appointments/German.lproj/Localizable.strings create mode 100644 SoObjects/Contacts/English.lproj/Localizable.strings create mode 100644 SoObjects/Contacts/French.lproj/Localizable.strings create mode 100644 SoObjects/Contacts/German.lproj/Localizable.strings create mode 100644 SoObjects/SOGo/SOGoParentFolder.h create mode 100644 SoObjects/SOGo/SOGoParentFolder.m rename UI/Scheduler/Toolbars/{SOGoAppointmentFolder.toolbar => SOGoAppointmentFolders.toolbar} (60%) create mode 100644 UI/WebServerResources/add-user-calendar.png diff --git a/ChangeLog b/ChangeLog index 6cf4ed54..8ce905a8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,79 @@ +2007-09-14 Wolfgang Sourdeau + + * SoObjects/SOGo/SOGoObject.m ([SOGoObject -labelForKey:key]): new + method that returns translated strings for controller bundles + (same as what UIxComponent does for view bundles). + ([SOGoObject -pathArrayToSOGoObject]): new method that returns + the real path to a subscribed folder (if subscribed). + ([SOGoObject +globallyUniqueObjectId]): move method from SOGoFolder. + ([SOGoObject -globallyUniqueObjectId]): new instance method + calling its class equivalent. + +2007-09-12 Wolfgang Sourdeau + + * UI/MainUI/SOGoRootPage.m ([SOGoRootPage -defaultAction]): test + whether the user is logged in and if so, redirect to his/her + homepage. + ([SOGoRootPage -appendToResponse:inContext:]): removed useless + method. + +2007-09-11 Wolfgang Sourdeau + + * SoObjects/SOGo/SOGoFolder.m ([SOGoFolder + +folderWithName:aNameandDisplayName:aDisplayNameinContainer:aContainer]): + new method. + ([SOGoFolder -displayName]): new method. + ([SOGoFolder -delete]): accept to proceed only if nameInContainer + != "personal". + + * SoObjects/Contacts/SOGoContactLDAPFolder.m + ([SOGoContactLDAPFolder + +folderWithName:aNameandDisplayName:aDisplayNameinContainer:aContainer]): + renamed from "contactFolderWithName..." for compatibility with SOGoFolder. + + * SoObjects/Contacts/SOGoContactGCSFolder.m ([SOGoContactGCSFolder + +contactFolderWithName:aNameandDisplayName:aDisplayNameinContainer:aContainer]): + removed method, reimplemented in SOGoFolder. + ([SOGoContactGCSFolder -displayName]): removed method, + reimplemented in SOGoFolder. + ([-delete]): removed method, modified in SOGoFolder. + + * SoObjects/Contacts/SOGoContactFolders.[hm]: modified class to be + a subclass of SOGoParentFolder. + + * SoObjects/SOGo/SOGoParentFolder.[hm]: new class module derived + from SOGoContactFolders and modified to be more content-independent. + + * UI/MailerUI/UIxMailActions.m ([UIxMailActions -markMessageUnreadAction]) + ([UIxMailActions -markMessageReadAction]): new methods moved from + UIxMailListView and adapted to invoke the client object directly, + since the previous versions had to to a lookup from the parent + SOGoMailFolder. + + * UI/MailerUI/UIxMailListView.m ([-markMessageUnreadAction]): move + method into UIxMailActions. + ([-markMessageReadAction]): same as above. + ([-viewAction]): removed useless method. + ([-javaScriptOK]): removed useless method. + ([-isJavaScriptRequest]): removed useless method. + ([-lookupActiveMessage]): removed useless method. + + * UI/Common/WODirectAction+SOGo.m ([WODirectAction + -responseWithStatus:status]): new method that returns a WOResponse + initialized with the specified status code. + ([WODirectAction -responseWith204]): new method that invokes the + above one with "204" as parameter. + ([WODirectAction -redirectToLocation:newLocation]): rewrote method + to make use of -responseWithStatus:. + + * UI/SOGoUI/UIxComponent.m ([UIxComponent -responseWith204]): new + method that returns a WOResponse initialized with the 204 status + code. + + * UI/MailerUI/UIxMailListView.m ([UIxMailListView -sortedUIDs]): + always use a "not deleted" search qualifier along with the user + qualifier (if present). + 2007-09-10 Wolfgang Sourdeau * UI/Contacts/UIxContactFoldersView.m ([UIxContactFoldersView diff --git a/NEWS b/NEWS index f0c0a1b0..b5142639 100644 --- a/NEWS +++ b/NEWS @@ -4,6 +4,8 @@ - fixed a bug where a false positive happening whenever a wrong user login was given during an indirect bind; - deleting a message no longer expunges its parent folder; +- implemented support for multiple calendars; +- it is not possible to rename folders; - fixed search in message content; - countless bugfixes; diff --git a/SoObjects/Appointments/English.lproj/Localizable.strings b/SoObjects/Appointments/English.lproj/Localizable.strings new file mode 100644 index 00000000..95feda9f --- /dev/null +++ b/SoObjects/Appointments/English.lproj/Localizable.strings @@ -0,0 +1 @@ +"Personal Calendar" = "Personal Calendar"; diff --git a/SoObjects/Appointments/French.lproj/Localizable.strings b/SoObjects/Appointments/French.lproj/Localizable.strings new file mode 100644 index 00000000..11b333c2 --- /dev/null +++ b/SoObjects/Appointments/French.lproj/Localizable.strings @@ -0,0 +1 @@ +"Personal Calendar" = "Agenda personnel"; diff --git a/SoObjects/Appointments/GNUmakefile b/SoObjects/Appointments/GNUmakefile index 789501a3..0ae0a98f 100644 --- a/SoObjects/Appointments/GNUmakefile +++ b/SoObjects/Appointments/GNUmakefile @@ -6,7 +6,9 @@ WOBUNDLE_NAME = Appointments Appointments_PRINCIPAL_CLASS = SOGoAppointmentsProduct -# Appointments_LANGUAGES = English French +Appointments_LANGUAGES = English French German + +Appointments_LOCALIZED_RESOURCE_FILES=Localizable.strings Appointments_OBJC_FILES = \ Product.m \ @@ -17,6 +19,7 @@ Appointments_OBJC_FILES = \ SOGoAppointmentObject.m \ SOGoTaskObject.m \ SOGoAppointmentFolder.m \ + SOGoAppointmentFolders.m \ SOGoGroupAppointmentFolder.m \ SOGoFreeBusyObject.m \ \ diff --git a/SoObjects/Appointments/German.lproj/Localizable.strings b/SoObjects/Appointments/German.lproj/Localizable.strings new file mode 100644 index 00000000..95feda9f --- /dev/null +++ b/SoObjects/Appointments/German.lproj/Localizable.strings @@ -0,0 +1 @@ +"Personal Calendar" = "Personal Calendar"; diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.h b/SoObjects/Appointments/SOGoAppointmentFolder.h index f3e71cd8..06edea0e 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.h +++ b/SoObjects/Appointments/SOGoAppointmentFolder.h @@ -53,6 +53,8 @@ NSMutableDictionary *uidToFilename; } +- (BOOL) isActive; + /* selection */ - (NSArray *) calendarUIDs; @@ -113,8 +115,6 @@ - (NSArray *) fetchAllSOGoAppointments; -- (NSArray *) calendarFolders; - - (NSString *) roleForComponentsWithAccessClass: (iCalAccessClass) accessClass forUser: (NSString *) uid; diff --git a/SoObjects/Appointments/SOGoAppointmentFolder.m b/SoObjects/Appointments/SOGoAppointmentFolder.m index 9fa1695f..38c34a94 100644 --- a/SoObjects/Appointments/SOGoAppointmentFolder.m +++ b/SoObjects/Appointments/SOGoAppointmentFolder.m @@ -125,11 +125,6 @@ static NSNumber *sharedYes = nil; return logger; } -- (BOOL) folderIsMandatory -{ - return YES; -} - /* selection */ - (NSArray *) calendarUIDs @@ -746,7 +741,7 @@ static NSNumber *sharedYes = nil; privacySqlString = @"and (c_isopaque = 1)"; else { -#warning we do not manage all the user's possible emails +#warning we do not manage all the possible user emails email = [[activeUser primaryIdentity] objectForKey: @"email"]; privacySqlString @@ -905,7 +900,6 @@ static NSNumber *sharedYes = nil; component: _component]; } - - (NSArray *) fetchFreeBusyInfosFrom: (NSCalendarDate *) _startDate to: (NSCalendarDate *) _endDate { @@ -1041,21 +1035,28 @@ static NSNumber *sharedYes = nil; /* Note: can return NSNull objects in the array! */ NSMutableArray *folders; NSEnumerator *e; - NSString *uid; - + NSString *uid, *ownerLogin; + id folder; + + ownerLogin = [self ownerInContext: context]; + if ([_uids count] == 0) return nil; folders = [NSMutableArray arrayWithCapacity:16]; e = [_uids objectEnumerator]; - while ((uid = [e nextObject])) { - id folder; - - folder = [self lookupCalendarFolderForUID: uid]; - if (![folder isNotNull]) - [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid]; + while ((uid = [e nextObject])) + { + if ([uid isEqualToString: ownerLogin]) + folder = self; + else + { + folder = [self lookupCalendarFolderForUID: uid]; + if (![folder isNotNull]) + [self logWithFormat:@"Note: did not find folder for uid: '%@'", uid]; + } - /* Note: intentionally add 'null' folders to allow a mapping */ - [folders addObject:folder ? folder : [NSNull null]]; - } + [folders addObject: folder]; + } + return folders; } @@ -1196,67 +1197,67 @@ static NSNumber *sharedYes = nil; return events; } -#warning We only support ONE calendar per user at this time -- (BOOL) _appendSubscribedFolders: (NSDictionary *) subscribedFolders - toFolderList: (NSMutableArray *) calendarFolders -{ - NSEnumerator *keys; - NSString *currentKey; - NSMutableDictionary *currentCalendar; - BOOL firstShouldBeActive; - unsigned int count; - - firstShouldBeActive = YES; - - keys = [[subscribedFolders allKeys] objectEnumerator]; - currentKey = [keys nextObject]; - count = 1; - while (currentKey) - { - currentCalendar = [NSMutableDictionary new]; - [currentCalendar autorelease]; - [currentCalendar - setDictionary: [subscribedFolders objectForKey: currentKey]]; - [currentCalendar setObject: currentKey forKey: @"folder"]; - [calendarFolders addObject: currentCalendar]; - if ([[currentCalendar objectForKey: @"active"] boolValue]) - firstShouldBeActive = NO; - count++; - currentKey = [keys nextObject]; - } - - return firstShouldBeActive; -} +// #warning We only support ONE calendar per user at this time +// - (BOOL) _appendSubscribedFolders: (NSDictionary *) subscribedFolders +// toFolderList: (NSMutableArray *) calendarFolders +// { +// NSEnumerator *keys; +// NSString *currentKey; +// NSMutableDictionary *currentCalendar; +// BOOL firstShouldBeActive; +// unsigned int count; + +// firstShouldBeActive = YES; + +// keys = [[subscribedFolders allKeys] objectEnumerator]; +// currentKey = [keys nextObject]; +// count = 1; +// while (currentKey) +// { +// currentCalendar = [NSMutableDictionary new]; +// [currentCalendar autorelease]; +// [currentCalendar +// setDictionary: [subscribedFolders objectForKey: currentKey]]; +// [currentCalendar setObject: currentKey forKey: @"folder"]; +// [calendarFolders addObject: currentCalendar]; +// if ([[currentCalendar objectForKey: @"active"] boolValue]) +// firstShouldBeActive = NO; +// count++; +// currentKey = [keys nextObject]; +// } + +// return firstShouldBeActive; +// } -- (NSArray *) calendarFolders -{ - NSMutableDictionary *userCalendar, *calendarDict; - NSMutableArray *calendarFolders; - SOGoUser *calendarUser; - BOOL firstActive; - - calendarFolders = [NSMutableArray new]; - [calendarFolders autorelease]; - - calendarUser = [SOGoUser userWithLogin: [self ownerInContext: context] - roles: nil]; - userCalendar = [NSMutableDictionary new]; - [userCalendar autorelease]; - [userCalendar setObject: @"/" forKey: @"folder"]; - [userCalendar setObject: @"Calendar" forKey: @"displayName"]; - [calendarFolders addObject: userCalendar]; - - calendarDict = [[calendarUser userSettings] objectForKey: @"Calendar"]; - firstActive = [[calendarDict objectForKey: @"activateUserFolder"] boolValue]; - firstActive = ([self _appendSubscribedFolders: - [calendarDict objectForKey: @"SubscribedFolders"] - toFolderList: calendarFolders] - || firstActive); - [userCalendar setObject: [NSNumber numberWithBool: firstActive] - forKey: @"active"]; - - return calendarFolders; -} +// - (NSArray *) calendarFolders +// { +// NSMutableDictionary *userCalendar, *calendarDict; +// NSMutableArray *calendarFolders; +// SOGoUser *calendarUser; +// BOOL firstActive; + +// calendarFolders = [NSMutableArray new]; +// [calendarFolders autorelease]; + +// calendarUser = [SOGoUser userWithLogin: [self ownerInContext: context] +// roles: nil]; +// userCalendar = [NSMutableDictionary new]; +// [userCalendar autorelease]; +// [userCalendar setObject: @"/" forKey: @"folder"]; +// [userCalendar setObject: @"Calendar" forKey: @"displayName"]; +// [calendarFolders addObject: userCalendar]; + +// calendarDict = [[calendarUser userSettings] objectForKey: @"Calendar"]; +// firstActive = [[calendarDict objectForKey: @"activateUserFolder"] boolValue]; +// firstActive = ([self _appendSubscribedFolders: +// [calendarDict objectForKey: @"SubscribedFolders"] +// toFolderList: calendarFolders] +// || firstActive); +// [userCalendar setObject: [NSNumber numberWithBool: firstActive] +// forKey: @"active"]; + +// return calendarFolders; +// } // - (NSArray *) fetchContentObjectNames // { @@ -1297,49 +1298,16 @@ static NSNumber *sharedYes = nil; return @"IPF.Appointment"; } -/* hack until we permit more than 1 cal per user */ -- (NSArray *) _fixedPath: (NSArray *) objectPath +- (BOOL) isActive { - NSMutableArray *newPath; + NSUserDefaults *settings; + NSArray *activeFolders; - newPath = [NSMutableArray arrayWithArray: objectPath]; - if ([newPath count] > 2) - { - if (![[newPath objectAtIndex: 2] isEqualToString: @"personal"]) - [newPath insertObject: @"personal" atIndex: 2]; - } - else - [newPath addObject: @"personal"]; - - return newPath; -} + settings = [[context activeUser] userSettings]; + activeFolders + = [[settings objectForKey: @"Calendar"] objectForKey: @"ActiveFolders"]; -- (NSArray *) aclUsersForObjectAtPath: (NSArray *) objectPathArray -{ - return [super aclUsersForObjectAtPath: [self _fixedPath: objectPathArray]]; -} - -- (NSArray *) aclsForUser: (NSString *) uid - forObjectAtPath: (NSArray *) objectPathArray -{ - return [super aclsForUser: uid - forObjectAtPath: [self _fixedPath: objectPathArray]]; -} - -- (void) setRoles: (NSArray *) roles - forUser: (NSString *) uid - forObjectAtPath: (NSArray *) objectPathArray -{ - [super setRoles: roles - forUser: uid - forObjectAtPath: [self _fixedPath: objectPathArray]]; -} - -- (void) removeAclsForUsers: (NSArray *) users - forObjectAtPath: (NSArray *) objectPathArray -{ - [super removeAclsForUsers: users - forObjectAtPath: [self _fixedPath: objectPathArray]]; + return [activeFolders containsObject: nameInContainer]; } @end /* SOGoAppointmentFolder */ diff --git a/SoObjects/Appointments/SOGoAppointmentObject.h b/SoObjects/Appointments/SOGoAppointmentObject.h index d6fe91e8..8262970f 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.h +++ b/SoObjects/Appointments/SOGoAppointmentObject.h @@ -46,13 +46,6 @@ @interface SOGoAppointmentObject : SOGoCalendarComponent -/* folder management */ - -- (id) lookupHomeFolderForUID: (NSString *) _uid - inContext: (id) _ctx; -- (NSArray *) lookupCalendarFoldersForUIDs: (NSArray *) _uids - inContext: (id) _ctx; - /* "iCal multifolder saves" */ - (NSException *) saveContentString: (NSString *) _iCal diff --git a/SoObjects/Appointments/SOGoAppointmentObject.m b/SoObjects/Appointments/SOGoAppointmentObject.m index 33e14028..404428ea 100644 --- a/SoObjects/Appointments/SOGoAppointmentObject.m +++ b/SoObjects/Appointments/SOGoAppointmentObject.m @@ -34,6 +34,8 @@ #import #import "NSArray+Appointments.h" +#import "SOGoAppointmentFolder.h" + #import "SOGoAppointmentObject.h" @implementation SOGoAppointmentObject @@ -96,16 +98,6 @@ return uids; } -/* folder management */ - -- (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx { - // TODO: what does this do? lookup the home of the organizer? - return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx]; -} -- (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx { - return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx]; -} - /* store in all the other folders */ - (NSException *) saveContentString: (NSString *) _iCal @@ -115,8 +107,8 @@ id folder; NSException *allErrors = nil; - e = [[self lookupCalendarFoldersForUIDs:_uids inContext: context] - objectEnumerator]; + e = [[container lookupCalendarFoldersForUIDs:_uids inContext: context] + objectEnumerator]; while ((folder = [e nextObject]) != nil) { NSException *error; SOGoAppointmentObject *apt; @@ -160,8 +152,8 @@ id folder; NSException *allErrors = nil; - e = [[self lookupCalendarFoldersForUIDs:_uids inContext: context] - objectEnumerator]; + e = [[container lookupCalendarFoldersForUIDs:_uids inContext: context] + objectEnumerator]; while ((folder = [e nextObject])) { NSException *error; SOGoAppointmentObject *apt; diff --git a/SoObjects/Appointments/SOGoTaskObject.h b/SoObjects/Appointments/SOGoTaskObject.h index 9f57bf58..da442022 100644 --- a/SoObjects/Appointments/SOGoTaskObject.h +++ b/SoObjects/Appointments/SOGoTaskObject.h @@ -44,13 +44,6 @@ @interface SOGoTaskObject : SOGoCalendarComponent -/* folder management */ - -- (id) lookupHomeFolderForUID: (NSString *) _uid - inContext: (id) _ctx; -- (NSArray *) lookupCalendarFoldersForUIDs: (NSArray *) _uids - inContext: (id) _ctx; - /* "iCal multifolder saves" */ - (NSException *) saveContentString: (NSString *) _iCal diff --git a/SoObjects/Appointments/SOGoTaskObject.m b/SoObjects/Appointments/SOGoTaskObject.m index 547d19e0..2357ac8b 100644 --- a/SoObjects/Appointments/SOGoTaskObject.m +++ b/SoObjects/Appointments/SOGoTaskObject.m @@ -34,6 +34,7 @@ #import "NSArray+Appointments.h" #import "SOGoAptMailNotification.h" +#import "SOGoAppointmentFolder.h" #import "SOGoTaskObject.h" @@ -118,17 +119,6 @@ static NSString *mailTemplateDefaultLanguage = nil; return uids; } -/* folder management */ - -- (id)lookupHomeFolderForUID:(NSString *)_uid inContext:(id)_ctx { - // TODO: what does this do? lookup the home of the organizer? - return [[self container] lookupHomeFolderForUID:_uid inContext:_ctx]; -} - -- (NSArray *)lookupCalendarFoldersForUIDs:(NSArray *)_uids inContext:(id)_ctx { - return [[self container] lookupCalendarFoldersForUIDs:_uids inContext:_ctx]; -} - /* store in all the other folders */ - (NSException *)saveContentString:(NSString *)_iCal inUIDs:(NSArray *)_uids { @@ -136,7 +126,7 @@ static NSString *mailTemplateDefaultLanguage = nil; id folder; NSException *allErrors = nil; - e = [[self lookupCalendarFoldersForUIDs: _uids inContext: context] + e = [[container lookupCalendarFoldersForUIDs: _uids inContext: context] objectEnumerator]; while ((folder = [e nextObject]) != nil) { NSException *error; @@ -175,7 +165,7 @@ static NSString *mailTemplateDefaultLanguage = nil; id folder; NSException *allErrors = nil; - e = [[self lookupCalendarFoldersForUIDs: _uids inContext: context] + e = [[container lookupCalendarFoldersForUIDs: _uids inContext: context] objectEnumerator]; while ((folder = [e nextObject])) { NSException *error; diff --git a/SoObjects/Appointments/product.plist b/SoObjects/Appointments/product.plist index d70b2faa..1fdd67e8 100644 --- a/SoObjects/Appointments/product.plist +++ b/SoObjects/Appointments/product.plist @@ -8,6 +8,9 @@ }; classes = { + SOGoAppointmentFolder = { + superclass = "SOGoParentFolder"; + }; SOGoAppointmentFolder = { superclass = "SOGoFolder"; defaultRoles = { diff --git a/SoObjects/Contacts/English.lproj/Localizable.strings b/SoObjects/Contacts/English.lproj/Localizable.strings new file mode 100644 index 00000000..56404b01 --- /dev/null +++ b/SoObjects/Contacts/English.lproj/Localizable.strings @@ -0,0 +1 @@ +"Personal Address Book" = "Personal Address Book"; diff --git a/SoObjects/Contacts/French.lproj/Localizable.strings b/SoObjects/Contacts/French.lproj/Localizable.strings new file mode 100644 index 00000000..af4d87c3 --- /dev/null +++ b/SoObjects/Contacts/French.lproj/Localizable.strings @@ -0,0 +1 @@ +"Personal Address Book" = "Carnet d'adresses personnel"; diff --git a/SoObjects/Contacts/German.lproj/Localizable.strings b/SoObjects/Contacts/German.lproj/Localizable.strings new file mode 100644 index 00000000..95feda9f --- /dev/null +++ b/SoObjects/Contacts/German.lproj/Localizable.strings @@ -0,0 +1 @@ +"Personal Calendar" = "Personal Calendar"; diff --git a/SoObjects/Contacts/SOGoContactFolder.h b/SoObjects/Contacts/SOGoContactFolder.h index 6ced261a..c1ff8968 100644 --- a/SoObjects/Contacts/SOGoContactFolder.h +++ b/SoObjects/Contacts/SOGoContactFolder.h @@ -43,16 +43,6 @@ @protocol SOGoContactFolder -+ (id ) contactFolderWithName: (NSString *) aName - andDisplayName: (NSString *) aDisplayName - inContainer: (SOGoObject *) aContainer; - -- (id ) initWithName: (NSString *) aName - andDisplayName: (NSString *) aDisplayName - inContainer: (SOGoObject *) aContainer; - -- (NSString *) displayName; - - (NSArray *) lookupContactsWithFilter: (NSString *) filter sortBy: (NSString *) sortKey ordering: (NSComparisonResult) sortOrdering; diff --git a/SoObjects/Contacts/SOGoContactFolders.h b/SoObjects/Contacts/SOGoContactFolders.h index 0272f3bb..4e271e5c 100644 --- a/SoObjects/Contacts/SOGoContactFolders.h +++ b/SoObjects/Contacts/SOGoContactFolders.h @@ -1,6 +1,6 @@ /* SOGoContactFolders.h - this file is part of SOGo * - * Copyright (C) 2006 Inverse groupe conseil + * Copyright (C) 2006, 2007 Inverse groupe conseil * * Author: Wolfgang Sourdeau * @@ -23,25 +23,9 @@ #ifndef SOGOCONTACTFOLDERS_H #define SOGOCONTACTFOLDERS_H -#import +#import -@class NSMutableDictionary; -@class NSString; -@class WOResponse; - -@interface SOGoContactFolders : SOGoObject -{ - NSMutableDictionary *contactFolders; - NSString *OCSPath; -} - -- (NSString *) defaultSourceName; - -- (void) setBaseOCSPath: (NSString *) newOCSPath; - -- (NSArray *) contactFolders; - -- (WOResponse *) newFolderWithName: (NSString *) name; +@interface SOGoContactFolders : SOGoParentFolder @end diff --git a/SoObjects/Contacts/SOGoContactFolders.m b/SoObjects/Contacts/SOGoContactFolders.m index 21587097..2d75c8f0 100644 --- a/SoObjects/Contacts/SOGoContactFolders.m +++ b/SoObjects/Contacts/SOGoContactFolders.m @@ -1,6 +1,6 @@ /* SOGoContactFolders.m - this file is part of SOGo * - * Copyright (C) 2006 Inverse groupe conseil + * Copyright (C) 2006, 2007 Inverse groupe conseil * * Author: Wolfgang Sourdeau * @@ -20,7 +20,6 @@ * Boston, MA 02111-1307, USA. */ -/* exchange folder types: */ /* MailItems IPF.Note ContactItems IPF.Contact AppointmentItems IPF.Appointment @@ -28,23 +27,11 @@ TaskItems IPF.Task JournalItems IPF.Journal */ -#import +#import #import - -#import -#import -#import -#import -#import -#import - -#import -#import -#import -#import +#import #import -#import #import "SOGoContactGCSFolder.h" #import "SOGoContactLDAPFolder.h" @@ -52,89 +39,14 @@ @implementation SOGoContactFolders -- (id) init -{ - if ((self = [super init])) - { - contactFolders = nil; - OCSPath = nil; - } - - return self; -} - -- (void) dealloc ++ (NSString *) gcsFolderType { - if (contactFolders) - [contactFolders release]; - if (OCSPath) - [OCSPath release]; - [super dealloc]; -} - -- (void) _fetchPersonalFolders: (NSString *) sql - withChannel: (EOAdaptorChannel *) fc -{ - NSArray *attrs; - NSDictionary *row; - SOGoContactGCSFolder *ab; - BOOL hasPersonal; - NSString *key, *path; - - hasPersonal = NO; - [fc evaluateExpressionX: sql]; - attrs = [fc describeResults: NO]; - row = [fc fetchAttributes: attrs withZone: NULL]; - while (row) - { - ab = [SOGoContactGCSFolder - contactFolderWithName: [row objectForKey: @"c_path4"] - andDisplayName: [row objectForKey: @"c_foldername"] - inContainer: self]; - key = [row objectForKey: @"c_path4"]; - hasPersonal = (hasPersonal || [key isEqualToString: @"personal"]); - [ab setOCSPath: [NSString stringWithFormat: @"%@/%@", - OCSPath, key]]; - [contactFolders setObject: ab forKey: key]; - row = [fc fetchAttributes: attrs withZone: NULL]; - } - - if (!hasPersonal) - { - ab = [SOGoContactGCSFolder contactFolderWithName: @"personal" - andDisplayName: @"Contacts" - inContainer: self]; - path = [NSString stringWithFormat: - @"/Users/%@/Contacts/personal", - [self ownerInContext: context]]; - [ab setOCSPath: path]; - [contactFolders setObject: ab forKey: @"personal"]; - } + return @"Contact"; } -- (void) appendPersonalSources ++ (Class) subFolderClass { - GCSChannelManager *cm; - EOAdaptorChannel *fc; - NSURL *folderLocation; - NSString *sql; - - cm = [GCSChannelManager defaultChannelManager]; - folderLocation - = [[GCSFolderManager defaultFolderManager] folderInfoLocation]; - fc = [cm acquireOpenChannelForURL: folderLocation]; - if (fc) - { - sql = [NSString - stringWithFormat: (@"SELECT c_path4, c_foldername FROM %@" - @" WHERE c_path2 = '%@'" - @" AND c_folder_type = 'Contact'"), - [folderLocation gcsTableName], [self ownerInContext: context]]; - [self _fetchPersonalFolders: sql withChannel: fc]; - [cm releaseChannel: fc]; -// sql = [sql stringByAppendingFormat:@" WHERE %@ = '%@'", -// uidColumnName, [self uid]]; - } + return [SOGoContactGCSFolder class]; } - (void) appendSystemSources @@ -150,119 +62,18 @@ while (currentSourceID) { displayName = [um displayNameForSourceWithID: currentSourceID]; - currentFolder = [SOGoContactLDAPFolder contactFolderWithName: currentSourceID + currentFolder = [SOGoContactLDAPFolder folderWithName: currentSourceID andDisplayName: displayName inContainer: self]; [currentFolder setLDAPSource: [um sourceWithID: currentSourceID]]; - [contactFolders setObject: currentFolder forKey: currentSourceID]; + [subFolders setObject: currentFolder forKey: currentSourceID]; currentSourceID = [sourceIDs nextObject]; } } -- (WOResponse *) newFolderWithName: (NSString *) name -{ - SOGoContactGCSFolder *newFolder; - WOResponse *response; - - newFolder = [SOGoContactGCSFolder contactFolderWithName: name - andDisplayName: name - inContainer: self]; - if ([newFolder isKindOfClass: [NSException class]]) - response = (WOResponse *) newFolder; - else - { - [newFolder setOCSPath: [NSString stringWithFormat: @"%@/%@", - OCSPath, name]]; - if ([newFolder create]) - { - response = [WOResponse new]; - [response setStatus: 201]; - [response autorelease]; - } - else - response = [NSException exceptionWithHTTPStatus: 400 - reason: @"The new folder could not be created"]; - } - - return response; -} - -- (void) initContactSources -{ - if (!contactFolders) - { - contactFolders = [NSMutableDictionary new]; - [self appendPersonalSources]; - [self appendSystemSources]; - } -} - -- (id) lookupName: (NSString *) name - inContext: (WOContext *) lookupContext - acquire: (BOOL) acquire -{ - id obj; - - /* first check attributes directly bound to the application */ - obj = [super lookupName: name inContext: lookupContext acquire: NO]; - if (!obj) - { - if (!contactFolders) - [self initContactSources]; - - obj = [contactFolders objectForKey: name]; - if (!obj) - obj = [NSException exceptionWithHTTPStatus: 404]; - } - - return obj; -} - -- (NSArray *) toManyRelationshipKeys -{ - if (!contactFolders) - [self initContactSources]; - - return [contactFolders allKeys]; -} - -- (NSArray *) contactFolders -{ - if (!contactFolders) - [self initContactSources]; - - return [contactFolders allValues]; -} - -/* acls */ -- (NSArray *) aclsForUser: (NSString *) uid -{ - return nil; -} - -- (BOOL) davIsCollection -{ - return YES; -} - -- (NSString *) davContentType -{ - return @"httpd/unix-directory"; -} - -- (void) setBaseOCSPath: (NSString *) newOCSPath -{ - if (OCSPath) - [OCSPath release]; - OCSPath = newOCSPath; - if (OCSPath) - [OCSPath retain]; -} - -/* web interface */ -- (NSString *) defaultSourceName +- (NSString *) defaultFolderName { - return @"personal"; + return @"Personal Address Book"; } @end diff --git a/SoObjects/Contacts/SOGoContactGCSFolder.h b/SoObjects/Contacts/SOGoContactGCSFolder.h index 1dd91a53..d10913d0 100644 --- a/SoObjects/Contacts/SOGoContactGCSFolder.h +++ b/SoObjects/Contacts/SOGoContactGCSFolder.h @@ -30,9 +30,6 @@ @class NSString; @interface SOGoContactGCSFolder : SOGoFolder -{ - NSString *displayName; -} @end diff --git a/SoObjects/Contacts/SOGoContactGCSFolder.m b/SoObjects/Contacts/SOGoContactGCSFolder.m index 47eb5f13..e00e9674 100644 --- a/SoObjects/Contacts/SOGoContactGCSFolder.m +++ b/SoObjects/Contacts/SOGoContactGCSFolder.m @@ -21,6 +21,7 @@ #import #import + #import #import #import @@ -41,47 +42,6 @@ @implementation SOGoContactGCSFolder -+ (id ) contactFolderWithName: (NSString *) aName - andDisplayName: (NSString *) aDisplayName - inContainer: (SOGoObject *) aContainer -{ - SOGoContactGCSFolder *folder; - - folder = [[self alloc] initWithName: aName - andDisplayName: aDisplayName - inContainer: aContainer]; - [folder autorelease]; - - return folder; -} - -- (void) dealloc -{ - [displayName release]; - [super dealloc]; -} - -- (id ) initWithName: (NSString *) newName - andDisplayName: (NSString *) newDisplayName - inContainer: (SOGoObject *) newContainer -{ - if ((self = [self initWithName: newName - inContainer: newContainer])) - ASSIGN (displayName, newDisplayName); - - return self; -} - -- (BOOL) folderIsMandatory -{ - return [nameInContainer isEqualToString: @"personal"]; -} - -- (NSString *) displayName -{ - return displayName; -} - /* name lookup */ - (id ) lookupContactWithId: (NSString *) recordId @@ -105,7 +65,6 @@ BOOL isPut; isPut = NO; - /* first check attributes directly bound to the application */ obj = [super lookupName:_key inContext:_ctx acquire:NO]; if (!obj) { @@ -273,14 +232,6 @@ return @"vcard-collection"; } -- (NSException *) delete -{ - return (([nameInContainer isEqualToString: @"personal"]) - ? [NSException exceptionWithHTTPStatus: 403 - reason: @"the 'personal' folder cannot be deleted"] - : [super delete]); -} - // /* GET */ // - (id) GETAction: (id)_ctx diff --git a/SoObjects/Contacts/SOGoContactLDAPFolder.h b/SoObjects/Contacts/SOGoContactLDAPFolder.h index ad1ceb89..e405a3fa 100644 --- a/SoObjects/Contacts/SOGoContactLDAPFolder.h +++ b/SoObjects/Contacts/SOGoContactLDAPFolder.h @@ -37,9 +37,12 @@ BOOL ignoreSoObjectHunger; } -- (id ) initWithName: (NSString *) newName - andDisplayName: (NSString *) newDisplayName - inContainer: (SOGoObject *) newContainer; ++ (id) folderWithName: (NSString *) aName + andDisplayName: (NSString *) aDisplayName + inContainer: (id) aContainer; +- (id) initWithName: (NSString *) newName + andDisplayName: (NSString *) newDisplayName + inContainer: (id) newContainer; - (void) setLDAPSource: (LDAPSource *) newLdapSource; @end diff --git a/SoObjects/Contacts/SOGoContactLDAPFolder.m b/SoObjects/Contacts/SOGoContactLDAPFolder.m index 2704be33..459f2998 100644 --- a/SoObjects/Contacts/SOGoContactLDAPFolder.m +++ b/SoObjects/Contacts/SOGoContactLDAPFolder.m @@ -41,11 +41,11 @@ @implementation SOGoContactLDAPFolder -+ (id ) contactFolderWithName: (NSString *) aName - andDisplayName: (NSString *) aDisplayName - inContainer: (SOGoObject *) aContainer ++ (id) folderWithName: (NSString *) aName + andDisplayName: (NSString *) aDisplayName + inContainer: (id) aContainer { - SOGoContactLDAPFolder *folder; + id folder; folder = [[self alloc] initWithName: aName andDisplayName: aDisplayName @@ -68,9 +68,9 @@ return self; } -- (id ) initWithName: (NSString *) newName - andDisplayName: (NSString *) newDisplayName - inContainer: (SOGoObject *) newContainer +- (id) initWithName: (NSString *) newName + andDisplayName: (NSString *) newDisplayName + inContainer: (id) newContainer { if ((self = [self initWithName: newName inContainer: newContainer])) @@ -257,6 +257,11 @@ } /* acls */ +- (NSString *) ownerInContext: (WOContext *) noContext +{ + return @"nobody"; +} + /* TODO: this might change one day when we support LDAP acls */ - (NSArray *) aclsForUser: (NSString *) uid { diff --git a/SoObjects/Contacts/product.plist b/SoObjects/Contacts/product.plist index cb6b5124..e1557d61 100644 --- a/SoObjects/Contacts/product.plist +++ b/SoObjects/Contacts/product.plist @@ -9,12 +9,7 @@ classes = { SOGoContactFolders = { - superclass = "SOGoFolder"; - protectedBy = "Access Contents Information"; - defaultRoles = { - "Access Contents Information" = ( "Authenticated" ); - "WebDAV Access" = ( "Authenticated" ); - }; + superclass = "SOGoParentFolder"; }; SOGoContactGCSFolder = { superclass = "SOGoFolder"; diff --git a/SoObjects/SOGo/GNUmakefile b/SoObjects/SOGo/GNUmakefile index f8a602b7..d2b9d356 100644 --- a/SoObjects/SOGo/GNUmakefile +++ b/SoObjects/SOGo/GNUmakefile @@ -21,8 +21,9 @@ FHS_HEADER_DIRS = SOGo libSOGo_HEADER_FILES = \ SOGoObject.h \ - SOGoFolder.h \ SOGoContentObject.h \ + SOGoFolder.h \ + SOGoParentFolder.h \ SOGoUserFolder.h \ SOGoGroupsFolder.h \ SOGoGroupFolder.h \ @@ -50,8 +51,9 @@ libSOGo_HEADER_FILES = \ libSOGo_OBJC_FILES = \ SOGoObject.m \ - SOGoFolder.m \ SOGoContentObject.m \ + SOGoFolder.m \ + SOGoParentFolder.m \ SOGoUserFolder.m \ SOGoGroupsFolder.m \ SOGoGroupFolder.m \ diff --git a/SoObjects/SOGo/NSDictionary+Utilities.h b/SoObjects/SOGo/NSDictionary+Utilities.h index bc054c81..1c5622f1 100644 --- a/SoObjects/SOGo/NSDictionary+Utilities.h +++ b/SoObjects/SOGo/NSDictionary+Utilities.h @@ -29,6 +29,8 @@ @interface NSDictionary (SOGoDictionaryUtilities) ++ (NSDictionary *) dictionaryFromStringsFile: (NSString *) file; + - (NSString *) jsonRepresentation; - (NSString *) keysWithFormat: (NSString *) keyFormat; diff --git a/SoObjects/SOGo/NSDictionary+Utilities.m b/SoObjects/SOGo/NSDictionary+Utilities.m index 6caaeaa6..666aef02 100644 --- a/SoObjects/SOGo/NSDictionary+Utilities.m +++ b/SoObjects/SOGo/NSDictionary+Utilities.m @@ -21,6 +21,7 @@ */ #import +#import #import #import "NSArray+Utilities.h" @@ -29,6 +30,25 @@ @implementation NSDictionary (SOGoDictionaryUtilities) ++ (NSDictionary *) dictionaryFromStringsFile: (NSString *) file +{ + NSString *serialized; + NSMutableData *content; + NSDictionary *newDictionary; + + content = [NSMutableData new]; + [content appendBytes: "{" length: 1]; + [content appendData: [NSData dataWithContentsOfFile: file]]; + [content appendBytes: "}" length: 1]; + serialized = [[NSString alloc] initWithData: content + encoding: NSUTF8StringEncoding]; + [content release]; + newDictionary = [serialized propertyList]; + [serialized release]; + + return newDictionary; +} + - (NSString *) jsonRepresentation { NSMutableArray *values; diff --git a/SoObjects/SOGo/SOGoContentObject.m b/SoObjects/SOGo/SOGoContentObject.m index 945382da..255db3b6 100644 --- a/SoObjects/SOGo/SOGoContentObject.m +++ b/SoObjects/SOGo/SOGoContentObject.m @@ -231,7 +231,7 @@ needsLocation = NO; tmp = [[self nameInContainer] stringByDeletingPathExtension]; if ([tmp isEqualToString:@"new"]) { - tmp = [[[self container] class] globallyUniqueObjectId]; + tmp = [self globallyUniqueObjectId]; needsLocation = YES; [self debugWithFormat: @@ -359,7 +359,7 @@ - (NSArray *) aclUsers { - return [container aclUsersForObjectAtPath: [self pathArrayToSoObject]]; + return [container aclUsersForObjectAtPath: [self pathArrayToSOGoObject]]; } - (NSArray *) aclsForUser: (NSString *) uid @@ -369,7 +369,7 @@ acls = [NSMutableArray array]; ownAcls = [container aclsForUser: uid - forObjectAtPath: [self pathArrayToSoObject]]; + forObjectAtPath: [self pathArrayToSOGoObject]]; [acls addObjectsFromArray: ownAcls]; containerAcls = [container aclsForUser: uid]; if ([containerAcls count] > 0) diff --git a/SoObjects/SOGo/SOGoFolder.h b/SoObjects/SOGo/SOGoFolder.h index 5358299d..ed57b805 100644 --- a/SoObjects/SOGo/SOGoFolder.h +++ b/SoObjects/SOGo/SOGoFolder.h @@ -43,15 +43,20 @@ @interface SOGoFolder : SOGoObject { - NSString *ocsPath; + NSMutableString *displayName; + NSString *ocsPath; GCSFolder *ocsFolder; NSMutableDictionary *aclCache; } -+ (NSString *) globallyUniqueObjectId; ++ (id) folderWithSubscriptionReference: (NSString *) reference + inContainer: (id) aContainer; /* accessors */ +- (void) setDisplayName: (NSString *) newDisplayName; +- (NSString *) displayName; + - (void) setOCSPath: (NSString *)_Path; - (NSString *) ocsPath; @@ -69,10 +74,11 @@ - (NSString *) outlookFolderClass; - (BOOL) folderIsMandatory; +- (NSString *) folderType; - (BOOL) create; - (NSException *) delete; - +- (void) renameTo: (NSString *) newName; /* dav */ - (NSArray *) davNamespaces; diff --git a/SoObjects/SOGo/SOGoFolder.m b/SoObjects/SOGo/SOGoFolder.m index 20a3b894..a78d5074 100644 --- a/SoObjects/SOGo/SOGoFolder.m +++ b/SoObjects/SOGo/SOGoFolder.m @@ -19,9 +19,6 @@ 02111-1307, USA. */ -#import -#import - #import #import #import @@ -29,6 +26,7 @@ #import #import +#import #import #import #import @@ -37,9 +35,11 @@ #import #import #import +#import #import #import #import +#import #import #import "NSArray+Utilities.h" @@ -66,33 +66,38 @@ static NSString *defaultUserID = @""; NSStringFromClass([self superclass]), [super version]); } -+ (NSString *) globallyUniqueObjectId ++ (id) folderWithSubscriptionReference: (NSString *) reference + inContainer: (id) aContainer { - /* - 4C08AE1A-A808-11D8-AC5A-000393BBAFF6 - SOGo-Web-28273-18283-288182 - printf( "%x", *(int *) &f); - */ - static int pid = 0; - static int sequence = 0; - static float rndm = 0; - float f; - - if (pid == 0) - { /* break if we fork ;-) */ - pid = getpid(); - rndm = random(); - } - sequence++; - f = [[NSDate date] timeIntervalSince1970]; - return [NSString stringWithFormat:@"%0X-%0X-%0X-%0X", - pid, *(int *)&f, sequence++, random]; + id newFolder; + NSArray *elements, *pathElements; + NSString *ocsPath, *objectPath, *owner, *ocsName, *folderName; + + elements = [reference componentsSeparatedByString: @":"]; + owner = [elements objectAtIndex: 0]; + objectPath = [elements objectAtIndex: 1]; + pathElements = [objectPath componentsSeparatedByString: @"/"]; + if ([pathElements count] > 1) + ocsName = [pathElements objectAtIndex: 1]; + else + ocsName = @"personal"; + + ocsPath = [NSString stringWithFormat: @"/Users/%@/%@/%@", + owner, [pathElements objectAtIndex: 0], ocsName]; + folderName = [NSString stringWithFormat: @"%@_%@", owner, ocsName]; + newFolder = [[self alloc] initWithName: folderName + inContainer: aContainer]; + [newFolder setOCSPath: ocsPath]; + [newFolder setOwner: owner]; + + return newFolder; } - (id) init { if ((self = [super init])) { + displayName = nil; ocsPath = nil; ocsFolder = nil; aclCache = [NSMutableDictionary new]; @@ -106,6 +111,7 @@ static NSString *defaultUserID = @""; [ocsFolder release]; [ocsPath release]; [aclCache release]; + [displayName release]; [super dealloc]; } @@ -118,13 +124,12 @@ static NSString *defaultUserID = @""; - (void) setOCSPath: (NSString *) _path { - if ([ocsPath isEqualToString:_path]) - return; - - if (ocsPath) - [self warnWithFormat:@"GCS path is already set! '%@'", _path]; - - ASSIGNCOPY(ocsPath, _path); + if (![ocsPath isEqualToString:_path]) + { + if (ocsPath) + [self warnWithFormat: @"GCS path is already set! '%@'", _path]; + ASSIGN (ocsPath, _path); + } } - (NSString *) ocsPath @@ -149,9 +154,73 @@ static NSString *defaultUserID = @""; - (BOOL) folderIsMandatory { - [self subclassResponsibility: _cmd]; + return [nameInContainer isEqualToString: @"personal"]; +} + +- (void) _setDisplayNameFromRow: (NSDictionary *) row +{ + NSString *currentLogin, *ownerLogin; + NSDictionary *ownerIdentity; - return NO; + displayName + = [NSMutableString stringWithString: [row objectForKey: @"c_foldername"]]; + currentLogin = [[context activeUser] login]; + ownerLogin = [self ownerInContext: context]; + if (![currentLogin isEqualToString: ownerLogin]) + { + ownerIdentity = [[SOGoUser userWithLogin: ownerLogin roles: nil] + primaryIdentity]; + [displayName appendFormat: @" (%@ <%@>)", + [ownerIdentity objectForKey: @"fullName"], + [ownerIdentity objectForKey: @"email"]]; + } +} + +- (void) _fetchDisplayName +{ + GCSChannelManager *cm; + EOAdaptorChannel *fc; + NSURL *folderLocation; + NSString *sql; + NSArray *attrs; + NSDictionary *row; + + cm = [GCSChannelManager defaultChannelManager]; + folderLocation + = [[GCSFolderManager defaultFolderManager] folderInfoLocation]; + fc = [cm acquireOpenChannelForURL: folderLocation]; + if (fc) + { + sql + = [NSString stringWithFormat: (@"SELECT c_foldername FROM %@" + @" WHERE c_path = '%@'"), + [folderLocation gcsTableName], ocsPath]; + [fc evaluateExpressionX: sql]; + attrs = [fc describeResults: NO]; + row = [fc fetchAttributes: attrs withZone: NULL]; + if (row) + [self _setDisplayNameFromRow: row]; + [fc cancelFetch]; + [cm releaseChannel: fc]; + } +} + +- (void) setDisplayName: (NSString *) newDisplayName +{ + ASSIGN (displayName, newDisplayName); +} + +- (NSString *) displayName +{ + if (!displayName) + [self _fetchDisplayName]; + + return displayName; +} + +- (NSString *) davDisplayName +{ + return [self displayName]; } - (GCSFolder *) ocsFolder @@ -188,7 +257,9 @@ static NSString *defaultUserID = @""; { NSException *result; +// [self dieHard]; result = [[self folderManager] createFolderOfType: [self folderType] + withName: displayName atPath: ocsPath]; return (result == nil); @@ -196,7 +267,42 @@ static NSString *defaultUserID = @""; - (NSException *) delete { - return [[self folderManager] deleteFolderAtPath: ocsPath]; + NSException *error; + + if ([nameInContainer isEqualToString: @"personal"]) + error = [NSException exceptionWithHTTPStatus: 403 + reason: @"folder 'personal' cannot be deleted"]; + else + error = [[self folderManager] deleteFolderAtPath: ocsPath]; + + return error; +} + +- (void) renameTo: (NSString *) newName +{ + GCSChannelManager *cm; + EOAdaptorChannel *fc; + NSURL *folderLocation; + NSString *sql; + + [displayName release]; + displayName = nil; + + cm = [GCSChannelManager defaultChannelManager]; + folderLocation + = [[GCSFolderManager defaultFolderManager] folderInfoLocation]; + fc = [cm acquireOpenChannelForURL: folderLocation]; + if (fc) + { + sql + = [NSString stringWithFormat: (@"UPDATE %@ SET c_foldername = '%@'" + @" WHERE c_path = '%@'"), + [folderLocation gcsTableName], newName, ocsPath]; + [fc evaluateExpressionX: sql]; + [cm releaseChannel: fc]; +// sql = [sql stringByAppendingFormat:@" WHERE %@ = '%@'", +// uidColumnName, [self uid]]; + } } - (NSArray *) fetchContentObjectNames @@ -470,7 +576,7 @@ static NSString *defaultUserID = @""; /* acls */ - (NSArray *) aclUsers { - return [self aclUsersForObjectAtPath: [self pathArrayToSoObject]]; + return [self aclUsersForObjectAtPath: [self pathArrayToSOGoObject]]; } - (NSArray *) aclsForUser: (NSString *) uid @@ -480,7 +586,7 @@ static NSString *defaultUserID = @""; acls = [NSMutableArray array]; ownAcls = [self aclsForUser: uid - forObjectAtPath: [self pathArrayToSoObject]]; + forObjectAtPath: [self pathArrayToSOGoObject]]; [acls addObjectsFromArray: ownAcls]; if ([container respondsToSelector: @selector (aclsForUser:)]) { @@ -503,13 +609,13 @@ static NSString *defaultUserID = @""; { return [self setRoles: roles forUser: uid - forObjectAtPath: [self pathArrayToSoObject]]; + forObjectAtPath: [self pathArrayToSOGoObject]]; } - (void) removeAclsForUsers: (NSArray *) users { return [self removeAclsForUsers: users - forObjectAtPath: [self pathArrayToSoObject]]; + forObjectAtPath: [self pathArrayToSOGoObject]]; } - (NSString *) defaultUserID diff --git a/SoObjects/SOGo/SOGoObject.h b/SoObjects/SOGo/SOGoObject.h index 67142bfe..5404508a 100644 --- a/SoObjects/SOGo/SOGoObject.h +++ b/SoObjects/SOGo/SOGoObject.h @@ -61,6 +61,9 @@ id container; } ++ (NSString *) globallyUniqueObjectId; +- (NSString *) globallyUniqueObjectId; + + (id) objectWithName: (NSString *)_name inContainer:(id)_container; - (id) initWithName: (NSString *) _name inContainer:(id)_container; @@ -70,11 +73,15 @@ - (NSString *) nameInContainer; - (id) container; +- (NSArray *) pathArrayToSOGoObject; + - (NSURL *) davURL; - (NSURL *) soURL; - (NSURL *) soURLToBaseContainerForUser: (NSString *) uid; - (NSURL *) soURLToBaseContainerForCurrentUser; +- (NSString *) labelForKey: (NSString *) key; + /* ownership */ - (void) setOwner: (NSString *) newOwner; @@ -100,7 +107,7 @@ /* etag support */ -- (NSException *)matchesRequestConditionInContext:(id)_ctx; +- (NSException *) matchesRequestConditionInContext:(id)_ctx; /* acls */ diff --git a/SoObjects/SOGo/SOGoObject.m b/SoObjects/SOGo/SOGoObject.m index 280211e5..31a6b6ce 100644 --- a/SoObjects/SOGo/SOGoObject.m +++ b/SoObjects/SOGo/SOGoObject.m @@ -24,6 +24,9 @@ Please use gnustep-base instead. #endif +#import + +#import #import #import #import @@ -35,6 +38,7 @@ #import #import #import +#import #import #import #import @@ -52,6 +56,7 @@ #import "SOGoDAVRendererTypes.h" #import "NSArray+Utilities.h" +#import "NSDictionary+Utilities.h" #import "NSString+Utilities.h" #import "SOGoObject.h" @@ -179,6 +184,35 @@ static BOOL kontactGroupDAV = YES; // asDefaultForPermission: SoPerm_WebDAVAccess]; } ++ (NSString *) globallyUniqueObjectId +{ + /* + 4C08AE1A-A808-11D8-AC5A-000393BBAFF6 + SOGo-Web-28273-18283-288182 + printf( "%x", *(int *) &f); + */ + static int pid = 0; + static int sequence = 0; + static float rndm = 0; + float f; + + if (pid == 0) + { /* break if we fork ;-) */ + pid = getpid(); + rndm = random(); + } + sequence++; + f = [[NSDate date] timeIntervalSince1970]; + + return [NSString stringWithFormat:@"%0X-%0X-%0X-%0X", + pid, (int) f, sequence++, random]; +} + +- (NSString *) globallyUniqueObjectId +{ + return [[self class] globallyUniqueObjectId]; +} + + (void) _fillDictionary: (NSMutableDictionary *) dictionary withDAVMethods: (NSString *) firstMethod, ... { @@ -447,6 +481,30 @@ static BOOL kontactGroupDAV = YES; return container; } +- (NSArray *) pathArrayToSOGoObject +{ + NSMutableArray *realPathArray; + NSString *objectName; + NSArray *objectDescription; + + realPathArray + = [NSMutableArray arrayWithArray: [self pathArrayToSoObject]]; + if ([realPathArray count] > 2) + { + objectName = [realPathArray objectAtIndex: 2]; + objectDescription = [objectName componentsSeparatedByString: @"_"]; + if ([objectDescription count] > 1) + { + [realPathArray replaceObjectAtIndex: 0 + withObject: [objectDescription objectAtIndex: 0]]; + [realPathArray replaceObjectAtIndex: 2 + withObject: [objectDescription objectAtIndex: 1]]; + } + } + + return realPathArray; +} + /* ownership */ - (void) setOwner: (NSString *) newOwner @@ -493,14 +551,16 @@ static BOOL kontactGroupDAV = YES; /* looking up shared objects */ -- (SOGoUserFolder *)lookupUserFolder { +- (SOGoUserFolder *) lookupUserFolder +{ if (![container respondsToSelector:_cmd]) return nil; return [container lookupUserFolder]; } -- (SOGoGroupsFolder *)lookupGroupsFolder { +- (SOGoGroupsFolder *) lookupGroupsFolder +{ return [[self lookupUserFolder] lookupGroupsFolder]; } @@ -905,6 +965,34 @@ static BOOL kontactGroupDAV = YES; return nil; } +- (NSString *) labelForKey: (NSString *) key +{ + NSString *userLanguage, *label; + NSArray *paths; + NSBundle *bundle; + NSDictionary *strings; + + bundle = [NSBundle bundleForClass: [self class]]; + if (!bundle) + bundle = [NSBundle mainBundle]; + + userLanguage = [[context activeUser] language]; + paths = [bundle pathsForResourcesOfType: @"strings" + inDirectory: [NSString stringWithFormat: @"%@.lproj", userLanguage] + forLocalization: userLanguage]; + if ([paths count] > 0) + { + strings = [NSDictionary dictionaryFromStringsFile: [paths objectAtIndex: 0]]; + label = [strings objectForKey: key]; + if (!label) + label = key; + } + else + label = key; + + return label; +} + /* description */ - (void)appendAttributesToDescription:(NSMutableString *)_ms { diff --git a/SoObjects/SOGo/SOGoParentFolder.h b/SoObjects/SOGo/SOGoParentFolder.h new file mode 100644 index 00000000..c54d08f8 --- /dev/null +++ b/SoObjects/SOGo/SOGoParentFolder.h @@ -0,0 +1,51 @@ +/* SOGoParentFolder.h - this file is part of SOGo + * + * Copyright (C) 2006, 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 SOGOPARENTFOLDERS_H +#define SOGOPARENTFOLDERS_H + +#import "SOGoObject.h" + +@class NSMutableDictionary; +@class NSString; +@class WOResponse; + +@interface SOGoParentFolder : SOGoObject +{ + NSMutableDictionary *subFolders; + NSString *OCSPath; + Class subFolderClass; +} + ++ (NSString *) gcsFolderType; ++ (Class) subFolderClass; + +- (void) setBaseOCSPath: (NSString *) newOCSPath; + +- (NSArray *) subFolders; + +- (NSException *) newFolderWithName: (NSString *) name + nameInContainer: (NSString **) newNameInContainer; + +@end + +#endif /* SOGOPARENTFOLDERS_H */ diff --git a/SoObjects/SOGo/SOGoParentFolder.m b/SoObjects/SOGo/SOGoParentFolder.m new file mode 100644 index 00000000..6bdf0f78 --- /dev/null +++ b/SoObjects/SOGo/SOGoParentFolder.m @@ -0,0 +1,285 @@ +/* SOGoParentFolder.m - this file is part of SOGo + * + * Copyright (C) 2006, 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 "SOGoFolder.h" +#import "SOGoUser.h" + +#import "SOGoParentFolder.h" + +@implementation SOGoParentFolder + +- (id) init +{ + if ((self = [super init])) + { + subFolders = nil; + OCSPath = nil; + subFolderClass = Nil; + } + + return self; +} + +- (void) dealloc +{ + [subFolders release]; + [OCSPath release]; + [super dealloc]; +} + ++ (Class) subFolderClass +{ + [self subclassResponsibility: _cmd]; + + return Nil; +} + ++ (NSString *) gcsFolderType +{ + [self subclassResponsibility: _cmd]; + + return nil; +} + +- (void) setBaseOCSPath: (NSString *) newOCSPath +{ + ASSIGN (OCSPath, newOCSPath); +} + +- (NSString *) defaultFolderName +{ + return @"Personal"; +} + +- (void) _fetchPersonalFolders: (NSString *) sql + withChannel: (EOAdaptorChannel *) fc +{ + NSArray *attrs; + NSDictionary *row; + SOGoFolder *folder; + BOOL hasPersonal; + NSString *key, *path, *personalName; + + if (!subFolderClass) + subFolderClass = [[self class] subFolderClass]; + + hasPersonal = NO; + [fc evaluateExpressionX: sql]; + attrs = [fc describeResults: NO]; + row = [fc fetchAttributes: attrs withZone: NULL]; + while (row) + { + folder + = [subFolderClass objectWithName: [row objectForKey: @"c_path4"] + inContainer: self]; + key = [row objectForKey: @"c_path4"]; + hasPersonal = (hasPersonal || [key isEqualToString: @"personal"]); + [folder setOCSPath: [NSString stringWithFormat: @"%@/%@", + OCSPath, key]]; + [subFolders setObject: folder forKey: key]; + row = [fc fetchAttributes: attrs withZone: NULL]; + } + + if (!hasPersonal) + { + folder = [subFolderClass objectWithName: @"personal" inContainer: self]; + personalName = [self labelForKey: [self defaultFolderName]]; + [folder setDisplayName: personalName]; + path = [NSString stringWithFormat: @"/Users/%@/%@/personal", + [self ownerInContext: context], + nameInContainer]; + [folder setOCSPath: path]; + [subFolders setObject: folder forKey: @"personal"]; + } +} + +- (void) appendPersonalSources +{ + GCSChannelManager *cm; + EOAdaptorChannel *fc; + NSURL *folderLocation; + NSString *sql, *gcsFolderType; + + cm = [GCSChannelManager defaultChannelManager]; + folderLocation + = [[GCSFolderManager defaultFolderManager] folderInfoLocation]; + fc = [cm acquireOpenChannelForURL: folderLocation]; + if (fc) + { + gcsFolderType = [[self class] gcsFolderType]; + + sql + = [NSString stringWithFormat: (@"SELECT c_path4 FROM %@" + @" WHERE c_path2 = '%@'" + @" AND c_folder_type = '%@'"), + [folderLocation gcsTableName], + [self ownerInContext: context], + gcsFolderType]; + [self _fetchPersonalFolders: sql withChannel: fc]; + [cm releaseChannel: fc]; +// sql = [sql stringByAppendingFormat:@" WHERE %@ = '%@'", +// uidColumnName, [self uid]]; + } +} + +- (void) appendSystemSources +{ +} + +- (void) appendSubscribedSources +{ + NSArray *subscribedReferences; + NSUserDefaults *settings; + NSEnumerator *allKeys; + NSString *currentKey; + SOGoFolder *subscribedFolder; + + settings = [[context activeUser] userSettings]; + subscribedReferences = [[settings objectForKey: nameInContainer] + objectForKey: @"SubscribedFolders"]; + if ([subscribedReferences isKindOfClass: [NSArray class]]) + { + allKeys = [subscribedReferences objectEnumerator]; + currentKey = [allKeys nextObject]; + while (currentKey) + { + subscribedFolder + = [subFolderClass folderWithSubscriptionReference: currentKey + inContainer: self]; + [subFolders setObject: subscribedFolder + forKey: [subscribedFolder nameInContainer]]; + currentKey = [allKeys nextObject]; + } + } +} + +- (NSException *) newFolderWithName: (NSString *) name + nameInContainer: (NSString **) newNameInContainer +{ + NSString *newFolderID; + SOGoFolder *newFolder; + NSException *error; + + if (!subFolderClass) + subFolderClass = [[self class] subFolderClass]; + + *newNameInContainer = nil; + newFolderID = [self globallyUniqueObjectId]; + newFolder = [subFolderClass objectWithName: newFolderID inContainer: self]; + if ([newFolder isKindOfClass: [NSException class]]) + error = (NSException *) newFolder; + else + { + [newFolder setDisplayName: name]; + [newFolder setOCSPath: [NSString stringWithFormat: @"%@/%@", + OCSPath, newFolderID]]; + if ([newFolder create]) + { + error = nil; + *newNameInContainer = newFolderID; + } + else + error = [NSException exceptionWithHTTPStatus: 400 + reason: @"The new folder could not be created"]; + } + + return error; +} + +- (void) initSubFolders +{ + NSString *login; + + if (!subFolders) + { + subFolders = [NSMutableDictionary new]; + [self appendPersonalSources]; + [self appendSystemSources]; + login = [[context activeUser] login]; + if ([login isEqualToString: owner]) + [self appendSubscribedSources]; + } +} + +- (id) lookupName: (NSString *) name + inContext: (WOContext *) lookupContext + acquire: (BOOL) acquire +{ + id obj; + + /* first check attributes directly bound to the application */ + obj = [super lookupName: name inContext: lookupContext acquire: NO]; + if (!obj) + { + if (!subFolders) + [self initSubFolders]; + + obj = [subFolders objectForKey: name]; + } + + return obj; +} + +- (NSArray *) toManyRelationshipKeys +{ + if (!subFolders) + [self initSubFolders]; + + return [subFolders allKeys]; +} + +- (NSArray *) subFolders +{ + if (!subFolders) + [self initSubFolders]; + + return [subFolders allValues]; +} + +/* acls */ +- (NSArray *) aclsForUser: (NSString *) uid +{ + return nil; +} + +- (BOOL) davIsCollection +{ + return YES; +} + +- (NSString *) davContentType +{ + return @"httpd/unix-directory"; +} + +@end diff --git a/SoObjects/SOGo/SOGoUserFolder.m b/SoObjects/SOGo/SOGoUserFolder.m index 10194d27..84297af2 100644 --- a/SoObjects/SOGo/SOGoUserFolder.m +++ b/SoObjects/SOGo/SOGoUserFolder.m @@ -26,7 +26,7 @@ #import #import -#import +#import #import #import #import @@ -117,7 +117,7 @@ - (NSString *) ocsPrivateCalendarPath { - return [[self ocsUserPath] stringByAppendingString:@"/Calendar/personal"]; + return [[self ocsUserPath] stringByAppendingString:@"/Calendar"]; } - (NSString *) ocsPrivateContactsPath @@ -134,15 +134,15 @@ // : [super permissionForKey: key]); // } -- (SOGoAppointmentFolder *) privateCalendar: (NSString *) _key - inContext: (WOContext *) _ctx +- (SOGoAppointmentFolders *) privateCalendars: (NSString *) _key + inContext: (WOContext *) _ctx { - SOGoAppointmentFolder *calendar; + SOGoAppointmentFolders *calendars; - calendar = [$(@"SOGoAppointmentFolder") objectWithName: _key inContainer: self]; - [calendar setOCSPath: [self ocsPrivateCalendarPath]]; + calendars = [$(@"SOGoAppointmentFolders") objectWithName: _key inContainer: self]; + [calendars setBaseOCSPath: [self ocsPrivateCalendarPath]]; - return calendar; + return calendars; } - (SOGoContactFolders *) privateContacts: (NSString *) _key @@ -185,7 +185,7 @@ if (!obj) { if ([_key isEqualToString: @"Calendar"]) - obj = [self privateCalendar: @"Calendar" inContext: _ctx]; + obj = [self privateCalendars: @"Calendar" inContext: _ctx]; // if (![_key isEqualToString: @"Calendar"]) // obj = [obj lookupName: [_key pathExtension] // inContext: _ctx acquire: NO]; diff --git a/UI/Common/English.lproj/Localizable.strings b/UI/Common/English.lproj/Localizable.strings index 9af10007..9f20244c 100644 --- a/UI/Common/English.lproj/Localizable.strings +++ b/UI/Common/English.lproj/Localizable.strings @@ -32,3 +32,4 @@ "You cannot subscribe to a folder that you own!" = "You cannot subscribe to a folder that you own!"; "Unable to unsubscribe from that folder!" = "Unable to unsubscribe from that folder!"; "You cannot unsubscribe from a folder that you own!" = "You cannot unsubscribe from a folder that you own!"; +"Unable to rename that folder!" = "Unable to rename that folder!"; diff --git a/UI/Common/French.lproj/Localizable.strings b/UI/Common/French.lproj/Localizable.strings index 18a984d3..e77de6d7 100644 --- a/UI/Common/French.lproj/Localizable.strings +++ b/UI/Common/French.lproj/Localizable.strings @@ -33,3 +33,4 @@ "You cannot subscribe to a folder that you own!" = "Impossible de vous abonner à un dossier qui vous appartient."; "Unable to unsubscribe from that folder!" = "Impossible de se désabonner de ce dossier."; "You cannot unsubscribe from a folder that you own!" = "Impossible de vous désabonner d'un dossier qui vous appartient."; +"Unable to rename that folder!" = "Impossible de renommer ce dossier."; diff --git a/UI/Common/GNUmakefile b/UI/Common/GNUmakefile index ebdc4fea..80a1a594 100644 --- a/UI/Common/GNUmakefile +++ b/UI/Common/GNUmakefile @@ -17,6 +17,7 @@ CommonUI_OBJC_FILES += \ UIxAclEditor.m \ UIxObjectActions.m \ UIxFolderActions.m \ + UIxParentFolderActions.m \ UIxElemBuilder.m \ UIxTabView.m \ UIxTabItem.m \ diff --git a/UI/Common/German.lproj/Localizable.strings b/UI/Common/German.lproj/Localizable.strings index d1b779ba..9a0bb696 100644 --- a/UI/Common/German.lproj/Localizable.strings +++ b/UI/Common/German.lproj/Localizable.strings @@ -35,3 +35,4 @@ "You cannot subscribe to a folder that you own!" = "Unmöglich sich an einem Ordner zu abonnieren, der Ihnen selbst gehört."; "Unable to unsubscribe from that folder!" = "Unmöglich sich von diesem Ordner zu des-abonnieren."; "You cannot unsubscribe from a folder that you own!" = "Unmöglich sich von einem Ordner zu des-abonnieren, der Ihnen selbst gehört."; +"Unable to rename that folder!" = "Unable to rename that folder!"; diff --git a/UI/Common/UIxFolderActions.h b/UI/Common/UIxFolderActions.h index b74a20a3..8794bbea 100644 --- a/UI/Common/UIxFolderActions.h +++ b/UI/Common/UIxFolderActions.h @@ -41,7 +41,7 @@ NSString *owner; NSString *login; NSString *baseFolder; - NSMutableString *subscriptionPointer; + NSString *subscriptionPointer; NSMutableDictionary *moduleSettings; BOOL isMailInvitation; } diff --git a/UI/Common/UIxFolderActions.m b/UI/Common/UIxFolderActions.m index ca14b9d5..e152ed33 100644 --- a/UI/Common/UIxFolderActions.m +++ b/UI/Common/UIxFolderActions.m @@ -33,10 +33,13 @@ #import #import +#import #import -#import +#import #import +#import "WODirectAction+SOGo.h" + #import "UIxFolderActions.h" @implementation UIxFolderActions @@ -72,27 +75,24 @@ } [ud setObject: moduleSettings forKey: baseFolder]; - subscriptionPointer = [NSMutableString stringWithFormat: @"%@:%@", - owner, baseFolder]; - if ([baseFolder isEqualToString: @"Contacts"]) - [subscriptionPointer appendFormat: @"/%@", - [clientObject nameInContainer]]; + subscriptionPointer = [NSString stringWithFormat: @"%@:%@/%@", + owner, baseFolder, + [clientObject nameInContainer]]; mailInvitationParam = [[context request] formValueForKey: @"mail-invitation"]; isMailInvitation = [mailInvitationParam boolValue]; } -- (WOResponse *) _realActionWithFolderName: (NSDictionary *) folderDict +- (WOResponse *) _realSubscribe: (BOOL) reallyDo { WOResponse *response; - NSMutableDictionary *folderSubscription; + NSMutableArray *folderSubscription; NSString *mailInvitationURL; - response = [context response]; if ([owner isEqualToString: login]) { - [response setStatus: 403]; + response = [self responseWithStatus: 403]; [response appendContentString: @"You cannot (un)subscribe to a folder that you own!"]; } @@ -100,17 +100,17 @@ { folderSubscription = [moduleSettings objectForKey: @"SubscribedFolders"]; - if (!folderSubscription) + if (!(folderSubscription + && [folderSubscription isKindOfClass: [NSMutableArray class]])) { - folderSubscription = [NSMutableDictionary dictionary]; + folderSubscription = [NSMutableArray array]; [moduleSettings setObject: folderSubscription forKey: @"SubscribedFolders"]; } - if (folderDict) - [folderSubscription setObject: folderDict - forKey: subscriptionPointer]; + if (reallyDo) + [folderSubscription addObjectUniquely: subscriptionPointer]; else - [folderSubscription removeObjectForKey: subscriptionPointer]; + [folderSubscription removeObject: subscriptionPointer]; [ud synchronize]; @@ -119,12 +119,12 @@ mailInvitationURL = [[clientObject soURLToBaseContainerForCurrentUser] absoluteString]; - [response setStatus: 302]; + response = [self responseWithStatus: 302]; [response setHeader: mailInvitationURL forKey: @"location"]; } else - [response setStatus: 204]; + response = [self responseWith204]; } return response; @@ -132,73 +132,46 @@ - (WOResponse *) subscribeAction { - NSString *email; - NSMutableDictionary *folderDict; - NSString *folderName; - [self _setupContext]; - email = [NSString stringWithFormat: @"%@ <%@>", - [um getCNForUID: owner], - [um getEmailForUID: owner]]; - if ([baseFolder isEqualToString: @"Contacts"]) - folderName = [NSString stringWithFormat: @"%@ (%@)", - [clientObject nameInContainer], email]; - else - folderName = email; - - folderDict = [NSMutableDictionary dictionary]; - [folderDict setObject: folderName forKey: @"displayName"]; - [folderDict setObject: [NSNumber numberWithBool: NO] forKey: @"active"]; - return [self _realActionWithFolderName: folderDict]; + return [self _realSubscribe: YES]; } - (WOResponse *) unsubscribeAction { [self _setupContext]; - return [self _realActionWithFolderName: nil]; + return [self _realSubscribe: NO]; } - (WOResponse *) canAccessContentAction { - WOResponse *response; - - response = [context response]; - [response setStatus: 204]; - - return response; + return [self responseWith204]; } - (WOResponse *) _realFolderActivation: (BOOL) makeActive { - WOResponse *response; - NSMutableDictionary *folderSubscription, *folderDict; - NSNumber *active; - - response = [context response]; + NSMutableArray *folderSubscription; + NSString *folderName; [self _setupContext]; - active = [NSNumber numberWithBool: makeActive]; - if ([owner isEqualToString: login]) - [moduleSettings setObject: active forKey: @"activateUserFolder"]; - else + folderSubscription + = [moduleSettings objectForKey: @"ActiveFolders"]; + if (!folderSubscription) { - folderSubscription - = [moduleSettings objectForKey: @"SubscribedFolders"]; - if (folderSubscription) - { - folderDict = [folderSubscription objectForKey: subscriptionPointer]; - if (folderDict) - [folderDict setObject: active - forKey: @"active"]; - } + folderSubscription = [NSMutableArray array]; + [moduleSettings setObject: folderSubscription forKey: @"ActiveFolders"]; } + folderName = [clientObject nameInContainer]; + if (makeActive) + [folderSubscription addObjectUniquely: folderName]; + else + [folderSubscription removeObject: folderName]; + [ud synchronize]; - [response setStatus: 204]; - return response; + return [self responseWith204]; } - (WOResponse *) activateFolderAction @@ -211,4 +184,36 @@ return [self _realFolderActivation: NO]; } +- (WOResponse *) deleteFolderAction +{ + WOResponse *response; + + response = (WOResponse *) [[self clientObject] delete]; + if (!response) + response = [self responseWith204]; + + return response; +} + +- (WOResponse *) renameFolderAction +{ + WOResponse *response; + NSString *folderName; + + folderName = [[context request] formValueForKey: @"name"]; + if ([folderName length] > 0) + { + clientObject = [self clientObject]; + [clientObject renameTo: folderName]; + response = [self responseWith204]; + } + else + { + response = [self responseWithStatus: 500]; + [response appendContentString: @"Missing 'name' parameter."]; + } + + return response; +} + @end diff --git a/UI/Common/WODirectAction+SOGo.h b/UI/Common/WODirectAction+SOGo.h index 92ec0320..067046b6 100644 --- a/UI/Common/WODirectAction+SOGo.h +++ b/UI/Common/WODirectAction+SOGo.h @@ -30,6 +30,8 @@ @interface WODirectAction (SOGoExtension) +- (WOResponse *) responseWithStatus: (unsigned int) status; +- (WOResponse *) responseWith204; - (WOResponse *) redirectToLocation: (NSString *) newLocation; @end diff --git a/UI/Common/WODirectAction+SOGo.m b/UI/Common/WODirectAction+SOGo.m index ddeea2aa..13c8b86d 100644 --- a/UI/Common/WODirectAction+SOGo.m +++ b/UI/Common/WODirectAction+SOGo.m @@ -28,15 +28,30 @@ @implementation WODirectAction (SOGoExtension) -- (WOResponse *) redirectToLocation: (NSString *) newLocation +- (WOResponse *) responseWithStatus: (unsigned int) status { WOResponse *response; response = [context response]; - [response setStatus: 302 /* moved */]; + [response setStatus: status]; + + return response; +} + +- (WOResponse *) responseWith204 +{ + return [self responseWithStatus: 204]; +} + +- (WOResponse *) redirectToLocation: (NSString *) newLocation +{ + WOResponse *response; + + response = [self responseWithStatus: 302]; [response setHeader: newLocation forKey: @"location"]; return response; } + @end diff --git a/UI/Common/product.plist b/UI/Common/product.plist index 9e014e8e..704880c1 100644 --- a/UI/Common/product.plist +++ b/UI/Common/product.plist @@ -1,95 +1,114 @@ -{ /* -*-javascript-*- */ - requires = ( MAIN, Mailer ); +{ /* -*-java-*- */ + requires = ( MAIN, Mailer ); + + publicResources = ( + calendar.css, + uix.css, + menu_logo_top.gif, + line_left.gif, + line_stretch.gif, + line_right.gif, + box_topleft.gif, + box_top.gif, + box_topright.gif, + box_left.gif, + box_right.gif, + box_botleft.gif, + box_bottom.gif, + box_botright.gif, + tab_selected.gif, + tab_.gif, + corner_right.gif, + closewindow.gif, + OGoLogo.gif, + upward_sorted.gif, + downward_sorted.gif, + non_sorted.gif + ); - publicResources = ( - calendar.css, - uix.css, - menu_logo_top.gif, - line_left.gif, - line_stretch.gif, - line_right.gif, - box_topleft.gif, - box_top.gif, - box_topright.gif, - box_left.gif, - box_right.gif, - box_botleft.gif, - box_bottom.gif, - box_botright.gif, - tab_selected.gif, - tab_.gif, - corner_right.gif, - closewindow.gif, - OGoLogo.gif, - upward_sorted.gif, - downward_sorted.gif, - non_sorted.gif - ); + factories = { + }; - factories = { - }; - - categories = { - SOGoObject = { - methods = { - addUserInAcls = { - protectedBy = "SaveAcls"; - actionClass = "UIxObjectActions"; - actionName = "addUserInAcls"; - }; - removeUserFromAcls = { - protectedBy = "SaveAcls"; - actionClass = "UIxObjectActions"; - actionName = "removeUserFromAcls"; - }; - acls = { - protectedBy = "ReadAcls"; - pageName = "UIxAclEditor"; - }; - saveAcls = { - protectedBy = "SaveAcls"; - pageName = "UIxAclEditor"; - actionName = "saveAcls"; - }; - userRights = { - protectedBy = "ReadAcls"; - pageName = "UIxUserRightsEditor"; - }; - saveUserRights = { - protectedBy = "ReadAcls"; - pageName = "UIxUserRightsEditor"; - actionName = "saveUserRights"; - }; - }; + categories = { + SOGoObject = { + methods = { + addUserInAcls = { + protectedBy = "SaveAcls"; + actionClass = "UIxObjectActions"; + actionName = "addUserInAcls"; + }; + removeUserFromAcls = { + protectedBy = "SaveAcls"; + actionClass = "UIxObjectActions"; + actionName = "removeUserFromAcls"; + }; + acls = { + protectedBy = "ReadAcls"; + pageName = "UIxAclEditor"; + }; + saveAcls = { + protectedBy = "SaveAcls"; + pageName = "UIxAclEditor"; + actionName = "saveAcls"; + }; + userRights = { + protectedBy = "ReadAcls"; + pageName = "UIxUserRightsEditor"; + }; + saveUserRights = { + protectedBy = "ReadAcls"; + pageName = "UIxUserRightsEditor"; + actionName = "saveUserRights"; + }; + }; + }; + SOGoParentFolder = { + methods = { + createFolder = { + protectedBy = "View"; + actionClass = "UIxParentFolderActions"; + actionName = "createFolder"; + }; }; - SOGoFolder = { - methods = { - subscribe = { - protectedBy = ""; - actionClass = "UIxFolderActions"; - actionName = "subscribe"; - }; - unsubscribe = { - protectedBy = ""; - actionClass = "UIxFolderActions"; - actionName = "unsubscribe"; - }; - canAccessContent = { - protectedBy = ""; - actionClass = "UIxFolderActions"; - actionName = "canAccessContent"; - }; - activateFolder = { - protectedBy = ""; - actionClass = "UIxFolderActions"; - actionName = "activateFolder"; - }; - deactivateFolder = { - protectedBy = ""; - actionClass = "UIxFolderActions"; - actionName = "deactivateFolder"; - }; - }; + }; + SOGoFolder = { + methods = { + subscribe = { + protectedBy = ""; + actionClass = "UIxFolderActions"; + actionName = "subscribe"; + }; + unsubscribe = { + protectedBy = ""; + actionClass = "UIxFolderActions"; + actionName = "unsubscribe"; + }; + canAccessContent = { + protectedBy = ""; + actionClass = "UIxFolderActions"; + actionName = "canAccessContent"; + }; + activateFolder = { + protectedBy = ""; + actionClass = "UIxFolderActions"; + actionName = "activateFolder"; + }; + deactivateFolder = { + protectedBy = ""; + actionClass = "UIxFolderActions"; + actionName = "deactivateFolder"; + }; + deleteFolder = { + protectedBy = "SaveAcls"; /* a hack to force "owner" */ + actionClass = "UIxFolderActions"; + actionName = "deleteFolder"; + }; + renameFolder = { + protectedBy = "SaveAcls"; + actionClass = "UIxFolderActions"; + actionName = "renameFolder"; + }; }; - }; + }; + }; } diff --git a/UI/Contacts/English.lproj/Localizable.strings b/UI/Contacts/English.lproj/Localizable.strings index 9f8a0ac2..f7d9db02 100644 --- a/UI/Contacts/English.lproj/Localizable.strings +++ b/UI/Contacts/English.lproj/Localizable.strings @@ -97,6 +97,8 @@ "Are you sure you want to delete the selected address book?" = "Are you sure you want to delete the selected address book?"; +"Address Book Name" = "Address Book Name"; + "You cannot subscribe to a folder that you own!" = "You cannot subscribe to a folder that you own!"; "Unable to subscribe to that folder!" diff --git a/UI/Contacts/French.lproj/Localizable.strings b/UI/Contacts/French.lproj/Localizable.strings index f0ee23b3..75749406 100644 --- a/UI/Contacts/French.lproj/Localizable.strings +++ b/UI/Contacts/French.lproj/Localizable.strings @@ -110,6 +110,8 @@ "Are you sure you want to delete the selected address book?" = "Voulez-vous vraiment supprimer le carnet d'adresses sélectionné ?"; +"Address Book Name" = "Nom du carnet d'adresses"; + "You cannot subscribe to a folder that you own!" = "Vous ne pouvez pas vous inscrire à un dossier qui vous appartient!"; "Unable to subscribe to that folder!" diff --git a/UI/Contacts/German.lproj/Localizable.strings b/UI/Contacts/German.lproj/Localizable.strings index 9f4500ab..19bd4387 100644 --- a/UI/Contacts/German.lproj/Localizable.strings +++ b/UI/Contacts/German.lproj/Localizable.strings @@ -109,6 +109,8 @@ "Are you sure you want to delete the selected address book?" = "Voulez-vous vraiment supprimer le carnet d'adresses sélectionné ?"; +"Address Book Name" = "Address Book Name"; + "You cannot subscribe to a folder that you own!" = "Vous ne pouvez pas vous inscrire à un dossier qui vous appartient!"; "Unable to subscribe to that folder!" diff --git a/UI/Contacts/Toolbars/SOGoContactFolder.toolbar b/UI/Contacts/Toolbars/SOGoContactFolder.toolbar index 2bcf4b68..6c6971f5 100644 --- a/UI/Contacts/Toolbars/SOGoContactFolder.toolbar +++ b/UI/Contacts/Toolbars/SOGoContactFolder.toolbar @@ -4,26 +4,31 @@ jsLink="js_card"; label="New Card"; image="new-card.png"; - onclick = "newContact(this); return false;"; }, + onclick = "newContact(this); return false;"; + tooltip = "Create a new address book card"; }, { link = "new_list"; enabled = "NO"; label="New List"; - image="new-list.png"; } + image="new-list.png"; + tooltip = "Create a new list"; } ), ( { link = "edit"; label = "Modify"; onclick = "return onToolbarEditSelectedContacts(this);"; - image = "properties.png"; }, + image = "properties.png"; + tooltip = "Edit the selected card"; }, { link = "write"; label="Write"; onclick = "return onToolbarWriteToSelectedContacts(this);"; - image="write.png"; } + image="write.png"; + tooltip = "Send a mail message"; } ), ( { link = "delete"; label="Delete"; onclick = "return uixDeleteSelectedContacts(this);"; - image="delete.png"; } + image="delete.png"; + tooltip = "Delete selected card or address book"; } ) ) diff --git a/UI/Contacts/UIxContactEditor.m b/UI/Contacts/UIxContactEditor.m index 578c65ce..6f395411 100644 --- a/UI/Contacts/UIxContactEditor.m +++ b/UI/Contacts/UIxContactEditor.m @@ -584,8 +584,8 @@ id co; co = [self clientObject]; - if ([[co class] respondsToSelector: @selector (globallyUniqueObjectId)]) - objectId = [[[self clientObject] class] globallyUniqueObjectId]; + if ([co respondsToSelector: @selector (globallyUniqueObjectId)]) + objectId = [co globallyUniqueObjectId]; else objectId = nil; diff --git a/UI/Contacts/UIxContactFoldersView.m b/UI/Contacts/UIxContactFoldersView.m index 1509f4a5..fa43d810 100644 --- a/UI/Contacts/UIxContactFoldersView.m +++ b/UI/Contacts/UIxContactFoldersView.m @@ -52,9 +52,7 @@ WORequest *request; folders = [self clientObject]; - action = [NSString stringWithFormat: @"../%@/%@", - [folders defaultSourceName], - actionName]; + action = [NSString stringWithFormat: @"../personal/%@", actionName]; request = [[self context] request]; @@ -75,21 +73,6 @@ return [self _selectActionForApplication: @"new"]; } -- (id ) newAbAction -{ - id response; - NSString *name; - - name = [self queryParameterForKey: @"name"]; - if ([name length] > 0) - response = [[self clientObject] newFolderWithName: name]; - else - response = [NSException exceptionWithHTTPStatus: 400 - reason: @"The name is missing"]; - - return response; -} - - (id) selectForMailerAction { return [self _selectActionForApplication: @"mailer-contacts"]; @@ -111,8 +94,7 @@ { uid = [currentContact objectForKey: @"c_uid"]; if (uid && ![results objectForKey: uid]) - [results setObject: currentContact - forKey: uid]; + [results setObject: currentContact forKey: uid]; currentContact = [folderResults nextObject]; } } @@ -191,50 +173,55 @@ return result; } -- (NSArray *) _gcsFoldersFromFolder: (SOGoContactFolders *) contactFolders +- (NSArray *) _subFoldersFromFolder: (SOGoParentFolder *) parentFolder { - NSMutableArray *gcsFolders; - NSEnumerator *contactSubfolders; - SOGoContactGCSFolder *currentContactFolder; - NSString *folderName, *displayName; + NSMutableArray *folders; + NSEnumerator *subfolders; + SOGoFolder *currentFolder; + NSString *folderName; NSMutableDictionary *currentDictionary; + SoSecurityManager *securityManager; - gcsFolders = [NSMutableArray new]; - [gcsFolders autorelease]; + securityManager = [SoSecurityManager sharedSecurityManager]; + +// return (([securityManager validatePermission: SoPerm_AccessContentsInformation +// onObject: contactFolder +// inContext: context] == nil) - contactSubfolders = [[contactFolders contactFolders] objectEnumerator]; - currentContactFolder = [contactSubfolders nextObject]; - while (currentContactFolder) + folders = [NSMutableArray new]; + [folders autorelease]; + + subfolders = [[parentFolder subFolders] objectEnumerator]; + currentFolder = [subfolders nextObject]; + while (currentFolder) { - if ([currentContactFolder - isKindOfClass: [SOGoContactGCSFolder class]]) + if (![securityManager validatePermission: SOGoPerm_AccessObject + onObject: currentFolder inContext: context]) { - displayName = [[currentContactFolder ocsFolder] folderName]; - if (displayName) - { - folderName = [NSString stringWithFormat: @"/Contacts/%@", - [currentContactFolder nameInContainer]]; - currentDictionary - = [NSMutableDictionary dictionaryWithCapacity: 3]; - [currentDictionary setObject: displayName forKey: @"displayName"]; - [currentDictionary setObject: folderName forKey: @"name"]; - [currentDictionary setObject: @"contact" forKey: @"type"]; - [gcsFolders addObject: currentDictionary]; - } + folderName = [NSString stringWithFormat: @"/%@/%@", + [parentFolder nameInContainer], + [currentFolder nameInContainer]]; + currentDictionary + = [NSMutableDictionary dictionaryWithCapacity: 3]; + [currentDictionary setObject: [currentFolder displayName] + forKey: @"displayName"]; + [currentDictionary setObject: folderName forKey: @"name"]; + [currentDictionary setObject: [currentFolder folderType] + forKey: @"type"]; + [folders addObject: currentDictionary]; } - currentContactFolder = [contactSubfolders nextObject]; + currentFolder = [subfolders nextObject]; } - return gcsFolders; + return folders; } - (NSArray *) _foldersForUID: (NSString *) uid ofType: (NSString *) folderType { NSObject *topFolder, *userFolder; - SOGoContactFolders *contactFolders; + SOGoParentFolder *parentFolder; NSMutableArray *folders; - NSMutableDictionary *currentDictionary; folders = [NSMutableArray new]; [folders autorelease]; @@ -243,23 +230,19 @@ userFolder = [topFolder lookupName: uid inContext: context acquire: NO]; /* FIXME: should be moved in the SOGo* classes. Maybe by having a SOGoFolderManager. */ -#warning this might need adjustments whenever we permit multiple calendar folders per-user if ([folderType length] == 0 || [folderType isEqualToString: @"calendar"]) { - currentDictionary = [NSMutableDictionary new]; - [currentDictionary autorelease]; - [currentDictionary setObject: [self labelForKey: @"Calendar"] - forKey: @"displayName"]; - [currentDictionary setObject: @"/Calendar" forKey: @"name"]; - [currentDictionary setObject: @"calendar" forKey: @"type"]; - [folders addObject: currentDictionary]; + parentFolder = [userFolder lookupName: @"Calendar" + inContext: context acquire: NO]; + [folders + addObjectsFromArray: [self _subFoldersFromFolder: parentFolder]]; } if ([folderType length] == 0 || [folderType isEqualToString: @"contact"]) { - contactFolders = [userFolder lookupName: @"Contacts" - inContext: context acquire: NO]; + parentFolder = [userFolder lookupName: @"Contacts" + inContext: context acquire: NO]; [folders - addObjectsFromArray: [self _gcsFoldersFromFolder: contactFolders]]; + addObjectsFromArray: [self _subFoldersFromFolder: parentFolder]]; } return folders; @@ -350,31 +333,30 @@ return result; } -- (SOGoContactGCSFolder *) contactFolderForUID: (NSString *) uid -{ - SOGoFolder *upperContainer; - SOGoUserFolder *userFolder; - SOGoContactFolders *contactFolders; - SOGoContactGCSFolder *contactFolder; - SoSecurityManager *securityManager; - - upperContainer = [[[self clientObject] container] container]; - userFolder = [SOGoUserFolder objectWithName: uid - inContainer: upperContainer]; - contactFolders = [SOGoContactFolders objectWithName: @"Contacts" - inContainer: userFolder]; - contactFolder = [SOGoContactGCSFolder objectWithName: @"personal" - inContainer: contactFolders]; - [contactFolder - setOCSPath: [NSString stringWithFormat: @"/Users/%@/Contacts/personal", uid]]; - [contactFolder setOwner: uid]; - - securityManager = [SoSecurityManager sharedSecurityManager]; - - return (([securityManager validatePermission: SoPerm_AccessContentsInformation - onObject: contactFolder - inContext: context] == nil) - ? contactFolder : nil); -} +// - (SOGoContactGCSFolder *) contactFolderForUID: (NSString *) uid +// { +// SOGoFolder *upperContainer; +// SOGoUserFolder *userFolder; +// SOGoContactFolders *contactFolders; +// SOGoContactGCSFolder *contactFolder; +// SoSecurityManager *securityManager; + +// upperContainer = [[[self clientObject] container] container]; +// userFolder = [SOGoUserFolder objectWithName: uid +// inContainer: upperContainer]; +// contactFolders = [SOGoUserFolder lookupName: @"Contacts" +// inContext: context +// acquire: NO]; +// contactFolder = [contactFolders lookupName: @"personal" +// inContext: context +// acquire: NO]; + +// securityManager = [SoSecurityManager sharedSecurityManager]; + +// return (([securityManager validatePermission: SoPerm_AccessContentsInformation +// onObject: contactFolder +// inContext: context] == nil) +// ? contactFolder : nil); +// } @end diff --git a/UI/Contacts/UIxContactsListView.m b/UI/Contacts/UIxContactsListView.m index 45dadfa6..e6b2e2ad 100644 --- a/UI/Contacts/UIxContactsListView.m +++ b/UI/Contacts/UIxContactsListView.m @@ -81,25 +81,6 @@ return selectorComponentClass; } -- (id ) deleteAction -{ - id result; - NSException *ex; - WOResponse *response; - - ex = [[self clientObject] delete]; - if (ex) - result = ex; - else - { - response = [context response]; - [response setStatus: 200]; - result = response; - } - - return result; -} - - (NSString *) defaultSortKey { return @"displayName"; diff --git a/UI/Contacts/UIxContactsListViewContainer.h b/UI/Contacts/UIxContactsListViewContainer.h index 5be9899f..eb0d72d8 100644 --- a/UI/Contacts/UIxContactsListViewContainer.h +++ b/UI/Contacts/UIxContactsListViewContainer.h @@ -31,28 +31,18 @@ @interface UIxContactsListViewContainer : UIxComponent { - NSString *foldersPrefix; NSString *selectorComponentClass; - NSString *currentAdditionalFolder; - NSDictionary *additionalFolders; id currentFolder; } - (void) setCurrentFolder: (id) folder; -- (NSString *) foldersPrefix; - - (NSArray *) contactFolders; - (NSString *) currentContactFolderId; +- (NSString *) currentContactFolderOwner; - (NSString *) currentContactFolderName; -- (NSArray *) additionalFolders; - -- (void) setCurrentAdditionalFolder: (NSString *) newCurrentAdditionalFolder; -- (NSString *) currentAdditionalFolder; -- (NSString *) currentAdditionalFolderName; - @end #endif /* UIXCONTACTSLISTVIEWCONTAINERBASE_H */ diff --git a/UI/Contacts/UIxContactsListViewContainer.m b/UI/Contacts/UIxContactsListViewContainer.m index 2bf4f0c0..f472b156 100644 --- a/UI/Contacts/UIxContactsListViewContainer.m +++ b/UI/Contacts/UIxContactsListViewContainer.m @@ -29,6 +29,7 @@ #import #import +#import #import "UIxContactsListViewContainer.h" @@ -40,20 +41,12 @@ { if ((self = [super init])) { - foldersPrefix = nil; selectorComponentClass = nil; - additionalFolders = nil; } return self; } -- (void) dealloc -{ - [additionalFolders release]; - [super dealloc]; -} - - (void) setSelectorComponentClass: (NSString *) aComponentClass { selectorComponentClass = aComponentClass; @@ -85,43 +78,18 @@ currentFolder = folder; } -- (NSString *) foldersPrefix -{ - NSMutableArray *folders; - SOGoObject *currentObject; - - if (!foldersPrefix) - { - folders = [NSMutableArray new]; - [folders autorelease]; - - currentObject = [[self clientObject] container]; - while (![currentObject isKindOfClass: [SOGoContactFolders class]]) - { - [folders insertObject: [currentObject nameInContainer] atIndex: 0]; - currentObject = [currentObject container]; - } - - foldersPrefix = [folders componentsJoinedByString: @"/"]; - [foldersPrefix retain]; - } - - return foldersPrefix; -} - - (NSArray *) contactFolders { SOGoContactFolders *folderContainer; folderContainer = [[self clientObject] container]; - return [folderContainer contactFolders]; + return [folderContainer subFolders]; } - (NSString *) currentContactFolderId { - return [NSString stringWithFormat: @"%@/%@", - [self foldersPrefix], + return [NSString stringWithFormat: @"/%@", [currentFolder nameInContainer]]; } @@ -130,35 +98,9 @@ return [currentFolder displayName]; } -- (NSArray *) additionalFolders -{ - NSUserDefaults *ud; - - if (!additionalFolders) - { - ud = [[context activeUser] userSettings]; - additionalFolders - = [[ud objectForKey: @"Contacts"] objectForKey: @"SubscribedFolders"]; - [additionalFolders retain]; - } - - return [additionalFolders allKeys]; -} - -- (void) setCurrentAdditionalFolder: (NSString *) newCurrentAdditionalFolder -{ - currentAdditionalFolder = newCurrentAdditionalFolder; -} - -- (NSString *) currentAdditionalFolder -{ - return currentAdditionalFolder; -} - -- (NSString *) currentAdditionalFolderName +- (NSString *) currentContactFolderOwner { - return [[additionalFolders objectForKey: currentAdditionalFolder] - objectForKey: @"displayName"]; + return [currentFolder ownerInContext: context]; } - (BOOL) hasContactSelectionButtons diff --git a/UI/Contacts/product.plist b/UI/Contacts/product.plist index cff8e800..3602be7a 100644 --- a/UI/Contacts/product.plist +++ b/UI/Contacts/product.plist @@ -17,11 +17,6 @@ pageName = "UIxContactFoldersView"; actionName = "new"; }; - newAb = { - protectedBy = "View"; - pageName = "UIxContactFoldersView"; - actionName = "newAb"; - }; mailer-contacts = { protectedBy = "View"; pageName = "UIxContactFoldersView"; @@ -87,11 +82,6 @@ pageName = "UIxContactsListView"; actionName = "mailerContacts"; }; - delete = { - protectedBy = "SaveAcls"; /* a hack to force "owner" */ - pageName = "UIxContactsListView"; - actionName = "delete"; - }; userRights = { protectedBy = "ReadAcls"; pageName = "UIxContactsUserRightsEditor"; diff --git a/UI/MailerUI/English.lproj/Localizable.strings b/UI/MailerUI/English.lproj/Localizable.strings index 01d4440b..9e18ecd3 100644 --- a/UI/MailerUI/English.lproj/Localizable.strings +++ b/UI/MailerUI/English.lproj/Localizable.strings @@ -23,7 +23,7 @@ "Home" = "Home"; "Calendar" = "Calendar"; -"Addressbook" = "Addressbook"; +"Addressbook" = "Address Book"; "Mail" = "Mail"; "Right Administration" = "Right Administration"; @@ -71,7 +71,7 @@ "cc" = "Cc"; "bcc" = "Bcc"; -"Addressbook" = "Addressbook"; +"Addressbook" = "Address Book"; "Anais" = "Anais"; "Edit Draft..." = "Edit Draft..."; diff --git a/UI/MailerUI/Toolbars/SOGoDraftObject.toolbar b/UI/MailerUI/Toolbars/SOGoDraftObject.toolbar index 6355b0a0..c78528b9 100644 --- a/UI/MailerUI/Toolbars/SOGoDraftObject.toolbar +++ b/UI/MailerUI/Toolbars/SOGoDraftObject.toolbar @@ -5,23 +5,27 @@ onclick = "return clickedEditorSend(this);"; image = "tb-compose-send-flat-24x24.png"; cssClass = "tbicon_send"; - label = "Send"; }, + label = "Send"; + tooltip = "Send this message now"; }, { link = "#"; onclick = "return onContactAdd(null);"; image = "tb-compose-contacts-flat-24x24.png"; cssClass = "tbicon_addressbook"; - label = "Contacts"; }, + label = "Contacts"; + tooltip = "Select a recipient from an Address Book"; }, { link = "#"; isSafe = NO; onclick = "return clickedEditorAttach(this)"; image = "tb-compose-attach-flat-24x24.png"; cssClass = "tbicon_attach"; - label = "Attach"; }, + label = "Attach"; + tooltip = "Include an attachment"; }, { link = "#"; isSafe = NO; onclick = "return clickedEditorSave(this);"; image = "tb-mail-file-flat-24x24.png"; cssClass = "tbicon_save"; - label = "Save"; }, + label = "Save"; + tooltip = "Save this message"; }, ) ) diff --git a/UI/MailerUI/Toolbars/SOGoMailObject.toolbar b/UI/MailerUI/Toolbars/SOGoMailObject.toolbar index ac4ad5a8..a2ba2dbe 100644 --- a/UI/MailerUI/Toolbars/SOGoMailObject.toolbar +++ b/UI/MailerUI/Toolbars/SOGoMailObject.toolbar @@ -6,16 +6,21 @@ cssClass = "tbicon_getmail"; label = "Get Mail"; onclick = "return refreshMailbox(this);"; - }, + tooltip = "Get new messages"; }, { link = "#"; isSafe = NO; image = "tb-mail-write-flat-24x24.png"; onclick = "return onComposeMessage();"; - cssClass = "tbicon_compose"; label = "Write"; }, - { link = "#"; target = "addressbook"; + cssClass = "tbicon_compose"; + label = "Write"; + tooltip = "Create a new message"; }, + { link = "#"; + target = "addressbook"; onclick = "openAddressbook(this);return false;"; image = "tb-mail-addressbook-flat-24x24.png"; - cssClass = "tbicon_addressbook"; label = "Addressbook"; }, + cssClass = "tbicon_addressbook"; + label = "Addressbook"; + tooltip = "Go to address book"; }, ), ( // second group @@ -23,19 +28,23 @@ onclick = "return openMessageWindowsForSelection('reply');"; isSafe = NO; image = "tb-mail-reply-flat-24x24.png"; - cssClass = "tbicon_reply"; label = "Reply"; }, - + cssClass = "tbicon_reply"; + label = "Reply"; + tooltip = "Reply to the message"; }, { link = "replyall"; onclick = "return openMessageWindowsForSelection('replyall');"; isSafe = NO; image = "tb-mail-replyall-flat-24x24.png"; - cssClass = "tbicon_replyall"; label = "Reply All"; }, - + cssClass = "tbicon_replyall"; + label = "Reply All"; + tooltip = "Reply to sender and all recipients"; }, { link = "forward"; onclick = "return openMessageWindowsForSelection('forward');"; isSafe = NO; image = "tb-mail-forward-flat-24x24.png"; - cssClass = "tbicon_forward"; label = "Forward"; }, + cssClass = "tbicon_forward"; + label = "Forward"; + tooltip = "Forward selected message"; }, ), ( // third group @@ -44,22 +53,27 @@ onclick = "onMenuDeleteMessage(event);"; // enabled = showMarkDeletedButton; image = "tb-mail-delete-flat-24x24.png"; - cssClass = "tbicon_delete"; label = "Delete"; }, + cssClass = "tbicon_delete"; + label = "Delete"; + tooltip = "Delete selected message or folder"; }, { link = "#"; isSafe = NO; image = "tb-mail-junk-flat-24x24.png"; - cssClass = "tbicon_junk"; label = "Junk"; - }, + cssClass = "tbicon_junk"; + label = "Junk"; + tooltip = "Mark the selected messages as junk"; }, ), ( { link = "#"; onclick = "return onPrintCurrentMessage(event);"; cssClass = "tbicon_print"; image = "tb-mail-print-flat-24x24.png"; - label = "Print"; }, + label = "Print"; + tooltip = "Print this message"; }, { link = "#"; image = "tb-mail-stop-flat-24x24.png"; cssClass = "tbicon_stop"; - label = "Stop"; }, + label = "Stop"; + tooltip = "Stop the current transfer"; }, ), ) diff --git a/UI/MailerUI/UIxMailAccountActions.m b/UI/MailerUI/UIxMailAccountActions.m index 004c3a63..7f3a66cb 100644 --- a/UI/MailerUI/UIxMailAccountActions.m +++ b/UI/MailerUI/UIxMailAccountActions.m @@ -108,8 +108,7 @@ rawFolders = [co allFolderPaths]; folders = [self _jsonFolders: [rawFolders objectEnumerator]]; - response = [context response]; - [response setStatus: 200]; + response = [self responseWithStatus: 200]; [response setHeader: @"text/plain; charset=utf-8" forKey: @"content-type"]; [response appendContentString: [folders jsonRepresentation]]; diff --git a/UI/MailerUI/UIxMailActions.m b/UI/MailerUI/UIxMailActions.m index 42b5fa0e..3ebf019c 100644 --- a/UI/MailerUI/UIxMailActions.m +++ b/UI/MailerUI/UIxMailActions.m @@ -94,10 +94,7 @@ response = [[self clientObject] trashInContext: context]; if (!response) - { - response = [context response]; - [response setStatus: 204]; - } + response = [self responseWith204]; return response; } @@ -113,10 +110,7 @@ response = [[self clientObject] moveToFolderNamed: destinationFolder inContext: context]; if (!response) - { - response = [context response]; - [response setStatus: 204]; - } + response = [self responseWith204]; } else response = [NSException exceptionWithHTTPStatus: 500 /* Server Error */ @@ -125,6 +119,30 @@ return response; } +/* active message */ + +- (id) markMessageUnreadAction +{ + id response; + + response = [[self clientObject] removeFlags: @"seen"]; + if (!response) + response = [self responseWith204]; + + return response; +} + +- (id) markMessageReadAction +{ + id response; + + response = [[self clientObject] addFlags: @"seen"]; + if (!response) + response = [self responseWith204]; + + return response; +} + /* SOGoDraftObject */ - (WOResponse *) editAction { @@ -158,10 +176,7 @@ if (error) response = error; else - { - response = [context response]; - [response setStatus: 204]; - } + response = [self responseWith204]; return response; } @@ -171,17 +186,15 @@ WOResponse *response; NSString *filename; - response = [context response]; - filename = [[context request] formValueForKey: @"filename"]; if ([filename length] > 0) { + response = [self responseWith204]; [[self clientObject] deleteAttachmentWithName: filename]; - [response setStatus: 204]; } else { - [response setStatus: 500]; + response = [self responseWithStatus: 500]; [response appendContentString: @"How did you end up here?"]; } diff --git a/UI/MailerUI/UIxMailEditor.m b/UI/MailerUI/UIxMailEditor.m index 726b7970..d09189a6 100644 --- a/UI/MailerUI/UIxMailEditor.m +++ b/UI/MailerUI/UIxMailEditor.m @@ -404,10 +404,7 @@ static NSArray *infoKeys = nil; { result = [[self clientObject] save]; if (!result) - { - result = [context response]; - [result setStatus: 204]; - } + result = [self responseWith204]; } else result = [self failedToSaveFormResponse]; diff --git a/UI/MailerUI/UIxMailFolderActions.m b/UI/MailerUI/UIxMailFolderActions.m index b4a184b8..84d7fa95 100644 --- a/UI/MailerUI/UIxMailFolderActions.m +++ b/UI/MailerUI/UIxMailFolderActions.m @@ -36,6 +36,8 @@ #import #import +#import + #import "UIxMailFolderActions.h" @implementation UIxMailFolderActions @@ -49,7 +51,6 @@ NSString *folderName; co = [self clientObject]; - response = [context response]; folderName = [[context request] formValueForKey: @"name"]; if ([folderName length] > 0) @@ -58,15 +59,15 @@ error = [connection createMailbox: folderName atURL: [co imap4URL]]; if (error) { - [response setStatus: 500]; + response = [self responseWithStatus: 500]; [response appendContentString: @"Unable to create folder."]; } else - [response setStatus: 204]; + response = [self responseWith204]; } else { - [response setStatus: 500]; + response = [self responseWithStatus: 500]; [response appendContentString: @"Missing 'name' parameter."]; } @@ -100,7 +101,6 @@ NSURL *srcURL, *destURL; co = [self clientObject]; - response = [context response]; folderName = [[context request] formValueForKey: @"name"]; if ([folderName length] > 0) @@ -112,15 +112,15 @@ toURL: destURL]; if (error) { - [response setStatus: 500]; + response = [self responseWithStatus: 500]; [response appendContentString: @"Unable to rename folder."]; } else - [response setStatus: 204]; + response = [self responseWith204]; } else { - [response setStatus: 500]; + response = [self responseWithStatus: 500]; [response appendContentString: @"Missing 'name' parameter."]; } @@ -154,7 +154,6 @@ NSURL *srcURL, *destURL; co = [self clientObject]; - response = [context response]; connection = [co imap4Connection]; srcURL = [co imap4URL]; destURL = [self _trashedURLOfFolder: srcURL @@ -164,11 +163,11 @@ toURL: destURL]; if (error) { - [response setStatus: 500]; + response = [self responseWithStatus: 500]; [response appendContentString: @"Unable to move folder."]; } else - [response setStatus: 204]; + response = [self responseWith204]; return response; } @@ -180,18 +179,17 @@ WOResponse *response; co = [self clientObject]; - response = [context response]; error = [co expunge]; if (error) { - [response setStatus: 500]; + response = [self responseWithStatus: 500]; [response appendContentString: @"Unable to expunge folder."]; } else { [co flushMailCaches]; - [response setStatus: 204]; + response = [self responseWith204]; } return response; @@ -207,7 +205,6 @@ NSURL *currentURL; co = [self clientObject]; - response = [context response]; error = [co addFlagsToAllMessages: @"deleted"]; if (!error) @@ -226,11 +223,11 @@ } if (error) { - [response setStatus: 500]; + response = [self responseWithStatus: 500]; [response appendContentString: @"Unable to empty the trash folder."]; } else - [response setStatus: 204]; + response = [self responseWith204]; return response; } @@ -242,7 +239,6 @@ WOResponse *response; SOGoMailFolder *clientObject; - response = [context response]; mailInvitationParam = [[context request] formValueForKey: @"mail-invitation"]; if ([mailInvitationParam boolValue]) @@ -257,7 +253,7 @@ } else { - [response setStatus: 500]; + response = [self responseWithStatus: 500]; [response appendContentString: @"How did you end up here?"]; } @@ -283,8 +279,7 @@ NGImap4Client *client; NSString *responseString; - response = [context response]; - [response setStatus: 200]; + response = [self responseWithStatus: 200]; [response setHeader: @"text/plain; charset=UTF-8" forKey: @"content-type"]; diff --git a/UI/MailerUI/UIxMailListView.m b/UI/MailerUI/UIxMailListView.m index 46f7505e..ac5b7184 100644 --- a/UI/MailerUI/UIxMailListView.m +++ b/UI/MailerUI/UIxMailListView.m @@ -221,10 +221,24 @@ static int attachmentFlagSize = 8096; - (NSArray *) sortedUIDs { + EOQualifier *fetchQualifier, *notDeleted; if (!sortedUIDs) { + notDeleted = [EOQualifier qualifierWithQualifierFormat: + @"(not (flags = %@))", + @"deleted"]; + if (qualifier) + { + fetchQualifier = [[EOAndQualifier alloc] initWithQualifiers: + notDeleted, qualifier, + nil]; + [fetchQualifier autorelease]; + } + else + fetchQualifier = notDeleted; + sortedUIDs - = [[self clientObject] fetchUIDsMatchingQualifier: qualifier + = [[self clientObject] fetchUIDsMatchingQualifier: fetchQualifier sortOrdering: [self imap4SortOrdering]]; [sortedUIDs retain]; } @@ -407,36 +421,8 @@ static int attachmentFlagSize = 8096; return [self redirectToLocation:url]; } -/* active message */ - -- (SOGoMailObject *) lookupActiveMessage -{ - NSString *uid; - - if ((uid = [[context request] formValueForKey: @"uid"]) == nil) - return nil; - - return [[self clientObject] lookupName: uid - inContext: context - acquire: NO]; -} - /* actions */ -- (BOOL) isJavaScriptRequest -{ - return [[[context request] formValueForKey:@"jsonly"] boolValue]; -} - -- (id) javaScriptOK -{ - WOResponse *r; - - r = [context response]; - [r setStatus:200 /* OK */]; - return r; -} - - (int) firstMessageOfPageFor: (int) messageNbr { NSArray *messageNbrs; @@ -462,14 +448,13 @@ static int attachmentFlagSize = 8096; if ([criteria isEqualToString: @"subject"]) qualifier = [EOQualifier qualifierWithQualifierFormat: - @"(subject doesContain: %@)", - value]; + @"(subject doesContain: %@)", value]; else if ([criteria isEqualToString: @"sender"]) qualifier = [EOQualifier qualifierWithQualifierFormat: - @"(sender doesContain: %@)", value]; + @"(from doesContain: %@)", value]; else if ([criteria isEqualToString: @"subject_or_sender"]) qualifier = [EOQualifier qualifierWithQualifierFormat: - @"((sender doesContain: %@)" + @"((subject doesContain: %@)" @" OR (from doesContain: %@))", value, value]; else if ([criteria isEqualToString: @"to_or_cc"]) @@ -506,43 +491,9 @@ static int attachmentFlagSize = 8096; = ((specificMessage) ? [self firstMessageOfPageFor: [specificMessage intValue]] : [[request formValueForKey:@"idx"] intValue]); - return self; } -- (id) viewAction -{ - return [self defaultAction]; -} - -- (id) markMessageUnreadAction -{ - NSException *error; - - if ((error = [[self lookupActiveMessage] removeFlags:@"seen"]) != nil) - // TODO: improve error handling - return error; - - if ([self isJavaScriptRequest]) - return [self javaScriptOK]; - - return [self redirectToLocation:@"view"]; -} - -- (id) markMessageReadAction -{ - NSException *error; - - if ((error = [[self lookupActiveMessage] addFlags:@"seen"]) != nil) - // TODO: improve error handling - return error; - - if ([self isJavaScriptRequest]) - return [self javaScriptOK]; - - return [self redirectToLocation:@"view"]; -} - - (id) getMailAction { // TODO: we might want to flush the caches? diff --git a/UI/MailerUI/UIxMailSourceView.m b/UI/MailerUI/UIxMailSourceView.m index c0586221..60976e53 100644 --- a/UI/MailerUI/UIxMailSourceView.m +++ b/UI/MailerUI/UIxMailSourceView.m @@ -25,6 +25,8 @@ #import #import +#import + #import "UIxMailSourceView.h" @implementation UIxMailSourceView @@ -36,8 +38,7 @@ source = [[self clientObject] contentAsString]; - response = [context response]; - [response setStatus: 200]; + response = [self responseWithStatus: 200]; [response setHeader: @"text/plain; charset=utf-8" forKey: @"content-type"]; [response appendContentString: source]; diff --git a/UI/MailerUI/UIxMailView.m b/UI/MailerUI/UIxMailView.m index 107e9c2a..e3da019f 100644 --- a/UI/MailerUI/UIxMailView.m +++ b/UI/MailerUI/UIxMailView.m @@ -142,37 +142,42 @@ static NSString *mailETag = nil; - (id) defaultAction { + WOResponse *response; + NSString *s; + /* check etag to see whether we really must rerender */ - if (mailETag != nil ) { - /* - Note: There is one thing which *can* change for an existing message, - those are the IMAP4 flags (and annotations, which we do not use). - Since we don't render the flags, it should be OK, if this changes - we must embed the flagging into the etag. - */ - NSString *s; - - if ((s = [[context request] headerForKey:@"if-none-match"])) { - if ([s rangeOfString:mailETag].length > 0) { /* not perfectly correct */ - /* client already has the proper entity */ - // [self logWithFormat:@"MATCH: %@ (tag %@)", s, mailETag]; - - if (![[self clientObject] doesMailExist]) { - return [NSException exceptionWithHTTPStatus:404 /* Not Found */ - reason:@"message got deleted"]; + if (mailETag) + { + /* + Note: There is one thing which *can* change for an existing message, + those are the IMAP4 flags (and annotations, which we do not use). + Since we don't render the flags, it should be OK, if this changes + we must embed the flagging into the etag. + */ + s = [[context request] headerForKey: @"if-none-match"]; + if (s) + { + if ([s rangeOfString:mailETag].length > 0) /* not perfectly correct */ + { + /* client already has the proper entity */ + // [self logWithFormat:@"MATCH: %@ (tag %@)", s, mailETag]; + + if (![[self clientObject] doesMailExist]) { + return [NSException exceptionWithHTTPStatus:404 /* Not Found */ + reason:@"message got deleted"]; + } + + response = [context response]; + [response setStatus: 304 /* Not Modified */]; + + return response; + } } - - [[context response] setStatus:304 /* Not Modified */]; - return [context response]; - } } - } - if ([self message] == nil) { - // TODO: redirect to proper error + if (![self message]) // TODO: redirect to proper error return [NSException exceptionWithHTTPStatus:404 /* Not Found */ reason:@"did not find specified message!"]; - } return self; } diff --git a/UI/MailerUI/product.plist b/UI/MailerUI/product.plist index d6806567..73ed0809 100644 --- a/UI/MailerUI/product.plist +++ b/UI/MailerUI/product.plist @@ -115,16 +115,6 @@ protectedBy = "View"; pageName = "UIxMailListView"; }; - markMessageUnread = { - protectedBy = "View"; - pageName = "UIxMailListView"; - actionName = "markMessageUnread"; - }; - markMessageRead = { - protectedBy = "View"; - pageName = "UIxMailListView"; - actionName = "markMessageRead"; - }; getMail = { protectedBy = "View"; pageName = "UIxMailListView"; @@ -235,6 +225,16 @@ actionClass = "UIxMailActions"; actionName = "forward"; }; + markMessageUnread = { + protectedBy = "View"; + actionClass = "UIxMailActions"; + actionName = "markMessageUnread"; + }; + markMessageRead = { + protectedBy = "View"; + actionClass = "UIxMailActions"; + actionName = "markMessageRead"; + }; }; }; diff --git a/UI/MainUI/SOGoRootPage.m b/UI/MainUI/SOGoRootPage.m index c88e9af4..9749d13b 100644 --- a/UI/MainUI/SOGoRootPage.m +++ b/UI/MainUI/SOGoRootPage.m @@ -69,103 +69,38 @@ auth = [[WOApplication application] authenticatorInContext: context]; - response = [context response]; + response = [self responseWith204]; cookieString = [NSString stringWithFormat: @"%@:%@", [self queryParameterForKey: @"userName"], [self queryParameterForKey: @"password"]]; - cookieValue = [NSString stringWithFormat: @"basic%@", + cookieValue = [NSString stringWithFormat: @"basic %@", [cookieString stringByEncodingBase64]]; authCookie = [WOCookie cookieWithName: [auth cookieNameInContext: context] value: cookieValue]; [authCookie setPath: @"/"]; - [response setStatus: 204]; [response addCookie: authCookie]; return response; } -// - (id ) defaultAction -// { -// WOResponse *r; -// NSString *login, *rhk; -// SOGoWebAuthenticator *auth; -// SOGoUser *user; -// SOGoUserFolder *home; -// WOApplication *base; - -// /* -// Note: ctx.activeUser is NOT set here. Don't know why, so we retrieve -// the user from the authenticator. -// */ - -// auth = [[self clientObject] authenticatorInContext: context]; -// user = [auth userInContext: context]; -// login = [user login]; - -// if ([login isEqualToString:@"anonymous"]) { -// /* use root page for unauthenticated users */ -// return self; -// } - -// /* check base */ - -// base = [self application]; -// rhk = [[context request] requestHandlerKey]; -// if (([rhk length] == 0) || ([base requestHandlerForKey:rhk] == nil)) { -// base = [base lookupName: @"so" inContext: context acquire: NO]; - -// if (![base isNotNull] || [base isKindOfClass:[NSException class]]) { -// /* use root page if home could not be found */ -// [self errorWithFormat:@"Did not find 'so' request handler!"]; -// return self; -// } -// } - -// /* lookup home-page */ - -// home = [base lookupName: login inContext: context acquire: NO]; -// if (![home isNotNull] || [home isKindOfClass:[NSException class]]) { -// /* use root page if home could not be found */ -// return self; -// } - -// /* redirect to home-page */ - -// r = [context response]; -// [r setStatus: 302 /* moved */]; -// [r setHeader: [home baseURLInContext: context] -// forKey: @"location"]; - -// return r; -// } - -/* response generation */ - -// - (void) appendToResponse: (WOResponse *) response -// inContext: (WOContext *) ctx -// { -// NSString *rhk; - -// // TODO: we might also want to look into the HTTP basic-auth to redirect to -// // the login URL! - -// rhk = [[ctx request] requestHandlerKey]; -// if ([rhk length] == 0 -// || [[self application] requestHandlerForKey: rhk] == nil) -// { -// /* a small hack to redirect to a valid URL */ -// NSString *url; - -// url = [ctx urlWithRequestHandlerKey: @"so" path: @"/" queryString: nil]; -// [response setStatus: 302 /* moved */]; -// [response setHeader: url forKey: @"location"]; -// [self logWithFormat: @"URL: %@", url]; -// return; -// } - -// [response setHeader: @"text/html" forKey: @"content-type"]; -// [super appendToResponse: response inContext: ctx]; -// } +- (id ) defaultAction +{ + id response; + NSString *login, *oldLocation; + + login = [[context activeUser] login]; + if ([login isEqualToString: @"anonymous"]) + response = self; + else + { + oldLocation = [[self clientObject] baseURLInContext: context]; + response + = [self redirectToLocation: [NSString stringWithFormat: @"%@/%@", + oldLocation, login]]; + } + + return response; +} - (BOOL) isPublicInContext: (WOContext *) localContext { diff --git a/UI/MainUI/product.plist b/UI/MainUI/product.plist index c3ebb099..11e28282 100644 --- a/UI/MainUI/product.plist +++ b/UI/MainUI/product.plist @@ -46,6 +46,14 @@ "Access Contents Information" = ( "Owner", "ObjectViewer" ); }; }; + SOGoParentFolder = { + superclass = "SOGoObject"; + protectedBy = "Access Contents Information"; + defaultRoles = { + "Access Contents Information" = ( "Authenticated" ); + "WebDAV Access" = ( "Authenticated" ); + }; + }; SOGoUserFolder = { superclass = "SOGoFolder"; protectedBy = "Access Contents Information"; diff --git a/UI/PreferencesUI/UIxPreferences.m b/UI/PreferencesUI/UIxPreferences.m index 4131db0a..7259f60d 100644 --- a/UI/PreferencesUI/UIxPreferences.m +++ b/UI/PreferencesUI/UIxPreferences.m @@ -125,7 +125,8 @@ static BOOL shouldDisplayPasswordChange = NO; - (NSArray *) timeZonesList { - return [NSTimeZone knownTimeZoneNames]; + return [[NSTimeZone knownTimeZoneNames] + sortedArrayUsingSelector: @selector (localizedCaseInsensitiveCompare:)]; } - (NSString *) userTimeZone diff --git a/UI/SOGoUI/UIxComponent.h b/UI/SOGoUI/UIxComponent.h index 4435c730..f0c01908 100644 --- a/UI/SOGoUI/UIxComponent.h +++ b/UI/SOGoUI/UIxComponent.h @@ -68,25 +68,25 @@ /* date selection */ - (NSCalendarDate *) selectedDate; -- (NSString *)dateStringForDate:(NSCalendarDate *)_date; +- (NSString *) dateStringForDate: (NSCalendarDate *)_date; - (BOOL) hideFrame; - (UIxComponent *) jsCloseWithRefreshMethod: (NSString *) methodName; /* SoUser */ -- (NSString *)shortUserNameForDisplay; +- (NSString *) shortUserNameForDisplay; /* labels */ -- (NSString *)labelForKey:(NSString *)_key; +- (NSString *) labelForKey:(NSString *)_key; -- (NSString *)localizedNameForDayOfWeek:(unsigned)_dayOfWeek; -- (NSString *)localizedAbbreviatedNameForDayOfWeek:(unsigned)_dayOfWeek; -- (NSString *)localizedNameForMonthOfYear:(unsigned)_monthOfYear; -- (NSString *)localizedAbbreviatedNameForMonthOfYear:(unsigned)_monthOfYear; +- (NSString *) localizedNameForDayOfWeek:(unsigned)_dayOfWeek; +- (NSString *) localizedAbbreviatedNameForDayOfWeek:(unsigned)_dayOfWeek; +- (NSString *) localizedNameForMonthOfYear:(unsigned)_monthOfYear; +- (NSString *) localizedAbbreviatedNameForMonthOfYear:(unsigned)_monthOfYear; /* HTTP method safety */ -- (BOOL)isInvokedBySafeMethod; +- (BOOL) isInvokedBySafeMethod; /* locale */ - (NSDictionary *)locale; @@ -95,6 +95,8 @@ - (WOResourceManager *) pageResourceManager; - (NSString *) urlForResourceFilename: (NSString *) filename; +- (WOResponse *) responseWith204; + /* Debugging */ - (BOOL)isUIxDebugEnabled; diff --git a/UI/SOGoUI/UIxComponent.m b/UI/SOGoUI/UIxComponent.m index 95e48cf9..643dca56 100644 --- a/UI/SOGoUI/UIxComponent.m +++ b/UI/SOGoUI/UIxComponent.m @@ -400,7 +400,7 @@ static BOOL uixDebugEnabled = NO; userTimeZone = [[context activeUser] timeZone]; [_date setTimeZone: userTimeZone]; - return [_date descriptionWithCalendarFormat:@"%Y%m%d"]; + return [_date descriptionWithCalendarFormat: @"%Y%m%d"]; } - (BOOL) hideFrame @@ -569,6 +569,16 @@ static BOOL uixDebugEnabled = NO; return url; } +- (WOResponse *) responseWith204 +{ + WOResponse *response; + + response = [context response]; + [response setStatus: 204]; + + return response; +} + /* debugging */ - (BOOL)isUIxDebugEnabled { diff --git a/UI/Scheduler/English.lproj/Localizable.strings b/UI/Scheduler/English.lproj/Localizable.strings index f49a55a9..5e7ee587 100644 --- a/UI/Scheduler/English.lproj/Localizable.strings +++ b/UI/Scheduler/English.lproj/Localizable.strings @@ -99,8 +99,11 @@ /* Button Titles */ -"Add..." = "Add..."; -"Remove" = "Remove"; +"New Calendar..." = "New Calendar..."; +"Subscribe to a Calendar..." = "Subscribe to a Calendar..."; +"Remove the selected Calendar" = "Remove the selected Calendar"; + +"Name of the Calendar" = "Name of the Calendar"; "new" = "New"; "printview" = "Print View"; diff --git a/UI/Scheduler/French.lproj/Localizable.strings b/UI/Scheduler/French.lproj/Localizable.strings index 9fb1d9fc..1d8f083e 100644 --- a/UI/Scheduler/French.lproj/Localizable.strings +++ b/UI/Scheduler/French.lproj/Localizable.strings @@ -100,8 +100,11 @@ /* Button Titles */ -"Add..." = "Ajouter..."; -"Remove" = "Enlever"; +"New Calendar..." = "Nouvel agenda..."; +"Subscribe to a Calendar..." = "S'inscrire à un agenda..."; +"Remove the selected Calendar" = "Enlever l'agenda sélectionné"; + +"Name of the Calendar" = "Nom de l'agenda"; "new" = "Nouveau"; "printview" = "Version imprimable"; diff --git a/UI/Scheduler/GNUmakefile b/UI/Scheduler/GNUmakefile index 0089f139..8c65e344 100644 --- a/UI/Scheduler/GNUmakefile +++ b/UI/Scheduler/GNUmakefile @@ -48,7 +48,7 @@ SchedulerUI_RESOURCE_FILES += \ product.plist SchedulerUI_RESOURCE_FILES += \ - Toolbars/SOGoAppointmentFolder.toolbar \ + Toolbars/SOGoAppointmentFolders.toolbar \ Toolbars/SOGoAppointmentObject.toolbar \ Toolbars/SOGoAppointmentObjectAccept.toolbar \ Toolbars/SOGoAppointmentObjectDecline.toolbar \ diff --git a/UI/Scheduler/German.lproj/Localizable.strings b/UI/Scheduler/German.lproj/Localizable.strings index 9a52d23d..b05adbc7 100644 --- a/UI/Scheduler/German.lproj/Localizable.strings +++ b/UI/Scheduler/German.lproj/Localizable.strings @@ -100,8 +100,11 @@ /* Button Titles */ -"Add..." = "Hinzufügen..."; -"Remove" = "Löschen"; +"New Calendar..." = "New Calendar..."; +"Subscribe to a Calendar..." = "Subscribe to a Calendar..."; +"Remove the selected Calendar" = "Remove the selected Calendar"; + +"Name of the Calendar" = "Name of the Calendar"; "new" = "Neu"; "printview" = "Version imprimable"; diff --git a/UI/Scheduler/Toolbars/SOGoAppointmentFolder.toolbar b/UI/Scheduler/Toolbars/SOGoAppointmentFolders.toolbar similarity index 60% rename from UI/Scheduler/Toolbars/SOGoAppointmentFolder.toolbar rename to UI/Scheduler/Toolbars/SOGoAppointmentFolders.toolbar index 296c3069..b50fa350 100644 --- a/UI/Scheduler/Toolbars/SOGoAppointmentFolder.toolbar +++ b/UI/Scheduler/Toolbars/SOGoAppointmentFolders.toolbar @@ -3,39 +3,48 @@ isSafe = NO; label = "New Event"; onclick = "return newEvent(this, 'event');"; - image = "new-event.png"; }, + image = "new-event.png"; + tooltip = "Create a new event"; }, { link = "new_task"; label="New Task"; image = "new-task.png"; onclick = "return newEvent(this, 'task');"; - image = "new-task.png"; }, + image = "new-task.png"; + tooltip = "Create a new task"; }, { link = "edit"; label="Edit"; onclick = "return editEvent(this);"; - image = "edit.png"; }, + image = "edit.png"; + tooltip = "Edit this event or task"; }, { link = "delete"; label="Delete"; onclick = "return deleteEvent(this);"; - image = "delete.png"; } ), + image = "delete.png"; + tooltip = "Delete this event or task"; } ), ( { link = "today"; label="Go to Today"; onclick = "return gotoToday();"; - image = "goto-today.png" } ), + image = "goto-today.png"; + tooltip = "Go to today"; } ), ( { link = "dayoverview"; label="Day View"; onclick = "return onDayOverview();"; - image = "day-view.png"; }, + image = "day-view.png"; + tooltip = "Switch to day view"; }, /* disabled until we fix the view */ /* { link = "dayoverview"; label="Multicolumn Day View"; onclick = "return onMulticolumnDayOverview();"; - image = "day-view-multicolumn.png"; }, */ + image = "day-view-multicolumn.png"; + tooltip = ""; }, */ { link = "weekoverview"; label="Week View"; onclick = "return onWeekOverview();"; - image = "week-view.png"; }, + image = "week-view.png"; + tooltip = "Switch to week view"; }, { link = "monthoverview"; label="Month View"; onclick = "return onMonthOverview();"; - image = "month-view.png"; } ) + image = "month-view.png"; + tooltip = "Switch to month view"; } ) ) diff --git a/UI/Scheduler/UIxAppointmentEditor.m b/UI/Scheduler/UIxAppointmentEditor.m index f4f04197..a88b3f38 100644 --- a/UI/Scheduler/UIxAppointmentEditor.m +++ b/UI/Scheduler/UIxAppointmentEditor.m @@ -286,14 +286,14 @@ { NSString *objectId, *method, *uri; id result; - Class clientKlazz; + SOGoAppointmentFolder *co; - clientKlazz = [[self clientObject] class]; - objectId = [clientKlazz globallyUniqueObjectId]; + co = [self clientObject]; + objectId = [co globallyUniqueObjectId]; if ([objectId length] > 0) { - method = [NSString stringWithFormat:@"%@/Calendar/%@/editAsAppointment", - [self userFolderPath], objectId]; + method = [NSString stringWithFormat:@"%@/%@/editAsAppointment", + [co soURL], objectId]; uri = [self completeHrefForMethod: method]; result = [self redirectToLocation: uri]; } diff --git a/UI/Scheduler/UIxCalListingActions.m b/UI/Scheduler/UIxCalListingActions.m index 03f647f9..b036060f 100644 --- a/UI/Scheduler/UIxCalListingActions.m +++ b/UI/Scheduler/UIxCalListingActions.m @@ -25,6 +25,7 @@ #import #import #import +#import #import #import @@ -38,6 +39,9 @@ #import #import #import +#import + +#import #import "NSArray+Scheduler.h" @@ -212,36 +216,6 @@ return aptFolder; } -- (NSArray *) _activeCalendarFolders -{ - NSMutableArray *activeFolders; - NSEnumerator *folders; - NSDictionary *currentFolderDict; - SOGoAppointmentFolder *currentFolder, *clientObject; - - activeFolders = [NSMutableArray new]; - [activeFolders autorelease]; - - clientObject = [self clientObject]; - - folders = [[clientObject calendarFolders] objectEnumerator]; - currentFolderDict = [folders nextObject]; - while (currentFolderDict) - { - if ([[currentFolderDict objectForKey: @"active"] boolValue]) - { - currentFolder - = [self _aptFolder: [currentFolderDict objectForKey: @"folder"] - withClientObject: clientObject]; - [activeFolders addObject: currentFolder]; - } - - currentFolderDict = [folders nextObject]; - } - - return activeFolders; -} - - (NSArray *) _fetchFields: (NSArray *) fields forComponentOfType: (NSString *) component { @@ -250,34 +224,42 @@ NSMutableDictionary *infos, *currentInfo, *newInfo; NSString *owner, *uid; NSNull *marker; + SOGoAppointmentFolders *clientObject; marker = [NSNull null]; infos = [NSMutableDictionary dictionary]; - folders = [[self _activeCalendarFolders] objectEnumerator]; + clientObject = [self clientObject]; + + folders = [[clientObject subFolders] objectEnumerator]; currentFolder = [folders nextObject]; while (currentFolder) { - owner = [currentFolder ownerInContext: context]; - currentInfos = [[currentFolder fetchCoreInfosFrom: startDate - to: endDate - component: component] objectEnumerator]; - newInfo = [currentInfos nextObject]; - while (newInfo) + if ([currentFolder isActive]) { - uid = [newInfo objectForKey: @"c_uid"]; - currentInfo = [infos objectForKey: uid]; - if (!currentInfo - || [owner isEqualToString: userLogin]) + owner = [currentFolder ownerInContext: context]; + currentInfos = [[currentFolder fetchCoreInfosFrom: startDate + to: endDate + component: component] objectEnumerator]; + newInfo = [currentInfos nextObject]; + while (newInfo) { - [self _updatePrivacyInComponent: newInfo - fromFolder: currentFolder]; - [newInfo setObject: owner forKey: @"c_owner"]; - [infos setObject: [newInfo objectsForKeys: fields - notFoundMarker: marker] - forKey: uid]; + uid = [newInfo objectForKey: @"c_uid"]; + currentInfo = [infos objectForKey: uid]; + if (!currentInfo + || [owner isEqualToString: userLogin]) + { + [self _updatePrivacyInComponent: newInfo + fromFolder: currentFolder]; + [newInfo setObject: [currentFolder nameInContainer] + forKey: @"c_folder"]; + // [newInfo setObject: owner forKey: @"c_owner"]; + [infos setObject: [newInfo objectsForKeys: fields + notFoundMarker: marker] + forKey: uid]; + } + newInfo = [currentInfos nextObject]; } - newInfo = [currentInfos nextObject]; } currentFolder = [folders nextObject]; } @@ -289,10 +271,9 @@ { WOResponse *response; - response = [context response]; + response = [self responseWithStatus: 200]; [response setHeader: @"text/plain; charset=utf-8" forKey: @"content-type"]; - [response setStatus: 200]; [response appendContentString: [data jsonRepresentation]]; return response; @@ -325,7 +306,7 @@ [self _setupContext]; newEvents = [NSMutableArray array]; - fields = [NSArray arrayWithObjects: @"c_name", @"c_owner", @"c_status", + fields = [NSArray arrayWithObjects: @"c_name", @"c_folder", @"c_status", @"c_title", @"c_startdate", @"c_enddate", @"c_location", @"c_isallday", nil]; events = [[self _fetchFields: fields @@ -396,7 +377,7 @@ [self _setupContext]; - fields = [NSArray arrayWithObjects: @"c_name", @"c_owner", @"c_status", + fields = [NSArray arrayWithObjects: @"c_name", @"c_folder", @"c_status", @"c_title", @"c_enddate", nil]; tasks = [[self _fetchFields: fields diff --git a/UI/Scheduler/UIxCalMainView.m b/UI/Scheduler/UIxCalMainView.m index 80b66f66..ca3933a0 100644 --- a/UI/Scheduler/UIxCalMainView.m +++ b/UI/Scheduler/UIxCalMainView.m @@ -23,6 +23,7 @@ #import #import #import +#import #import #import @@ -41,6 +42,15 @@ static NSMutableArray *yearMenuItems = nil; @implementation UIxCalMainView +- (NSString *) userUTCOffset +{ + NSTimeZone *userTZ; + + userTZ = [[context activeUser] timeZone]; + + return [NSString stringWithFormat: @"%d", [userTZ secondsFromGMT]]; +} + - (NSArray *) monthMenuItems { unsigned int count; diff --git a/UI/Scheduler/UIxCalendarSelector.h b/UI/Scheduler/UIxCalendarSelector.h index 410ce6db..21feda2f 100644 --- a/UI/Scheduler/UIxCalendarSelector.h +++ b/UI/Scheduler/UIxCalendarSelector.h @@ -23,29 +23,23 @@ #ifndef UIXCALENDARSELECTOR_H #define UIXCALENDARSELECTOR_H +#import + @class NSArray; @class NSMutableArray; @class NSDictionary; -@class NSMutableDictionary; @class NSString; -@class iCalPerson; @interface UIxCalendarSelector : UIxComponent { - NSMutableDictionary *colors; - - NSDictionary *currentCalendarFolder; - NSString *currentCalendarLogin; + NSMutableArray *calendars; + NSDictionary *currentCalendar; } -- (NSArray *) calendarFolders; - -- (void) setCurrentCalendarFolder: (NSDictionary *) newCurrentCalendarFolder; -- (NSDictionary *) currentCalendarFolder; +- (NSArray *) calendars; -- (NSString *) currentCalendarSpanBG; -- (NSString *) currentCalendarLogin; -- (NSString *) currentCalendarStyle; +- (void) setCurrentCalendar: (NSDictionary *) newCalendar; +- (NSDictionary *) currentCalendar; @end diff --git a/UI/Scheduler/UIxCalendarSelector.m b/UI/Scheduler/UIxCalendarSelector.m index 1beef4b4..b411f6b4 100644 --- a/UI/Scheduler/UIxCalendarSelector.m +++ b/UI/Scheduler/UIxCalendarSelector.m @@ -22,34 +22,30 @@ #import #import -#import -#import +#import -#import -#import - -#import -#import +#import #import +#import #import "UIxCalendarSelector.h" -static inline char -darkenedColor (const char value) -{ - char newValue; - - if (value >= '0' && value <= '9') - newValue = ((value - '0') / 2) + '0'; - else if (value >= 'a' && value <= 'f') - newValue = ((value + 10 - 'a') / 2) + '0'; - else if (value >= 'A' && value <= 'F') - newValue = ((value + 10 - 'A') / 2) + '0'; - else - newValue = value; +// static inline char +// darkenedColor (const char value) +// { +// char newValue; - return newValue; -} +// if (value >= '0' && value <= '9') +// newValue = ((value - '0') / 2) + '0'; +// else if (value >= 'a' && value <= 'f') +// newValue = ((value + 10 - 'a') / 2) + '0'; +// else if (value >= 'A' && value <= 'F') +// newValue = ((value + 10 - 'A') / 2) + '0'; +// else +// newValue = value; + +// return newValue; +// } static inline NSString * colorForNumber (unsigned int number) @@ -90,8 +86,8 @@ colorForNumber (unsigned int number) { if ((self = [super init])) { - colors = nil; - currentCalendarFolder = nil; + calendars = nil; + currentCalendar = nil; } return self; @@ -99,82 +95,61 @@ colorForNumber (unsigned int number) - (void) dealloc { - [currentCalendarFolder release]; - [colors release]; + [calendars release]; + [currentCalendar release]; [super dealloc]; } -- (NSArray *) calendarFolders +- (NSArray *) calendars { - NSArray *calendarFolders; - NSEnumerator *newFolders; - NSDictionary *currentFolder; - unsigned int count; - - calendarFolders = [[self clientObject] calendarFolders]; - if (!colors) + NSArray *folders; + SOGoAppointmentFolder *folder; + NSMutableDictionary *calendar; + unsigned int count, max; + NSString *folderId, *folderName; + NSNumber *isActive; + + if (!calendars) { - colors = [NSMutableDictionary new]; - count = 0; - newFolders = [calendarFolders objectEnumerator]; - currentFolder = [newFolders nextObject]; - while (currentFolder) + folders = [[self clientObject] subFolders]; + max = [folders count]; + calendars = [[NSMutableArray alloc] initWithCapacity: max]; + for (count = 0; count < max; count++) { - [colors setObject: colorForNumber (count) - forKey: [currentFolder objectForKey: @"folder"]]; - count++; - currentFolder = [newFolders nextObject]; + folder = [folders objectAtIndex: count]; + calendar = [NSMutableDictionary dictionary]; + folderName = [folder nameInContainer]; + [calendar setObject: + [NSString stringWithFormat: @"/%@", folderName] + forKey: @"id"]; + [calendar setObject: [folder displayName] + forKey: @"displayName"]; + [calendar setObject: folderName forKey: @"folder"]; + [calendar setObject: colorForNumber (count) + forKey: @"color"]; + isActive = [NSNumber numberWithBool: [folder isActive]]; + [calendar setObject: isActive forKey: @"active"]; + [calendars addObject: calendar]; } } - return calendarFolders; + return calendars; } -- (void) setCurrentCalendarFolder: (NSDictionary *) newCurrentCalendarFolder +- (void) setCurrentCalendar: (NSDictionary *) newCalendar { - ASSIGN (currentCalendarFolder, newCurrentCalendarFolder); + ASSIGN (currentCalendar, newCalendar); } -- (NSDictionary *) currentCalendarFolder +- (NSDictionary *) currentCalendar { - return currentCalendarFolder; -} - -- (NSString *) currentCalendarSpanBG -{ - NSString *colorKey; - - colorKey = [currentCalendarFolder objectForKey: @"folder"]; - - return [colors objectForKey: colorKey]; -} - -- (NSString *) currentCalendarLogin -{ - NSArray *parts; - NSMutableString *login; - - login = [NSMutableString string]; - parts = [[currentCalendarFolder objectForKey: @"folder"] - componentsSeparatedByString: @":"]; - [login appendString: (([parts count] > 1) - ? [parts objectAtIndex: 0] - : [[context activeUser] login])]; - [login replaceString: @"." withString: @"_"]; - [login replaceString: @"#" withString: @"_"]; - [login replaceString: @"@" withString: @"_"]; - - return login; + return currentCalendar; } - (NSString *) currentCalendarStyle { - NSString *color; - - color = [self currentCalendarSpanBG]; - - return [NSString stringWithFormat: @"color: %@; background-color: %@;", - color, color]; + return [currentCalendar + keysWithFormat: @"color: %{color}; background-color: %{color};"]; } @end /* UIxCalendarSelector */ diff --git a/UI/Scheduler/UIxComponentEditor.m b/UI/Scheduler/UIxComponentEditor.m index 7937e8fa..5cfeecaf 100644 --- a/UI/Scheduler/UIxComponentEditor.m +++ b/UI/Scheduler/UIxComponentEditor.m @@ -42,6 +42,7 @@ #import #import +#import #import #import #import @@ -331,19 +332,20 @@ - (NSArray *) calendarList { - SOGoAppointmentFolder *folder; + SOGoAppointmentFolder *calendar, *currentCalendar; + SOGoAppointmentFolders *calendarParent; NSEnumerator *allCalendars; - NSDictionary *currentCalendar; if (!calendarList) { calendarList = [NSMutableArray new]; - folder = [[self clientObject] container]; - allCalendars = [[folder calendarFolders] objectEnumerator]; + calendar = [[self clientObject] container]; + calendarParent = [calendar container]; + allCalendars = [[calendarParent subFolders] objectEnumerator]; currentCalendar = [allCalendars nextObject]; while (currentCalendar) { - if ([[currentCalendar objectForKey: @"active"] boolValue]) + if ([currentCalendar isActive]) [calendarList addObject: currentCalendar]; currentCalendar = [allCalendars nextObject]; } diff --git a/UI/Scheduler/UIxTaskEditor.m b/UI/Scheduler/UIxTaskEditor.m index 0258ae2d..bc7e1bed 100644 --- a/UI/Scheduler/UIxTaskEditor.m +++ b/UI/Scheduler/UIxTaskEditor.m @@ -324,14 +324,14 @@ { NSString *objectId, *method, *uri; id result; - Class clientKlazz; + SOGoAppointmentFolder *co; - clientKlazz = [[self clientObject] class]; - objectId = [clientKlazz globallyUniqueObjectId]; + co = [self clientObject]; + objectId = [co globallyUniqueObjectId]; if ([objectId length] > 0) { - method = [NSString stringWithFormat:@"%@/Calendar/%@/editAsTask", - [self userFolderPath], objectId]; + method = [NSString stringWithFormat:@"%@/%@/editAsTask", + [co soURL], objectId]; uri = [self completeHrefForMethod: method]; result = [self redirectToLocation: uri]; } diff --git a/UI/Scheduler/product.plist b/UI/Scheduler/product.plist index 2041d757..c5afd5cc 100644 --- a/UI/Scheduler/product.plist +++ b/UI/Scheduler/product.plist @@ -28,11 +28,11 @@ }; categories = { - SOGoAppointmentFolder = { + SOGoAppointmentFolders = { slots = { toolbar = { protectedBy = "View"; - value = "SOGoAppointmentFolder.toolbar"; + value = "SOGoAppointmentFolders.toolbar"; }; }; methods = { @@ -70,16 +70,6 @@ protectedBy = "View"; pageName = "UIxCalMonthView"; }; - newevent = { - protectedBy = "Add Documents, Images, and Files"; - pageName = "UIxAppointmentEditor"; - actionName = "new"; - }; - newtask = { - protectedBy = "Add Documents, Images, and Files"; - pageName = "UIxTaskEditor"; - actionName = "new"; - }; show = { protectedBy = "View"; pageName = "UIxCalView"; @@ -94,15 +84,45 @@ pageName = "UIxAppointmentProposal"; actionName = "proposalSearch"; }; + userRights = { + protectedBy = "ReadAcls"; + pageName = "UIxCalUserRightsEditor"; + }; + saveUserRights = { + protectedBy = "SaveAcls"; + pageName = "UIxCalUserRightsEditor"; + actionName = "saveUserRights"; + }; + }; + }; + + SOGoAppointmentFolder = { + methods = { + newevent = { + protectedBy = "Add Documents, Images, and Files"; + pageName = "UIxAppointmentEditor"; + actionName = "new"; + }; + newtask = { + protectedBy = "Add Documents, Images, and Files"; + pageName = "UIxTaskEditor"; + actionName = "new"; + }; batchDelete = { protectedBy = "Delete Objects"; pageName = "UIxCalMainView"; actionName = "batchDelete"; }; - updateCalendars = { + + show = { protectedBy = "View"; + pageName = "UIxCalView"; + actionName = "redirectForUIDs"; + }; + batchDelete = { + protectedBy = "Delete Objects"; pageName = "UIxCalMainView"; - actionName = "updateCalendars"; + actionName = "batchDelete"; }; editAttendees = { protectedBy = "View"; @@ -119,6 +139,7 @@ }; }; }; + SOGoCalendarComponent = { }; @@ -158,6 +179,10 @@ pageName = "UIxAppointmentEditor"; actionName = "decline"; }; + editAttendees = { + protectedBy = "View"; + pageName = "UIxAttendeesEditor"; + }; }; }; diff --git a/UI/Templates/ContactsUI/UIxContactsListViewContainer.wox b/UI/Templates/ContactsUI/UIxContactsListViewContainer.wox index 8a2d8655..1733b896 100644 --- a/UI/Templates/ContactsUI/UIxContactsListViewContainer.wox +++ b/UI/Templates/ContactsUI/UIxContactsListViewContainer.wox @@ -62,13 +62,10 @@
  • + >
diff --git a/UI/Templates/MailerUI/UIxMailListView.wox b/UI/Templates/MailerUI/UIxMailListView.wox index b6514b3a..c5a3f0de 100644 --- a/UI/Templates/MailerUI/UIxMailListView.wox +++ b/UI/Templates/MailerUI/UIxMailListView.wox @@ -63,7 +63,6 @@ var:class="messageSubjectCellStyleClass" var:id="msgDivID" > + diff --git a/UI/Templates/SchedulerUI/UIxCalendarSelector.wox b/UI/Templates/SchedulerUI/UIxCalendarSelector.wox index bee2ff03..a54fcfb6 100644 --- a/UI/Templates/SchedulerUI/UIxCalendarSelector.wox +++ b/UI/Templates/SchedulerUI/UIxCalendarSelector.wox @@ -7,30 +7,34 @@ xmlns:rsrc="OGo:url" xmlns:label="OGo:label">
+
    -
  • +
  • + var:checked="currentCalendar.active" />
    OO
    -
diff --git a/UI/Templates/UIxPageFrame.wox b/UI/Templates/UIxPageFrame.wox index 6bad635b..59244aed 100644 --- a/UI/Templates/UIxPageFrame.wox +++ b/UI/Templates/UIxPageFrame.wox @@ -5,7 +5,7 @@ xmlns:var="http://www.skyrix.com/od/binding" xmlns:const="http://www.skyrix.com/od/constant" xmlns:rsrc="OGo:url" - xmlns:label="OGo:label" + xmlns:label="OGo:label" > diff --git a/UI/Templates/UIxToolbar.wox b/UI/Templates/UIxToolbar.wox index 5ba95b34..38ac3b24 100644 --- a/UI/Templates/UIxToolbar.wox +++ b/UI/Templates/UIxToolbar.wox @@ -18,11 +18,11 @@ >

0) { - alert(labels.error_validationfailed.decodeEntities() + ":\n" - + errortext.decodeEntities()); + alert(labels.error_validationfailed + ":\n" + + errortext); return false; } return true; @@ -468,28 +468,24 @@ function refreshContacts(contactId) { } function onAddressBookNew(event) { - var name = window.prompt(labels["Name of the Address Book"].decodeEntities()); - if (name) { - if (document.newAbAjaxRequest) { - document.newAbAjaxRequest.aborted = true; - document.newAbAjaxRequest.abort(); - } - var url = ApplicationBaseURL + "/newAb?name=" + name; - document.newAbAjaxRequest - = triggerAjaxRequest(url, newAbCallback, name); - } + createFolder(window.prompt(labels["Name of the Address Book"]), + appendAddressBook); preventDefault(event); } function appendAddressBook(name, folder) { - var li = document.createElement("li"); - $("contactFolders").appendChild(li); - li.setAttribute("id", folder); - li.appendChild(document.createTextNode(name)); - setEventsOnContactFolder(li); + if (folder) + folder = accessToSubscribedFolder(folder); + else + folder = "/" + name; + var li = document.createElement("li"); + $("contactFolders").appendChild(li); + li.setAttribute("id", folder); + li.appendChild(document.createTextNode(name)); + setEventsOnContactFolder(li); } -function newAbCallback(http) { +function newFolderCallback(http) { if (http.readyState == 4 && http.status == 201) { var name = http.callbackData; @@ -523,60 +519,60 @@ function onAddressBookRemove(event) { var selector = $("contactFolders"); var nodes = selector.getSelectedNodes(); if (nodes.length > 0) { - nodes[0].deselect(); - var folderId = nodes[0].getAttribute("id"); - var folderIdElements = folderId.split(":"); - if (folderIdElements.length > 1) - unsubscribeFromFolder(folderId, onFolderUnsubscribeCB, folderId); - else { - var abId = folderIdElements[0].substr(1); - deletePersonalAddressBook(abId); - var personal = $("/personal"); - personal.select(); - onFolderSelectionChange(); - } + nodes[0].deselect(); + var folderId = nodes[0].getAttribute("id"); + var folderIdElements = folderId.split("_"); + if (folderIdElements.length > 1) + unsubscribeFromFolder(folderId, onFolderUnsubscribeCB, folderId); + else { + var abId = folderIdElements[0].substr(1); + deletePersonalAddressBook(abId); + var personal = $("/personal"); + personal.select(); + onFolderSelectionChange(); + } } preventDefault(event); } function deletePersonalAddressBook(folderId) { - var label - = labels["Are you sure you want to delete the selected address book?"]; - if (window.confirm(label.decodeEntities())) { - if (document.deletePersonalABAjaxRequest) { - document.deletePersonalABAjaxRequest.aborted = true; - document.deletePersonalABAjaxRequest.abort(); - } - var url = ApplicationBaseURL + "/" + folderId + "/delete"; - document.deletePersonalABAjaxRequest - = triggerAjaxRequest(url, deletePersonalAddressBookCallback, - folderId); - } + var label + = labels["Are you sure you want to delete the selected address book?"]; + if (window.confirm(label)) { + if (document.deletePersonalABAjaxRequest) { + document.deletePersonalABAjaxRequest.aborted = true; + document.deletePersonalABAjaxRequest.abort(); + } + var url = ApplicationBaseURL + "/" + folderId + "/deleteFolder"; + document.deletePersonalABAjaxRequest + = triggerAjaxRequest(url, deletePersonalAddressBookCallback, + folderId); + } } function deletePersonalAddressBookCallback(http) { if (http.readyState == 4) { - if (http.status == 200) { - var ul = $("contactFolders"); + if (isHttpStatus204(http.status)) { + var ul = $("contactFolders"); - var children = ul.childNodesWithTag("li"); - var i = 0; - var done = false; - while (!done && i < children.length) { - var currentFolderId = children[i].getAttribute("id").substr(1); - if (currentFolderId == http.callbackData) { - ul.removeChild(children[i]); - done = true; - } - else - i++; + var children = ul.childNodesWithTag("li"); + var i = 0; + var done = false; + while (!done && i < children.length) { + var currentFolderId = children[i].getAttribute("id").substr(1); + if (currentFolderId == http.callbackData) { + ul.removeChild(children[i]); + done = true; } - } - document.deletePersonalABAjaxRequest = null; + else + i++; + } + } + document.deletePersonalABAjaxRequest = null; } else - log ("ajax problem 5: " + http.status); + log ("ajax problem 5: " + http.status); } function configureDragHandles() { @@ -648,6 +644,34 @@ function setEventsOnContactFolder(node) { onContactFoldersContextMenu.bindAsEventListener(node), false); } +function onMenuModify(event) { + var folders = $("contactFolders"); + var selected = folders.getSelectedNodes()[0]; + + if (UserLogin == selected.getAttribute("owner")) { + var currentName = selected.innerHTML; + var newName = window.prompt(labels["Address Book Name"], + currentName); + if (newName && newName.length > 0 + && newName != currentName) { + var url = (URLForFolderID(selected.getAttribute("id")) + + "/renameFolder?name=" + escape(newName.utf8encode())); + triggerAjaxRequest(url, folderRenameCallback, + {node: selected, name: newName}); + } + } else + window.alert(clabels["Unable to rename that folder!"]); +} + +function folderRenameCallback(http) { + if (http.readyState == 4) { + if (isHttpStatus204(http.status)) { + var dict = http.callbackData; + dict["node"].innerHTML = dict["name"]; + } + } +} + function onMenuSharing(event) { var folders = $("contactFolders"); var selected = folders.getSelectedNodes()[0]; @@ -659,7 +683,7 @@ function onMenuSharing(event) { function getMenus() { var menus = {}; - menus["contactFoldersMenu"] = new Array(null, "-", null, + menus["contactFoldersMenu"] = new Array(onMenuModify, "-", null, null, "-", null, "-", onMenuSharing); menus["contactMenu"] = new Array(onMenuEditContact, "-", diff --git a/UI/WebServerResources/MailerUI.js b/UI/WebServerResources/MailerUI.js index 09814bc5..6b626482 100644 --- a/UI/WebServerResources/MailerUI.js +++ b/UI/WebServerResources/MailerUI.js @@ -149,7 +149,7 @@ function openMessageWindowsForSelection(action, firstOnly) { function mailListMarkMessage(event) { var http = createHTTPClient(); - var url = ApplicationBaseURL + currentMailbox + "/" + action + "?uid=" + msguid; + var url = ApplicationBaseURL + currentMailbox + "/" + msguid + "/" + action; if (http) { // TODO: add parameter to signal that we are only interested in OK @@ -207,7 +207,7 @@ function ctxFolderAdd(sender) { } function ctxFolderDelete(sender) { - if (!confirm("Delete current folder?").decodeEntities()) + if (!confirm("Delete current folder?")) return false; // TODO: should use a form-POST or AJAX @@ -306,10 +306,10 @@ function onMenuDeleteMessage(event) { function onPrintCurrentMessage(event) { var rowIds = $("messageList").getSelectedRowsId(); if (rowIds.length == 0) { - window.alert(labels["Please select a message to print."].decodeEntities()); + window.alert(labels["Please select a message to print."]); } else if (rowIds.length > 1) { - window.alert(labels["Please select only one message to print."].decodeEntities()); + window.alert(labels["Please select only one message to print."]); } else window.print(); @@ -489,7 +489,7 @@ function quotasCallback(http) { var used = mbQuotas["usedSpace"]; var max = mbQuotas["maxQuota"]; var percents = (Math.round(used * 10000 / max) / 100); - var format = labels["quotasFormat"].decodeEntities(); + var format = labels["quotasFormat"]; var text = format.formatted(used, max, percents); window.status = text; } @@ -1263,7 +1263,7 @@ function buildMailboxes(accountName, encoded) { } function onMenuCreateFolder(event) { - var name = window.prompt(labels["Name :"].decodeEntities(), ""); + var name = window.prompt(labels["Name :"], ""); if (name && name.length > 0) { var folderID = document.menuTarget.getAttribute("dataname"); var urlstr = URLForFolderID(folderID) + "/createFolder?name=" + name; @@ -1273,7 +1273,7 @@ function onMenuCreateFolder(event) { function onMenuRenameFolder(event) { var name = window.prompt(labels["Enter the new name of your folder :"] - .decodeEntities(), + , ""); if (name && name.length > 0) { var folderID = document.menuTarget.getAttribute("dataname"); @@ -1283,7 +1283,7 @@ function onMenuRenameFolder(event) { } function onMenuDeleteFolder(event) { - var answer = window.confirm(labels["Do you really want to move this folder into the trash ?"].decodeEntities()); + var answer = window.confirm(labels["Do you really want to move this folder into the trash ?"]); if (answer) { var folderID = document.menuTarget.getAttribute("dataname"); var urlstr = URLForFolderID(folderID) + "/deleteFolder"; @@ -1318,7 +1318,7 @@ function folderOperationCallback(http) { && http.status == 204) initMailboxTree(); else - window.alert(labels["Operation failed"].decodeEntities()); + window.alert(labels["Operation failed"]); } function folderRefreshCallback(http) { @@ -1329,7 +1329,7 @@ function folderRefreshCallback(http) { refreshCurrentFolder(); } else - window.alert(labels["Operation failed"].decodeEntities()); + window.alert(labels["Operation failed"]); } function getMenus() { diff --git a/UI/WebServerResources/SOGoRootPage.css b/UI/WebServerResources/SOGoRootPage.css index fa534ef4..15d0c1f4 100644 --- a/UI/WebServerResources/SOGoRootPage.css +++ b/UI/WebServerResources/SOGoRootPage.css @@ -12,10 +12,10 @@ DIV#loginScreen background-color: #d4d0c8; margin: 0px auto; margin-top: 5em; - padding: 10px; + padding: 5px; border: 2px solid transparent; - width: 197px; - height: 300px; + width: 200px; + height: 315px; -moz-border-top-colors: #efebe7 #fff; -moz-border-left-colors: #efebe7 #fff; -moz-border-right-colors: #000 #9c9a94 transparent; @@ -26,8 +26,8 @@ DIV#loginScreen IMG { border: 0px; margin: 0px; padding: 0px; - height: 192px; - width: 192px; } + height: 200px; + width: 200px; } DIV#loginScreen INPUT.textField { width: 187px; } diff --git a/UI/WebServerResources/SchedulerUI.js b/UI/WebServerResources/SchedulerUI.js index fc864847..7c992039 100644 --- a/UI/WebServerResources/SchedulerUI.js +++ b/UI/WebServerResources/SchedulerUI.js @@ -17,7 +17,7 @@ var cachedDateSelectors = new Array(); var contactSelectorAction = 'calendars-contacts'; var eventsToDelete = new Array(); -var ownersOfEventsToDelete = new Array(); +var calendarsOfEventsToDelete = new Array(); var usersRightsWindowHeight = 250; var usersRightsWindowWidth = 502; @@ -27,15 +27,11 @@ function newEvent(sender, type) { if (!day) day = currentDay; - var user = UserLogin; - if (sender.parentNode.getAttribute("id") != "toolbar" - && currentView == "multicolumndayview" && type == "event") - user = sender.parentNode.parentNode.getAttribute("user"); - var hour = sender.hour; if (!hour) hour = sender.getAttribute("hour"); - var urlstr = UserFolderURL + "../" + user + "/Calendar/new" + type; + var folderID = getSelectedFolder(); + var urlstr = ApplicationBaseURL + folderID + "/new" + type; var params = new Array(); if (day) params.push("day=" + day); @@ -49,6 +45,18 @@ function newEvent(sender, type) { return false; /* stop following the link */ } +function getSelectedFolder() { + var folder; + + var nodes = $("calendarList").getSelectedRows(); + if (nodes.length > 0) + folder = nodes[0].getAttribute("id"); + else + folder = "/personal"; + + return folder; +} + function onMenuNewEventClick(event) { newEvent(this, "event"); } @@ -57,13 +65,8 @@ function onMenuNewTaskClick(event) { newEvent(this, "task"); } -function _editEventId(id, owner) { - var urlBase; - if (owner) - urlBase = UserFolderURL + "../" + owner + "/"; - urlBase += "Calendar/" - - var urlstr = urlBase + id + "/edit"; +function _editEventId(id, calendar) { + var urlstr = ApplicationBaseURL + "/" + calendar + "/" + id + "/edit"; var targetname = "SOGo_edit_" + id; var win = window.open(urlstr, "_blank", "width=490,height=470,resizable=0"); @@ -76,10 +79,10 @@ function editEvent() { for (var i = 0; i < nodes.length; i++) _editEventId(nodes[i].getAttribute("id"), - nodes[i].owner); + nodes[i].calendar); } else if (selectedCalendarCell) { _editEventId(selectedCalendarCell[0].cname, - selectedCalendarCell[0].owner); + selectedCalendarCell[0].calendar); } return false; /* stop following the link */ @@ -87,9 +90,9 @@ function editEvent() { function _batchDeleteEvents() { var events = eventsToDelete.shift(); - var owner = ownersOfEventsToDelete.shift(); - var urlstr = (UserFolderURL + "../" + owner + "/Calendar/batchDelete?ids=" - + events.join('/')); + var calendar = calendarsOfEventsToDelete.shift(); + var urlstr = (ApplicationBaseURL + "/" + calendar + + "/batchDelete?ids=" + events.join('/')); document.deleteEventAjaxRequest = triggerAjaxRequest(urlstr, deleteEventCallback, events); @@ -102,9 +105,9 @@ function deleteEvent() { if (nodes.length > 0) { var label = ""; if (listOfSelection == $("tasksList")) - label = labels["taskDeleteConfirmation"].decodeEntities(); + label = labels["taskDeleteConfirmation"]; else - label = labels["eventDeleteConfirmation"].decodeEntities(); + label = labels["eventDeleteConfirmation"]; if (confirm(label)) { if (document.deleteEventAjaxRequest) { @@ -112,33 +115,33 @@ function deleteEvent() { document.deleteEventAjaxRequest.abort(); } var sortedNodes = new Array(); - var owners = new Array(); + var calendars = new Array(); for (var i = 0; i < nodes.length; i++) { - var owner = nodes[i].owner; - if (!sortedNodes[owner]) { - sortedNodes[owner] = new Array(); - owners.push(owner); + var calendar = nodes[i].calendar; + if (!sortedNodes[calendar]) { + sortedNodes[calendar] = new Array(); + calendars.push(calendar); } - sortedNodes[owner].push(nodes[i].cname); + sortedNodes[calendar].push(nodes[i].cname); } - for (var i = 0; i < owners.length; i++) { - ownersOfEventsToDelete.push(owners[i]); - eventsToDelete.push(sortedNodes[owners[i]]); + for (var i = 0; i < calendars.length; i++) { + calendarsOfEventsToDelete.push(calendars[i]); + eventsToDelete.push(sortedNodes[calendars[i]]); } _batchDeleteEvents(); } } } else if (selectedCalendarCell) { - var label = labels["eventDeleteConfirmation"].decodeEntities(); + var label = labels["eventDeleteConfirmation"]; if (confirm(label)) { if (document.deleteEventAjaxRequest) { document.deleteEventAjaxRequest.aborted = true; document.deleteEventAjaxRequest.abort(); } eventsToDelete.push([selectedCalendarCell[0].cname]); - ownersOfEventsToDelete.push(selectedCalendarCell[0].owner); + calendarsOfEventsToDelete.push(selectedCalendarCell[0].calendar); _batchDeleteEvents(); } } @@ -168,7 +171,7 @@ function closeInvitationWindow() { closePseudoWin.style.top = "0px;"; closePseudoWin.style.left = "0px;"; closePseudoWin.style.right = "0px;"; - closePseudoWin.appendChild(document.createTextNode(labels["closeThisWindowMessage"].decodeEntities())); + closePseudoWin.appendChild(document.createTextNode(labels["closeThisWindowMessage"])); document.body.appendChild(closeDiv); document.body.appendChild(closePseudoWin); } @@ -215,7 +218,7 @@ function deleteEventCallback(http) { } function editDoubleClickedEvent(event) { - _editEventId(this.cname, this.owner); + _editEventId(this.cname, this.calendar); preventDefault(event); event.cancelBubble = true; @@ -316,7 +319,7 @@ function eventsListCallback(http) { $(row).addClassName("eventRow"); row.setAttribute("id", escape(data[i][0])); row.cname = escape(data[i][0]); - row.owner = data[i][1]; + row.calendar = data[i][1]; var startDate = new Date(); startDate.setTime(data[i][4] * 1000); @@ -372,9 +375,8 @@ function tasksListCallback(http) { Event.observe(listItem, "dblclick", editDoubleClickedEvent.bindAsEventListener(listItem)); listItem.setAttribute("id", data[i][0]); $(listItem).addClassName(data[i][5]); - var owner = data[i][1]; - listItem.owner = owner; - $(listItem).addClassName("ownerIs" + owner.cssSafeString()); + listItem.calendar = data[i][1]; + $(listItem).addClassName("calendarFolder" + data[i][1]); listItem.cname = escape(data[i][0]); var input = document.createElement("input"); input.setAttribute("type", "checkbox"); @@ -429,7 +431,7 @@ function restoreCurrentDaySelection(div) { } function changeDateSelectorDisplay(day, keepCurrentDay) { - var url = ApplicationBaseURL + "dateselector"; + var url = ApplicationBaseURL + "/dateselector"; if (day) url += "?day=" + day; @@ -457,7 +459,7 @@ function changeDateSelectorDisplay(day, keepCurrentDay) { } function changeCalendarDisplay(time, newView) { - var url = ApplicationBaseURL + ((newView) ? newView : currentView); + var url = ApplicationBaseURL + "/" + ((newView) ? newView : currentView); selectedCalendarCell = null; @@ -583,7 +585,7 @@ function refreshCalendarEvents() { document.refreshCalendarEventsAjaxRequest.aborted = true; document.refreshCalendarEventsAjaxRequest.abort(); } - var url = ApplicationBaseURL + "eventslist?sd=" + sd + "&ed=" + ed; + var url = ApplicationBaseURL + "/eventslist?sd=" + sd + "&ed=" + ed; document.refreshCalendarEventsAjaxRequest = triggerAjaxRequest(url, refreshCalendarEventsCallback, {"startDate": sd, "endDate": ed}); @@ -608,9 +610,9 @@ function drawCalendarEvent(eventData, sd, ed) { var viewEndDate = ed.asDate(); var startDate = new Date(); - startDate.setTime(eventData[4] * 1000); + startDate.setTime(eventData[4] * 1000 + (1000 * UTCOffset)); var endDate = new Date(); - endDate.setTime(eventData[5] * 1000); + endDate.setTime(eventData[5] * 1000 + (1000 * UTCOffset)); var days = startDate.daysUpTo(endDate); @@ -636,8 +638,8 @@ function drawCalendarEvent(eventData, sd, ed) { // log("day: " + days[i]); if (i == 0) { - var quarters = (startDate.getHours() * 4 - + Math.floor(startDate.getMinutes() / 15)); + var quarters = (startDate.getUTCHours() * 4 + + Math.floor(startDate.getUTCMinutes() / 15)); starts = quarters; startHour = startDate.getDisplayHoursString(); endHour = endDate.getDisplayHoursString(); @@ -648,8 +650,8 @@ function drawCalendarEvent(eventData, sd, ed) { var ends; var lasts; if (i == days.length - 1) { - var quarters = (endDate.getHours() * 4 - + Math.ceil(endDate.getMinutes() / 15)); + var quarters = (endDate.getUTCHours() * 4 + + Math.ceil(endDate.getUTCMinutes() / 15)); ends = quarters; } else @@ -706,11 +708,11 @@ function drawCalendarEvent(eventData, sd, ed) { } } -function newEventDIV(cname, owner, starts, lasts, +function newEventDIV(cname, calendar, starts, lasts, startHour, endHour, title) { var eventDiv = document.createElement("div"); eventDiv.cname = escape(cname); - eventDiv.owner = owner; + eventDiv.calendar = calendar; $(eventDiv).addClassName("event"); $(eventDiv).addClassName("starts" + starts); $(eventDiv).addClassName("lasts" + lasts); @@ -723,7 +725,7 @@ function newEventDIV(cname, owner, starts, lasts, var innerDiv = document.createElement("div"); eventDiv.appendChild(innerDiv); $(innerDiv).addClassName("eventInside"); - $(innerDiv).addClassName("ownerIs" + owner.cssSafeString()); + $(innerDiv).addClassName("calendarFolder" + calendar); var gradientDiv = document.createElement("div"); innerDiv.appendChild(gradientDiv); @@ -881,7 +883,7 @@ function _loadEventHref(href) { document.eventsListAjaxRequest.aborted = true; document.eventsListAjaxRequest.abort(); } - var url = ApplicationBaseURL + href; + var url = ApplicationBaseURL + "/" + href; document.eventsListAjaxRequest = triggerAjaxRequest(url, eventsListCallback, href); @@ -897,7 +899,7 @@ function _loadTasksHref(href) { document.tasksListAjaxRequest.aborted = true; document.tasksListAjaxRequest.abort(); } - url = ApplicationBaseURL + href; + url = ApplicationBaseURL + "/" + href; var tasksList = $("tasksList"); var selectedIds; @@ -1119,7 +1121,6 @@ function onShowCompletedTasks(event) { function updateTaskStatus(event) { var taskId = this.parentNode.getAttribute("id"); - var taskOwner = this.parentNode.owner; var newStatus = (this.checked ? 1 : 0); var http = createHTTPClient(); @@ -1128,9 +1129,8 @@ function updateTaskStatus(event) { //log("update task status: " + taskId + " to " + this.checked); event.cancelBubble = true; - url = (UserFolderURL + "../" + taskOwner - + "/Calendar/" + taskId - + "/changeStatus?status=" + newStatus); + url = (ApplicationBaseURL + "/" + this.parentNode.calendar + + "/" + taskId + "/changeStatus?status=" + newStatus); if (http) { // log ("url: " + url); @@ -1162,10 +1162,11 @@ function updateCalendarStatus(event) { } } - if (!list.length) { - list.push(UserLogin); - nodes[0].childNodesWithTag("input")[0].checked = true; - } +// if (!list.length) { +// list.push(UserLogin); +// nodes[0].childNodesWithTag("input")[0].checked = true; +// } + // ApplicationBaseURL = (UserFolderURL + "Groups/_custom_" // + list.join(",") + "/Calendar/"); @@ -1207,7 +1208,7 @@ function calendarStatusCallback(http) { } function calendarEntryCallback(http) { - if (http.readyState == 4) { + if (http.readyState == 4) { var denied = !isHttpStatus204(http.status); var entry = $(http.callbackData); if (denied) @@ -1339,79 +1340,94 @@ function initCalendarSelector() { } var links = $("calendarSelectorButtons").childNodesWithTag("a"); - Event.observe(links[0], "click", onCalendarAdd); - Event.observe(links[1], "click", onCalendarRemove); + Event.observe(links[0], "click", onCalendarNew); + Event.observe(links[1], "click", onCalendarAdd); + Event.observe(links[2], "click", onCalendarRemove); } -function onCalendarAdd(event) { - openUserFolderSelector(onFolderSubscribeCB, "calendar"); +function onCalendarNew(event) { + createFolder(window.prompt(labels["Name of the Calendar"]), + appendCalendar); + preventDefault(event); +} - preventDefault(event); +function onCalendarAdd(event) { + openUserFolderSelector(onFolderSubscribeCB, "calendar"); + preventDefault(event); } function appendCalendar(folderName, folder) { - var calendarList = $("calendarList"); - var lis = calendarList.childNodesWithTag("li"); - var color = indexColor(lis.length); - //log ("color: " + color); - - var li = document.createElement("li"); - calendarList.appendChild(li); - - var checkBox = document.createElement("input"); - checkBox.setAttribute("type", "checkbox"); - li.appendChild(checkBox); - - li.appendChild(document.createTextNode(" ")); - - var colorBox = document.createElement("div"); - li.appendChild(colorBox); - li.appendChild(document.createTextNode(" " + folderName)); - colorBox.appendChild(document.createTextNode("OO")); + if (folder) + folder = accessToSubscribedFolder(folder); + else + folder = "/" + folderName; - li.setAttribute("id", folder); - Event.observe(li, "mousedown", listRowMouseDownHandler); - Event.observe(li, "click", onRowClick); - $(checkBox).addClassName("checkBox"); +// log ("append: " + folderName + "; folder: " + folder); - Event.observe(checkBox, "click", updateCalendarStatus.bindAsEventListener(checkBox)); + var calendarList = $("calendarList"); + var lis = calendarList.childNodesWithTag("li"); + var color = indexColor(lis.length + 100); + //log ("color: " + color); - $(colorBox).addClassName("colorBox"); - if (color) { - $(colorBox).setStyle({ color: color, - backgroundColor: color }); - } + var li = document.createElement("li"); + calendarList.appendChild(li); - var contactId = folder.split(":")[0]; - var url = URLForFolderID(folder) + "/canAccessContent"; - triggerAjaxRequest(url, calendarEntryCallback, folder); - - if (!document.styleSheets) return; - var theRules = new Array(); - var lastSheet = document.styleSheets[document.styleSheets.length - 1]; - if (lastSheet.insertRule) { // Mozilla - lastSheet.insertRule('.ownerIs' + contactId.cssSafeString() + ' {' - + ' background-color: ' - + color - + ' !important; }', 0); - } - else { // IE - lastSheet.addRule('.ownerIs' + contactId.cssSafeString(), - ' background-color: ' - + color - + ' !important; }'); - } + var checkBox = document.createElement("input"); + checkBox.setAttribute("type", "checkbox"); + li.appendChild(checkBox); + + li.appendChild(document.createTextNode(" ")); + + var colorBox = document.createElement("div"); + li.appendChild(colorBox); + li.appendChild(document.createTextNode(" " + folderName)); + colorBox.appendChild(document.createTextNode("OO")); + + li.setAttribute("id", folder); + Event.observe(li, "mousedown", listRowMouseDownHandler); + Event.observe(li, "click", onRowClick); + $(checkBox).addClassName("checkBox"); + + Event.observe(checkBox, "click", + updateCalendarStatus.bindAsEventListener(checkBox)); + + $(colorBox).addClassName("colorBox"); + if (color) + $(colorBox).setStyle({color: color, + backgroundColor: color}); + + var url = URLForFolderID(folder) + "/canAccessContent"; + triggerAjaxRequest(url, calendarEntryCallback, folder); + + if (!document.styleSheets) return; + var theRules = new Array(); + var lastSheet = document.styleSheets[document.styleSheets.length - 1]; + if (lastSheet.insertRule) { // Mozilla + lastSheet.insertRule('.calendarFolder' + folder.substr(1) + ' {' + + ' background-color: ' + + color + + ' !important; }', 0); + } + else { // IE + lastSheet.addRule('.calendarFolder' + folder.substr(1), + ' background-color: ' + + color + + ' !important; }'); + } } function onFolderSubscribeCB(folderData) { var folder = $(folderData["folder"]); if (!folder) - appendCalendar(folderData["folderName"], folderData["folder"]); + appendCalendar(folderData["folderName"], folderData["folder"]); } function onFolderUnsubscribeCB(folderId) { - var node = $(folderId); - node.parentNode.removeChild(node); + var node = $(folderId); + node.parentNode.removeChild(node); + refreshEvents(); + refreshTasks(); + changeCalendarDisplay(); } function onCalendarRemove(event) { @@ -1419,15 +1435,59 @@ function onCalendarRemove(event) { if (nodes.length > 0) { nodes[0].deselect(); var folderId = nodes[0].getAttribute("id"); - var folderIdElements = folderId.split(":"); + var folderIdElements = folderId.split("_"); if (folderIdElements.length > 1) { - unsubscribeFromFolder(folderId, onFolderUnsubscribeCB, folderId); + unsubscribeFromFolder(folderId, onFolderUnsubscribeCB, folderId); + } + else { + var calId = folderIdElements[0].substr(1); + deletePersonalCalendar(calId); } } preventDefault(event); } +function deletePersonalCalendar(folderId) { + var label + = labels["Are you sure you want to delete the selected calendar?"]; + if (window.confirm(label)) { + if (document.deletePersonalCalendarAjaxRequest) { + document.deletePersonalCalendarAjaxRequest.aborted = true; + document.deletePersonalCalendarAjaxRequest.abort(); + } + var url = ApplicationBaseURL + "/" + folderId + "/deleteFolder"; + document.deletePersonalCalendarAjaxRequest + = triggerAjaxRequest(url, deletePersonalCalendarCallback, folderId); + } +} + +function deletePersonalCalendarCallback(http) { + if (http.readyState == 4) { + if (isHttpStatus204(http.status)) { + var ul = $("calendarList"); + var children = ul.childNodesWithTag("li"); + var i = 0; + var done = false; + while (!done && i < children.length) { + var currentFolderId = children[i].getAttribute("id").substr(1); + if (currentFolderId == http.callbackData) { + ul.removeChild(children[i]); + done = true; + } + else + i++; + } + refreshEvents(); + refreshTasks(); + changeCalendarDisplay(); + } + document.deletePersonalCalendarAjaxRequest = null; + } + else + log ("ajax problem 5: " + http.status); +} + function configureLists() { var list = $("tasksList"); list.multiselect = true; diff --git a/UI/WebServerResources/UIxAclEditor.js b/UI/WebServerResources/UIxAclEditor.js index 91c249ab..2bef5e1d 100644 --- a/UI/WebServerResources/UIxAclEditor.js +++ b/UI/WebServerResources/UIxAclEditor.js @@ -88,8 +88,7 @@ function subscribeToFolder(refreshCallback, refreshCallbackData) { refreshCallbackData["folder"]); } else - refreshCallbackData["window"].alert(clabels["You cannot subscribe to a folder that you own!"] - .decodeEntities()); + refreshCallbackData["window"].alert(clabels["You cannot subscribe to a folder that you own!"]); } function openRightsForUserID(userID) { diff --git a/UI/WebServerResources/UIxAppointmentEditor.js b/UI/WebServerResources/UIxAppointmentEditor.js index 91c58eca..9f89c798 100644 --- a/UI/WebServerResources/UIxAppointmentEditor.js +++ b/UI/WebServerResources/UIxAppointmentEditor.js @@ -43,36 +43,36 @@ function validateAptEditor() { e = $('summary'); if (e.value.length == 0) { - if (!confirm(labels.validate_notitle.decodeEntities())) + if (!confirm(labels.validate_notitle)) return false; } e = $('startTime_date'); if (e.value.length != 10) { - alert(labels.validate_invalid_startdate.decodeEntities()); + alert(labels.validate_invalid_startdate); return false; } startdate = e.calendar.prs_date(e.value); if (startdate == null) { - alert(labels.validate_invalid_startdate.decodeEntities()); + alert(labels.validate_invalid_startdate); return false; } e = $('endTime_date'); if (e.value.length != 10) { - alert(labels.validate_invalid_enddate.decodeEntities()); + alert(labels.validate_invalid_enddate); return false; } enddate = e.calendar.prs_date(e.value); if (enddate == null) { - alert(labels.validate_invalid_enddate.decodeEntities()); + alert(labels.validate_invalid_enddate); return false; } // cuicui = ''; tmpdate = uixEarlierDate(startdate, enddate); if (tmpdate == enddate) { // window.alert(cuicui); - alert(labels.validate_endbeforestart.decodeEntities()); + alert(labels.validate_endbeforestart); return false; } else if (tmpdate == null /* means: same date */) { @@ -83,14 +83,14 @@ function validateAptEditor() { end = parseInt(document.forms[0]['endTime_time_hour'].value); if (start > end) { - alert(labels.validate_endbeforestart.decodeEntities()); + alert(labels.validate_endbeforestart); return false; } else if (start == end) { start = parseInt(document.forms[0]['startTime_time_minute'].value); end = parseInt(document.forms[0]['endTime_time_minute'].value); if (start > end) { - alert(labels.validate_endbeforestart.decodeEntities()); + alert(labels.validate_endbeforestart); return false; } } diff --git a/UI/WebServerResources/UIxComponentEditor.js b/UI/WebServerResources/UIxComponentEditor.js index 82213035..af61b807 100644 --- a/UI/WebServerResources/UIxComponentEditor.js +++ b/UI/WebServerResources/UIxComponentEditor.js @@ -24,7 +24,7 @@ function onPopupUrlWindow(event) { preventDefault(event); var urlInput = document.getElementById("url"); - var newUrl = window.prompt(labels["Target:"].decodeEntities(), urlInput.value); + var newUrl = window.prompt(labels["Target:"], urlInput.value); if (newUrl != null) { var documentHref = $("documentHref"); var documentLabel = $("documentLabel"); diff --git a/UI/WebServerResources/UIxContactEditor.js b/UI/WebServerResources/UIxContactEditor.js index 32670f0b..b17720da 100644 --- a/UI/WebServerResources/UIxContactEditor.js +++ b/UI/WebServerResources/UIxContactEditor.js @@ -84,13 +84,13 @@ function validateContactEditor() { if (e.value.length == 0) return true; if (uixEmailRegex.test(e.value) != true) - return confirm(labels.invalidemailwarn.decodeEntities()); + return confirm(labels.invalidemailwarn); e = $('homeMail'); if (e.value.length == 0) return true; if (uixEmailRegex.test(e.value) != true) - return confirm(labels.invalidemailwarn.decodeEntities()); + return confirm(labels.invalidemailwarn); return true; } diff --git a/UI/WebServerResources/UIxContactsUserFolders.js b/UI/WebServerResources/UIxContactsUserFolders.js index d060d319..bfcbd245 100644 --- a/UI/WebServerResources/UIxContactsUserFolders.js +++ b/UI/WebServerResources/UIxContactsUserFolders.js @@ -30,7 +30,7 @@ function addLineToTree(tree, parent, line) { for (var i = 1; i < nodes.length; i++) { var folderInfos = nodes[i].split(":"); var icon = ResourcesURL + '/'; - if (folderInfos[2] == 'contact') + if (folderInfos[2] == 'Contacts') icon += 'tb-mail-addressbook-flat-16x16.png'; else icon += 'calendar-folder-16x16.png'; diff --git a/UI/WebServerResources/UIxMailEditor.js b/UI/WebServerResources/UIxMailEditor.js index d84345b7..bddc2b23 100644 --- a/UI/WebServerResources/UIxMailEditor.js +++ b/UI/WebServerResources/UIxMailEditor.js @@ -133,8 +133,7 @@ function validateEditorInput(sender) { errortext = errortext + labels.error_missingrecipients + "\n"; if (errortext.length > 0) { - alert(labels.error_validationfailed.decodeEntities() + ":\n" - + errortext.decodeEntities()); + alert(labels.error_validationfailed + ":\n" + errortext); return false; } return true; diff --git a/UI/WebServerResources/UIxTaskEditor.js b/UI/WebServerResources/UIxTaskEditor.js index b68d3dac..5207d57e 100644 --- a/UI/WebServerResources/UIxTaskEditor.js +++ b/UI/WebServerResources/UIxTaskEditor.js @@ -24,7 +24,7 @@ function validateDate(date, label) { dateValue = date.calendar.prs_date(date.value); if (date.value.length != 10 || !dateValue) { - alert(label.decodeEntities()); + alert(label); result = false; } else result = dateValue; @@ -37,7 +37,7 @@ function validateTaskEditor() { e = document.getElementById('summary'); if (e.value.length == 0 - && !confirm(labels.validate_notitle.decodeEntities())) + && !confirm(labels.validate_notitle)) return false; e = document.getElementById('startTime_date'); @@ -58,7 +58,7 @@ function validateTaskEditor() { tmpdate = uixEarlierDate(startdate, enddate); if (tmpdate == enddate) { // window.alert(cuicui); - alert(labels.validate_endbeforestart.decodeEntities()); + alert(labels.validate_endbeforestart); return false; } else if (tmpdate == null /* means: same date */) { @@ -69,14 +69,14 @@ function validateTaskEditor() { end = parseInt(document.forms[0]['dueTime_time_hour'].value); if (start > end) { - alert(labels.validate_endbeforestart.decodeEntities()); + alert(labels.validate_endbeforestart); return false; } else if (start == end) { start = parseInt(document.forms[0]['startTime_time_minute'].value); end = parseInt(document.forms[0]['dueTime_time_minute'].value); if (start > end) { - alert(labels.validate_endbeforestart.decodeEntities()); + alert(labels.validate_endbeforestart); return false; } } diff --git a/UI/WebServerResources/add-user-calendar.png b/UI/WebServerResources/add-user-calendar.png new file mode 100644 index 0000000000000000000000000000000000000000..30c3926729341e6139a73fb00c1d19a20da78ead GIT binary patch literal 1168 zcmV;B1aJF^P){B`O6&2%rD}1RY64 zK~zY`?UhStlvfnTf0N8iCYi*^#2Bk%s)D9A@s$`OM*7f29~1(%T~sI(3Y9`BNU_jG z7jCag)7_DehwMK(^CzCjjZ@%Y!b@7cciAJG|u6p3Yx%WHg z{_pvp#|Qt*Vx21Y<(YEO0U=f+<^2{$|QbPQq&!-1&Q>_mtXb zgswwRfz;s=8k7QI7M!WjSea0l2Ba$6hCU}p+(i*oJTg8LS@On6xQtmCeEIDDE}xq_ z_GkdJWMd*_u!TTn-2f4@>>KAPI`QUdx(;{XiZEKKLnf30Ap~y8LJC1~>?R|>e@9KG zg*DH2aMbvXT`cF=y>57QZd2b zD+jrJ_Av)d5x&o@o6}UpBed?`!s^BpUVes#bOUQM zO;kPE%v@6&s_d%+@%TP4Cgd}oOyS2@kvQ;z8Z2Xy@ zhTmh#y0LtjY9Zz;!6<_QqBSX!+xC%&3!=s_J2?%2TXHBBET--j8EVLJaL;~nr2+%} zeQ3F6d5V+)1xT$!AR?$}HMtG1F!slnv}Nb0N+fV?hkV}RjJq4T!b0JrD+3(gLIsV> zR8bJj*S^vw94%6F?#5k)^aiG%ewRP3)y#Tv227rF%?^c~SxjsUTi7_lW_Yrnk@ydM z@YRu?j-xxeAEc<*DF?T2M&MzKT8cptM?36% zxe6-~WOIe~c&_qmpm{+j2b?|qTc|6;`7^)Jd)1|7#{payTn}pM+vvSGN^^(66*fLD zwzRQ@gC%^dKwt?$DG-SA?UhpmB8rgUcm^e7L@P4f>OaJ_OQ&$X zBGKyG>^qpI;Q0LYy@$+3X 1) { + var paths = parts[1].split("/"); + folder = "/" + parts[0] + "_" + paths[2]; + } + else + folder = serverFolder; + + return folder; } function listRowMouseDownHandler(event) { @@ -1277,12 +1291,25 @@ function onLoadHandler(event) { configureDragHandles(); configureSortableTableHeaders(); configureLinkBanner(); + translateLabels(); var progressImage = $("progressIndicator"); if (progressImage) progressImage.parentNode.removeChild(progressImage); Event.observe(document.body, "contextmenu", onBodyClickContextMenu); } +function translateLabels() { + if (typeof labels != "undefined") { + for (var key in labels) + labels[key] = labels[key].decodeEntities(); + } + + if (typeof clabels != "undefined") { + for (var key in clabels) + clabels[key] = clabels[key].decodeEntities(); + } +} + function onBodyClickContextMenu(event) { preventDefault(event); } @@ -1328,6 +1355,38 @@ function configureLinkBanner() { } } +/* folder creation */ +function createFolder(name, okCB, notOkCB) { + if (name) { + if (document.newFolderAjaxRequest) { + document.newFolderAjaxRequest.aborted = true; + document.newFolderAjaxRequest.abort(); + } + var url = ApplicationBaseURL + "/createFolder?name=" + name; + document.newFolderAjaxRequest + = triggerAjaxRequest(url, createFolderCallback, + {name: name, + okCB: okCB, + notOkCB: notOkCB}); + } +} + +function createFolderCallback(http) { + if (http.readyState == 4) { + var data = http.callbackData; + if (http.status == 201) { + if (data.okCB) + data.okCB(data.name, "/" + http.responseText); + } + else { + if (data.notOkCB) + data.notOkCB(name); + else + log("ajax problem:" + http.status); + } + } +} + addEvent(window, 'load', onLoadHandler); function parent$(element) { diff --git a/UI/WebServerResources/lori-login.jpg b/UI/WebServerResources/lori-login.jpg index a8947edb4f8a3cf9c76ed99f2effec29f4af838c..7ab42d121e19921b798c31fad3efe3ccee1cb29b 100644 GIT binary patch literal 7861 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