From 51f1521dba2d0ca8bdbadffadb16db26b975f668 Mon Sep 17 00:00:00 2001 From: Hivert Quentin Date: Mon, 23 Jun 2025 17:14:39 +0200 Subject: [PATCH] feat(api): add endpoint for caldav/cardav url --- .gitignore | 1 + API/GNUmakefile | 28 ++ API/GNUmakefile.preamble | 16 + API/SOGoAPI.h | 35 ++ API/SOGoAPI.m | 52 +++ API/SOGoAPIConstants.h | 0 API/SOGoAPIDispatcher.h | 54 +++ API/SOGoAPIDispatcher.m | 399 ++++++++++++++++++++ API/SOGoAPIProduct.m | 11 + API/SOGoAPIUserFolder.h | 33 ++ API/SOGoAPIUserFolder.m | 110 ++++++ API/SOGoAPIVersion.h | 33 ++ API/SOGoAPIVersion.m | 42 +++ API/common.make | 34 ++ API/product.plist | 31 ++ GNUmakefile | 1 + Main/SOGo.m | 10 +- SOPE/GDLContentStore/GCSFolderManager.h | 1 + SOPE/GDLContentStore/GCSFolderManager.m | 87 +++++ SoObjects/SOGo/SOGoOpenIdSession.m | 9 +- SoObjects/SOGo/SOGoProductLoader.m | 36 +- SoObjects/SOGo/SOGoSystemDefaults.h | 1 + SoObjects/SOGo/SOGoSystemDefaults.m | 5 + UI/MainUI/GNUmakefile | 1 + UI/MainUI/SOGoAPIActions.m | 73 ++++ UI/MainUI/product.plist | 5 + UI/WebServerResources/js/vendor/punycode.js | 2 +- 27 files changed, 1088 insertions(+), 22 deletions(-) create mode 100644 API/GNUmakefile create mode 100644 API/GNUmakefile.preamble create mode 100644 API/SOGoAPI.h create mode 100644 API/SOGoAPI.m create mode 100644 API/SOGoAPIConstants.h create mode 100644 API/SOGoAPIDispatcher.h create mode 100644 API/SOGoAPIDispatcher.m create mode 100644 API/SOGoAPIProduct.m create mode 100644 API/SOGoAPIUserFolder.h create mode 100644 API/SOGoAPIUserFolder.m create mode 100644 API/SOGoAPIVersion.h create mode 100644 API/SOGoAPIVersion.m create mode 100644 API/common.make create mode 100644 API/product.plist create mode 100644 UI/MainUI/SOGoAPIActions.m diff --git a/.gitignore b/.gitignore index a5ed35220..c34fbc107 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ */obj/ .scss-lint-config.yml_ ActiveSync/ActiveSync.SOGo +API/API.SOGo Documentation/*.docbook Documentation/*.pdf Documentation/*.html diff --git a/API/GNUmakefile b/API/GNUmakefile new file mode 100644 index 000000000..861ba25e4 --- /dev/null +++ b/API/GNUmakefile @@ -0,0 +1,28 @@ + +# GNUstep makefile + +include common.make + +BUNDLE_NAME = API + +API_PRINCIPAL_CLASS = SOGoAPIProduct + +API_OBJC_FILES = \ + SOGoAPIProduct.m \ + SOGoAPI.m \ + SOGoAPIVersion.m \ + SOGoAPIUserFolder.m \ + SOGoAPIDispatcher.m + +API_RESOURCE_FILES += \ + product.plist + +API_LANGUAGES = $(SOGO_LANGUAGES) + +API_LOCALIZED_RESOURCE_FILES = Localizable.strings + +ADDITIONAL_INCLUDE_DIRS += -I../SOPE/ -I../SoObjects/ + +-include GNUmakefile.preamble +include $(GNUSTEP_MAKEFILES)/bundle.make +-include GNUmakefile.postamble diff --git a/API/GNUmakefile.preamble b/API/GNUmakefile.preamble new file mode 100644 index 000000000..ce44611a4 --- /dev/null +++ b/API/GNUmakefile.preamble @@ -0,0 +1,16 @@ +# compile settings + +ADDITIONAL_CPPFLAGS += \ + -DSOGO_MAJOR_VERSION=$(MAJOR_VERSION) \ + -DSOGO_MINOR_VERSION=$(MINOR_VERSION) \ + -DSOGO_PATCH_VERSION=$(SUBMINOR_VERSION) \ + -DSOGO_LIBDIR="@\"$(SOGO_LIBDIR)\"" + +ADDITIONAL_INCLUDE_DIRS += \ + -D_GNU_SOURCE -I../SOPE/ -I../SoObjects/ + +ADDITIONAL_LIB_DIRS += \ + -L../SoObjects/SOGo/SOGo.framework/sogo -lSOGo \ + -L../SOPE/GDLContentStore/$(GNUSTEP_OBJ_DIR)/ -lGDLContentStore \ + -L../SOPE/NGCards/$(GNUSTEP_OBJ_DIR)/ -lNGCards \ + -lEOControl -lNGStreams -lNGMime -lNGExtensions -lNGObjWeb -lWEExtensions \ No newline at end of file diff --git a/API/SOGoAPI.h b/API/SOGoAPI.h new file mode 100644 index 000000000..2dcf15942 --- /dev/null +++ b/API/SOGoAPI.h @@ -0,0 +1,35 @@ +/* + Copyright (C) 2004-2005 SKYRIX Software AG + + This file is part of SOPE. + + SOPE is free software; you can redistribute it and/or modify it under + the terms of the GNU Lesser General Public License as published by the + Free Software Foundation; either version 2, or (at your option) any + later version. + + SOPE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with SOPE; see the file COPYING. If not, write to the + Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA. +*/ + +#import +#import +#import +#import + +@class WOContext; + +@interface SOGoAPI : NSObject +- (NSArray *) methodAllowed; +- (BOOL) needAuth; +- (NSArray *) paramNeeded; +- (NSDictionary *) action: (WOContext*) ctx withParam: (NSDictionary *) param; + +@end diff --git a/API/SOGoAPI.m b/API/SOGoAPI.m new file mode 100644 index 000000000..6804db0e1 --- /dev/null +++ b/API/SOGoAPI.m @@ -0,0 +1,52 @@ +/* + Copyright (C) todo... +*/ + +#import + + +@implementation SOGoAPI + +- (id) init +{ + [super init]; + + return self; +} + +- (void) dealloc +{ + [super dealloc]; +} + +- (NSArray *) methodAllowed +{ + NSArray *result; + + result = [NSArray arrayWithObjects:@"GET",nil]; + return result; +} + +- (BOOL) needAuth +{ + return YES; +} + +- (NSArray *) paramNeeded +{ + return nil; +} + +- (NSDictionary *) action: (WOContext*) ctx withParam: (NSDictionary *) param +{ + NSDictionary* result; + + result = [[NSDictionary alloc] initWithObjectsAndKeys: + @"API not defined", @"error", + nil]; + [result autorelease]; + return result; +} + + +@end /* SOGoAPI */ \ No newline at end of file diff --git a/API/SOGoAPIConstants.h b/API/SOGoAPIConstants.h new file mode 100644 index 000000000..e69de29bb diff --git a/API/SOGoAPIDispatcher.h b/API/SOGoAPIDispatcher.h new file mode 100644 index 000000000..525744b01 --- /dev/null +++ b/API/SOGoAPIDispatcher.h @@ -0,0 +1,54 @@ +/* + +Copyright (c) 2014-2015, Inverse inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Inverse inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*/ + +#import +#import +#import + +@class NSCalendarDate; +@class NSException; +@class NSMutableDictionary; +@class NSURL; +@class NSNumber; + +static volatile BOOL apiShouldTerminate = NO; + +@interface SOGoAPIDispatcher : NSObject +{ + + id context; + BOOL debugOn; +} + +- (NSException *) dispatchRequest: (WORequest*) theRequest + inResponse: (WOResponse*) theResponse + context: (id) theContext; + +@end \ No newline at end of file diff --git a/API/SOGoAPIDispatcher.m b/API/SOGoAPIDispatcher.m new file mode 100644 index 000000000..ac820618c --- /dev/null +++ b/API/SOGoAPIDispatcher.m @@ -0,0 +1,399 @@ +/* + +Copyright (c) 2014, Inverse inc. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of the Inverse inc. nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +*/ + +#include "SOGoAPIDispatcher.h" + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + + +void handle_api_terminate(int signum) +{ + NSLog(@"Forcing termination of API loop."); + apiShouldTerminate = YES; + [[WOCoreApplication application] terminateAfterTimeInterval: 1]; +} + +@implementation SOGoAPIDispatcher + +- (id) init +{ + [super init]; + + debugOn = [[SOGoSystemDefaults sharedSystemDefaults] apiDebugEnabled]; + apiShouldTerminate = NO; + + signal(SIGTERM, handle_api_terminate); + + return self; +} + +- (void) dealloc +{ + [super dealloc]; +} + +- (void) _sendAPIErrorResponse: (WOResponse* ) response + withMessage: (NSString *) message + withStatus: (unsigned int) status +{ + NSDictionary *msg; + msg = [[NSDictionary alloc] initWithObjectsAndKeys: + message, @"error", + nil]; + [response setStatus: status]; + [response setContent: [msg jsonRepresentation]]; +} + +- (NSString *) _getActionFromUri: (NSString *) _uri +{ + /* + _uri always start with /SOGo/SOGoAPI + full _uri example /SOGo/SOGoAPI/Action/subaction1?param1¶m2 + */ + + NSString *uriWithoutParams, *action, *prefix; + NSArray *uriSplits; + + prefix = @"/SOGo/SOGoAPI"; + action = @""; + + uriWithoutParams = [_uri urlWithoutParameters]; + if(![uriWithoutParams hasPrefix: prefix]) + { + [self errorWithFormat: @"Uri for API request does not start with /SOGo/SOGoAPI: %@", uriWithoutParams]; + return nil; + } + else + { + uriWithoutParams = [uriWithoutParams substringFromIndex:[prefix length]]; + } + + //remove first and last '/'' if needed + if([uriWithoutParams hasPrefix: @"/"]) + { + uriWithoutParams = [uriWithoutParams substringFromIndex:1]; + } + if([uriWithoutParams hasSuffix: @"/"]) + { + uriWithoutParams = [uriWithoutParams substringToIndex:([uriWithoutParams length] -1)]; + } + if([uriWithoutParams length] == 0) + { + [self warnWithFormat: @"Uri for API request has no action, make Version instead: %@", uriWithoutParams]; + return @"SOGoAPIVersion"; + } + else + { + uriSplits = [uriWithoutParams componentsSeparatedByString: @"/"]; + action = [@"SOGoAPI" stringByAppendingString: [uriSplits objectAtIndex: 0]]; + if(debugOn) + [self logWithFormat: @"API request, action made is %@", action]; + } + + return action; +} + +- (NSDictionary *) _authBasicCheck: (NSString *) auth +{ + NSDictionary *user; + NSRange rng; + NSString *decodeCred, *domain, *login, *pwd; + SOGoUserManager *lm; + SOGoPasswordPolicyError perr; + int expire, grace; + BOOL rc; + + user = nil; + + decodeCred = [[auth substringFromIndex:5] stringByTrimmingLeadWhiteSpaces]; + decodeCred = [decodeCred stringByDecodingBase64]; + + rng = [decodeCred rangeOfString:@":"]; + login = [decodeCred substringToIndex:rng.location]; + pwd = [decodeCred substringFromIndex:(rng.location + rng.length)]; + + domain = nil; + perr = PolicyNoError; + rc = ([[SOGoUserManager sharedUserManager] + checkLogin: [login stringByReplacingString: @"%40" + withString: @"@"] + password: pwd + domain: &domain + perr: &perr + expire: &expire + grace: &grace + additionalInfo: nil] + && perr == PolicyNoError); + + if(rc) + { + //Fecth user info + lm = [SOGoUserManager sharedUserManager]; + user = [lm contactInfosForUserWithUIDorEmail: login]; + } + else + user = nil; + + return user; +} + +- (NSDictionary *) _authOpenId: (NSString *) auth withDomain: (NSString *) domain +{ + NSDictionary *user; + NSString *token, *login; + SOGoOpenIdSession *openIdSession; + SOGoUserManager *lm; + + user = nil; + token = [[auth substringFromIndex:6] stringByTrimmingLeadWhiteSpaces]; + + openIdSession = [SOGoOpenIdSession OpenIdSession: domain]; + if(![openIdSession sessionIsOk]) + { + [self errorWithFormat: @"API - OpenId server not found or has unexpected behavior, contact your admin."]; + return nil; + } + + [openIdSession setAccessToken: token]; + login = [openIdSession login: @""]; + + if(login && ![login isEqualToString: @"anonymous"]) + { + //Fecth user info + lm = [SOGoUserManager sharedUserManager]; + user = [lm contactInfosForUserWithUIDorEmail: login]; + } + else + user = nil; + + return user; +} + + +- (NSException *) dispatchRequest: (WORequest*) theRequest + inResponse: (WOResponse*) theResponse + context: (id) theContext +{ + NSAutoreleasePool *pool; + id activeUser; + NSString *method, *action, *error; + NSDictionary *form; + NSArray *paramNeeded; + NSMutableDictionary *paramInjected, *ret; + NSBundle *bundle; + id classAction; + Class clazz; + + + pool = [[NSAutoreleasePool alloc] init]; + ASSIGN(context, theContext); + + //Get the api action, check it + action = [self _getActionFromUri: [theRequest uri]]; + if(!action) + { + error = [NSString stringWithFormat: @"No actions found for request to API: %@", [theRequest uri]]; + [self errorWithFormat: error]; + [self _sendAPIErrorResponse: theResponse withMessage: error withStatus: 400]; + RELEASE(context); + RELEASE(pool); + return nil; + } + + //Get the class for this action, check it + bundle = [NSBundle bundleForClass: NSClassFromString(@"SOGoAPIProduct")]; + clazz = [bundle classNamed: action]; + if(!clazz) + { + error = [NSString stringWithFormat: @"No backend API found for action: %@", action]; + [self errorWithFormat: error]; + [self _sendAPIErrorResponse: theResponse withMessage: error withStatus: 400]; + RELEASE(context); + RELEASE(pool); + return nil; + } + + //try to instantiate the class + NS_DURING + { + classAction = [[clazz alloc] init]; + } + NS_HANDLER + { + error = [NSString stringWithFormat: @"Can't alloc and init class: %@", classAction]; + [self errorWithFormat: error]; + [self _sendAPIErrorResponse: theResponse withMessage: error withStatus: 500]; + RELEASE(context); + RELEASE(pool); + return nil; + } + NS_ENDHANDLER; + + //Check method + method = [theRequest method]; + if(![[classAction methodAllowed] containsObject: method]) + { + error = [NSString stringWithFormat: @"Method %@ not allowed for action %@", method, action]; + [self errorWithFormat: error]; + [self _sendAPIErrorResponse: theResponse withMessage: error withStatus: 400]; + RELEASE(context); + RELEASE(pool); + return nil; + } + + + paramInjected = [NSMutableDictionary dictionary]; + //Check parameters + if((paramNeeded = [classAction paramNeeded]) && [paramNeeded count] >= 1) + { + NSDictionary* formAndQuery = [theRequest formValues]; + for(NSString *param in paramNeeded) + { + id value; + if((value = [formAndQuery objectForKey: param])) + { + NSString* trueValue; + if ([value isKindOfClass: [NSArray class]]) + trueValue = [value lastObject]; + else + trueValue = value; + [paramInjected setObject: trueValue forKey: param]; + } + else + { + error = [NSString stringWithFormat: @"Missing param %@ for action %@", param, action]; + [self errorWithFormat: error]; + [self _sendAPIErrorResponse: theResponse withMessage: error withStatus: 400]; + RELEASE(context); + RELEASE(pool); + return nil; + } + } + } + + if([classAction needAuth]) + { + if(debugOn) + [self logWithFormat: @"Check auth for action %@", action]; + //check auth + NSString *auth = [theRequest headerForKey: @"authorization"]; + if(auth) + { + NSDictionary* user; + if([[auth lowercaseString] hasPrefix: @"basic"]) + { + //basic auth + user = [self _authBasicCheck: auth]; + } + else if([[auth lowercaseString] hasPrefix: @"bearer"]) + { + //openid auth, we may need to know the user-domain to know which openid server to fetch + NSString *domain = [theRequest headerForKey: @"user-domain"]; + user = [self _authOpenId: auth withDomain: domain]; + } + else + { + error = [NSString stringWithFormat: @"Authorization method incorrect: %@ for action %@", auth, action]; + [self errorWithFormat: error]; + [self _sendAPIErrorResponse: theResponse withMessage: error withStatus: 401]; + RELEASE(context); + RELEASE(pool); + return nil; + } + + //add current user in paramInjected + if(user){ + if(debugOn) + [self logWithFormat: @"User authenticated %@", user]; + [paramInjected setObject: user forKey: @"user"]; + } + else + { + error = [NSString stringWithFormat: @"User wrong login or not found for action %@", action]; + [self errorWithFormat: error]; + [self _sendAPIErrorResponse: theResponse withMessage: error withStatus: 401]; + RELEASE(context); + RELEASE(pool); + return nil; + } + } + else + { + error = [NSString stringWithFormat: @"No authorization header found for action %@", action]; + [self errorWithFormat: error]; + [self _sendAPIErrorResponse: theResponse withMessage: error withStatus: 401]; + RELEASE(context); + RELEASE(pool); + return nil; + } + } + + //Execute action + // NS_DURING + // { + ret = [classAction action: context withParam: paramInjected]; + // } + // NS_HANDLER + // { + // error = [NSString stringWithFormat: @"Internal error during: %@", action]; + // [self errorWithFormat: error]; + // [self _sendAPIErrorResponse: theResponse withMessage: error withStatus: 500]; + // RELEASE(context); + // RELEASE(pool); + // return nil; + // } + // NS_ENDHANDLER; + + + //Make the response + [theResponse setContent: [ret jsonRepresentation]]; + + RELEASE(context); + RELEASE(pool); + + return nil; +} + +@end \ No newline at end of file diff --git a/API/SOGoAPIProduct.m b/API/SOGoAPIProduct.m new file mode 100644 index 000000000..cbc833874 --- /dev/null +++ b/API/SOGoAPIProduct.m @@ -0,0 +1,11 @@ +/* + Copyright (C) todo... +*/ + +#import + +@interface SOGoAPIProduct : NSObject +@end + +@implementation SOGoAPIProduct +@end /* SOGoAPIProduct */ diff --git a/API/SOGoAPIUserFolder.h b/API/SOGoAPIUserFolder.h new file mode 100644 index 000000000..d58778eca --- /dev/null +++ b/API/SOGoAPIUserFolder.h @@ -0,0 +1,33 @@ +/* + Copyright (C) 2004-2005 SKYRIX Software AG + + This file is part of SOPE. + + SOPE is free software; you can redistribute it and/or modify it under + the terms of the GNU Lesser General Public License as published by the + Free Software Foundation; either version 2, or (at your option) any + later version. + + SOPE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with SOPE; see the file COPYING. If not, write to the + Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA. +*/ + +#import +#import +#import +#import +#import + +@class WOContext; + +@interface SOGoAPIUserFolder : SOGoAPI +- (NSArray *) methodAllowed; +- (NSDictionary *) action: (WOContext*) ctx withParam: (NSDictionary *) param; +@end diff --git a/API/SOGoAPIUserFolder.m b/API/SOGoAPIUserFolder.m new file mode 100644 index 000000000..c66d49fbd --- /dev/null +++ b/API/SOGoAPIUserFolder.m @@ -0,0 +1,110 @@ +/* + Copyright (C) todo... +*/ + +#import + +#import +#import + +@implementation SOGoAPIUserFolder + +- (id) init +{ + [super init]; + + return self; +} + +- (void) dealloc +{ + [super dealloc]; +} + +/** + + + +{ + "calendar":[ + { + "name":"DidyShared", + "url":"http://127.0.0.1/SOGo/dav/sogo-tests1@example.org/Calendar/12509-67F67D00-1-3105AF40" + }, + { + "name":"LocalDidy", + "url":"http://127.0.0.1/SOGo/dav/sogo-tests1@example.org/Calendar/1BC38-67B60000-1-6E4B6880" + }, + { + "name":"Personal Calendar", + "url":"http://127.0.0.1/SOGo/dav/sogo-tests1@example.org/Calendar/personal" + } + ], + "username":"sogo-tests1@example.org", + "contact":[ + { + "name":"Personal Address Book", + "url":"http://127.0.0.1/SOGo/dav/sogo-tests1@example.org/Contacts/personal" + } + ] +} +**/ + +- (NSDictionary *) action: (WOContext*) ctx withParam: (NSDictionary *) param +{ + NSDictionary* result; + NSArray *folders; + NSMutableArray *cardavLinks, *caldavLinks; + NSString *serverUrl, *basePath, *c_uid, *url; + GCSFolderManager *fm; + GCSFolder *folder; + + int max, i; + + //Should be a user + c_uid = [[[param objectForKey: @"user"] objectForKey: @"emails"] objectAtIndex: 0]; + + //fetch folders + fm = [GCSFolderManager defaultFolderManager]; + basePath = [NSString stringWithFormat: @"/Users/%@", c_uid]; + folders = [fm listSubFoldersAndNamesAtPath: basePath recursive: YES]; + + //Generate dav link + max = [folders count]; + serverUrl = [[ctx serverURL] absoluteString]; + + cardavLinks = [NSMutableArray array]; + caldavLinks = [NSMutableArray array]; + serverUrl = [[ctx serverURL] absoluteString]; + for (i = 0; i < max; i++) + { + NSMutableDictionary *folderRet; + folderRet = [NSMutableDictionary dictionary]; + folder = [folders objectAtIndex: i]; + url = [NSString stringWithFormat: @"%@/SOGo/dav/%@/%@", serverUrl, c_uid, [folder objectForKey: @"path"]]; + [folderRet setObject: url forKey: @"url"]; + [folderRet setObject: [folder objectForKey: @"name"] forKey: @"name"]; + if([url rangeOfString:@"/Calendar/"].location == NSNotFound) + { + //Contacts + [cardavLinks addObject: folderRet]; + } + else + { + //Calendar + [caldavLinks addObject: folderRet]; + } + } + + result = [[NSDictionary alloc] initWithObjectsAndKeys: + c_uid, @"username", + cardavLinks, @"contact", + caldavLinks, @"calendar", + nil]; + + [result autorelease]; + return result; +} + + +@end /* SOGoAPIVersion */ \ No newline at end of file diff --git a/API/SOGoAPIVersion.h b/API/SOGoAPIVersion.h new file mode 100644 index 000000000..062cd156a --- /dev/null +++ b/API/SOGoAPIVersion.h @@ -0,0 +1,33 @@ +/* + Copyright (C) 2004-2005 SKYRIX Software AG + + This file is part of SOPE. + + SOPE is free software; you can redistribute it and/or modify it under + the terms of the GNU Lesser General Public License as published by the + Free Software Foundation; either version 2, or (at your option) any + later version. + + SOPE is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with SOPE; see the file COPYING. If not, write to the + Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA. +*/ + +#import +#import +#import +#import +#import + +@class WOContext; + +@interface SOGoAPIVersion : SOGoAPI +- (NSArray *) methodAllowed; +- (NSDictionary *) action: (WOContext*) ctx withParam: (NSDictionary *) param; +@end diff --git a/API/SOGoAPIVersion.m b/API/SOGoAPIVersion.m new file mode 100644 index 000000000..4af1dd363 --- /dev/null +++ b/API/SOGoAPIVersion.m @@ -0,0 +1,42 @@ +/* + Copyright (C) todo... +*/ + +#import + + +@implementation SOGoAPIVersion + +- (id) init +{ + [super init]; + + return self; +} + +- (void) dealloc +{ + [super dealloc]; +} + +- (BOOL) needAuth +{ + return NO; +} + +- (NSDictionary *) action: (WOContext*) ctx withParam: (NSDictionary *) param +{ +NSDictionary* result; + +result = [[NSDictionary alloc] initWithObjectsAndKeys: + [NSNumber numberWithInt:SOGO_MAJOR_VERSION], @"major", + [NSNumber numberWithInt:SOGO_MINOR_VERSION], @"minor", + [NSNumber numberWithInt:SOGO_PATCH_VERSION], @"patch", + nil]; + +[result autorelease]; +return result; +} + + +@end /* SOGoAPIVersion */ \ No newline at end of file diff --git a/API/common.make b/API/common.make new file mode 100644 index 000000000..bf77fa3be --- /dev/null +++ b/API/common.make @@ -0,0 +1,34 @@ +include ../config.make +include $(GNUSTEP_MAKEFILES)/common.make +include ../Version + +NEEDS_GUI=no +BUNDLE_EXTENSION = .SOGo +BUNDLE_INSTALL_DIR = $(SOGO_LIBDIR) +WOBUNDLE_EXTENSION = $(BUNDLE_EXTENSION) +WOBUNDLE_INSTALL_DIR = $(BUNDLE_INSTALL_DIR) + +# SYSTEM_LIB_DIR += -L/usr/local/lib -L/usr/lib + +ADDITIONAL_INCLUDE_DIRS += \ + -I.. \ + -I../.. \ + -I../../SOPE + +ADDITIONAL_LIB_DIRS += \ + -L../SoObjects/SOGo/SOGo.framework/Versions/Current/sogo \ + -L../SoObjects/SOGo/$(GNUSTEP_OBJ_DIR)/ \ + -L../SOPE/NGCards/$(GNUSTEP_OBJ_DIR)/ \ + -L/usr/local/lib \ + -Wl,-rpath,$(SOGO_SYSLIBDIR)/sogo + +BUNDLE_LIBS += \ + -lSOGo \ + -lGDLContentStore \ + -lGDLAccess \ + -lNGObjWeb \ + -lNGCards -lNGMime -lNGLdap \ + -lNGStreams -lNGExtensions -lEOControl \ + -lDOM -lSaxObjC -lSBJson + +ADDITIONAL_BUNDLE_LIBS += $(BUNDLE_LIBS) diff --git a/API/product.plist b/API/product.plist new file mode 100644 index 000000000..809b101b9 --- /dev/null +++ b/API/product.plist @@ -0,0 +1,31 @@ +{ + requires = ( MAIN, Appointments, Contacts, Mailer ); + + publicResources = (); + + factories = {}; + + classes = { + SOGoAPI = { + protectedBy = ""; + defaultRoles = { + "View" = ( "Authenticated", "PublicUser" ); + }; + }; + }; + + categories = { + SOGoAPI = { + slots = { + }; + methods = { + Version = { + protectedBy = "View"; + pageName = "SOGoAPI"; + actionName = "sogoVersion"; + }; + }; + }; + }; + +} \ No newline at end of file diff --git a/GNUmakefile b/GNUmakefile index fdff4d793..ccb4631b8 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -9,6 +9,7 @@ SUBPROJECTS = \ SoObjects \ Main \ UI \ + API \ Tools \ Tests/Unit \ diff --git a/Main/SOGo.m b/Main/SOGo.m index 9218a53ad..5faf318a0 100644 --- a/Main/SOGo.m +++ b/Main/SOGo.m @@ -417,7 +417,7 @@ static BOOL debugLeaks; { id obj; WORequest *request; - BOOL isDAVRequest; + BOOL isDAVRequest, isAPIRequest; SOGoSystemDefaults *sd; /* put locale info into the context in case it's not there */ @@ -425,8 +425,14 @@ static BOOL debugLeaks; sd = [SOGoSystemDefaults sharedSystemDefaults]; request = [_ctx request]; + isAPIRequest = [[request requestHandlerKey] isEqualToString:@"SOGoAPI"]; isDAVRequest = [[request requestHandlerKey] isEqualToString:@"dav"]; - if (isDAVRequest || [sd isWebAccessEnabled]) + if(isAPIRequest && ![_key isEqualToString:@"SOGo"] && ![_key isEqualToString:@"SOGoAPI"]) + { + //The request will be handle by the API Dispatcher + obj = nil; + } + else if (isDAVRequest || [sd isWebAccessEnabled]) { if (isDAVRequest) { diff --git a/SOPE/GDLContentStore/GCSFolderManager.h b/SOPE/GDLContentStore/GCSFolderManager.h index d6d59802d..c59fe0386 100644 --- a/SOPE/GDLContentStore/GCSFolderManager.h +++ b/SOPE/GDLContentStore/GCSFolderManager.h @@ -76,6 +76,7 @@ - (BOOL)folderExistsAtPath:(NSString *)_path; - (NSArray *)listSubFoldersAtPath:(NSString *)_path recursive:(BOOL)_flag; +- (NSArray *)listSubFoldersAndNamesAtPath:(NSString *)_path recursive:(BOOL)_recursive; - (NSDictionary *) recordAtPath: (NSString *) _path; - (GCSFolder *)folderAtPath:(NSString *)_path; diff --git a/SOPE/GDLContentStore/GCSFolderManager.m b/SOPE/GDLContentStore/GCSFolderManager.m index 616a67350..8b277049c 100644 --- a/SOPE/GDLContentStore/GCSFolderManager.m +++ b/SOPE/GDLContentStore/GCSFolderManager.m @@ -598,6 +598,27 @@ static BOOL _singleStoreMode = NO; return sql; } +- (NSString *)generateSQLPathAndNameFetchForInternalNames:(NSArray *)_names + exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs +{ + /* fetches the 'path' subset for a given quick-names */ + NSMutableString *sql; + NSString *ws; + + ws = [self generateSQLWhereForInternalNames:_names + exactMatch:_beExact orDirectSubfolderMatch:_directSubs]; + if ([ws length] == 0) + return nil; + + sql = [NSMutableString stringWithCapacity:256]; + [sql appendString:@"SELECT c_path, c_foldername FROM "]; + [sql appendString:[self folderInfoTableName]]; + [sql appendString:@" WHERE "]; + [sql appendString:ws]; + if (debugSQLGen) [self logWithFormat:@"PathFetch-SQL: %@", sql]; + return sql; +} + /* handling folder names */ - (BOOL)_isStandardizedPath:(NSString *)_path { @@ -806,6 +827,72 @@ static BOOL _singleStoreMode = NO; return result; } +- (NSArray *)listSubFoldersAndNamesAtPath:(NSString *)_path recursive:(BOOL)_recursive{ + NSMutableArray *result; + NSString *fname; + NSArray *fnames, *records; + NSString *sql; + unsigned i, count; + + if ((fnames = [self internalNamesFromPath:_path]) == nil) { + [self debugWithFormat:@"got no internal names for path: '%@'", _path]; + return nil; + } + + sql = [self generateSQLPathAndNameFetchForInternalNames:fnames + exactMatch:NO orDirectSubfolderMatch:(_recursive ? NO : YES)]; + if ([sql length] == 0) { + [self debugWithFormat:@"got no SQL for names: %@", fnames]; + return nil; + } + + if ((records = [self performSQL:sql]) == nil) { + [self logWithFormat:@"ERROR(%s): executing SQL failed: '%@'", + __PRETTY_FUNCTION__, sql]; + return nil; + } + + if ((count = [records count]) == 0) + return emptyArray; + + result = [NSMutableArray arrayWithCapacity:(count > 128 ? 128 : count)]; + + fname = [self internalNameFromPath:_path]; + fname = [fname stringByAppendingString:@"/"]; /* add slash */ + for (i = 0; i < count; i++) { + NSDictionary *record, *folderInfo; + NSString *sname, *spath, *foldername; + + record = [records objectAtIndex:i]; + sname = [record objectForKey:GCSPathRecordName]; + if (![sname hasPrefix:fname]) /* does not match at all ... */ + continue; + + /* strip prefix and following slash */ + sname = [sname substringFromIndex:[fname length]]; + spath = [self pathPartFromInternalName:sname]; + + if (_recursive) { + if ([spath length] > 0){ + foldername = [record objectForKey: @"c_foldername"]; + folderInfo = [NSDictionary dictionaryWithObjectsAndKeys: spath, @"path", foldername, @"name", nil]; + [result addObject:folderInfo]; + } + } + else { + /* direct children only, so exclude everything with a slash */ + if ([sname rangeOfString:@"/"].length == 0 && [spath length] > 0) + { + foldername = [record objectForKey: @"c_foldername"]; + folderInfo = [NSDictionary dictionaryWithObjectsAndKeys: spath, @"path", foldername, @"name", nil]; + [result addObject:folderInfo]; + } + } + } + + return result; +} + - (NSDictionary *) recordAtPath: (NSString *) _path { NSMutableString *sql; diff --git a/SoObjects/SOGo/SOGoOpenIdSession.m b/SoObjects/SOGo/SOGoOpenIdSession.m index 98fc5bb81..fda50d85e 100644 --- a/SoObjects/SOGo/SOGoOpenIdSession.m +++ b/SoObjects/SOGo/SOGoOpenIdSession.m @@ -139,6 +139,13 @@ size_t curl_body_function(void *ptr, size_t size, size_t nmemb, void *buffer) return NO; } sd = [SOGoSystemDefaults sharedSystemDefaults]; + + if(![[sd authenticationType] isEqualToString: @"openid"]) + { + [self errorWithFormat: @"Sogo SOGoAuthenticationType is not openid"]; + return NO; + } + return ([sd openIdConfigUrl] && [sd openIdScope] && [sd openIdClient] && [sd openIdClientSecret]); } @@ -206,7 +213,7 @@ size_t curl_body_function(void *ptr, size_t size, size_t nmemb, void *buffer) } else { - [self errorWithFormat: @"Missing parameters from sogo.conf"]; + [self errorWithFormat: @"LoginTypebyDOmain - Openid not found or missing parameters for domain", _domain]; } } else if ([[self class] checkUserConfig]) diff --git a/SoObjects/SOGo/SOGoProductLoader.m b/SoObjects/SOGo/SOGoProductLoader.m index 5f531134d..050637ee1 100644 --- a/SoObjects/SOGo/SOGoProductLoader.m +++ b/SoObjects/SOGo/SOGoProductLoader.m @@ -122,28 +122,28 @@ static NSString *productDirectoryName = @"SOGo"; pathes = [[self productSearchPathes] objectEnumerator]; while ((lpath = [pathes nextObject])) + { + productNames = [[fm directoryContentsAtPath: lpath] objectEnumerator]; + while ((productName = [productNames nextObject])) { - productNames = [[fm directoryContentsAtPath: lpath] objectEnumerator]; - while ((productName = [productNames nextObject])) - { - if ([[productName pathExtension] isEqualToString: @"SOGo"]) - { - bpath = [lpath stringByAppendingPathComponent: productName]; - [registry registerProductAtPath: bpath]; - [loadedProducts addObject: productName]; - } - } - if ([loadedProducts count]) + if ([[productName pathExtension] isEqualToString: @"SOGo"]) { - if (verbose) - { - [self logWithFormat: @"SOGo products loaded from '%@':", lpath]; - [self logWithFormat: @" %@", - [loadedProducts componentsJoinedByString: @", "]]; - } - [loadedProducts removeAllObjects]; + bpath = [lpath stringByAppendingPathComponent: productName]; + [registry registerProductAtPath: bpath]; + [loadedProducts addObject: productName]; } } + if ([loadedProducts count]) + { + if (verbose) + { + [self logWithFormat: @"SOGo products loaded from '%@':", lpath]; + [self logWithFormat: @" %@", + [loadedProducts componentsJoinedByString: @", "]]; + } + [loadedProducts removeAllObjects]; + } + } if (![registry loadAllProducts] && verbose) [self warnWithFormat: @"could not load all products !"]; diff --git a/SoObjects/SOGo/SOGoSystemDefaults.h b/SoObjects/SOGo/SOGoSystemDefaults.h index fc5729ee3..671ba294e 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.h +++ b/SoObjects/SOGo/SOGoSystemDefaults.h @@ -83,6 +83,7 @@ static const NSString *kDisableSharingCalendar = @"Calendar"; - (BOOL) uixDebugEnabled; - (BOOL) easDebugEnabled; - (BOOL) openIdDebugEnabled; +- (BOOL) apiDebugEnabled; - (BOOL) tnefDecoderDebugEnabled; - (BOOL) xsrfValidationEnabled; diff --git a/SoObjects/SOGo/SOGoSystemDefaults.m b/SoObjects/SOGo/SOGoSystemDefaults.m index a64ff76c9..38fdc5b85 100644 --- a/SoObjects/SOGo/SOGoSystemDefaults.m +++ b/SoObjects/SOGo/SOGoSystemDefaults.m @@ -654,6 +654,11 @@ _injectConfigurationFromFile (NSMutableDictionary *defaultsDict, return [self boolForKey: @"SOGoOpenIDDebugEnabled"]; } +- (BOOL) apiDebugEnabled +{ + return [self boolForKey: @"SOGoAPIDebugEnabled"]; +} + - (BOOL) tnefDecoderDebugEnabled { return [self boolForKey: @"SOGoTnefDecoderDebugEnabled"]; diff --git a/UI/MainUI/GNUmakefile b/UI/MainUI/GNUmakefile index f248ee848..d019955d1 100644 --- a/UI/MainUI/GNUmakefile +++ b/UI/MainUI/GNUmakefile @@ -14,6 +14,7 @@ MainUI_OBJC_FILES += \ SOGoUserHomePage.m \ SOGoBrowsersPanel.m \ SOGoMicrosoftActiveSyncActions.m \ + SOGoAPIActions.m \ ifeq ($(saml2_config), yes) MainUI_OBJC_FILES += SOGoSAML2Actions.m diff --git a/UI/MainUI/SOGoAPIActions.m b/UI/MainUI/SOGoAPIActions.m new file mode 100644 index 000000000..07bf15fe2 --- /dev/null +++ b/UI/MainUI/SOGoAPIActions.m @@ -0,0 +1,73 @@ +/* + Copyright (C) 2014-2016 Inverse inc. + + This file is part of SOGo. + + SOGo is free software; you can redistribute it and/or modify it under + the terms of the GNU Lesser General Public License as published by the + Free Software Foundation; either version 2, or (at your option) any + later version. + + SOGo is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or + FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public + License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with SOGo; see the file COPYING. If not, write to the + Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA + 02111-1307, USA. +*/ + + +#import +#import + +#import +#import +#import +#import + +#import + +@interface SOGoAPIActions : WODirectAction +@end + +@implementation SOGoAPIActions + + +- (WOResponse *) sogoAPIAction +{ + WOResponse *response; + WORequest *request; + NSBundle *bundle; + NSException *ex; + id dispatcher; + Class clazz; + + request = (WORequest *)[context request]; + response = (WOResponse *)[context response]; + [response setStatus: 200]; + [response setHeader: @"application/json; charset=utf-8" forKey: @"content-type"]; + + bundle = [NSBundle bundleForClass: NSClassFromString(@"SOGoAPIProduct")]; + clazz = [bundle classNamed: @"SOGoAPIDispatcher"]; + dispatcher = [[clazz alloc] init]; + + ex = [dispatcher dispatchRequest: request inResponse: response context: context]; + + //[[self class] memoryStatistics]; + + if (ex) + { + return [NSException exceptionWithHTTPStatus: 500]; + } + + RELEASE(dispatcher); + + //[[SOGoCache sharedCache] killCache]; + + return response; +} + +@end diff --git a/UI/MainUI/product.plist b/UI/MainUI/product.plist index 69844cc82..f4ae8b7cc 100644 --- a/UI/MainUI/product.plist +++ b/UI/MainUI/product.plist @@ -118,6 +118,11 @@ actionClass = "SOGoMicrosoftActiveSyncActions"; actionName = "microsoftServerActiveSync"; }; + SOGoAPI = { + protectedBy = ""; + actionClass = "SOGoAPIActions"; + actionName = "sogoAPI"; + }; casProxy = { protectedBy = ""; pageName = "SOGoRootPage"; diff --git a/UI/WebServerResources/js/vendor/punycode.js b/UI/WebServerResources/js/vendor/punycode.js index 5b81f1763..8fdd43303 100644 --- a/UI/WebServerResources/js/vendor/punycode.js +++ b/UI/WebServerResources/js/vendor/punycode.js @@ -422,7 +422,7 @@ const punycode = { * @memberOf punycode * @type String */ - 'version': '2.3.1', + 'version': '2.1.0', /** * An object of methods to convert from JavaScript's internal character * representation (UCS-2) to Unicode code points, and back.