feat(core): Add message of the day

This commit is contained in:
smizrahi
2024-01-22 14:34:22 +01:00
parent 0d0eda2698
commit eee50697b0
32 changed files with 1362 additions and 326 deletions

View File

@@ -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`.
|=======================================================================

View File

@@ -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];

View File

@@ -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 */

View File

@@ -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 <NGExtensions/NSObject+Logs.h>
#import <NGExtensions/NSNull+misc.h>
#import <GDLAccess/EOAdaptorContext.h>
#import <GDLAccess/EOAttribute.h>
#import <GDLAccess/EOEntity.h>
#import <GDLAccess/EOSQLQualifier.h>
#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

View File

@@ -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

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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

View File

@@ -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

View File

@@ -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 \

View File

@@ -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,

View File

@@ -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

View File

@@ -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 <Foundation/Foundation.h>
@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__ */

105
SoObjects/SOGo/SOGoAdmin.m Normal file
View File

@@ -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 <GDLContentStore/GCSFolderManager.h>
#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 */

View File

@@ -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";

View File

@@ -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é";

View File

@@ -13,7 +13,8 @@ AdministrationUI_OBJC_FILES = \
\
UIxAdministration.m \
UIxAdministrationAclEditor.m \
UIxAdministrationFilterPanel.m
UIxAdministrationFilterPanel.m \
UIxAdministrationMotd.m
AdministrationUI_RESOURCE_FILES += \
product.plist

View File

@@ -23,6 +23,8 @@
#import <SoObjects/SOGo/SOGoUser.h>
//#import "../../Main/SOGo.h"
#import <SOGo/SOGoAdmin.h>
#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 */

View File

@@ -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 <SOGoUI/UIxComponent.h>
@interface UIxAdministrationMotd : UIxComponent
{
}
@end
#endif /* UIXMOTD_H */

View File

@@ -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 <SOGo/SOGoUser.h>
#import <SOGo/SOGoAdmin.h>
@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

View File

@@ -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";
};
};
};
};

View File

@@ -51,6 +51,7 @@
#import <SOGo/SOGoWebAuthenticator.h>
#import <SOGo/SOGoEmptyAuthenticator.h>
#import <SOGo/SOGoMailer.h>
#import <SOGo/SOGoAdmin.h>
#if defined(MFA_CONFIG)
#include <liboath/oath.h>
@@ -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 */

View File

@@ -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?";

View File

@@ -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?";

View File

@@ -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">
<main class="view"
layout="row" layout-fill="layout-fill"
@@ -38,6 +38,14 @@
<md-icon>palette</md-icon>
<p class="sg-item-name"><var:string label:value="Theme Preview"/></p>
</md-list-item>
<var:if condition="isAdminTableConfigured">
<md-list-item ng-click="app.go('motd')"
ui-sref="administration.motd"
ui-sref-active="md-default-theme md-background md-bg md-hue-1">
<md-icon>insert_comment</md-icon>
<p class="sg-item-name"><var:string label:value="Message of the day"/></p>
</md-list-item>
</var:if>
</md-list>
</md-content>
</md-sidenav>

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE container>
<container
xmlns="http://www.w3.org/1999/xhtml"
xmlns:var="http://www.skyrix.com/od/binding"
xmlns:const="http://www.skyrix.com/od/constant"
xmlns:label="OGo:label"
>
<md-toolbar layout="row" layout-align="space-between center" class="sg-toolbar-main">
<var:component className="UIxTopnavToolbar" />
</md-toolbar>
<md-content class="md-padding ng-scope">
<div class="pseudo-input-container ng-scope">
<label class="pseudo-input-label"><var:string label:value="Message of the day"/></label>
<sg-ckeditor
class="ng-cloak"
config="ctrl.ckConfig"
ck-margin="8px"
ng-model="ctrl.motd"><!-- HTML editor --></sg-ckeditor>
<div layout="row" layout-align="end center" class="layout-align-end-center layout-row">
<button class="md-primary md-button ng-binding md-ink-ripple" ng-click="ctrl.clear()"><var:string label:value="Clear"/></button>
<button class="md-primary md-button ng-binding md-ink-ripple" ng-click="ctrl.save()"><var:string label:value="Save"/></button>
</div>
</div>
</md-content>
</container>

View File

@@ -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">
<md-content id="loginContent" class="ng-cloak md-whiteframe-3dp" flex="100"
layout="column" layout-gt-md="row" layout-align="start stretch"
<md-content class="ng-cloak" flex="100"
layout="column" layout-align="start stretch"
ng-show="app.showLogin">
<div class="sg-logo" flex-gt-md="50">
<div layout="row" class="md-padding">
<div class="md-flex hide show-gt-md"><!-- push logo to the right on larger screens --></div>
<img const:alt="*" class="md-margin" rsrc:src="img/sogo-full.svg"/>
<var:if condition="hasMotd">
<div flex="100" class="motd hide show-gt-md">
<var:string var:value="motd" const:escapeHTML="NO" />
</div>
</div>
<div class="sg-login md-default-theme md-bg md-accent" flex-gt-md="50">
<div id="login" class="sg-login-content md-padding">
<form name="loginForm" layout="column"
ng-cloak="ng-cloak"
ng-submit="app.login()">
<var:if condition="hasLoginSuffix">
<input type="hidden" ng-model="app.creds.loginSuffix" var:value="loginSuffix"/>
</var:if>
<div id="loginContent" layout="column" layout-gt-md="row" flex="100" class="md-whiteframe-3dp">
<div class="sg-logo" flex-gt-md="50">
<div layout="row" class="md-padding">
<div class="md-flex hide show-gt-md"><!-- push logo to the right on larger screens --></div>
<img const:alt="*" class="md-margin" rsrc:src="img/sogo-full.svg"/>
</div>
<var:if condition="hasMotd">
<div layout="row">
<div class="motd hide-gt-md"><var:string var:value="motdEscaped" const:escapeHTML="NO" /></div>
</div>
</var:if>
<div ng-if="!app.loginState">
<md-input-container class="md-block">
<label><var:string label:value="Username"/></label>
<md-icon>person</md-icon>
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.creds.username" ng-required="true" ng-change="app.usernameChanged()" ng-blur="app.retrievePasswordRecoveryEnabled()" />
</md-input-container>
<md-input-container class="md-block">
<label><var:string label:value="Password"/></label>
<md-icon>vpn_key</md-icon>
<input id="passwordField" type="password" ng-model="app.creds.password" ng-required="true"/>
<md-icon id="password-visibility-icon" ng-click="app.changePasswordVisibility()">visibility</md-icon>
</md-input-container>
</div>
<div class="sg-login md-default-theme md-bg md-accent" flex-gt-md="50">
<div id="login" class="sg-login-content md-padding">
<form name="loginForm" layout="column"
ng-cloak="ng-cloak"
ng-submit="app.login()">
<var:if condition="hasLoginSuffix">
<input type="hidden" ng-model="app.creds.loginSuffix" var:value="loginSuffix"/>
</var:if>
<div ng-if="!app.loginState">
<md-input-container class="md-block">
<label><var:string label:value="Username"/></label>
<md-icon>person</md-icon>
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.creds.username" ng-required="true" ng-change="app.usernameChanged()" ng-blur="app.retrievePasswordRecoveryEnabled()" />
</md-input-container>
<md-input-container class="md-block">
<label><var:string label:value="Password"/></label>
<md-icon>vpn_key</md-icon>
<input id="passwordField" type="password" ng-model="app.creds.password" ng-required="true"/>
<md-icon id="password-visibility-icon" ng-click="app.changePasswordVisibility()">visibility</md-icon>
</md-input-container>
<!-- LANGUAGES SELECT -->
<div layout="row" layout-align="start end">
<md-icon>language</md-icon>
<md-input-container class="md-flex">
<label><var:string label:value="choose"/></label>
<md-select ng-model="app.creds.language"
var:placeholder="localizedLanguage"
ng-change="app.changeLanguage($event)">
<var:foreach list="languages" item="item">
<md-option var:value="item">
<var:string value="languageText"/>
</md-option>
</var:foreach>
</md-select>
</md-input-container>
</div>
<!-- LANGUAGES SELECT -->
<div layout="row" layout-align="start end">
<md-icon>language</md-icon>
<md-input-container class="md-flex">
<label><var:string label:value="choose"/></label>
<md-select ng-model="app.creds.language"
var:placeholder="localizedLanguage"
ng-change="app.changeLanguage($event)">
<var:foreach list="languages" item="item">
<md-option var:value="item">
<var:string value="languageText"/>
</md-option>
</var:foreach>
</md-select>
</md-input-container>
</div>
<!-- DOMAINS SELECT -->
<var:if condition="hasLoginDomains">
<div layout="row" layout-align="start end">
<md-icon>domain</md-icon>
<md-input-container class="md-flex">
<md-select class="md-flex" ng-model="app.creds.domain" label:placeholder="choose" ng-change="app.retrievePasswordRecoveryEnabled()">
<var:foreach list="loginDomains" item="item">
<md-option var:value="item">
<var:string value="item"/>
</md-option>
</var:foreach>
</md-select>
</md-input-container>
</div>
</var:if>
<div layout="row" layout-align="center center">
<md-switch class="md-accent md-hue-2"
ng-model="app.creds.rememberLogin"
label:arial-label="Remember username">
<var:string label:value="Remember username"/>
</md-switch>
</div>
</div>
<!-- Password recovery -->
<div layout="row" layout-align="center center" ng-if="app.passwordRecovery.passwordRecoveryEnabled">
<div ng-if="app.showLogin">
<a href="#" ng-click="app.passwordRecoveryInfo()" sg-ripple-click="loginContent" class="password-lost-link"><var:string label:value="Password lost"/></a>
</div>
</div>
<!-- CONNECT BUTTON -->
<div layout="row" layout-align="space-between center" ng-if="!app.loginState">
<md-button class="md-icon-button"
label:aria-label="About"
ng-click="app.showAbout()">
<md-icon>info</md-icon>
</md-button>
<div>
<md-button class="md-fab md-accent md-hue-2" type="submit"
label:aria-label="Connect"
ng-if="!app.loginState"
ng-disabled="loginForm.$invalid"
sg-ripple-click="loginContent">
<md-icon>arrow_forward</md-icon>
</md-button>
</div>
</div>
<sg-ripple class="md-default-theme md-accent md-bg"
ng-class="{ 'md-warn': app.loginState == 'error' }"><!-- ripple background --></sg-ripple>
<sg-ripple-content class="md-flex ng-hide"
layout="column" layout-align="center center" layout-fill="layout-fill"
ng-switch="app.loginState">
<!-- Authenticating -->
<div layout="column" layout-align="center center"
ng-switch-when="authenticating">
<md-progress-circular class="md-hue-1"
md-mode="indeterminate"
md-diameter="32"><!-- mailbox loading progress --></md-progress-circular>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
<var:string label:value="Authenticating"/>
</div>
</div>
<var:if condition="isTotpEnabled">
<!-- TOTP Code -->
<div layout="row" layout-align="center center" layout-fill="layout-fill"
ng-switch-when="totpcode">
<div flex="80" flex-sm="50" flex-gt-sm="40">
<md-input-container class="md-block">
<label><var:string label:value="Verification Code"/></label>
<md-icon>lock</md-icon>
<input type="text"
ng-pattern="app.verificationCodePattern"
ng-model="app.creds.verificationCode"
ng-required="app.loginState == 'totpcode'"
sg-focus-on="totpcode"/>
<div class="sg-hint"><var:string label:value="Enter the 6-digit verification code from your TOTP application."/></div>
</md-input-container>
<div layout="row" layout-align="space-between center">
<md-button class="md-icon-button"
label:aria-label="Cancel"
ng-click="app.restoreLogin()"
sg-ripple-click="loginContent">
<md-icon>arrow_backward</md-icon>
</md-button>
<md-button class="md-fab md-accent md-hue-2" type="submit"
label:aria-label="Connect"
ng-if="app.loginState == 'totpcode'"
ng-disabled="loginForm.$invalid"
ng-click="app.login()">
<md-icon>arrow_forward</md-icon>
</md-button>
</div>
</div>
</div>
<!-- TOTP has been disabled -->
<div layout="row" layout-align="center center" layout-fill="layout-fill"
ng-switch-when="totpdisabled">
<div layout="column" layout-align="center center" flex-xs="flex-xs" flex-gt-xs="50">
<md-icon class="md-accent md-hue-1 sg-icon--large">warning</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="app.cn">
<var:string label:value="Welcome"/> {{app.cn}}
</div>
<div class="md-padding" layout="row" layout-align="start center">
<md-icon>priority_high</md-icon>
<div class="md-padding">
<var:string label:value="Two-factor authentication has been disabled. Visit the Preferences module to restore two-factor authentication and reconfigure your TOTP application."/>
<!-- DOMAINS SELECT -->
<var:if condition="hasLoginDomains">
<div layout="row" layout-align="start end">
<md-icon>domain</md-icon>
<md-input-container class="md-flex">
<md-select class="md-flex" ng-model="app.creds.domain" label:placeholder="choose" ng-change="app.retrievePasswordRecoveryEnabled()">
<var:foreach list="loginDomains" item="item">
<md-option var:value="item">
<var:string value="item"/>
</md-option>
</var:foreach>
</md-select>
</md-input-container>
</div>
</var:if>
<div layout="row" layout-align="center center">
<md-switch class="md-accent md-hue-2"
ng-model="app.creds.rememberLogin"
label:arial-label="Remember username">
<var:string label:value="Remember username"/>
</md-switch>
</div>
</div>
<div layout="row" layout-align="end center">
<!-- Password recovery -->
<div layout="row" layout-align="center center" ng-if="app.passwordRecovery.passwordRecoveryEnabled">
<div ng-if="app.showLogin">
<a href="#" ng-click="app.passwordRecoveryInfo()" sg-ripple-click="loginContent" class="password-lost-link"><var:string label:value="Password lost"/></a>
</div>
</div>
<!-- CONNECT BUTTON -->
<div layout="row" layout-align="space-between center" ng-if="!app.loginState">
<md-button class="md-icon-button"
label:aria-label="About"
ng-click="app.showAbout()">
<md-icon>info</md-icon>
</md-button>
<div>
<md-button class="md-fab md-accent md-hue-2" type="submit"
label:aria-label="Connect"
ng-if="!app.loginState"
ng-disabled="loginForm.$invalid"
sg-ripple-click="loginContent">
<md-icon>arrow_forward</md-icon>
</md-button>
</div>
</div>
<sg-ripple class="md-default-theme md-accent md-bg"
ng-class="{ 'md-warn': app.loginState == 'error' }"><!-- ripple background --></sg-ripple>
<sg-ripple-content class="md-flex ng-hide"
layout="column" layout-align="center center" layout-fill="layout-fill"
ng-switch="app.loginState">
<!-- Authenticating -->
<div layout="column" layout-align="center center"
ng-switch-when="authenticating">
<md-progress-circular class="md-hue-1"
md-mode="indeterminate"
md-diameter="32"><!-- mailbox loading progress --></md-progress-circular>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
<var:string label:value="Authenticating"/>
</div>
</div>
<var:if condition="isTotpEnabled">
<!-- TOTP Code -->
<div layout="row" layout-align="center center" layout-fill="layout-fill"
ng-switch-when="totpcode">
<div flex="80" flex-sm="50" flex-gt-sm="40">
<md-input-container class="md-block">
<label><var:string label:value="Verification Code"/></label>
<md-icon>lock</md-icon>
<input type="text"
ng-pattern="app.verificationCodePattern"
ng-model="app.creds.verificationCode"
ng-required="app.loginState == 'totpcode'"
sg-focus-on="totpcode"/>
<div class="sg-hint"><var:string label:value="Enter the 6-digit verification code from your TOTP application."/></div>
</md-input-container>
<div layout="row" layout-align="space-between center">
<md-button class="md-icon-button"
label:aria-label="Cancel"
ng-click="app.restoreLogin()"
sg-ripple-click="loginContent">
<md-icon>arrow_backward</md-icon>
</md-button>
<md-button class="md-fab md-accent md-hue-2" type="submit"
label:aria-label="Connect"
ng-if="app.loginState == 'totpcode'"
ng-disabled="loginForm.$invalid"
ng-click="app.login()">
<md-icon>arrow_forward</md-icon>
</md-button>
</div>
</div>
</div>
<!-- TOTP has been disabled -->
<div layout="row" layout-align="center center" layout-fill="layout-fill"
ng-switch-when="totpdisabled">
<div layout="column" layout-align="center center" flex-xs="flex-xs" flex-gt-xs="50">
<md-icon class="md-accent md-hue-1 sg-icon--large">warning</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="app.cn">
<var:string label:value="Welcome"/> {{app.cn}}
</div>
<div class="md-padding" layout="row" layout-align="start center">
<md-icon>priority_high</md-icon>
<div class="md-padding">
<var:string label:value="Two-factor authentication has been disabled. Visit the Preferences module to restore two-factor authentication and reconfigure your TOTP application."/>
</div>
</div>
<div layout="row" layout-align="end center">
<md-button
ng-click="app.continueLogin()"
sg-ripple-click="loginContent"><var:string label:value="Continue"/></md-button>
</div>
</div>
</div>
</var:if>
<!-- Password policy: Password is expired / password recovery-->
<div layout="column" layout-align="center center"
ng-switch-when="passwordchange">
<md-icon class="md-accent md-hue-1 sg-icon--large" ng-if="!app.isInPasswordRecoveryMode()">watch_later</md-icon>
<md-icon class="md-accent md-hue-1 sg-icon--large" ng-if="app.isInPasswordRecoveryMode()">vpn_key</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="!app.isInPasswordRecoveryMode()">
<var:string label:value="Your password has expired, please enter a new one below"/>
</div>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="app.isInPasswordRecoveryMode()">
<var:string label:value="Please enter a new password below"/>
</div>
<div flex="100">
<div layout="row" layout-xs="column">
<md-input-container class="md-block" flex="flex" ng-if="!app.isInPasswordRecoveryMode()">
<label><var:string label:value="Current password"/>
</label>
<input type="password" sg-no-dirty-check="true" ng-model="app.passwords.oldPassword"/>
</md-input-container>
<md-input-container class="md-block" flex="flex">
<label><var:string label:value="New password"/>
</label>
<input type="password" sg-no-dirty-check="true" ng-model="app.passwords.newPassword"/>
</md-input-container>
<md-input-container class="md-block" flex="flex">
<label><var:string label:value="Confirmation"/>
</label>
<input type="password" name="newPasswordConfirmation" sg-no-dirty-check="true" ng-model="app.passwords.newPasswordConfirmation"/>
<div ng-messages="loginForm.newPasswordConfirmation.$error">
<div ng-message="newPasswordMismatch"><var:string label:value="Passwords don't match"/></div>
</div>
</md-input-container>
</div>
<div layout="row" layout-align="end center">
<md-button ng-click="app.changePassword()" type="button" ng-disabled="!app.canChangePassword(loginForm)">
<var:string label:value="Change"/>
</md-button>
</div>
</div>
</div>
<!-- Password policy: Grace period -->
<div layout="row" layout-align="center center" layout-fill="layout-fill"
ng-switch-when="passwordwillexpire">
<div layout="column" layout-align="center center" flex-xs="flex-xs" flex-gt-xs="50">
<md-icon class="md-accent md-hue-1 sg-icon--large">warning</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="app.cn">
<var:string label:value="Welcome"/> {{app.cn}}
</div>
<div class="md-padding" layout="row" layout-align="start center">
<md-icon>priority_high</md-icon>
<div class="md-padding">{{app.errorMessage}}</div>
</div>
<div layout="row" layout-align="end center">
<md-button
ng-click="app.loginState = 'passwordexpired'"><var:string label:value="Change your Password"/></md-button>
<md-button
ng-click="app.continueLogin()"
sg-ripple-click="loginContent"><var:string label:value="Continue"/></md-button>
</div>
</div>
</div>
<!-- Password recovery -->
<var:if condition="hasPasswordRecovery">
<div layout="column" layout-align="center center"
ng-switch-when="passwordrecovery">
<md-icon class="md-accent md-hue-1 sg-icon--large">vpn_key</md-icon>
<div flex="100">
<div layout="row" layout-xs="column" class="md-padding" layout-align="center center">
<div ng-if="app.passwordRecovery.showLoader">
<md-progress-circular class="md-hue-1"
md-mode="indeterminate"
md-diameter="32"><!-- password recovery progress --></md-progress-circular>
</div>
<div ng-if="'SecretQuestion' === app.passwordRecovery.passwordRecoveryMode">
<div ng-if="!app.passwordRecovery.showLoader">
{{ app.passwordRecovery.passwordRecoveryQuestion }}
<md-input-container class="md-block">
<label><var:string label:value="Answer"/></label>
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.passwordRecovery.passwordRecoveryQuestionAnswer" />
</md-input-container>
</div>
</div>
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<div ng-if="!app.passwordRecovery.showLoader">
{{ app.passwordRecovery.passwordRecoverySecondaryEmailText }}
</div>
</div>
</div>
<div layout="row" layout-align="end center" ng-if="!app.passwordRecovery.showLoader">
<md-button ng-click="app.passwordRecoveryAbort()" type="button" >
<var:string label:value="Back"/>
</md-button>
<div ng-if="'SecretQuestion' === app.passwordRecovery.passwordRecoveryMode">
<md-button ng-click="app.passwordRecoveryCheck()" type="button" >
<var:string label:value="Next"/>
</md-button>
</div>
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<md-button ng-click="app.passwordRecoveryEmail()" type="button" >
<var:string label:value="Next"/>
</md-button>
</div>
</div>
</div>
</div>
<div layout="column" layout-align="center center"
ng-switch-when="sendrecoverymail">
<md-icon class="md-accent md-hue-1 sg-icon--large">local_shipping</md-icon>
<div flex="100">
<div layout="row" layout-xs="column" class="md-padding">
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<var:string label:value="A password reset link has been sent, please check your recovery e-mail mailbox and click on the link"/>
</div>
</div>
<div layout="row" layout-align="end center">
<md-button ng-click="app.passwordRecoveryAbort()" type="button" >
<var:string label:value="Back"/>
</md-button>
</div>
</div>
</div>
</var:if>
<!-- Logged in -->
<div layout="column" layout-align="center center"
ng-switch-when="logged">
<md-icon class="md-accent md-hue-1 sg-icon--large">done</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
<var:string label:value="Welcome"/> {{app.cn}}
</div>
</div>
<div layout="column" layout-align="center center"
ng-switch-when="message">
<md-icon class="md-accent md-hue-1 sg-icon--large">done</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
{{app.errorMessage}}
</div>
<md-button
ng-click="app.continueLogin()"
sg-ripple-click="loginContent"><var:string label:value="Continue"/></md-button>
</div>
</div>
</div>
</var:if>
<!-- Password policy: Password is expired / password recovery-->
<div layout="column" layout-align="center center"
ng-switch-when="passwordchange">
<md-icon class="md-accent md-hue-1 sg-icon--large" ng-if="!app.isInPasswordRecoveryMode()">watch_later</md-icon>
<md-icon class="md-accent md-hue-1 sg-icon--large" ng-if="app.isInPasswordRecoveryMode()">vpn_key</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="!app.isInPasswordRecoveryMode()">
<var:string label:value="Your password has expired, please enter a new one below"/>
</div>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="app.isInPasswordRecoveryMode()">
<var:string label:value="Please enter a new password below"/>
</div>
<div flex="100">
<div layout="row" layout-xs="column">
<md-input-container class="md-block" flex="flex" ng-if="!app.isInPasswordRecoveryMode()">
<label><var:string label:value="Current password"/>
</label>
<input type="password" sg-no-dirty-check="true" ng-model="app.passwords.oldPassword"/>
</md-input-container>
<md-input-container class="md-block" flex="flex">
<label><var:string label:value="New password"/>
</label>
<input type="password" sg-no-dirty-check="true" ng-model="app.passwords.newPassword"/>
</md-input-container>
<md-input-container class="md-block" flex="flex">
<label><var:string label:value="Confirmation"/>
</label>
<input type="password" name="newPasswordConfirmation" sg-no-dirty-check="true" ng-model="app.passwords.newPasswordConfirmation"/>
<div ng-messages="loginForm.newPasswordConfirmation.$error">
<div ng-message="newPasswordMismatch"><var:string label:value="Passwords don't match"/></div>
</div>
</md-input-container>
</div>
<div layout="row" layout-align="end center">
<md-button ng-click="app.changePassword()" type="button" ng-disabled="!app.canChangePassword(loginForm)">
<var:string label:value="Change"/>
</md-button>
</div>
</div>
</div>
<!-- Password policy: Grace period -->
<div layout="row" layout-align="center center" layout-fill="layout-fill"
ng-switch-when="passwordwillexpire">
<div layout="column" layout-align="center center" flex-xs="flex-xs" flex-gt-xs="50">
<md-icon class="md-accent md-hue-1 sg-icon--large">warning</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding" ng-if="app.cn">
<var:string label:value="Welcome"/> {{app.cn}}
</div>
<div class="md-padding" layout="row" layout-align="start center">
<md-icon>priority_high</md-icon>
<div class="md-padding">{{app.errorMessage}}</div>
</div>
<div layout="row" layout-align="end center">
<!-- Error -->
<div layout="column" layout-align="center center"
ng-switch-when="error">
<md-icon class="md-accent md-hue-1 sg-icon--large">error</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
{{app.errorMessage}}
</div>
<md-button
ng-click="app.loginState = 'passwordexpired'"><var:string label:value="Change your Password"/></md-button>
<md-button
ng-click="app.continueLogin()"
sg-ripple-click="loginContent"><var:string label:value="Continue"/></md-button>
ng-click="app.restoreLogin()"
sg-ripple-click="loginContent"><var:string label:value="Retry"/></md-button>
</div>
</div>
</sg-ripple-content>
</form>
</div>
<!-- Password recovery -->
<var:if condition="hasPasswordRecovery">
<div layout="column" layout-align="center center"
ng-switch-when="passwordrecovery">
<md-icon class="md-accent md-hue-1 sg-icon--large">vpn_key</md-icon>
<div flex="100">
<div layout="row" layout-xs="column" class="md-padding" layout-align="center center">
<div ng-if="app.passwordRecovery.showLoader">
<md-progress-circular class="md-hue-1"
md-mode="indeterminate"
md-diameter="32"><!-- password recovery progress --></md-progress-circular>
</div>
<div ng-if="'SecretQuestion' === app.passwordRecovery.passwordRecoveryMode">
<div ng-if="!app.passwordRecovery.showLoader">
{{ app.passwordRecovery.passwordRecoveryQuestion }}
<md-input-container class="md-block">
<label><var:string label:value="Answer"/></label>
<input autocorrect="off" autocapitalize="off" type="text" ng-model="app.passwordRecovery.passwordRecoveryQuestionAnswer" />
</md-input-container>
</div>
</div>
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<div ng-if="!app.passwordRecovery.showLoader">
{{ app.passwordRecovery.passwordRecoverySecondaryEmailText }}
</div>
</div>
</div>
<div layout="row" layout-align="end center" ng-if="!app.passwordRecovery.showLoader">
<md-button ng-click="app.passwordRecoveryAbort()" type="button" >
<var:string label:value="Back"/>
</md-button>
<div ng-if="'SecretQuestion' === app.passwordRecovery.passwordRecoveryMode">
<md-button ng-click="app.passwordRecoveryCheck()" type="button" >
<var:string label:value="Next"/>
</md-button>
</div>
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<md-button ng-click="app.passwordRecoveryEmail()" type="button" >
<var:string label:value="Next"/>
</md-button>
</div>
</div>
</div>
</div>
<div layout="column" layout-align="center center"
ng-switch-when="sendrecoverymail">
<md-icon class="md-accent md-hue-1 sg-icon--large">local_shipping</md-icon>
<div flex="100">
<div layout="row" layout-xs="column" class="md-padding">
<div ng-if="'SecondaryEmail' === app.passwordRecovery.passwordRecoveryMode">
<var:string label:value="A password reset link has been sent, please check your recovery e-mail mailbox and click on the link"/>
</div>
</div>
<div layout="row" layout-align="end center">
<md-button ng-click="app.passwordRecoveryAbort()" type="button" >
<var:string label:value="Back"/>
</md-button>
</div>
</div>
</div>
</var:if>
<!-- Logged in -->
<div layout="column" layout-align="center center"
ng-switch-when="logged">
<md-icon class="md-accent md-hue-1 sg-icon--large">done</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
<var:string label:value="Welcome"/> {{app.cn}}
</div>
</div>
<div layout="column" layout-align="center center"
ng-switch-when="message">
<md-icon class="md-accent md-hue-1 sg-icon--large">done</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
{{app.errorMessage}}
</div>
<md-button
ng-click="app.continueLogin()"
sg-ripple-click="loginContent"><var:string label:value="Continue"/></md-button>
</div>
<!-- Error -->
<div layout="column" layout-align="center center"
ng-switch-when="error">
<md-icon class="md-accent md-hue-1 sg-icon--large">error</md-icon>
<div class="md-default-theme md-accent md-hue-1 md-fg md-padding">
{{app.errorMessage}}
</div>
<md-button
ng-click="app.restoreLogin()"
sg-ripple-click="loginContent"><var:string label:value="Retry"/></md-button>
</div>
</sg-ripple-content>
</form>
</div>
</div>
</div>
</md-content>
</main>

View File

@@ -1493,4 +1493,4 @@
</script>
</var:component>
</var:component>

View File

@@ -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

View File

@@ -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');

View File

@@ -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);
})();

View File

@@ -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;
}
}
/**