/* SOGoUserHomePage.m - this file is part of SOGo * * Copyright (C) 2007-2015 Inverse inc. * * This file is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This file is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. */ #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #if defined(SAML2_CONFIG) #import #endif #import #import #import #import #import #import #import #import #import #import #import #define INTERVALSECONDS 900 /* 15 minutes */ #define PADDING 8 #define HALFPADDING PADDING/2 @interface SOGoUserHomePage : UIxComponent @end @implementation SOGoUserHomePage - (id ) defaultAction { SOGoUserFolder *co; NSString *loginModule; SOGoUserDefaults *ud; NSURL *moduleURL; ud = [[context activeUser] userDefaults]; loginModule = [ud loginModule]; if (!([loginModule isEqualToString: @"Calendar"] || [loginModule isEqualToString: @"Contacts"] || [loginModule isEqualToString: @"Mail"])) { [self errorWithFormat: @"login module '%@' not accepted (must be" @"'Calendar', 'Contacts' or 'Mail')", loginModule]; loginModule = @"Calendar"; } co = [self clientObject]; moduleURL = [NSURL URLWithString: loginModule relativeToURL: [co soURL]]; return [self redirectToLocation: [moduleURL absoluteString]]; } - (void) _fillFreeBusyItems: (unsigned int *) items count: (unsigned int) itemCount withRecords: (NSArray *) records fromStartDate: (NSCalendarDate *) startDate toEndDate: (NSCalendarDate *) endDate { NSArray *emails, *partstates; NSCalendarDate *currentDate; NSDictionary *record; SOGoUser *user; int recordCount, recordMax, count, startInterval, endInterval, i, type, maxBookings, isResource, delta; recordMax = [records count]; user = [SOGoUser userWithLogin: [[self clientObject] ownerInContext: context] roles: nil]; maxBookings = [user numberOfSimultaneousBookings]; isResource = [user isResource]; // Fetch freebusy information if the user is NOT a resource or if multiplebookings isn't unlimited if (!isResource || maxBookings != 0) { for (recordCount = 0; recordCount < recordMax; recordCount++) { record = [records objectAtIndex: recordCount]; if ([[record objectForKey: @"c_isopaque"] boolValue]) { type = 0; // If the event has NO organizer (which means it's the user that has created it) OR // If we are the organizer of the event THEN we are automatically busy if ([[record objectForKey: @"c_orgmail"] length] == 0 || [user hasEmail: [record objectForKey: @"c_orgmail"]]) { type = 1; } else { // We check if the user has accepted/declined or needs action // on the current event. emails = [[record objectForKey: @"c_partmails"] componentsSeparatedByString: @"\n"]; for (i = 0; i < [emails count]; i++) { if ([user hasEmail: [emails objectAtIndex: i]]) { // We now fetch the c_partstates array and get the participation // status of the user for the event partstates = [[record objectForKey: @"c_partstates"] componentsSeparatedByString: @"\n"]; if (i < [partstates count]) { type = ([[partstates objectAtIndex: i] intValue] < 2 ? 1 : 0); } break; } } } if (type == 1) { // User is busy for this event; update items bit string currentDate = [record objectForKey: @"startDate"]; if ([currentDate earlierDate: startDate] == currentDate) startInterval = 0; else startInterval = ([currentDate timeIntervalSinceDate: startDate] / INTERVALSECONDS); delta = [[currentDate timeZoneDetail] timeZoneSecondsFromGMT] - [[startDate timeZoneDetail] timeZoneSecondsFromGMT]; startInterval += (delta/INTERVALSECONDS); startInterval = (startInterval < -(HALFPADDING) ? -(HALFPADDING) : startInterval); currentDate = [record objectForKey: @"endDate"]; if ([currentDate earlierDate: endDate] == endDate) endInterval = itemCount - 1; else endInterval = ([currentDate timeIntervalSinceDate: startDate] / INTERVALSECONDS); delta = [[currentDate timeZoneDetail] timeZoneSecondsFromGMT] - [[startDate timeZoneDetail] timeZoneSecondsFromGMT]; endInterval += (delta/INTERVALSECONDS); endInterval = (endInterval < 0 ? 0 : endInterval); endInterval = (endInterval > itemCount+HALFPADDING ? itemCount+HALFPADDING : endInterval); // Update bit string representation // If the user is a resource with restristed amount of bookings, keep the sum of overlapping events for (count = startInterval; count < endInterval; count++) { *(items + count) = (isResource && maxBookings > 0) ? *(items + count) + 1 : 1; } } } } if (maxBookings > 0) { // Reset the freebusy for the periods that are bellow the maximum number of bookings for (count = 0; count < itemCount; count++) { if (*(items + count) < maxBookings) *(items + count) = 0; else *(items + count) = 1; } } } } // // // - (NSString *) _freeBusyFromStartDate: (NSCalendarDate *) startDate toEndDate: (NSCalendarDate *) endDate forFreeBusy: (SOGoFreeBusyObject *) fb andContact: (NSString *) uid { NSCalendarDate *start, *end; NSMutableArray *freeBusy; unsigned int *freeBusyItems; NSTimeInterval interval; unsigned int count, intervals; // We "copy" the start/end date because -fetchFreeBusyInfosFrom will mess // with the timezone and we don't want that to properly calculate the delta // DO NOT USE -copy HERE - it'll simply return [self retain]. start = [NSCalendarDate dateWithYear: [startDate yearOfCommonEra] month: [startDate monthOfYear] day: [startDate dayOfMonth] hour: [startDate hourOfDay] minute: [startDate minuteOfHour] second: [startDate secondOfMinute] timeZone: [startDate timeZone]]; end = [NSCalendarDate dateWithYear: [endDate yearOfCommonEra] month: [endDate monthOfYear] day: [endDate dayOfMonth] hour: [endDate hourOfDay] minute: [endDate minuteOfHour] second: [endDate secondOfMinute] timeZone: [endDate timeZone]]; interval = [endDate timeIntervalSinceDate: startDate] + 60; // Slices of 15 minutes. The +8 is to take into account that we can // have a timezone change during the freebusy lookup. We have +4 at the // beginning and +4 at the end. intervals = interval / INTERVALSECONDS + PADDING; // Build a bit string representation of the freebusy data for the period freeBusyItems = calloc(intervals, sizeof (unsigned int)); [self _fillFreeBusyItems: (freeBusyItems+HALFPADDING) count: (intervals-PADDING) withRecords: [fb fetchFreeBusyInfosFrom: start to: end forContact: uid] fromStartDate: startDate toEndDate: endDate]; // Convert bit string to a NSArray. We also skip by the default the non-requested information. freeBusy = [NSMutableArray arrayWithCapacity: intervals]; for (count = HALFPADDING; count < (intervals-HALFPADDING); count++) { [freeBusy addObject: [NSString stringWithFormat: @"%d", *(freeBusyItems + count)]]; } free(freeBusyItems); // Return a NSString representation return [freeBusy componentsJoinedByString: @","]; } - (id ) readFreeBusyAction { WOResponse *response; SOGoFreeBusyObject *freebusy; NSCalendarDate *startDate, *endDate; NSString *queryDay, *uid; NSTimeZone *uTZ; SOGoUser *user; user = [context activeUser]; uTZ = [[user userDefaults] timeZone]; uid = [self queryParameterForKey: @"uid"]; queryDay = [self queryParameterForKey: @"sday"]; if ([queryDay length] == 8) { startDate = [NSCalendarDate dateFromShortDateString: queryDay andShortTimeString: @"0000" inTimeZone: uTZ]; queryDay = [self queryParameterForKey: @"eday"]; if ([queryDay length] == 8) { endDate = [NSCalendarDate dateFromShortDateString: queryDay andShortTimeString: @"2359" inTimeZone: uTZ]; if ([startDate earlierDate: endDate] == endDate) response = [self responseWithStatus: 403 andString: @"Start date is later than end date."]; else { freebusy = [self clientObject]; response = [self responseWithStatus: 200 andString: [self _freeBusyFromStartDate: startDate toEndDate: endDate forFreeBusy: freebusy andContact: uid]]; } } else response = [self responseWithStatus: 403 andString: @"Invalid end date."]; } else response = [self responseWithStatus: 403 andString: @"Invalid start date."]; return response; } - (NSString *) _logoutRedirectURL { NSString *redirectURL; SOGoSystemDefaults *sd; id container; sd = [SOGoSystemDefaults sharedSystemDefaults]; if ([[sd authenticationType] isEqualToString: @"cas"]) { redirectURL = [SOGoCASSession CASURLWithAction: @"logout" andParameters: nil]; } #if defined(SAML2_CONFIG) else if ([[sd authenticationType] isEqualToString: @"saml2"]) { NSString *username, *password, *domain, *value; SOGoSAML2Session *saml2Session; SOGoWebAuthenticator *auth; LassoServer *server; LassoLogout *logout; NSArray *creds; auth = [[self clientObject] authenticatorInContext: context]; value = [[context request] cookieValueForKey: [auth cookieNameInContext: context]]; creds = [auth parseCredentials: value]; value = [SOGoSession valueForSessionKey: [creds lastObject]]; domain = nil; [SOGoSession decodeValue: value usingKey: [creds objectAtIndex: 0] login: &username domain: &domain password: &password]; saml2Session = [SOGoSAML2Session SAML2SessionWithIdentifier: password inContext: context]; server = [SOGoSAML2Session lassoServerInContext: context]; logout = lasso_logout_new(server); lasso_profile_set_session_from_dump(LASSO_PROFILE(logout), [[saml2Session session] UTF8String]); lasso_profile_set_identity_from_dump(LASSO_PROFILE(logout), [[saml2Session session] UTF8String]); lasso_logout_init_request(logout, NULL, LASSO_HTTP_METHOD_REDIRECT); lasso_logout_build_request_msg(logout); redirectURL = [NSString stringWithFormat: @"%s", LASSO_PROFILE(logout)->msg_url]; // We destroy our cache entry, the session will be taken care by the caller [[SOGoCache sharedCache] removeSAML2LoginDumpsForIdentifier: password]; } #endif else { container = [[self clientObject] container]; redirectURL = [container baseURLInContext: context]; } return redirectURL; } - (WOCookie *) _logoutCookieWithDate: (NSCalendarDate *) date { SOGoWebAuthenticator *auth; NSString *cookieName, *appName; WOCookie *cookie; cookie = nil; auth = [[self clientObject] authenticatorInContext: context]; if ([auth respondsToSelector: @selector (cookieNameInContext:)]) { cookieName = [auth cookieNameInContext: context]; if ([cookieName length]) { cookie = [WOCookie cookieWithName: cookieName value: @"discard"]; appName = [[context request] applicationName]; [cookie setPath: [NSString stringWithFormat: @"/%@/", appName]]; [cookie setExpires: [date yesterday]]; } } return cookie; } - (id ) logoffAction { SOGoWebAuthenticator *auth; NSString *userName, *value; WOResponse *response; NSCalendarDate *date; WOCookie *cookie; NSArray *creds; userName = [[context activeUser] login]; [self logWithFormat: @"user '%@' logged off", userName]; response = [self redirectToLocation: [self _logoutRedirectURL]]; date = [NSCalendarDate calendarDate]; [date setTimeZone: [NSTimeZone timeZoneWithAbbreviation: @"GMT"]]; // We cleanup the memecached/database session cache. We do this before // invoking _logoutCookieWithDate: in order to obtain its value. auth = [[self clientObject] authenticatorInContext: context]; if ([auth respondsToSelector: @selector (cookieNameInContext:)]) { value = [[context request] cookieValueForKey: [auth cookieNameInContext: context]]; creds = [auth parseCredentials: value]; if ([creds count] > 1) [SOGoSession deleteValueForSessionKey: [creds objectAtIndex: 1]]; } cookie = [self _logoutCookieWithDate: date]; if (cookie) [response addCookie: cookie]; [response setHeader: [date rfc822DateString] forKey: @"Last-Modified"]; [response setHeader: @"no-store, no-cache, must-revalidate," @" max-age=0, post-check=0, pre-check=0" forKey: @"Cache-Control"]; [response setHeader: @"no-cache" forKey: @"Pragma"]; return response; } - (NSMutableArray *) _usersForResults: (NSArray *) users inDomain: (NSString *) domain { NSString *uid; NSDictionary *contact; NSString *contactInfo, *login; NSMutableArray *jsonResponse; NSMutableDictionary *jsonLine; NSArray *allUsers; int count, max; BOOL activeUserIsInDomain; login = [[context activeUser] login]; activeUserIsInDomain = ([domain length] == 0 || [[[context activeUser] domain] isEqualToString: domain]); // We sort our array - this is pretty useful for the Web // interface of SOGo. allUsers = [users sortedArrayUsingSelector: @selector (caseInsensitiveDisplayNameCompare:)]; max = [allUsers count]; jsonResponse = [NSMutableArray arrayWithCapacity: max]; for (count = 0; count < max; count++) { contact = [allUsers objectAtIndex: count]; uid = [contact objectForKey: @"c_uid"]; // We do NOT return the current authenticated user if (!activeUserIsInDomain || ![uid isEqualToString: login]) { jsonLine = [NSMutableDictionary dictionary]; if ([domain length]) uid = [NSString stringWithFormat: @"%@@%@", uid, domain]; [jsonLine setObject: uid forKey: @"uid"]; [jsonLine setObject: [contact objectForKey: @"cn"] forKey: @"cn"]; [jsonLine setObject: [contact objectForKey: @"c_email"] forKey: @"c_email"]; [jsonLine setObject: [NSNumber numberWithBool: [[contact objectForKey: @"isGroup"] boolValue]] forKey: @"isGroup"]; contactInfo = [contact objectForKey: @"c_info"]; if (contactInfo) [jsonLine setObject: contactInfo forKey: @"c_info"]; [jsonResponse addObject: jsonLine]; } } return jsonResponse; } /** * @api {get} /so/:username/usersSearch?search=:search Search for users * @apiVersion 1.0.0 * @apiName GetUsersSearch * @apiGroup Common * @apiExample {curl} Example usage: * curl -i http://localhost/SOGo/so/sogo1/usersSearch?search=john * * @apiParam {String} search Substring to match against username or email address * * @apiSuccess (Success 200) {Object[]} users List of matching users * @apiSuccess (Success 200) {String} users.uid User ID * @apiSuccess (Success 200) {String} users.c_email Main email address * @apiSuccess (Success 200) {String} users.cn Common name * @apiSuccess (Success 200) {Number} users.isGroup 1 if the user is a group * @apiError (Error 400) {Object} error The error message */ - (id ) usersSearchAction { NSMutableArray *users; NSArray *currentUsers; NSDictionary *message; NSString *contact, *domain, *uidDomain; NSEnumerator *visibleDomains; id result; SOGoUserManager *um; SOGoSystemDefaults *sd; contact = [self queryParameterForKey: @"search"]; if ([contact length]) { um = [SOGoUserManager sharedUserManager]; sd = [SOGoSystemDefaults sharedSystemDefaults]; domain = [[context activeUser] domain]; uidDomain = [sd enableDomainBasedUID]? domain : nil; users = [self _usersForResults: [um fetchUsersMatching: contact inDomain: domain] inDomain: uidDomain]; if ([domain length]) { // Add results from visible domains visibleDomains = [[sd visibleDomainsForDomain: domain] objectEnumerator]; while ((domain = [visibleDomains nextObject])) { currentUsers = [self _usersForResults: [um fetchUsersMatching: contact inDomain: domain] inDomain: uidDomain]; [users addObjectsFromArray: currentUsers]; } } result = [self responseWithStatus: 200 andJSONRepresentation: [NSDictionary dictionaryWithObject: users forKey: @"users"]]; } else { message = [NSDictionary dictionaryWithObject: [self labelForKey: @"Missing search parameter"] forKey: @"error"]; result = [self responseWithStatus: 400 andJSONRepresentation: message]; } return result; } - (WOResponse *) _foldersResponseForResults: (NSArray *) folders { WOResponse *response; response = [context response]; [response setStatus: 200]; [response setHeader: @"text/plain; charset=utf-8" forKey: @"Content-Type"]; [response appendContentString: [[NSDictionary dictionaryWithObject: folders forKey: @"folders"] JSONRepresentation]]; return response; } /** * @api {get} /so/:username/foldersSearch?type=:type Search for folders * @apiVersion 1.0.0 * @apiName GetFoldersSearch * @apiGroup Common * @apiExample {curl} Example usage: * curl -i http://localhost/SOGo/so/sogo1/foldersSearch?type=contact * * @apiParam {String} type Either 'calendar' or 'contact' * * @apiSuccess (Success 200) {Object[]} folders List of matching folders * @apiSuccess (Success 200) {String} folders.name Path of folder * @apiSuccess (Success 200) {String} folders.displayName Human readable name * @apiSuccess (Success 200) {String} folders.owner Username of owner * @apiSuccess (Success 200) {String} folders.type Either 'calendar' or 'contact' * @apiError (Error 400) {Object} error The error message */ - (id ) foldersSearchAction { NSString *folderType; NSArray *folders; NSDictionary *message; id result; SOGoUserFolder *userFolder; folderType = [self queryParameterForKey: @"type"]; if ([folderType length]) { userFolder = [self clientObject]; folders = [userFolder foldersOfType: folderType forUID: [userFolder ownerInContext: context]]; result = [self _foldersResponseForResults: folders]; } else { message = [NSDictionary dictionaryWithObject: [self labelForKey: @"Missing type parameter"] forKey: @"error"]; result = [self responseWithStatus: 400 andJSONRepresentation: message]; } return result; } /** * @api {get} /so/:username/date Get current day * @apiVersion 1.0.0 * @apiName GetCurrentDate * @apiGroup Common * @apiExample {curl} Example usage: * curl -i http://localhost/SOGo/so/sogo1/date * * @apiSuccess (Success 200) {String} weekday Full weekday name according to user's locale * @apiSuccess (Success 200) {String} month Full month name according to user's locale * @apiSuccess (Success 200) {String} day Day of month as two digit decimal number (leading zero) * @apiSuccess (Success 200) {String} year Year as a decimal number with century * @apiSuccess (Success 200) {Object} abbr Abbreviations * @apiSuccess (Success 200) {String} abbr.weekday Abbreviated weekday name according to user's locale * @apiSuccess (Success 200) {String} abbr.month Abbreviated month name according to user's locale */ - (id ) dateAction { return [self responseWithStatus: 200 andJSONRepresentation: [[context activeUser] currentDay]]; } - (id) recoverAction { return [self responseWithStatus: 200 andString: @"Full recovery in place."]; } @end