diff --git a/Documentation/SOGoInstallationGuide.asciidoc b/Documentation/SOGoInstallationGuide.asciidoc index 06d67e68d..d0f44c629 100644 --- a/Documentation/SOGoInstallationGuide.asciidoc +++ b/Documentation/SOGoInstallationGuide.asciidoc @@ -1586,6 +1586,11 @@ For PostgresSQL, set the database URL to something like: all cache data. You must also set `OCSStoreURL` and `OCSAclURL` if you set this parameter. +|S |OCSAdminURL +|Parameter used to set the database URL so that SOGo can use to store +all administration elements. + + For PostgresSQL, set the database URL to something like: `postgresql://sogo:sogo@127.0.0.1:5432/sogo/sogo_cache_folder`. |======================================================================= diff --git a/Main/SOGo.m b/Main/SOGo.m index d4edb3719..9880a55a0 100644 --- a/Main/SOGo.m +++ b/Main/SOGo.m @@ -274,7 +274,7 @@ static BOOL debugLeaks; if ([GCSFolderManager singleStoreMode]) { - urlStrings = [NSArray arrayWithObjects: @"SOGoProfileURL", @"OCSFolderInfoURL", @"OCSStoreURL", @"OCSAclURL", @"OCSCacheFolderURL", nil]; + urlStrings = [NSArray arrayWithObjects: @"SOGoProfileURL", @"OCSFolderInfoURL", @"OCSStoreURL", @"OCSAclURL", @"OCSCacheFolderURL", @"OCSAdminURL", nil]; quickTypeStrings = [NSArray arrayWithObjects: @"contact", @"appointment", nil]; combined = YES; } @@ -314,6 +314,9 @@ static BOOL debugLeaks; { fm = [GCSFolderManager defaultFolderManager]; + // Create the sessions table + [[fm adminFolder] createFolderIfNotExists]; + // Create the sessions table [[fm sessionsFolder] createFolderIfNotExists]; diff --git a/SOPE/GDLContentStore/GCSAdminFolder.h b/SOPE/GDLContentStore/GCSAdminFolder.h new file mode 100644 index 000000000..3dc346ebe --- /dev/null +++ b/SOPE/GDLContentStore/GCSAdminFolder.h @@ -0,0 +1,53 @@ +/* GCSAdminFolder.h - this file is part of $PROJECT_NAME_HERE$ + * + * Copyright (C) 2023 Alinto + * + * 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 GCSALARMSFOLDER_H +#define GCSALARMSFOLDER_H + + +@class NSCalendarDate; +@class NSException; +@class NSNumber; +@class NSString; + +@class GCSFolderManager; + +@interface GCSAdminFolder : NSObject +{ + GCSFolderManager *folderManager; +} + ++ (id) alarmsFolderWithFolderManager: (GCSFolderManager *) newFolderManager; + +- (void) setFolderManager: (GCSFolderManager *) newFolderManager; + +/* operations */ + +- (NSString *)getMotd; + +- (void) createFolderIfNotExists; +- (BOOL) canConnectStore; + +- (NSException *)writeMotd:(NSString *)motd; +- (NSException *)deleteMotd; + +@end + +#endif /* GCSALARMSFOLDER_H */ diff --git a/SOPE/GDLContentStore/GCSAdminFolder.m b/SOPE/GDLContentStore/GCSAdminFolder.m new file mode 100644 index 000000000..84d4587ca --- /dev/null +++ b/SOPE/GDLContentStore/GCSAdminFolder.m @@ -0,0 +1,347 @@ +/* GCSAdminFolder.m - this file is part of SOGo + * + * Copyright (C) 2023 Alinto + * + * 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 "EOQualifier+GCS.h" +#import "GCSChannelManager.h" +#import "GCSFolderManager.h" +#import "GCSSpecialQueries.h" +#import "NSURL+GCS.h" + +#import "GCSAdminFolder.h" + +static NSString *adminFolderURLString = nil; + +#warning GCSAdminFolder should share a common ancestor with GCSFolder + +@implementation GCSAdminFolder + ++ (void) initialize +{ + NSUserDefaults *ud; + + if (!adminFolderURLString) + { + ud = [NSUserDefaults standardUserDefaults]; + ASSIGN (adminFolderURLString, + [ud stringForKey: @"OCSAdminURL"]); + } +} + ++ (id) alarmsFolderWithFolderManager: (GCSFolderManager *) newFolderManager +{ + GCSAlarmsFolder *newFolder; + + if (adminFolderURLString) + { + newFolder = [self new]; + [newFolder autorelease]; + [newFolder setFolderManager: newFolderManager]; + } + else + { + [self errorWithFormat: @"'OCSAdminURL' is not set"]; + newFolder = nil; + } + + return newFolder; +} + +- (void) setFolderManager: (GCSFolderManager *) newFolderManager +{ + ASSIGN (folderManager, newFolderManager); +} + +/* accessors */ + +- (NSURL *) _location +{ + NSURL *location; + + if (adminFolderURLString) + location = [NSURL URLWithString: adminFolderURLString]; + else + { + [self warnWithFormat: @"'OCSAdminURL' is not set"]; + location = nil; + } + + return location; +} + +- (GCSChannelManager *) _channelManager +{ + return [folderManager channelManager]; +} + +- (NSString *) _storeTableName +{ + return [[self _location] gcsTableName]; +} + +- (EOEntity *) _storeTableEntityForChannel: (EOAdaptorChannel *) tc +{ + static EOEntity *entity = nil; + EOAttribute *attribute; + NSString *tableName; + NSString *columns[] = { @"c_key", @"c_content", nil }; + NSString **column; + NSMutableArray *keys; + NSDictionary *types; + + if (!entity) + { + entity = [EOEntity new]; + tableName = [self _storeTableName]; + [entity setName: tableName]; + [entity setExternalName: tableName]; + + types = [[tc specialQueries] adminAttributeTypes]; + + column = columns; + while (*column) + { + attribute = [EOAttribute new]; + [attribute setName: *column]; + [attribute setColumnName: *column]; + [attribute setExternalType: [types objectForKey: *column]]; + [entity addAttribute: attribute]; + [attribute release]; + column++; + } + + keys = [NSMutableArray arrayWithCapacity: 1]; + [keys addObject: [entity attributeNamed: @"c_key"]]; + [entity setPrimaryKeyAttributes: keys]; + + keys = [NSMutableArray arrayWithCapacity: 1]; + [keys addObject: [entity attributeNamed: @"c_content"]]; + [entity setClassProperties: keys]; + + [entity setAttributesUsedForLocking: [NSArray array]]; + } + + return entity; +} + +/* connection */ + +- (EOAdaptorChannel *) _acquireStoreChannel +{ + return [[self _channelManager] acquireOpenChannelForURL: [self _location]]; +} + +- (void) _releaseChannel: (EOAdaptorChannel *) _channel +{ + [[self _channelManager] releaseChannel:_channel immediately: YES]; +} + +- (BOOL) canConnectStore +{ + return [[self _channelManager] canConnect:[self _location]]; +} + +- (void) createFolderIfNotExists +{ + EOAdaptorChannel *tc; + NSString *sql, *tableName; + GCSSpecialQueries *queries; + + tc = [self _acquireStoreChannel]; + tableName = [self _storeTableName]; + + queries = [tc specialQueries]; + + sql = [NSString stringWithFormat: @"SELECT 1 FROM %@ WHERE 1 = 2", + [self _storeTableName]]; + if ([tc evaluateExpressionX: sql]) + { + sql = [queries createAdminFolderWithName: tableName]; + if (![tc evaluateExpressionX: sql]) + [self logWithFormat: + @"admin folder table '%@' successfully created!", + tableName]; + } + else + [tc cancelFetch]; + + [self _releaseChannel: tc]; +} + +/* operations */ + +/* table has the following fields: + c_key VARCHAR(255) NOT NULL + c_content MEDIUMTEXT NOT NULL +*/ + +- (NSDictionary *) recordForEntryWithKey: (NSString *) key +{ + EOAdaptorChannel *tc; + EOAdaptorContext *context; + NSException *error; + NSArray *attrs; + NSDictionary *record; + EOEntity *entity; + EOSQLQualifier *qualifier; + + record = nil; + + tc = [self _acquireStoreChannel]; + if (tc) + { + context = [tc adaptorContext]; + entity = [self _storeTableEntityForChannel: tc]; + qualifier = [[EOSQLQualifier alloc] initWithEntity: entity + qualifierFormat: + @"c_key='%@'", + key]; + [qualifier autorelease]; + + [context beginTransaction]; + error = [tc selectAttributesX: [entity attributesUsedForFetch] + describedByQualifier: qualifier + fetchOrder: nil + lock: NO]; + if (error) + [self errorWithFormat:@"%s: cannot execute fetch: %@", + __PRETTY_FUNCTION__, error]; + else + { + attrs = [tc describeResults: NO]; + record = [tc fetchAttributes: attrs withZone: NULL]; + [tc cancelFetch]; + } + [context rollbackTransaction]; + [self _releaseChannel: tc]; + } + + return record; +} + +- (NSString *) getMotd { + NSDictionary *r; + + r = [self recordForEntryWithKey: @"motd"]; + if (r && [r objectForKey:@"c_content"]) { + return [r objectForKey:@"c_content"]; + } + + return nil; +} + +- (NSDictionary *) _newRecordWithKey: (NSString *) key + content: (NSString *) content +{ + return [NSDictionary dictionaryWithObjectsAndKeys: key, @"c_key", + content, @"c_content", + nil]; +} + +- (NSException *) writeMotd: (NSString *)motd +{ + NSDictionary *record, *newRecord; + NSException *error; + EOAdaptorChannel *tc; + EOAdaptorContext *context; + EOEntity *entity; + EOSQLQualifier *qualifier; + + error = nil; + tc = [self _acquireStoreChannel]; + if (tc) + { + context = [tc adaptorContext]; + newRecord = [self _newRecordWithKey: @"motd" content: motd]; + record = [self recordForEntryWithKey: @"motd"]; + entity = [self _storeTableEntityForChannel: tc]; + [context beginTransaction]; + if (record) + { + qualifier = [[EOSQLQualifier alloc] initWithEntity: entity + qualifierFormat: + @"c_key='motd'"]; + [qualifier autorelease]; + error = [tc updateRowX: newRecord describedByQualifier: qualifier]; + } + else + error = [tc insertRowX: newRecord forEntity: entity]; + if (error) + { + [context rollbackTransaction]; + [self errorWithFormat:@"%s: cannot write record: %@", + __PRETTY_FUNCTION__, error]; + } + else + [context commitTransaction]; + [self _releaseChannel: tc]; + } + return error; +} + +- (NSException *) deleteRecordForKey: (NSString *) key +{ + EOAdaptorChannel *tc; + EOAdaptorContext *context; + EOEntity *entity; + EOSQLQualifier *qualifier; + NSException *error; + + error = nil; + tc = [self _acquireStoreChannel]; + if (tc) + { + context = [tc adaptorContext]; + entity = [self _storeTableEntityForChannel: tc]; + qualifier = [[EOSQLQualifier alloc] initWithEntity: entity + qualifierFormat: + @"c_key='%@'", + key]; + [qualifier autorelease]; + [context beginTransaction]; + error = [tc deleteRowsDescribedByQualifierX: qualifier]; + if (error) + { + [context rollbackTransaction]; + [self errorWithFormat:@"%s: cannot delete record: %@", + __PRETTY_FUNCTION__, error]; + } + else + [context commitTransaction]; + [self _releaseChannel: tc]; + } + + return error; +} + +- (NSException *) deleteMotd +{ + return [self deleteRecordForKey:@"motd"]; +} + +@end diff --git a/SOPE/GDLContentStore/GCSAlarmsFolder.m b/SOPE/GDLContentStore/GCSAlarmsFolder.m index d20727009..44e540308 100644 --- a/SOPE/GDLContentStore/GCSAlarmsFolder.m +++ b/SOPE/GDLContentStore/GCSAlarmsFolder.m @@ -202,7 +202,7 @@ static NSString *alarmsFolderURLString = nil; /* table has the following fields: c_path VARCHAR(255) NOT NULL - c_name VARCHAR(255) NOT NULLo + c_name VARCHAR(255) NOT NULL c_uid VARCHAR(255) NOT NULL c_recurrence_id INT NULL c_alarm_number INT NOT NULL diff --git a/SOPE/GDLContentStore/GCSFolderManager.h b/SOPE/GDLContentStore/GCSFolderManager.h index 23927252d..306d16635 100644 --- a/SOPE/GDLContentStore/GCSFolderManager.h +++ b/SOPE/GDLContentStore/GCSFolderManager.h @@ -31,7 +31,7 @@ */ @class NSString, NSArray, NSURL, NSDictionary, NSException; -@class GCSChannelManager, GCSAlarmsFolder, GCSFolder, GCSFolderType, GCSSessionsFolder; +@class GCSChannelManager, GCSAlarmsFolder, GCSAdminFolder, GCSFolder, GCSFolderType, GCSSessionsFolder; @interface GCSFolderManager : NSObject { @@ -89,6 +89,9 @@ /* sessions */ - (GCSSessionsFolder *)sessionsFolder; +/* admin */ +- (GCSAdminFolder *)adminFolder; + /* folder types */ - (GCSFolderType *)folderTypeWithName:(NSString *)_name; diff --git a/SOPE/GDLContentStore/GCSFolderManager.m b/SOPE/GDLContentStore/GCSFolderManager.m index 3178d1ca4..42a6dc8ce 100644 --- a/SOPE/GDLContentStore/GCSFolderManager.m +++ b/SOPE/GDLContentStore/GCSFolderManager.m @@ -37,6 +37,7 @@ #import "GCSChannelManager.h" #import "EOAdaptorChannel+GCS.h" #import "GCSAlarmsFolder.h" +#import "GCSAdminFolder.h" #import "GCSFolder.h" #import "GCSFolderType.h" #import "GCSSessionsFolder.h" @@ -497,6 +498,12 @@ static BOOL _singleStoreMode = NO; return [GCSSessionsFolder sessionsFolderWithFolderManager: self]; } +/* admin */ +- (GCSAdminFolder *) adminFolder +{ + return [GCSAdminFolder alarmsFolderWithFolderManager: self]; +} + - (NSString *)generateSQLWhereForInternalNames:(NSArray *)_names exactMatch:(BOOL)_beExact orDirectSubfolderMatch:(BOOL)_directSubs { diff --git a/SOPE/GDLContentStore/GCSSpecialQueries.h b/SOPE/GDLContentStore/GCSSpecialQueries.h index 9950a5669..d4f6e3272 100644 --- a/SOPE/GDLContentStore/GCSSpecialQueries.h +++ b/SOPE/GDLContentStore/GCSSpecialQueries.h @@ -39,8 +39,11 @@ - (NSString *) createSessionsFolderWithName: (NSString *) tableName; - (NSDictionary *) sessionsAttributeTypes; -- (NSString *) updateCPathInFolderInfo: (NSString *) tableName - withCPath2: (NSString *) c_path2; +- (NSString *)createAdminFolderWithName:(NSString *)tableName; +- (NSDictionary *)adminAttributeTypes; + +- (NSString *) updateCPathInFolderInfo:(NSString *)tableName +withCPath2:(NSString *)c_path2; @end diff --git a/SOPE/GDLContentStore/GCSSpecialQueries.m b/SOPE/GDLContentStore/GCSSpecialQueries.m index 5bcba80e9..327302025 100644 --- a/SOPE/GDLContentStore/GCSSpecialQueries.m +++ b/SOPE/GDLContentStore/GCSSpecialQueries.m @@ -81,6 +81,20 @@ return nil; } +- (NSString *) createAdminFolderWithName: (NSString *) tableName +{ + [self subclassResponsibility: _cmd]; + + return nil; +} + +- (NSDictionary *) adminAttributeTypes +{ + [self subclassResponsibility: _cmd]; + + return nil; +} + - (NSString *) createFolderTableWithName: (NSString *) tableName { [self subclassResponsibility: _cmd]; @@ -157,6 +171,30 @@ return types; } +- (NSString *) createAdminFolderWithName: (NSString *) tableName +{ + static NSString *sqlFolderFormat + = (@"CREATE TABLE %@ (" + @" c_key VARCHAR(255) NOT NULL," + @" c_content TEXT NOT NULL)"); + + return [NSString stringWithFormat: sqlFolderFormat, tableName]; +} + +- (NSDictionary *) adminAttributeTypes +{ + static NSMutableDictionary *types = nil; + + if (!types) + { + types = [NSMutableDictionary new]; + [types setObject: @"varchar" forKey: @"c_key"]; + [types setObject: @"varchar" forKey: @"c_content"]; + } + + return types; +} + - (NSString *) createFolderTableWithName: (NSString *) tableName { static NSString *sqlFolderFormat @@ -262,6 +300,30 @@ return types; } +- (NSString *) createAdminFolderWithName: (NSString *) tableName +{ + static NSString *sqlFolderFormat + = (@"CREATE TABLE %@ (" + @" c_key VARCHAR(255) NOT NULL," + @" c_content MEDIUMTEXT NOT NULL)"); + + return [NSString stringWithFormat: sqlFolderFormat, tableName]; +} + +- (NSDictionary *) adminAttributeTypes +{ + static NSMutableDictionary *types = nil; + + if (!types) + { + types = [NSMutableDictionary new]; + [types setObject: @"varchar" forKey: @"c_key"]; + [types setObject: @"varchar" forKey: @"c_content"]; + } + + return types; +} + - (NSString *) createFolderTableWithName: (NSString *) tableName { static NSString *sqlFolderFormat @@ -367,6 +429,30 @@ return types; } +- (NSString *) createAdminFolderWithName: (NSString *) tableName +{ + static NSString *sqlFolderFormat + = (@"CREATE TABLE %@ (" + @" c_key VARCHAR2(255) NOT NULL," + @" c_content VARCHAR2(65535) NOT NULL)"); + + return [NSString stringWithFormat: sqlFolderFormat, tableName]; +} + +- (NSDictionary *) adminAttributeTypes +{ + static NSMutableDictionary *types = nil; + + if (!types) + { + types = [NSMutableDictionary new]; + [types setObject: @"varchar2" forKey: @"c_key"]; + [types setObject: @"varchar2" forKey: @"c_content"]; + } + + return types; +} + - (NSString *) createFolderTableWithName: (NSString *) tableName { static NSString *sqlFolderFormat diff --git a/SOPE/GDLContentStore/GNUmakefile b/SOPE/GDLContentStore/GNUmakefile index b94b4057d..37db13280 100644 --- a/SOPE/GDLContentStore/GNUmakefile +++ b/SOPE/GDLContentStore/GNUmakefile @@ -25,6 +25,7 @@ libGDLContentStore_HEADER_FILES += \ EOAdaptorChannel+GCS.h \ \ GCSAlarmsFolder.h \ + GCSAdminFolder.h \ GCSContext.h \ GCSFieldInfo.h \ GCSFolder.h \ @@ -41,6 +42,7 @@ libGDLContentStore_OBJC_FILES += \ EOQualifier+GCS.m \ \ GCSAlarmsFolder.m \ + GCSAdminFolder.m \ GCSContext.m \ GCSFieldInfo.m \ GCSFolder.m \ diff --git a/Scripts/mysql-utf8mb4.sql b/Scripts/mysql-utf8mb4.sql index bd48c68be..9edda59b1 100644 --- a/Scripts/mysql-utf8mb4.sql +++ b/Scripts/mysql-utf8mb4.sql @@ -39,6 +39,7 @@ -- OCSSessionsFolderURL -> sogo_sessions_folder -- OCSStoreURL -> sogo_store -- SOGoProfileURL -> sogo_user_profile +-- OCSAdminURL -> sogo_admin -- -- SOGo needs to know MySQL has full Unicode coverage; -- the following needs to be put in sogo.conf: @@ -158,6 +159,12 @@ CREATE TABLE sogo_store ( PRIMARY KEY (c_folder_id,c_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; +CREATE TABLE sogo_admin ( + c_key varchar(255) NOT NULL DEFAULT '', + c_content mediumtext NOT NULL, + PRIMARY KEY (c_key) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; + CREATE TABLE sogo_user_profile ( c_uid varchar(255) NOT NULL, c_defaults longtext, diff --git a/SoObjects/SOGo/GNUmakefile b/SoObjects/SOGo/GNUmakefile index aa9372062..1b99182b9 100644 --- a/SoObjects/SOGo/GNUmakefile +++ b/SoObjects/SOGo/GNUmakefile @@ -96,7 +96,9 @@ SOGo_HEADER_FILES = \ NGMimeBodyPart+SOGo.h \ NGMimeFileData+SOGo.h \ \ - SOGoMobileProvision.h + SOGoMobileProvision.h \ + \ + SOGoAdmin.h all:: @touch SOGoBuild.m @@ -186,7 +188,9 @@ SOGo_OBJC_FILES = \ NGMimeBodyPart+SOGo.m \ NGMimeFileData+SOGo.m \ \ - SOGoMobileProvision.m + SOGoMobileProvision.m \ + \ + SOGoAdmin.m SOGo_C_FILES += lmhash.c aes.c crypt_blowfish.c pkcs5_pbkdf2.c diff --git a/SoObjects/SOGo/SOGoAdmin.h b/SoObjects/SOGo/SOGoAdmin.h new file mode 100644 index 000000000..b900d56ba --- /dev/null +++ b/SoObjects/SOGo/SOGoAdmin.h @@ -0,0 +1,45 @@ +/* + Copyright (C) 2023 Alinto + + 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. +*/ + +#ifndef __SOGoAdmin_H__ +#define __SOGoAdmin_H__ + +#import + +@class NSObject; +@class NSException; +@class NSString; + +@interface SOGoAdmin : NSObject +{ + +} + ++ (id)sharedInstance; + +- (BOOL)isConfigured; +- (NSString *)getMotd; +- (NSException *)deleteMotd; +- (NSException *)saveMotd:(NSString *)motd; + +@end + +#endif /* __SOGoAdmin_H__ */ diff --git a/SoObjects/SOGo/SOGoAdmin.m b/SoObjects/SOGo/SOGoAdmin.m new file mode 100644 index 000000000..18ced6727 --- /dev/null +++ b/SoObjects/SOGo/SOGoAdmin.m @@ -0,0 +1,105 @@ +/* + Copyright (C) 2023 Alinto + + 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 "SOGoAdmin.h" + +#import + +#import "NSString+Utilities.h" +#import "SOGoCache.h" + +static const NSString *kCacheMotdKey = @"admin-motd"; + +@implementation SOGoAdmin + ++ (id)sharedInstance +{ + static id admin = nil; + + if (!admin) + admin = [self new]; + + return admin; +} + +- (id)init { + if (self = [super init]) { + + } + return self; +} + +- (void)dealloc { + +} + +- (BOOL) isConfigured +{ + return nil != [[GCSFolderManager defaultFolderManager] adminFolder]; +} + +- (NSString *)getMotd +{ + NSString *cache; + NSString *value; + + cache = [[SOGoCache sharedCache] valueForKey:kCacheMotdKey]; + if (!cache) { + value = [[[GCSFolderManager defaultFolderManager] adminFolder] getMotd]; + if (value) { + [[SOGoCache sharedCache] setValue:[[[GCSFolderManager defaultFolderManager] adminFolder] getMotd] forKey:kCacheMotdKey]; + cache = value; + } else { + cache = @" "; // Empty string won't set cache + [[SOGoCache sharedCache] setValue:cache forKey:kCacheMotdKey]; + } + } + + return cache; +} + +- (NSException *)deleteMotd +{ + NSException *error; + + error = [[[GCSFolderManager defaultFolderManager] adminFolder] deleteMotd]; + if (!error) { + [[SOGoCache sharedCache] removeValueForKey:kCacheMotdKey]; + } + + return error; +} + +- (NSException *)saveMotd:(NSString *)motd +{ + NSException *error; + NSString *safeMotd; + + safeMotd = [motd stringWithoutHTMLInjection: NO]; + error = [[[GCSFolderManager defaultFolderManager] adminFolder] writeMotd: safeMotd]; + if (!error) { + [[SOGoCache sharedCache] setValue:safeMotd forKey:kCacheMotdKey]; + } + return error; +} + + +@end /* SOGoAdmin */ diff --git a/UI/AdministrationUI/English.lproj/Localizable.strings b/UI/AdministrationUI/English.lproj/Localizable.strings index c7b968e41..c8079b3cd 100644 --- a/UI/AdministrationUI/English.lproj/Localizable.strings +++ b/UI/AdministrationUI/English.lproj/Localizable.strings @@ -25,3 +25,7 @@ "No resource" = "No resource"; "Any Authenticated User" = "Any Authenticated User"; "Public Access" = "Public Access"; +"Save" = "Save"; +"Clear" = "Clear"; +"Message of the day" = "Message of the day"; +"Message of the day has been saved" = "Message of the day has been saved"; diff --git a/UI/AdministrationUI/French.lproj/Localizable.strings b/UI/AdministrationUI/French.lproj/Localizable.strings index 10f3f0797..8b5ee474b 100644 --- a/UI/AdministrationUI/French.lproj/Localizable.strings +++ b/UI/AdministrationUI/French.lproj/Localizable.strings @@ -25,3 +25,7 @@ "No resource" = "Aucune ressource"; "Any Authenticated User" = "Tout utilisateur authentifié"; "Public Access" = "Accès public"; +"Save" = "Sauvegarder"; +"Clear" = "Effacer"; +"Message of the day" = "Message du jour"; +"Message of the day has been saved" = "Le message du jour a été sauvegardé"; \ No newline at end of file diff --git a/UI/AdministrationUI/GNUmakefile b/UI/AdministrationUI/GNUmakefile index b57e5cbff..c10060c9f 100644 --- a/UI/AdministrationUI/GNUmakefile +++ b/UI/AdministrationUI/GNUmakefile @@ -13,7 +13,8 @@ AdministrationUI_OBJC_FILES = \ \ UIxAdministration.m \ UIxAdministrationAclEditor.m \ - UIxAdministrationFilterPanel.m + UIxAdministrationFilterPanel.m \ + UIxAdministrationMotd.m AdministrationUI_RESOURCE_FILES += \ product.plist diff --git a/UI/AdministrationUI/UIxAdministration.m b/UI/AdministrationUI/UIxAdministration.m index 249d46e5f..243a784b3 100644 --- a/UI/AdministrationUI/UIxAdministration.m +++ b/UI/AdministrationUI/UIxAdministration.m @@ -23,6 +23,8 @@ #import //#import "../../Main/SOGo.h" +#import + #import "UIxAdministration.h" @implementation UIxAdministration @@ -53,12 +55,18 @@ return @"Administration"; } + - (BOOL) shouldTakeValuesFromRequest: (WORequest *) request inContext: (WOContext*) context { return [[request method] isEqualToString: @"POST"]; } +- (BOOL) isAdminTableConfigured +{ + return [[SOGoAdmin sharedInstance] isConfigured]; +} + @end /* Theme Preview */ diff --git a/UI/AdministrationUI/UIxAdministrationMotd.h b/UI/AdministrationUI/UIxAdministrationMotd.h new file mode 100644 index 000000000..96035a077 --- /dev/null +++ b/UI/AdministrationUI/UIxAdministrationMotd.h @@ -0,0 +1,33 @@ +/* UIxAdministrationMotd.h - this file is part of SOGo + * + * Copyright (C) 2023 Alinto + * + * 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 UIXMOTD_H +#define UIXMOTD_H + +#import + +@interface UIxAdministrationMotd : UIxComponent +{ + +} + +@end + +#endif /* UIXMOTD_H */ diff --git a/UI/AdministrationUI/UIxAdministrationMotd.m b/UI/AdministrationUI/UIxAdministrationMotd.m new file mode 100644 index 000000000..bf8e35009 --- /dev/null +++ b/UI/AdministrationUI/UIxAdministrationMotd.m @@ -0,0 +1,110 @@ +/* UIxAdministrationMotd.m - this file is part of SOGo + * + * Copyright (C) 2023 Alinto + * + * 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 "UIxAdministrationMotd.h" + +#import +#import + +@implementation UIxAdministrationMotd + +- (id) init +{ + if ((self = [super init])) + { + + } + + return self; +} + +- (void) dealloc +{ + [super dealloc]; +} + +- (WOResponse *) getAction +{ + WOResponse *response; + SOGoAdmin *admin; + NSDictionary *jsonResponse; + + admin = [SOGoAdmin sharedInstance]; + if ([admin isConfigured]) { + jsonResponse = [NSDictionary dictionaryWithObject: nil != [admin getMotd] ? [admin getMotd] : @"" + forKey: @"motd"]; + response = [self responseWithStatus: 200 + andJSONRepresentation: jsonResponse]; + } else { + response = [self responseWithStatus: 500 + andString: @"Missing folder configuration"]; + } + + return response; +} + +- (WOResponse *) saveAction +{ + WORequest *request; + WOResponse *response; + SOGoUser *user; + NSException *error; + NSDictionary *data; + SOGoAdmin *admin; + + error = nil; + admin = [SOGoAdmin sharedInstance]; + user = [context activeUser]; + + if ([user isSuperUser]) { + if ([admin isConfigured]) { + data = [[[context request] contentAsString] objectFromJSONString]; + if ([data objectForKey: @"motd"] + && [[data objectForKey: @"motd"] isKindOfClass: [NSString class]] + && [[data objectForKey: @"motd"] length] > 0) { + error = [admin saveMotd: [data objectForKey: @"motd"]]; + } else { + error = [admin deleteMotd]; + } + + if (!error) { + response = [self responseWithStatus: 200 + andString: @"OK"]; + } else { + response = [self responseWithStatus: 500 + andString: @"Error while storing information"]; + } + + } else { + response = [self responseWithStatus: 500 + andString: @"Missing folder configuration"]; + } + } else { + response = [self responseWithStatus: 503 + andString: @"Forbidden"]; + } + + response = [self responseWithStatus: 200 + andString: @"OK"]; + + return response; +} + +@end diff --git a/UI/AdministrationUI/product.plist b/UI/AdministrationUI/product.plist index f39bab32c..e3c7c0dc3 100644 --- a/UI/AdministrationUI/product.plist +++ b/UI/AdministrationUI/product.plist @@ -22,6 +22,20 @@ protectedBy = "View"; pageName = "UIxThemePreview"; }; + UIxAdministrationMotd = { + protectedBy = "View"; + pageName = "UIxAdministrationMotd"; + }; + getMotd = { + protectedBy = "View"; + pageName = "UIxAdministrationMotd"; + actionName = "get"; + }; + saveMotd = { + protectedBy = "View"; + pageName = "UIxAdministrationMotd"; + actionName = "save"; + }; }; }; }; diff --git a/UI/MainUI/SOGoRootPage.m b/UI/MainUI/SOGoRootPage.m index 86d884d4e..7035357ab 100644 --- a/UI/MainUI/SOGoRootPage.m +++ b/UI/MainUI/SOGoRootPage.m @@ -51,6 +51,7 @@ #import #import #import +#import #if defined(MFA_CONFIG) #include @@ -1043,4 +1044,20 @@ static const NSString *kJwtKey = @"jwt"; #endif } + +- (NSString *)motd +{ + return [[SOGoAdmin sharedInstance] getMotd]; +} + +- (NSString *)motdEscaped +{ + return [[[SOGoAdmin sharedInstance] getMotd] stringWithoutHTMLInjection: YES]; +} + +- (BOOL)hasMotd +{ + return [[SOGoAdmin sharedInstance] getMotd] && [[[SOGoAdmin sharedInstance] getMotd] length] > 1; +} + @end /* SOGoRootPage */ diff --git a/UI/PreferencesUI/English.lproj/Localizable.strings b/UI/PreferencesUI/English.lproj/Localizable.strings index 0aea5dd48..46a5c90c8 100644 --- a/UI/PreferencesUI/English.lproj/Localizable.strings +++ b/UI/PreferencesUI/English.lproj/Localizable.strings @@ -303,6 +303,7 @@ "Last" = "Last used"; "Default Module " = "Default Module"; "SOGo Version" = "SOGo Version"; +"Administration" = "Administration"; /* Confirmation asked when changing the language */ "Save preferences and reload page now?" = "Save preferences and reload page now?"; diff --git a/UI/PreferencesUI/French.lproj/Localizable.strings b/UI/PreferencesUI/French.lproj/Localizable.strings index e39bebfb4..4f42bf44f 100644 --- a/UI/PreferencesUI/French.lproj/Localizable.strings +++ b/UI/PreferencesUI/French.lproj/Localizable.strings @@ -303,6 +303,7 @@ "Last" = "Dernier utilisé"; "Default Module " = "Module par défaut"; "SOGo Version" = "Version"; +"Administration" = "Administration"; /* Confirmation asked when changing the language */ "Save preferences and reload page now?" = "Sauvegarder les préférences et recharger maintenant?"; diff --git a/UI/Templates/AdministrationUI/UIxAdministration.wox b/UI/Templates/AdministrationUI/UIxAdministration.wox index 3c7e5b82d..5d8ab4c52 100644 --- a/UI/Templates/AdministrationUI/UIxAdministration.wox +++ b/UI/Templates/AdministrationUI/UIxAdministration.wox @@ -9,7 +9,7 @@ title="moduleName" const:jsFiles="Common.js, Administration.js, Administration.services.js, Preferences.services.js, - Contacts.services.js, Scheduler.services.js"> + Contacts.services.js, Scheduler.services.js, vendor/ckeditor/ckeditor.js, Common/sgCkeditor.component.js">
palette

+ + + insert_comment +

+
+
diff --git a/UI/Templates/AdministrationUI/UIxAdministrationMotd.wox b/UI/Templates/AdministrationUI/UIxAdministrationMotd.wox new file mode 100644 index 000000000..487cbcb93 --- /dev/null +++ b/UI/Templates/AdministrationUI/UIxAdministrationMotd.wox @@ -0,0 +1,27 @@ + + + + + + + +
+ + +
+ + +
+
+
+ +
diff --git a/UI/Templates/MainUI/SOGoRootPage.wox b/UI/Templates/MainUI/SOGoRootPage.wox index 729f489f7..827e687ab 100644 --- a/UI/Templates/MainUI/SOGoRootPage.wox +++ b/UI/Templates/MainUI/SOGoRootPage.wox @@ -24,337 +24,349 @@ layout-gt-md="row" layout-align-gt-md="center start" layout-fill="layout-fill" ui-view="login" ng-controller="LoginController as app"> - -
diff --git a/UI/Templates/PreferencesUI/UIxPreferences.wox b/UI/Templates/PreferencesUI/UIxPreferences.wox index d60e7bfc7..3d84c554a 100644 --- a/UI/Templates/PreferencesUI/UIxPreferences.wox +++ b/UI/Templates/PreferencesUI/UIxPreferences.wox @@ -1493,4 +1493,4 @@ - + \ No newline at end of file diff --git a/UI/WebServerResources/js/Administration/Administration.app.js b/UI/WebServerResources/js/Administration/Administration.app.js index 4d34ec48a..79437d29e 100644 --- a/UI/WebServerResources/js/Administration/Administration.app.js +++ b/UI/WebServerResources/js/Administration/Administration.app.js @@ -4,7 +4,7 @@ (function() { 'use strict'; - angular.module('SOGo.AdministrationUI', ['ui.router', 'SOGo.Common', 'SOGo.Authentication', 'SOGo.PreferencesUI', 'SOGo.ContactsUI', 'SOGo.SchedulerUI']) + angular.module('SOGo.AdministrationUI', ['ui.router', 'SOGo.Common', 'SOGo.Authentication', 'SOGo.PreferencesUI', 'SOGo.ContactsUI', 'SOGo.SchedulerUI', 'sgCkeditor']) .config(configure) .run(runBlock); @@ -56,6 +56,16 @@ controllerAs: 'ctrl' } } + }) + .state('administration.motd', { + url: '/motd', + views: { + module: { + templateUrl: 'UIxAdministrationMotd', // UI/Templates/Administration/UIxAdministrationMotd.wox + controller: 'AdministrationMotdController', + controllerAs: 'ctrl' + } + } }); // if none of the above states are matched, use this as the fallback diff --git a/UI/WebServerResources/js/Administration/Administration.service.js b/UI/WebServerResources/js/Administration/Administration.service.js index ac009f072..4d0cd623f 100644 --- a/UI/WebServerResources/js/Administration/Administration.service.js +++ b/UI/WebServerResources/js/Administration/Administration.service.js @@ -29,6 +29,34 @@ return new Administration(); // return unique instance }]; + /** + * @function $getMotd + * @memberof Administration.prototype + * @desc Get the motd to the server. + */ + Administration.prototype.$getMotd = function () { + var _this = this; + + return Administration.$$resource.fetch("Administration/getMotd") + .then(function (data) { + return data; + }); + }; + + /** + * @function $saveMotd + * @memberof Administration.prototype + * @desc Save the motd to the server. + */ + Administration.prototype.$saveMotd = function (message) { + var _this = this; + + return Administration.$$resource.save("Administration", { motd: message }, { action: "saveMotd" }) + .then(function (data) { + return data; + }); + }; + /* Initialize module if necessary */ try { angular.module('SOGo.AdministrationUI'); diff --git a/UI/WebServerResources/js/Administration/AdministrationMotdController.js b/UI/WebServerResources/js/Administration/AdministrationMotdController.js new file mode 100644 index 000000000..98eb54ffb --- /dev/null +++ b/UI/WebServerResources/js/Administration/AdministrationMotdController.js @@ -0,0 +1,48 @@ +/* -*- Mode: javascript; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* JavaScript for SOGoAdministration */ + +(function() { + 'use strict'; + + /** + * @ngInject + */ + AdministrationMotdController.$inject = ['$timeout', '$state', '$mdMedia', '$mdToast', 'sgConstant', 'Administration', 'sgSettings']; + function AdministrationMotdController($timeout, $state, $mdMedia, $mdToast, sgConstant, Administration, Settings) { + var vm = this; + vm.administration = Administration; + vm.motd = null; + vm.save = save; + vm.clear = clear; + vm.ckConfig = { + 'autoGrow_minHeight': 200, + removeButtons: 'Save,NewPage,Preview,Print,Templates,Cut,Copy,Paste,PasteText,PasteFromWord,Undo,Redo,Find,Replace,SelectAll,Scayt,Form,Checkbox,Radio,TextField,Textarea,Select,Button,Image,HiddenField,CopyFormatting,RemoveFormat,NumberedList,BulletedList,Outdent,Indent,Blockquote,CreateDiv,BidiLtr,BidiRtl,Language,Unlink,Anchor,Flash,Table,HorizontalRule,Smiley,SpecialChar,PageBreak,Iframe,Styles,Format,Maximize,ShowBlocks,About,Strike,Subscript,Superscript,Underline,Emojipanel,Emoji,' + }; + + this.administration.$getMotd().then(function (data) { + if (data && data.motd) { + vm.motd = data.motd; + } + }); + + function save() { + this.administration.$saveMotd(vm.motd).then(function () { + $mdToast.show( + $mdToast.simple() + .textContent(l('Message of the day has been saved')) + .position(sgConstant.toastPosition) + .hideDelay(3000)); + }); + } + + function clear() { + console.log('HEY'); + vm.motd = ''; + } + } + + angular + .module('SOGo.AdministrationUI') + .controller('AdministrationMotdController', AdministrationMotdController); + +})(); diff --git a/UI/WebServerResources/scss/views/LoginUI.scss b/UI/WebServerResources/scss/views/LoginUI.scss index c01e2f527..1ab6e31e3 100644 --- a/UI/WebServerResources/scss/views/LoginUI.scss +++ b/UI/WebServerResources/scss/views/LoginUI.scss @@ -1,4 +1,5 @@ /// LoginUI.scss -*- Mode: scss; indent-tabs-mode: nil; basic-offset: 2 -*- +@use "sass:color"; $sg-login-width: grid-step(5); @@ -7,6 +8,12 @@ $sg-login-width: grid-step(5); // Keep content centered margin: auto; overflow: hidden; + background: transparent; + + #loginContent { + background-color: $colorGrey50; + margin-top: 10px; + } .sg-logo { // Center image @@ -17,6 +24,31 @@ $sg-login-width: grid-step(5); } } + @keyframes fade-in { + 0% { + opacity: 0; + } + + 100% { + opacity: 1; + } + } + + .motd { + margin-top: 50px; + margin-bottom: 50px; + justify-content: center; + margin-left: 2%; + margin-right: 2%; + padding: 50px; + animation: fade-in ease 1.5s; + border: solid 5px sg-color($sogoGreen, 500); + } + + .md-whiteframe-3dp { + margin-bottom: 8px; + } + .password-lost-link { color: rgb(255, 255, 255); font-size: 0.9em; @@ -52,6 +84,9 @@ $sg-login-width: grid-step(5); max-width: 75%; } } + #loginContent { + margin-top: 0; + } .sg-login { // Let the form take all available space flex-grow: 1; @@ -61,6 +96,11 @@ $sg-login-width: grid-step(5); margin: auto; max-width: $sg-login-width; } + .motd { + margin-bottom: 10px; + padding: 10px; + border: none; + } } /**