Merge branch 'master' into plugin-hooks

# Conflicts:
#	dev/Common/Plugins.js
#	dev/Knoin/Knoin.js
#	dev/Remote/AbstractFetch.js
#	dev/View/User/Login.js
This commit is contained in:
djmaze 2022-03-15 14:27:54 +01:00
commit 827cfa5051
892 changed files with 110819 additions and 48675 deletions

View file

@ -1,9 +1,49 @@
PHP
```php
class Plugin extends \RainLoop\Plugins\AbstractPlugin
{
public function __construct();
public function Name() : string;
public function Description() : string;
public function UseLangs(?bool $bLangs = null) : bool;
public function Supported() : string;
public function Init() : void;
public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig) : void;
protected function configMapping() : array;
}
```
JavaScript
```javascript
class PluginPopupView extends rl.pluginPopupView
{
// Happens when DOM is created
onBuild(dom) {}
// Happens before showModal()
beforeShow(...params) {}
// Happens after showModal()
onShow(...params) {}
// Happens after showModal() animation transitionend
afterShow() {}
// Happens when user hits Escape or Close key
// return false to prevent closing, use close() manually
onClose() {}
// Happens before animation transitionend
onHide() {}
// Happens after animation transitionend
afterHide() {}
}
PluginPopupView.showModal();
```
# Hooks
```php
$Plugin->addHook('hook.name', 'functionName');
```
## Login
@ -28,48 +68,38 @@ $Plugin->addHook('hook.name', 'functionName');
## IMAP
### imap.credentials
params:
\RainLoop\Model\Account $oAccount
array &$aCredentials
### imap.before-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient
array $aCredentials
\MailSo\Imap\ImapClient $oImapClient
array &$aCredentials
### imap.after-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient
\MailSo\Imap\ImapClient $oImapClient
array $aCredentials
### imap.before-login
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient
array $aCredentials
\MailSo\Imap\ImapClient $oImapClient
array &$aCredentials
### imap.after-login
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mail\MailClient $oMailClient
\MailSo\Imap\ImapClient $oImapClient
bool $bSuccess
array $aCredentials
## Sieve
### sieve.credentials
params:
\RainLoop\Model\Account $oAccount
array &$aCredentials
### sieve.before-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient
array $aCredentials
array &$aCredentials
### sieve.after-connect
params:
@ -81,27 +111,22 @@ $Plugin->addHook('hook.name', 'functionName');
params:
\RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient
bool $bSuccess
array $aCredentials
array &$aCredentials
### sieve.after-login
params:
\RainLoop\Model\Account $oAccount
\MailSo\Sieve\ManageSieveClient $oSieveClient
bool $bSuccess
array $aCredentials
## SMTP
### smtp.credentials
params:
\RainLoop\Model\Account $oAccount
array &$aCredentials
### smtp.before-connect
params:
\RainLoop\Model\Account $oAccount
\MailSo\Smtp\SmtpClient $oSmtpClient
array $aCredentials
array &$aCredentials
### smtp.after-connect
params:
@ -113,7 +138,7 @@ $Plugin->addHook('hook.name', 'functionName');
params:
\RainLoop\Model\Account $oAccount
\MailSo\Smtp\SmtpClient $oSmtpClient
array $aCredentials
array &$aCredentials
### smtp.after-login
params:
@ -187,16 +212,17 @@ $Plugin->addHook('hook.name', 'functionName');
params:
array &$aPaths
### filter.http-query
params:
string &$sQuery
### filter.json-response
params:
string $sAction
array &$aResponseItem
### filter.message-html'
### filter.message-html
params:
\RainLoop\Model\Account $oAccount
\MailSo\Mime\Message $oMessage
string &$sTextConverted
### filter.message-plain
params:
\RainLoop\Model\Account $oAccount
@ -264,18 +290,13 @@ $Plugin->addHook('hook.name', 'functionName');
string $sAction
array &$aResponseItem
### json.action-post-call
params:
string $sAction
array &$aResponseItem
### json.action-pre-call
params:
string $sAction
### json.action-pre-call
### json.attachments
params:
string $sAction
\SnappyMail\AttachmentsAction $oData
### json.suggestions-input-parameters
params:
@ -297,31 +318,15 @@ $Plugin->addHook('hook.name', 'functionName');
\RainLoop\Model\Account $oAccount
int $iLimit
### main.default-response
### main.content-security-policy
params:
string $sActionName
array &$aResponseItem
\SnappyMail\HTTP\CSP $oCSP
### main.default-response
params:
string $sActionName
array &$aResponseItem
### main.default-response
params:
string $sActionName
array &$aResponseItem
### main.default-response-data
params:
string $sActionName
mixed &$mResult
### main.default-response-data
params:
string $sActionName
mixed &$mResult
### main.default-response-data
params:
string $sActionName
@ -343,3 +348,21 @@ $Plugin->addHook('hook.name', 'functionName');
### service.app-delay-start-end
no params
# JavaScript Events
## mailbox
### mailbox.message-list.selector.go-up
### mailbox.message-list.selector.go-down
### mailbox.message-view.toggle-full-screen
### mailbox.inbox-unread-count
### mailbox.message.show
## audio
### audio.start
### audio.stop
### audio.api.stop
## Misc
### idle
### rl-layout
### rl-view-model
event.detail = the ViewModel class

View file

@ -1,4 +1,4 @@
This plugin, written by https://github.com/jas8522
This extension, written by https://github.com/jas8522
and optimised by
https://github.com/rhyswilliamsza
https://github.com/rhysit

View file

@ -1,14 +1,21 @@
What is it?
In essense, this plugin allows for multiple users across multiple servers to use one RainLoop installation,
all without the administrator needing to create each domain separately. This plugin detects the required
hostname from the inputted email address (for example, it would detect "example.com" as the hostname if
"info@example.com" is inputted). This hostname is then used for IMAP and SMTP communication.
In essense, this extension allows multiple users across multiple servers to
use one Snappymail installation without the administrator having to create
and configure individual domans.
This extension detects the required hostname from a given email address (for
example, it would use "example.com" as the hostname if a user would
log in as "info@example.com").
This hostname is then used for IMAP and SMTP communication.
How to Use:
1) Activate plugin
2) Visit Settings, and add a new wildcard domain "*"
3) Set your ports/ssl requirements as needed. These will not be changed by the plugin.
4) In the SMTP and/or IMAP host fields, place the single word "auto". This will be replaced when the plugin is active.
5) That's it! The plugin should now work!
1) Activate extension
2) Go to settings, and add a new wildcard domain "*"
3) Set your ports/TLS/SSL configuration as needed. These settings will not be
altered by the extension.
4) Set the SMTP and/or IMAP host fields to "auto", this field will then be
auto-configured.
5) That's it!

View file

@ -1,9 +1,10 @@
<?php
/**
* This plug-in automatically detects the IMAP and SMTP settings by extracting them from the email address itself.
* For example, user inputs: "info@example.com"
* This plugin sets the IMAP and SMTP host to "example.com" upon login, and then connects to it.
* This extension automatically detects the IMAP and SMTP settings by
* extracting them from the email address itself. For example, if the user
* attemps to login as 'info@example.com', then the IMAP and SMTP host would
* be set to to 'example.com'.
*
* Based on:
* https://github.com/the-djmaze/snappymail/blob/master/plugins/override-smtp-credentials/index.php
@ -13,69 +14,59 @@
class AutoDomainGrabPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
NAME = 'Auto Domain Selection',
VERSION = '2.8',
REQUIRED = '2.8.0',
CATEGORY = 'General',
DESCRIPTION = '';
DESCRIPTION = 'Sets the IMAP/SMTP host based on the user\'s login';
private $imap_prefix = "mail.";
private $smtp_prefix = "mail.";
private $imap_prefix = 'mail.';
private $smtp_prefix = 'mail.';
public function Init() : void
{
$this->addHook('smtp.credentials', 'FilterSmtpCredentials');
$this->addHook('imap.credentials', 'FilterImapCredentials');
$this->addHook('smtp.before-connect', 'FilterSmtpCredentials');
$this->addHook('imap.before-connect', 'FilterImapCredentials');
}
/**
* This function detects the IMAP Host, and if it is set to "auto", replaces it with the MX or email domain.
*
* @param \RainLoop\Model\Account $oAccount
* @param array $aImapCredentials
* This function detects the IMAP Host, and if it is set to 'auto', replaces it with the MX or email domain.
*/
public function FilterImapCredentials($oAccount, &$aImapCredentials)
public function FilterImapCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Imap\ImapClient $oImapClient, array &$aImapCredentials)
{
if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aImapCredentials))
// Check for mail.$DOMAIN as entered value in RL settings
if (!empty($aImapCredentials['Host']) && 'auto' === $aImapCredentials['Host'])
{
// Check for mail.$DOMAIN as entered value in RL settings
if (!empty($aImapCredentials['Host']) && 'auto' === $aImapCredentials['Host'])
$domain = \substr(\strrchr($oAccount->Email(), '@'), 1);
$mxhosts = array();
if (\getmxrr($domain, $mxhosts) && $mxhosts)
{
$domain = substr(strrchr($oAccount->Email(), "@"), 1);
$mxhosts = array();
if(getmxrr($domain, $mxhosts) && sizeof($mxhosts) > 0)
{
$aImapCredentials['Host'] = $mxhosts[0];
}
else
{
$aImapCredentials['Host'] = $this->imap_prefix.$domain;
}
$aImapCredentials['Host'] = $mxhosts[0];
}
else
{
$aImapCredentials['Host'] = $this->imap_prefix.$domain;
}
}
}
/**
* This function detects the SMTP Host, and if it is set to "auto", replaces it with the MX or email domain.
*
* @param \RainLoop\Model\Account $oAccount
* @param array $aSmtpCredentials
* This function detects the SMTP Host, and if it is set to 'auto', replaces it with the MX or email domain.
*/
public function FilterSmtpCredentials($oAccount, &$aSmtpCredentials)
public function FilterSmtpCredentials(\RainLoop\Model\Account $oAccount, \MailSo\Smtp\SmtpClient $oSmtpClient, array &$aSmtpCredentials)
{
if ($oAccount instanceof \RainLoop\Model\Account && \is_array($aSmtpCredentials))
// Check for mail.$DOMAIN as entered value in RL settings
if (!empty($aSmtpCredentials['Host']) && 'auto' === $aSmtpCredentials['Host'])
{
// Check for mail.$DOMAIN as entered value in RL settings
if (!empty($aSmtpCredentials['Host']) && 'auto' === $aSmtpCredentials['Host'])
$domain = \substr(\strrchr($oAccount->Email(), '@'), 1);
$mxhosts = array();
if (\getmxrr($domain, $mxhosts) && $mxhosts)
{
$domain = substr(strrchr($oAccount->Email(), "@"), 1);
$mxhosts = array();
if(getmxrr($domain, $mxhosts) && sizeof($mxhosts) > 0)
{
$aSmtpCredentials['Host'] = $mxhosts[0];
}
else
{
$aSmtpCredentials['Host'] = $this->smtp_prefix.$domain;
}
$aSmtpCredentials['Host'] = $mxhosts[0];
}
else
{
$aSmtpCredentials['Host'] = $this->smtp_prefix . $domain;
}
}
}

View file

@ -3,10 +3,12 @@
class BlackListPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Black list',
VERSION = '2.0',
NAME = 'Blacklist',
VERSION = '2.1',
RELEASE = '2021-04-21',
REQUIRED = '2.5.0',
CATEGORY = 'Login',
DESCRIPTION = 'Simple black list plugin (with wildcard and exceptions functionality).';
DESCRIPTION = 'Simple blacklist extension (with wildcard and exceptions functionality).';
public function Init() : void
{

View file

@ -0,0 +1,104 @@
<?php
class ChangePasswordDriverISPConfig
{
const
NAME = 'ISPConfig',
DESCRIPTION = 'Change passwords in ISPConfig.';
/**
* @var \RainLoop\Config\Plugin
*/
private $oConfig = null;
/**
* @var \MailSo\Log\Logger
*/
private $oLogger = null;
function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger)
{
$this->oConfig = $oConfig;
$this->oLogger = $oLogger;
}
public static function isSupported() : bool
{
return \class_exists('PDO', false)
// The PHP extension PDO (mysql) must be installed to use this plugin
&& \in_array('mysql', \PDO::getAvailableDrivers());
}
public static function configMapping() : array
{
return array(
\RainLoop\Plugins\Property::NewInstance('ispconfig_dsn')->SetLabel('ISPConfig PDO dsn')
->SetDefaultValue('mysql:host=localhost;dbname=dbispconfig;charset=utf8'),
\RainLoop\Plugins\Property::NewInstance('ispconfig_user')->SetLabel('User'),
\RainLoop\Plugins\Property::NewInstance('ispconfig_password')->SetLabel('Password')
->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD),
\RainLoop\Plugins\Property::NewInstance('ispconfig_allowed_emails')->SetLabel('Allowed emails')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net')
->SetDefaultValue('*')
);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
{
if (!\RainLoop\Plugins\Helper::ValidateWildcardValues($oAccount->Email(), $this->oConfig->Get('plugin', 'ispconfig_allowed_emails', ''))) {
return false;
}
try
{
if ($this->oLogger) {
$this->oLogger->Write('ISPConfig: Try to change password for '.$oAccount->Email());
}
$oPdo = new \PDO(
$this->oConfig->Get('plugin', 'ispconfig_dsn', ''),
$this->oConfig->Get('plugin', 'ispconfig_user', ''),
$this->oConfig->Get('plugin', 'ispconfig_password', ''),
array(
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
)
);
$oStmt = $oPdo->prepare('SELECT password, mailuser_id FROM mail_user WHERE login = ? LIMIT 1');
if ($oStmt->execute(array($oAccount->IncLogin()))) {
$aFetchResult = $oStmt->fetch(\PDO::FETCH_ASSOC);
if (!empty($aFetchResult['mailuser_id'])) {
$sDbPassword = $aFetchResult['password'];
$sDbSalt = \substr($sDbPassword, 0, \strrpos($sDbPassword, '$'));
if (\crypt($sPrevPassword, $sDbSalt) === $sDbPassword) {
$oStmt = $oPdo->prepare('UPDATE mail_user SET password = ? WHERE mailuser_id = ?');
return !!$oStmt->execute(array(
$this->cryptPassword($sNewPassword),
$aFetchResult['mailuser_id']
));
}
}
}
}
catch (\Exception $oException)
{
if ($this->oLogger) {
$this->oLogger->WriteException($oException);
}
}
return false;
}
private function cryptPassword(string $sPassword) : string
{
if (\defined('CRYPT_SHA512') && CRYPT_SHA512) {
$sSalt = '$6$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
} elseif (\defined('CRYPT_SHA256') && CRYPT_SHA256) {
$sSalt = '$5$rounds=5000$' . \bin2hex(\random_bytes(8)) . '$';
} else {
$sSalt = '$1$' . \bin2hex(\random_bytes(6)) . '$';
}
return \crypt($sPassword, $sSalt);
}
}

View file

@ -7,8 +7,8 @@ class ChangePasswordDriverLDAP
DESCRIPTION = 'Change passwords in LDAP.';
private
$sHostName = 'localhost',
$iHostPort = 389,
$sLdapUri = 'ldap://localhost:389',
$bUseStartTLS = True,
$sUserDnFormat = '',
$sPasswordField = 'userPassword',
$sPasswordEncType = 'SHA';
@ -21,8 +21,8 @@ class ChangePasswordDriverLDAP
function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger)
{
$this->oLogger = $oLogger;
$this->sHostName = \trim($oConfig->Get('plugin', 'ldap_hostname', ''));
$this->iHostPort = (int) $oConfig->Get('plugin', 'ldap_port', 389);
$this->sLdapUri = \trim($oConfig->Get('plugin', 'ldap_uri', ''));
$this->bUseStartTLS = (bool) \trim($oConfig->Get('plugin', 'ldap_use_start_tls', ''));
$this->sUserDnFormat = \trim($oConfig->Get('plugin', 'ldap_user_dn_format', ''));
$this->sPasswordField = \trim($oConfig->Get('plugin', 'ldap_password_field', ''));
$this->sPasswordEncType = \trim($oConfig->Get('plugin', 'ldap_password_enc_type', ''));
@ -37,11 +37,12 @@ class ChangePasswordDriverLDAP
public static function configMapping() : array
{
return array(
\RainLoop\Plugins\Property::NewInstance('ldap_hostname')->SetLabel('Hostname')
->SetDefaultValue('localhost'),
\RainLoop\Plugins\Property::NewInstance('ldap_port')->SetLabel('Port')
->SetType(\RainLoop\Enumerations\PluginPropertyType::INT)
->SetDefaultValue(389),
\RainLoop\Plugins\Property::NewInstance('ldap_uri')->SetLabel('LDAP URI')
->SetDefaultValue('ldap://localhost:389')
->SetDescription('LDAP server URI(s), space separated'),
\RainLoop\Plugins\Property::NewInstance('ldap_use_start_tls')->SetLabel('Use StartTLS')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(True),
\RainLoop\Plugins\Property::NewInstance('ldap_user_dn_format')->SetLabel('User DN format')
->SetDescription('LDAP user dn format. Supported tokens: {email}, {email:user}, {email:domain}, {login}, {domain}, {domain:dc}, {imap:login}, {imap:host}, {imap:port}, {gecos}')
->SetDefaultValue('uid={imap:login},ou=Users,{domain:dc}'),
@ -65,12 +66,12 @@ class ChangePasswordDriverLDAP
'{email:domain}' => $sDomain,
'{login}' => $oAccount->Login(),
'{imap:login}' => $oAccount->Login(),
'{imap:host}' => $oAccount->DomainIncHost(),
'{imap:port}' => $oAccount->DomainIncPort(),
'{imap:host}' => $oAccount->Domain()->IncHost(),
'{imap:port}' => $oAccount->Domain()->IncPort(),
'{gecos}' => \function_exists('posix_getpwnam') ? \posix_getpwnam($oAccount->Login()) : ''
));
$oCon = \ldap_connect($this->sHostName, $this->iHostPort);
$oCon = \ldap_connect($this->sLdapUri);
if (!$oCon) {
return false;
}
@ -82,8 +83,10 @@ class ChangePasswordDriverLDAP
'LDAP'
);
}
else if (!\ldap_start_tls($oCon)) {
$this->oLogger->Write("ldap_start_tls failed: ".$oCon, \MailSo\Log\Enumerations\Type::WARNING, 'LDAP');
if ($this->bUseStartTLS && !@\ldap_start_tls($oCon))
{
throw new \Exception('ldap_start_tls error '.\ldap_errno($oCon).': '.\ldap_error($oCon));
}
if (!\ldap_bind($oCon, $sUserDn, $sPrevPassword)) {

View file

@ -6,13 +6,10 @@ class ChangePasswordDriverPDO
NAME = 'PDO',
DESCRIPTION = 'Use your own SQL (PDO) statement (with wildcards).';
private
$dsn = '',
$user = '',
$pass = '',
$sql = '',
$encrypt = '',
$encrypt_prefix = ''; // Like: {ARGON2I} {BLF-CRYPT} {SHA512-CRYPT}
/**
* @var \RainLoop\Config\Plugin
*/
private $oConfig = null;
/**
* @var \MailSo\Log\Logger
@ -21,13 +18,8 @@ class ChangePasswordDriverPDO
function __construct(\RainLoop\Config\Plugin $oConfig, \MailSo\Log\Logger $oLogger)
{
$this->oConfig = $oConfig;
$this->oLogger = $oLogger;
$this->dsn = $oConfig->Get('plugin', 'pdo_dsn', '');
$this->user = $oConfig->Get('plugin', 'pdo_user', '');
$this->pass = $oConfig->Get('plugin', 'pdo_password', '');
$this->sql = $oConfig->Get('plugin', 'pdo_sql', '');
$this->encrypt = $oConfig->Get('plugin', 'pdo_encrypt', '');
$this->encrypt_prefix = $oConfig->Get('plugin', 'pdo_encryptprefix', '');
}
public static function isSupported() : bool
@ -52,43 +44,73 @@ class ChangePasswordDriverPDO
->SetDefaultValue(array('none', 'bcrypt', 'Argon2i', 'Argon2id', 'SHA256-CRYPT', 'SHA512-CRYPT'))
->SetDescription('In what way do you want the passwords to be encrypted?'),
\RainLoop\Plugins\Property::NewInstance('pdo_encryptprefix')->SetLabel('Encrypt prefix')
->SetDescription('Optional encrypted password prefix, like: {BLF-CRYPT}'),
->SetDescription('Optional encrypted password prefix, like {ARGON2I} or {BLF-CRYPT} or {SHA512-CRYPT}'),
\RainLoop\Plugins\Property::NewInstance('pdo_mysql_ssl')->SetLabel('MySQL SSL connection')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(false),
\RainLoop\Plugins\Property::NewInstance('pdo_mysql_ssl_verify')->SetLabel('MySQL SSL verify server cert')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDescription('Verify that certificate\'s Common Name of SAN matches the database server\'s hostname.')
->SetDefaultValue(false),
\RainLoop\Plugins\Property::NewInstance('pdo_mysql_ssl_ca')->SetLabel('MySQL SSL CA certificate file')
->SetDescription('Path to a file containing the CA certificate used to sign the server certificate, or a CA bundle. Required for SSL/TLS connections to work.')
->SetDefaultValue('/etc/pki/tls/certs/ca-bundle.crt')
);
}
public function ChangePassword(\RainLoop\Model\Account $oAccount, string $sPrevPassword, string $sNewPassword) : bool
{
$options = array(
\PDO::ATTR_EMULATE_PREPARES => true,
\PDO::ATTR_PERSISTENT => true,
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
);
try
{
$pdo_attr = array(
\PDO::ATTR_EMULATE_PREPARES => true,
\PDO::ATTR_PERSISTENT => true,
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
);
if ($this->oConfig->Get('plugin', 'pdo_mysql_ssl', false)) {
$pdo_attr[\PDO::MYSQL_ATTR_SSL_CA] = $this->oConfig->Get('plugin', 'pdo_mysql_ssl_ca', '');
$pdo_attr[\PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] = $this->oConfig->Get('plugin', 'pdo_mysql_ssl_verify', true);
}
$conn = new \PDO($this->dsn, $this->user, $this->pass, $options);
$conn = new \PDO(
$this->oConfig->Get('plugin', 'pdo_dsn', ''),
$this->oConfig->Get('plugin', 'pdo_user', ''),
$this->oConfig->Get('plugin', 'pdo_password', ''),
$pdo_attr
);
//prepare SQL varaibles
$sEmail = $oAccount->Email();
$sEmailUser = \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail);
$sEmailDomain = \MailSo\Base\Utils::GetDomainFromEmail($sEmail);
$sEmail = $oAccount->Email();
$encrypt = $this->oConfig->Get('plugin', 'pdo_encrypt', '');
$encrypt_prefix = $this->oConfig->Get('plugin', 'pdo_encryptprefix', '');
$placeholders = array(
':email' => $sEmail,
':oldpass' => $this->encrypt_prefix . \ChangePasswordPlugin::encrypt($this->encrypt, $sPrevPassword),
':newpass' => $this->encrypt_prefix . \ChangePasswordPlugin::encrypt($this->encrypt, $sNewPassword),
':domain' => $sEmailDomain,
':username' => $sEmailUser
);
$placeholders = array(
':email' => $sEmail,
':oldpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sPrevPassword),
':newpass' => $encrypt_prefix . \ChangePasswordPlugin::encrypt($encrypt, $sNewPassword),
':domain' => \MailSo\Base\Utils::GetDomainFromEmail($sEmail),
':username' => \MailSo\Base\Utils::GetAccountNameFromEmail($sEmail)
);
$statement = $conn->prepare($this->sql);
$sql = $this->oConfig->Get('plugin', 'pdo_sql', '');
// we have to check that all placehoders are used in the query, passing any unused placeholders will generate an error
foreach ($placeholders as $placeholder => $value) {
if (\preg_match_all("/{$placeholder}(?![a-zA-Z0-9\-])/", $this->sql)) {
$statement->bindValue($placeholder, $value);
$statement = $conn->prepare($sql);
// we have to check that all placehoders are used in the query, passing any unused placeholders will generate an error
foreach ($placeholders as $placeholder => $value) {
if (\preg_match_all("/{$placeholder}(?![a-zA-Z0-9\-])/", $sql)) {
$statement->bindValue($placeholder, $value);
}
}
// and execute
return !!$statement->execute();
}
catch (\Exception $oException)
{
if ($this->oLogger) {
$this->oLogger->WriteException($oException);
}
}
// and execute
return !!$statement->execute();
return false;
}
}

View file

@ -6,11 +6,11 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Change Password',
VERSION = '2.1',
RELEASE = '2021-03-18',
REQUIRED = '2.4.0',
VERSION = '2.13.1',
RELEASE = '2022-03-10',
REQUIRED = '2.12.0',
CATEGORY = 'Security',
DESCRIPTION = 'This plugin allows you to change passwords of email accounts';
DESCRIPTION = 'Extension to allow users to change their passwords';
// \RainLoop\Notifications\
const
@ -23,6 +23,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
{
$this->UseLangs(true); // start use langs folder
// $this->addCss('style.css');
$this->addJs('js/ChangePasswordUserSettings.js'); // add js file
$this->addJsonHook('ChangePassword', 'ChangePassword');
$this->addTemplate('templates/SettingsChangePassword.html');
@ -95,16 +96,21 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
->SetDefaultValue(70),
];
foreach ($this->getSupportedDrivers(true) as $name => $class) {
$result[] = \RainLoop\Plugins\Property::NewInstance("driver_{$name}_enabled")
->SetLabel('Enable ' . $class::NAME)
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDescription($class::DESCRIPTION);
$result[] = \RainLoop\Plugins\Property::NewInstance("driver_{$name}_allowed_emails")
->SetLabel('Allowed emails')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@example.net user2@example1.net *@example2.net')
->SetDefaultValue('*');
$result = \array_merge($result, \call_user_func("{$class}::configMapping"));
$group = new \RainLoop\Plugins\PropertyCollection($name);
$props = [
\RainLoop\Plugins\Property::NewInstance("driver_{$name}_enabled")
->SetLabel('Enable ' . $class::NAME)
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDescription($class::DESCRIPTION)
->SetDefaultValue(false),
\RainLoop\Plugins\Property::NewInstance("driver_{$name}_allowed_emails")
->SetLabel('Allowed emails')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@example.net user2@example1.net *@example2.net')
->SetDefaultValue('*')
];
$group->exchangeArray(\array_merge($props, \call_user_func("{$class}::configMapping")));
$result[] = $group;
}
return $result;
}
@ -174,7 +180,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
$oAccount->SetPassword($sNewPassword);
$oActions->SetAuthToken($oAccount);
return $this->jsonResponse(__FUNCTION__, $oActions->GetSpecAuthToken());
return $this->jsonResponse(__FUNCTION__, $oActions->AppData(false));
}
public static function encrypt(string $algo, string $password)
@ -200,7 +206,7 @@ class ChangePasswordPlugin extends \RainLoop\Plugins\AbstractPlugin
break;
}
return $sPassword;
return $password;
}
private static function PasswordStrength(string $sPassword) : int

View file

@ -110,15 +110,14 @@
// Notification.CurrentPasswordIncorrect
this.currentPasswordError(true);
}
this.errorDescription((data && data.ErrorMessageAdditional)
this.errorDescription((data && rl.i18n(data.ErrorMessageAdditional))
|| rl.i18n('NOTIFICATIONS/COULD_NOT_SAVE_NEW_PASSWORD'));
} else {
this.currentPassword('');
this.newPassword('');
this.newPassword2('');
this.passwordUpdateSuccess(true);
rl.hash.set();
rl.settings.set('AuthAccountHash', data.Result);
rl.setData(data.Result);
}
}
}

View file

@ -0,0 +1,12 @@
[SETTINGS_CHANGE_PASSWORD]
LEGEND_CHANGE_PASSWORD = "Cambia password"
LABEL_CURRENT_PASSWORD = "Password attuale"
LABEL_NEW_PASSWORD = "Nuova password"
LABEL_REPEAT_PASSWORD = "Conferma nuova password"
BUTTON_UPDATE_PASSWORD = "Imposta nuova password"
ERROR_PASSWORD_MISMATCH = "Le password non corrispondono, riprova"
[NOTIFICATIONS]
COULD_NOT_SAVE_NEW_PASSWORD = "Non è stato possibile salvare la nuova password"
CURRENT_PASSWORD_INCORRECT = "La password attuale non è corretta"
NEW_PASSWORD_SHORT = "La password scelta è troppo breve"
NEW_PASSWORD_WEAK = "La password scelta non è abbastanza complessa"

View file

@ -0,0 +1,5 @@
.form-horizontal.change-password .control-group {
.control-label {
width: 160px;
}
}

View file

@ -1,5 +1,5 @@
<div class="b-settings-general g-ui-user-select-none">
<div class="form-horizontal long-label">
<div class="form-horizontal change-password">
<div class="legend" data-i18n="SETTINGS_CHANGE_PASSWORD/LEGEND_CHANGE_PASSWORD"></div>
<div class="row">
<div class="span6">

View file

@ -3,9 +3,9 @@
class ChangeSmtpEhloMessagePlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
NAME = 'Change SMTP EHLO Message',
CATEGORY = 'General',
DESCRIPTION = '';
DESCRIPTION = 'Extension to enable custom SMTP EHLO messages';
public function Init() : void
{

View file

@ -35,8 +35,7 @@
],
removePlugins: 'liststyle' + (rl.settings.app('allowHtmlEditorBitiButtons') ? '' : ',bidi'),
removeButtons: 'Format,Undo,Redo,Cut,Copy,Paste,Anchor,Strike,Subscript,Superscript,Image,SelectAll'
+ (rl.settings.app('allowHtmlEditorSourceButton') ? '' : ',Source'),
removeButtons: 'Format,Undo,Redo,Cut,Copy,Paste,Anchor,Strike,Subscript,Superscript,Image,SelectAll',
removeDialogTabs: 'link:advanced;link:target;image:advanced;images:advanced',
extraPlugins: 'plain,signature',

View file

@ -1,213 +0,0 @@
/*.cke_button__sourcedialog_label {
display: none !important;
}*/
.cke_chrome {
border: 1px solid #ccc !important;
}
.cke_toolgroup {
padding-right: 0 !important;
background: #FBFBFB !important;
}
.cke_toolgroup, .cke_combo_button {
border: 1px solid #A6A6A6 !important;
}
.cke_top {
padding: 2px;
box-shadow: none !important;
border-bottom: 1px solid #b6b6b6 !important;
background: #F0F0F0 !important;
}
.cke_combo_on a.cke_combo_button,
.cke_combo_off a.cke_combo_button:hover,
.cke_combo_off a.cke_combo_button:focus,
.cke_combo_off a.cke_combo_button:active {
padding: 1px !important;
margin-left: 0 !important;
}
.cke_toolgroup a.cke_button:last-child:after,
.cke_toolgroup a.cke_button.cke_button_disabled:hover:last-child:after {
border: none !important;
height: 0 !important;
width: 0 !important;
margin: 0 !important;
padding: 0 !important;
}
.cke_button_on {
background: #ddd !important;
}
.cke_combo {
margin-top: 1px !important;
}
.cke_combo__fontsize {
margin-left: 3px !important;
.cke_combo_text {
width: 40px !important;
}
}
.cke_combo_text {
line-height: 24px !important;
padding-left: 8px !important;
}
.cke_combo_button {
background: #FBFBFB !important;
}
.cke_source {
font-family: var(--fontMono) !important;
padding: 10px !important;
padding-right: 0 !important;
box-shadow: none !important;
}
.cke_plain {
background-color: #fff;
outline: none;
overflow: auto;
z-index: 1;
margin: 0;
border: 0;
padding: 10px;
padding-right: 0;
white-space: pre-wrap;
font-family: var(--fontMono), monospace;
font-size: 13px;
line-height: 16px;
color: #333;
resize: none;
border-radius: 0;
box-shadow: none;
box-sizing: border-box;
}
.cke_plain:focus, .cke_source:focus {
border: 0;
}
.b-compose .cke_chrome {
border-right: 0;
border-left: 0;
border-bottom: 0;
box-shadow: none;
}
.cke_wysiwyg_div {
padding: 10px !important;
font-family: var(--fontSans);
font-size: 13px;
line-height: 16px;
color: #333;
ul {
padding-left: 40px;
li {
list-style-type: disc !important;
}
}
ol {
padding-left: 40px;
li {
list-style-type: decimal !important;
}
}
pre, code {
margin: 0;
padding: 0;
background: #fff;
border: none;
border-radius: 0;
font-family: var(--fontMono);
display: block;
word-break: normal;
word-wrap: break-word;
background-color: #f9f9f9;
}
code {
display: inline;
padding: 2px 5px;
}
pre {
padding: 5px 10px;
border-radius: 5px;
background-color: #f9f9f9;
}
pre > code {
padding: 0;
}
blockquote {
border: 0;
border-left: solid 2px #444;
margin: 5px 0 5px 5px;
padding-left: 5px;
}
blockquote p {
margin: 0 0 10px;
font-size: 14px;
line-height: 20px;
}
img {
vertical-align: bottom;
}
a {
color: blue;
text-decoration: underline;
&:visited {
color: #609;
}
&:active {
color: red;
}
}
}
.cke_dialog {
a:hover {
text-decoration: none;
}
.cke_dialog_ui_labeled_content {
margin-top: 5px;
margin-bottom: 5px;
}
.cke_dialog_ui_input_select, .cke_dialog_ui_input_text, .cke_dialog_ui_input_textarea {
box-shadow: none;
border-radius: 2px;
&:focus {
outline: 0;
border: 1px solid #999;
}
}
.cke_dialog_ui_input_select, .cke_dialog_ui_input_text {
height: 25px;
line-height: 25px;
}
}

View file

@ -4,9 +4,9 @@ class CKEditorPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'CKEditor',
VERSION = '2.1',
RELEASE = '2021-04-01',
REQUIRED = '2.5.0',
VERSION = '2.2',
RELEASE = '2021-08-17',
REQUIRED = '2.6.2',
DESCRIPTION = 'Use CKEditor instead of Squire as WYSIWYG';
public function Init() : void
@ -16,6 +16,7 @@ class CKEditorPlugin extends \RainLoop\Plugins\AbstractPlugin
// Apache AH00037: Symbolic link not allowed or link target not accessible
// That's why we clone the source
if (!\is_dir($path)/* && !\is_link($path)*/) {
$old_mask = umask(0022);
// $active = \symlink(__DIR__ . '/src', $path);
\mkdir($path, 0755);
$iterator = new \RecursiveIteratorIterator(
@ -29,10 +30,11 @@ class CKEditorPlugin extends \RainLoop\Plugins\AbstractPlugin
\copy($item, $path . DIRECTORY_SEPARATOR . $iterator->getSubPathName());
}
}
umask($old_mask);
}
if (\is_file("{$path}/ckeditor.js")) {
$this->addCss('ckeditor.less');
$this->addCss('style.css');
$this->addJs('ckeditor.js');
}
}

207
plugins/ckeditor/style.css Normal file
View file

@ -0,0 +1,207 @@
/*.cke_button__sourcedialog_label {
display: none;
}*/
.cke, .cke_inner {
min-height: 99%;
}
.cke_chrome {
border: 1px solid #ccc;
}
.cke_toolgroup {
padding-right: 0;
background: #FBFBFB;
}
.cke_toolgroup, .cke_combo_button {
border: 1px solid #A6A6A6;
}
.cke_top {
padding: 2px;
box-shadow: none;
border-bottom: 1px solid #b6b6b6;
background: #F0F0F0;
}
.cke_combo_on a.cke_combo_button,
.cke_combo_off a.cke_combo_button:hover,
.cke_combo_off a.cke_combo_button:focus,
.cke_combo_off a.cke_combo_button:active {
padding: 1px;
margin-left: 0;
}
a.cke_button:last-child::after,
a.cke_button.cke_button_disabled:hover:last-child::after {
border: none;
height: 0;
width: 0;
margin: 0;
padding: 0;
}
.cke_button_on {
background: #ddd;
}
.cke_combo {
margin-top: 1px;
}
.cke_combo__fontsize {
margin-left: 3px;
}
.cke_combo__fontsize .cke_combo_text {
width: 40px;
}
.cke_combo_text {
line-height: 24px;
padding-left: 8px;
}
.cke_combo_button {
background: #FBFBFB;
}
.cke_source {
font-family: var(--fontMono);
padding: 10px;
padding-right: 0;
box-shadow: none;
}
.cke_plain {
background-color: #fff;
outline: none;
overflow: auto;
z-index: 1;
margin: 0;
border: 0;
padding: 10px;
padding-right: 0;
white-space: pre-wrap;
font-family: var(--fontMono), monospace;
font-size: 13px;
line-height: 16px;
color: #333;
resize: none;
border-radius: 0;
box-shadow: none;
box-sizing: border-box;
}
.cke_plain:focus, .cke_source:focus {
border: 0;
}
.b-compose .cke_chrome {
border-right: 0;
border-left: 0;
border-bottom: 0;
box-shadow: none;
}
.cke_inner,
.cke_wysiwyg_div {
background: transparent !important;
}
.cke_wysiwyg_div ul {
padding-left: 40px;
}
.cke_wysiwyg_div ul li {
list-style-type: disc;
}
.cke_wysiwyg_div ol {
padding-left: 40px;
}
.cke_wysiwyg_div ol li {
list-style-type: decimal;
}
.cke_wysiwyg_div pre,
.cke_wysiwyg_div code {
margin: 0;
padding: 0;
background: #fff;
border: none;
border-radius: 0;
font-family: var(--fontMono);
display: block;
word-break: normal;
word-wrap: break-word;
background-color: #f9f9f9;
}
.cke_wysiwyg_div code {
display: inline;
padding: 2px 5px;
}
.cke_wysiwyg_div pre {
padding: 5px 10px;
border-radius: 5px;
background-color: #f9f9f9;
}
.cke_wysiwyg_div pre > code {
padding: 0;
}
.cke_wysiwyg_div blockquote {
border: 0;
border-left: solid 2px #444;
margin: 5px 0 5px 5px;
padding-left: 5px;
}
.cke_wysiwyg_div blockquote p {
margin: 0 0 10px;
font-size: 14px;
line-height: 20px;
}
.cke_wysiwyg_div img {
vertical-align: bottom;
}
.cke_dialog {
z-index: 99999;
}
.cke_dialog_ui_labeled_content {
margin-top: 5px;
margin-bottom: 5px;
}
.cke_dialog_ui_input_select,
.cke_dialog_ui_input_text,
.cke_dialog_ui_input_textarea {
box-shadow: none;
border-radius: 2px;
}
.cke_dialog_ui_input_select:focus,
.cke_dialog_ui_input_text:focus,
.cke_dialog_ui_input_textarea:focus {
outline: 0;
border: 1px solid #999;
}
.cke_dialog_ui_input_select,
.cke_dialog_ui_input_text {
height: 25px;
line-height: 25px;
}
.RL-PopupsCompose[data-wysiwyg*=Forced] .cke_button__plain {
display: none;
}

View file

@ -3,9 +3,9 @@
class ContactsSuggestionsExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
NAME = 'Custom Contacts Suggestions Example',
CATEGORY = 'General',
DESCRIPTION = '';
DESCRIPTION = 'Custom contacts suggestions example';
public function Init() : void
{

View file

@ -3,9 +3,9 @@
class CustomAdminSettingsTabPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
NAME = 'Custom Admin Settings',
CATEGORY = 'General',
DESCRIPTION = '';
DESCRIPTION = 'Custom admin settings example';
/**
* @return void

View file

@ -1,4 +1,4 @@
[SETTINGS_CUSTOM_ADMIN_CUSTOM_TAB_PLUGIN]
TAB_NAME = "Custom Plugin Tab"
LEGEND_CUSTOM = "Custom Plugin Tab (Example)"
LABEL_PHP_VERSION = "PHP version"
TAB_NAME = "Custom Extension Tab"
LEGEND_CUSTOM = "Custom Extension Tab (Example)"
LABEL_PHP_VERSION = "PHP version"

View file

@ -3,9 +3,11 @@
class CustomAuthExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
NAME = 'Custom Auth Example Extension',
VERSION = '2.1',
REQUIRED = '2.5.0',
CATEGORY = 'Login',
DESCRIPTION = '';
DESCRIPTION = 'Custom auth mechanism example';
public function Init() : void
{

View file

@ -4,9 +4,11 @@ class CustomLoginMappingPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Custom Login Mapping',
VERSION = '2.0',
VERSION = '2.1',
RELEASE = '2021-04-21',
REQUIRED = '2.5.0',
CATEGORY = 'Login',
DESCRIPTION = 'Plugin which allows you to set up custom username by email address.';
DESCRIPTION = 'Enables custom usernames by email address.';
public function Init() : void
{

View file

@ -3,9 +3,9 @@
class CustomSettingsTabPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
NAME = 'Custom Settings Tab',
CATEGORY = 'General',
DESCRIPTION = '';
DESCRIPTION = 'Example extension for custom settings tabs';
/**
* @return void

View file

@ -1,6 +1,6 @@
[SETTINGS_CUSTOM_PLUGIN]
TAB_NAME = "Custom Plugin"
LEGEND_CUSTOM = "Custom Plugin (Example)"
TAB_NAME = "Custom Extension"
LEGEND_CUSTOM = "Custom Extension (Example)"
LABEL_SKYPE = "Skype"
LABEL_FACEBOOK = "Facebook"
BUTTON_SAVE = "Save"
BUTTON_SAVE = "Save"

View file

@ -3,7 +3,7 @@
class CustomSystemFoldersPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
NAME = 'Custom System Folders',
CATEGORY = 'General',
DESCRIPTION = 'Set custom sytem folders';

View file

@ -3,9 +3,10 @@
class DemoAccountPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
NAME = 'Demo Account Extension',
CATEGORY = 'Login',
DESCRIPTION = '';
REQUIRED = '2.10.1',
DESCRIPTION = 'Extension to enable a demo account';
/**
* @return void
@ -28,7 +29,9 @@ class DemoAccountPlugin extends \RainLoop\Plugins\AbstractPlugin
\RainLoop\Plugins\Property::NewInstance('email')->SetLabel('Demo Email')
->SetDefaultValue('demo@domain.com'),
\RainLoop\Plugins\Property::NewInstance('password')->SetLabel('Demo Password')
->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD)
->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD),
\RainLoop\Plugins\Property::NewInstance('recipient_delimiter')->SetLabel('recipient_delimiter')
->SetDefaultValue(''),
);
}
@ -37,8 +40,7 @@ class DemoAccountPlugin extends \RainLoop\Plugins\AbstractPlugin
*/
public function FilterAppData($bAdmin, &$aResult)
{
if (!$bAdmin && \is_array($aResult) && isset($aResult['Auth']) && !$aResult['Auth'])
{
if (!$bAdmin && \is_array($aResult) && isset($aResult['Auth']) && !$aResult['Auth']) {
$aResult['DevEmail'] = $this->Config()->Get('plugin', 'email', $aResult['DevEmail']);
$aResult['DevPassword'] = APP_DUMMY;
}
@ -49,12 +51,11 @@ class DemoAccountPlugin extends \RainLoop\Plugins\AbstractPlugin
*/
public function FilterActionParams($sMethodName, &$aActionParams)
{
if ('DoLogin' === $sMethodName && isset($aActionParams['Email']) && isset($aActionParams['Password']))
{
if ($this->Config()->Get('plugin', 'email') === $aActionParams['Email'])
{
$aActionParams['Password'] = $this->Config()->Get('plugin', 'password');
}
if ('DoLogin' === $sMethodName
&& isset($aActionParams['Email'])
&& isset($aActionParams['Password'])
&& $this->Config()->Get('plugin', 'email') === $aActionParams['Email']) {
$aActionParams['Password'] = $this->Config()->Get('plugin', 'password');
}
}
@ -63,25 +64,43 @@ class DemoAccountPlugin extends \RainLoop\Plugins\AbstractPlugin
*
* @return bool
*/
public function isDemoAccount($oAccount)
private function isDemoAccount()
{
$oAccount = $this->Manager()->Actions()->GetAccount();
return ($oAccount && $oAccount->Email() === $this->Config()->Get('plugin', 'email'));
}
public function JsonActionPreCall($sAction)
{
if ('AccountSetup' === $sAction &&
$this->isDemoAccount($this->Manager()->Actions()->GetAccount()))
{
if ('AccountSetup' === $sAction && $this->isDemoAccount()) {
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoAccountError);
}
}
public function FilterSendMessage(&$oMessage)
public function FilterSendMessage($oMessage)
{
if ($oMessage && $this->isDemoAccount($this->Manager()->Actions()->GetAccount()))
{
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoSendMessageError);
if ($oMessage && $this->isDemoAccount()) {
$recipient_delimiter = $this->Config()->Get('plugin', 'recipient_delimiter');
$regex = '/^' . \preg_quote($this->Config()->Get('plugin', 'email')) . '$/D';
if ($recipient_delimiter) {
$regex = \str_replace('@', '('.\preg_quote($recipient_delimiter).'.+)?@', $regex);
}
foreach ($oMessage->GetTo() as $oEmail) {
if (!\preg_match($regex, $oEmail->GetEmail())) {
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoSendMessageError);
}
}
foreach ($oMessage->GetCc() ?: [] as $oEmail) {
if (!\preg_match($regex, $oEmail->GetEmail())) {
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoSendMessageError);
}
}
foreach ($oMessage->GetBcc() ?: [] as $oEmail) {
if (!\preg_match($regex, $oEmail->GetEmail())) {
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoSendMessageError);
}
}
// throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::DemoSendMessageError);
}
}
@ -91,22 +110,10 @@ class DemoAccountPlugin extends \RainLoop\Plugins\AbstractPlugin
*/
public function MainFabrica($sName, &$oDriver)
{
switch ($sName)
{
case 'storage':
case 'storage-local':
if (\class_exists('\\RainLoop\\Providers\\Storage\\TemproryApcStorage') &&
\function_exists('apc_store'))
{
$oAccount = $this->Manager()->Actions()->GetAccount();
if ($this->isDemoAccount($oAccount))
{
$oDriver = new \RainLoop\Providers\Storage\TemproryApcStorage(APP_PRIVATE_DATA.'storage',
$sName === 'storage-local');
}
}
break;
if ('storage' === $sName || 'storage-local' === $sName) {
require_once __DIR__ . '/storage.php';
$oDriver = new \DemoStorage(APP_PRIVATE_DATA.'storage', $sName === 'storage-local');
$oDriver->setDemoEmail($this->Config()->Get('plugin', 'email'));
}
}
}

View file

@ -0,0 +1,58 @@
<?php
use RainLoop\Providers\Storage\Enumerations\StorageType;
class DemoStorage extends \RainLoop\Providers\Storage\FileStorage
{
private static $gc_done;
/**
* @param \RainLoop\Model\Account|string|null $mAccount
*/
public function GenerateFilePath($mAccount, int $iStorageType, bool $bMkDir = false, bool $bForDeleteAction = false) : string
{
$sEmail = '';
if ($mAccount instanceof \RainLoop\Model\MainAccount) {
$sEmail = $mAccount->Email();
} else if (\is_string($mAccount)) {
$sEmail = $mAccount;
}
if ($sEmail != $this->sDemoEmail) {
return parent::GenerateFilePath($mAccount, $iStorageType, $bMkDir, $bForDeleteAction);
}
$sDataPath = "{$this->sDataPath}/demo";
// Garbage collection
if (!static::$gc_done) {
static::$gc_done = true;
if (!\random_int(0, \max(50, \ini_get('session.gc_divisor')))) {
\MailSo\Base\Utils::RecTimeDirRemove($sDataPath, 3600 * 3); // 3 hours
}
}
$sDataPath .= '/' . \RainLoop\Utils::fixName(\RainLoop\Utils::GetConnectionToken());
if (StorageType::SIGN_ME === $iStorageType) {
$sDataPath .= '/.sign_me';
} else if (StorageType::SESSION === $iStorageType) {
$sDataPath .= '/.sessions';
} else if (StorageType::PGP === $iStorageType) {
$sDataPath .= '/.pgp';
$sDataPath = "{$this->sDataPath}/demo.pgp/.pgp";
$bMkDir = true;
}
if ($bMkDir && !\is_dir($sDataPath) && !\mkdir($sDataPath, 0700, true))
{
throw new \RainLoop\Exceptions\Exception('Can\'t make storage directory "'.$sDataPath.'"');
}
return $sDataPath . '/';
}
private $sDemoEmail;
public function setDemoEmail(string $sEmail)
{
$this->sDemoEmail = $sEmail;
}
}

View file

@ -5,47 +5,47 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
/**
* @var string
*/
private $sHostName = '127.0.0.1';
private $sLdapUri = 'ldap://localhost:389';
/**
* @var int
* @var bool
*/
private $iHostPort = 389;
private $bUseStartTLS = True;
/**
* @var string
*/
private $sAccessDn = null;
private $sBindDn = null;
/**
* @var string
*/
private $sAccessPassword = null;
private $sBindPassword = null;
/**
* @var string
*/
private $sUsersDn = '';
private $sBaseDn = 'ou=People,dc=example,dc=com';
/**
* @var string
*/
private $sObjectClass = 'inetOrgPerson';
private $sObjectClasses = 'inetOrgPerson';
/**
* @var string
*/
private $sUidField = 'uid';
private $sUidAttributes = 'uid';
/**
* @var string
*/
private $sNameField = 'givenname';
private $sNameAttributes = 'displayName,cn,givenName,sn';
/**
* @var string
*/
private $sEmailField = 'mail';
private $sEmailAttributes = 'mailAddress,mail,mailAlternateAddress,mailAlias';
/**
* @var \MailSo\Log\Logger
@ -55,45 +55,36 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
/**
* @var string
*/
private $sAllowedEmails = '';
/**
* @param string $sHostName
* @param int $iHostPort
* @param string $sAccessDn
* @param string $sAccessPassword
* @param string $sUsersDn
* @param string $sObjectClass
* @param string $sNameField
* @param string $sEmailField
*
* @return \LdapContactsSuggestions
*/
public function SetConfig($sHostName, $iHostPort, $sAccessDn, $sAccessPassword, $sUsersDn, $sObjectClass, $sUidField, $sNameField, $sEmailField)
{
$this->sHostName = $sHostName;
$this->iHostPort = $iHostPort;
if (0 < \strlen($sAccessDn))
{
$this->sAccessDn = $sAccessDn;
$this->sAccessPassword = $sAccessPassword;
}
$this->sUsersDn = $sUsersDn;
$this->sObjectClass = $sObjectClass;
$this->sUidField = $sUidField;
$this->sNameField = $sNameField;
$this->sEmailField = $sEmailField;
return $this;
}
private $sAllowedEmails = '*';
/**
* @param string $sLdapUri
* @param bool $bUseStartTLS
* @param string $sBindDn
* @param string $sBindPassword
* @param string $sBaseDn
* @param string $sObjectClasses
* @param string $sNameAttributes
* @param string $sEmailAttributes
* @param string $sUidAttributes
* @param string $sAllowedEmails
*
* @return \LdapContactsSuggestions
*/
public function SetAllowedEmails($sAllowedEmails)
public function SetConfig($sLdapUri, $bUseStartTLS, $sBindDn, $sBindPassword, $sBaseDn, $sObjectClasses, $sUidAttributes, $sNameAttributes, $sEmailAttributes, $sAllowedEmails)
{
$this->sLdapUri = $sLdapUri;
$this->bUseStartTLS = $bUseStartTLS;
if (0 < \strlen($sBindDn))
{
$this->sBindDn = $sBindDn;
$this->sBindPassword = $sBindPassword;
}
$this->sBaseDn = $sBaseDn;
$this->sObjectClasses = $sObjectClasses;
$this->sUidAttributes = $sUidAttributes;
$this->sNameAttributes = $sNameAttributes;
$this->sEmailAttributes = $sEmailAttributes;
$this->sAllowedEmails = $sAllowedEmails;
return $this;
@ -132,18 +123,20 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
/**
* @param array $aLdapItem
* @param array $aEmailFields
* @param array $aNameFields
* @param array $aEmailAttributes
* @param array $aNameAttributes
* @param array $aUidAttributes
*
* @return array
*/
private function findNameAndEmail($aLdapItem, $aEmailFields, $aNameFields, $aUidFields)
private function findNameAndEmail($aLdapItem, $aEmailAttributes, $aNameAttributes, $aUidAttributes)
{
$sEmail = $sName = $sUid = '';
if ($aLdapItem)
{
foreach ($aEmailFields as $sField)
foreach ($aEmailAttributes as $sField)
{
$sField = \strtolower($sField);
if (!empty($aLdapItem[$sField][0]))
{
$sEmail = \trim($aLdapItem[$sField][0]);
@ -154,8 +147,9 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
}
}
foreach ($aNameFields as $sField)
foreach ($aNameAttributes as $sField)
{
$sField = \strtolower($sField);
if (!empty($aLdapItem[$sField][0]))
{
$sName = \trim($aLdapItem[$sField][0]);
@ -166,8 +160,9 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
}
}
foreach ($aUidFields as $sField)
foreach ($aUidAttributes as $sField)
{
$sField = \strtolower($sField);
if (!empty($aLdapItem[$sField][0]))
{
$sUid = \trim($aLdapItem[$sField][0]);
@ -193,16 +188,22 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
$sSearchEscaped = $this->escape($sQuery);
$aResult = array();
$oCon = @\ldap_connect($this->sHostName, $this->iHostPort);
$oCon = @\ldap_connect($this->sLdapUri);
if ($oCon)
{
$this->oLogger->Write('ldap_connect: connected', \MailSo\Log\Enumerations\Type::INFO, 'LDAP');
@\ldap_set_option($oCon, LDAP_OPT_PROTOCOL_VERSION, 3);
if (!@\ldap_bind($oCon, $this->sAccessDn, $this->sAccessPassword))
if ($this->bUseStartTLS && !@\ldap_start_tls($oCon))
{
if (is_null($this->sAccessDn))
$this->logLdapError($oCon, 'ldap_start_tls');
return $aResult;
}
if (!@\ldap_bind($oCon, $this->sBindDn, $this->sBindPassword))
{
if (is_null($this->sBindDn))
{
$this->logLdapError($oCon, 'ldap_bind (anonymous)');
}
@ -215,7 +216,7 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
}
$sDomain = \MailSo\Base\Utils::GetDomainFromEmail($oAccount->Email());
$sSearchDn = \strtr($this->sUsersDn, array(
$sBaseDn = \strtr($this->sBaseDn, array(
'{domain}' => $sDomain,
'{domain:dc}' => 'dc='.\strtr($sDomain, array('.' => ',dc=')),
'{email}' => $oAccount->Email(),
@ -227,16 +228,30 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
'{imap:port}' => $oAccount->DomainIncPort()
));
$aEmails = empty($this->sEmailField) ? array() : \explode(',', $this->sEmailField);
$aNames = empty($this->sNameField) ? array() : \explode(',', $this->sNameField);
$aUIDs = empty($this->sUidField) ? array() : \explode(',', $this->sUidField);
$aObjectClasses = empty($this->sObjectClasses) ? array() : \explode(',', $this->sObjectClasses);
$aEmails = empty($this->sEmailAttributes) ? array() : \explode(',', $this->sEmailAttributes);
$aNames = empty($this->sNameAttributes) ? array() : \explode(',', $this->sNameAttributes);
$aUIDs = empty($this->sUidAttributes) ? array() : \explode(',', $this->sUidAttributes);
$aObjectClasses = \array_map('trim', $aObjectClasses);
$aEmails = \array_map('trim', $aEmails);
$aNames = \array_map('trim', $aNames);
$aUIDs = \array_map('trim', $aUIDs);
$aFields = \array_merge($aEmails, $aNames, $aUIDs);
$iObjCount = 0;
$sObjFilter = '';
foreach ($aObjectClasses as $sItem)
{
if (!empty($sItem))
{
$iObjCount++;
$sObjFilter .= '(objectClass='.$sItem.')';
}
}
$aItems = array();
$sSubFilter = '';
foreach ($aFields as $sItem)
@ -248,12 +263,13 @@ class LdapContactsSuggestions implements \RainLoop\Providers\Suggestions\ISugges
}
}
$sFilter = '(&(objectclass='.$this->sObjectClass.')';
$sFilter = '(&';
$sFilter .= (1 < $iObjCount ? '(|' : '').$sObjFilter.(1 < $iObjCount ? ')' : '');
$sFilter .= (1 < count($aItems) ? '(|' : '').$sSubFilter.(1 < count($aItems) ? ')' : '');
$sFilter .= ')';
$this->oLogger->Write('ldap_search: start: '.$sSearchDn.' / '.$sFilter, \MailSo\Log\Enumerations\Type::INFO, 'LDAP');
$oS = @\ldap_search($oCon, $sSearchDn, $sFilter, $aItems, 0, 30, 30);
$this->oLogger->Write('ldap_search: start: '.$sBaseDn.' / '.$sFilter, \MailSo\Log\Enumerations\Type::INFO, 'LDAP');
$oS = @\ldap_search($oCon, $sBaseDn, $sFilter, $aItems, 0, 30, 30);
if ($oS)
{
$aEntries = @\ldap_get_entries($oCon, $oS);

View file

@ -4,9 +4,9 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Contacts suggestions (LDAP)',
VERSION = '2.0',
VERSION = '2.1',
CATEGORY = 'Security',
DESCRIPTION = 'Plugin that adds functionality to get contacts from LDAP on compose page.';
DESCRIPTION = 'Get contacts suggestions from LDAP.';
public function Init() : void
{
@ -20,7 +20,7 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
{
if (!\function_exists('ldap_connect'))
{
return 'The LDAP PHP extension must be installed to use this plugin';
return 'The LDAP PHP extension must be installed to use this extension';
}
return '';
@ -41,22 +41,23 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
$mResult = array();
}
$sHostName = \trim($this->Config()->Get('plugin', 'hostname', ''));
$iHostPort = (int) $this->Config()->Get('plugin', 'port', 389);
$sAccessDn = \trim($this->Config()->Get('plugin', 'access_dn', ''));
$sAccessPassword = \trim($this->Config()->Get('plugin', 'access_password', ''));
$sUsersDn = \trim($this->Config()->Get('plugin', 'users_dn_format', ''));
$sObjectClass = \trim($this->Config()->Get('plugin', 'object_class', ''));
$sSearchField = \trim($this->Config()->Get('plugin', 'search_field', ''));
$sNameField = \trim($this->Config()->Get('plugin', 'name_field', ''));
$sEmailField = \trim($this->Config()->Get('plugin', 'mail_field', ''));
$sLdapUri = \trim($this->Config()->Get('plugin', 'ldap_uri', ''));
$bUseStartTLS = (bool) $this->Config()->Get('plugin', 'use_start_tls', True);
$sBindDn = \trim($this->Config()->Get('plugin', 'bind_dn', ''));
$sBindPassword = \trim($this->Config()->Get('plugin', 'bind_password', ''));
$sBaseDn = \trim($this->Config()->Get('plugin', 'base_dn', ''));
$sObjectClasses = \trim($this->Config()->Get('plugin', 'object_classes', ''));
$sUidAttributes = \trim($this->Config()->Get('plugin', 'uid_attributes', ''));
$sNameAttributes = \trim($this->Config()->Get('plugin', 'name_attributes', ''));
$sEmailAttributes = \trim($this->Config()->Get('plugin', 'mail_attributes', ''));
$sAllowedEmails = \trim($this->Config()->Get('plugin', 'allowed_emails', ''));
if (0 < \strlen($sUsersDn) && 0 < \strlen($sObjectClass) && 0 < \strlen($sEmailField))
if (0 < \strlen($sLdapUri) && 0 < \strlen($sBaseDn) && 0 < \strlen($sObjectClasses) && 0 < \strlen($sEmailAttributes))
{
include_once __DIR__.'/LdapContactsSuggestions.php';
$oProvider = new LdapContactsSuggestions();
$oProvider->SetConfig($sHostName, $iHostPort, $sAccessDn, $sAccessPassword, $sUsersDn, $sObjectClass, $sSearchField, $sNameField, $sEmailField);
$oProvider->SetConfig($sLdapUri, $bUseStartTLS, $sBindDn, $sBindPassword, $sBaseDn, $sObjectClasses, $sUidAttributes, $sNameAttributes, $sEmailAttributes, $sAllowedEmails);
$mResult[] = $oProvider;
}
@ -71,30 +72,35 @@ class LdapContactsSuggestionsPlugin extends \RainLoop\Plugins\AbstractPlugin
protected function configMapping() : array
{
return array(
\RainLoop\Plugins\Property::NewInstance('hostname')->SetLabel('LDAP hostname')
->SetDefaultValue('127.0.0.1'),
\RainLoop\Plugins\Property::NewInstance('port')->SetLabel('LDAP port')
->SetType(\RainLoop\Enumerations\PluginPropertyType::INT)
->SetDefaultValue(389),
\RainLoop\Plugins\Property::NewInstance('access_dn')->SetLabel('Access dn (login)')
->SetDescription('LDAP bind DN to authentifcate with. If left blank, anonymous bind will be tried and Access password will be ignored')
\RainLoop\Plugins\Property::NewInstance('ldap_uri')->SetLabel('LDAP URI')
->SetDescription('LDAP server URI(s), space separated')
->SetDefaultValue('ldap://localhost:389'),
\RainLoop\Plugins\Property::NewInstance('use_start_tls')->SetLabel('Use StartTLS')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDefaultValue(True),
\RainLoop\Plugins\Property::NewInstance('bind_dn')->SetLabel('Bind DN')
->SetDescription('DN to bind (login) with. If left blank, anonymous bind will be tried and the password will be ignored')
->SetDefaultValue(''),
\RainLoop\Plugins\Property::NewInstance('access_password')->SetLabel('Access password')
\RainLoop\Plugins\Property::NewInstance('bind_password')->SetLabel('Bind password')
->SetType(\RainLoop\Enumerations\PluginPropertyType::PASSWORD)
->SetDefaultValue(''),
\RainLoop\Plugins\Property::NewInstance('users_dn_format')->SetLabel('Users DN format')
->SetDescription('LDAP users dn format. Supported tokens: {email}, {login}, {domain}, {domain:dc}, {imap:login}, {imap:host}, {imap:port}')
->SetDefaultValue('ou=People,dc=domain,dc=com'),
\RainLoop\Plugins\Property::NewInstance('object_class')->SetLabel('objectClass value')
\RainLoop\Plugins\Property::NewInstance('base_dn')->SetLabel('Search base DN')
->SetDescription('DN to use as the search base. Supported tokens: {domain}, {domain:dc}, {email}, {email:user}, {email:domain}, {login}, {imap:login}, {imap:host}, {imap:port}')
->SetDefaultValue('ou=People,dc=example,dc=com'),
\RainLoop\Plugins\Property::NewInstance('object_classes')->SetLabel('objectClasses to use')
->SetDescription('LDAP objectClasses to search for, comma separated list')
->SetDefaultValue('inetOrgPerson'),
\RainLoop\Plugins\Property::NewInstance('search_field')->SetLabel('Search field')
\RainLoop\Plugins\Property::NewInstance('uid_attributes')->SetLabel('uid attributes')
->SetDescription('LDAP attributes for userids, comma separated list in order of preference')
->SetDefaultValue('uid'),
\RainLoop\Plugins\Property::NewInstance('name_field')->SetLabel('Name field')
->SetDefaultValue('givenname'),
\RainLoop\Plugins\Property::NewInstance('mail_field')->SetLabel('Mail field')
->SetDefaultValue('mail'),
\RainLoop\Plugins\Property::NewInstance('name_attributes')->SetLabel('Name attributes')
->SetDescription('LDAP attributes for user names, comma separated list in order of preference')
->SetDefaultValue('displayName,cn,givenName,sn'),
\RainLoop\Plugins\Property::NewInstance('mail_attributes')->SetLabel('Mail attributes')
->SetDescription('LDAP attributes for user email addresses, comma separated list in order of preference')
->SetDefaultValue('mailAddress,mail,mailAlternateAddress,mailAlias'),
\RainLoop\Plugins\Property::NewInstance('allowed_emails')->SetLabel('Allowed emails')
->SetDescription('Allowed emails, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net')
->SetDescription('Email addresses of users which should be allowed to do LDAP lookups, space as delimiter, wildcard supported. Example: user1@domain1.net user2@domain1.net *@domain2.net')
->SetDefaultValue('*')
);
}

View file

@ -40,7 +40,7 @@ class LdapIdentities implements IIdentities
// Check if LDAP is available
if (!extension_loaded('ldap') || !function_exists('ldap_connect')) {
$this->ldapAvailable = false;
$logger->Write("The LDAP plugin is not available!", Type::WARNING, self::LOG_KEY);
$logger->Write("The LDAP extension is not available!", Type::WARNING, self::LOG_KEY);
return;
}

View file

@ -14,7 +14,7 @@ class LdapIdentitiesPlugin extends AbstractPlugin
RELEASE = '2020-11-11',
REQUIRED = '2.1.0',
CATEGORY = 'Accounts',
DESCRIPTION = 'Plugin that adds functionality to import account identities from LDAP.';
DESCRIPTION = 'Adds functionality to import account identities from LDAP.';
public function __construct()
{

View file

@ -0,0 +1,19 @@
(rl => {
if (rl) {
addEventListener('rl-view-model', e => {
// instanceof LoginUserView
if (e.detail && 'Login' === e.detail.viewModelTemplateID) {
const LoginUserView = e.detail,
submitCommand = LoginUserView.submitCommand;
LoginUserView.submitCommand = (self, event) => {
if (LoginUserView.email().includes('@gmail.com')) {
// TODO: redirect to google
} else {
submitCommand.call(LoginUserView, self, event);
}
};
}
});
}
})(window.rl);

View file

@ -0,0 +1,323 @@
<?php
class LoginOAuth2Plugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'OAuth2',
VERSION = '1.0',
RELEASE = '2021-11-12',
REQUIRED = '2.9.1',
CATEGORY = 'Login',
DESCRIPTION = 'IMAP, Sieve & SMTP login using RFC 7628 OAuth2';
const
LOGIN_URI = 'https://accounts.google.com/o/oauth2/auth',
TOKEN_URI = 'https://accounts.google.com/o/oauth2/token',
GMAIL_TOKENS_PREFIX = ':GAT:';
public function Init() : void
{
$this->UseLangs(true);
$this->addJs('LoginOAuth2.js');
// $this->addHook('imap.before-connect', array($this, $oImapClient, &$aCredentials));
// $this->addHook('imap.after-connect', array($this, $oImapClient, $aCredentials));
$this->addHook('imap.before-login', 'clientLogin');
// $this->addHook('imap.after-login', array($this, $oImapClient, $aCredentials));
// $this->addHook('smtp.before-connect', array($this, $oSmtpClient, &$aCredentials));
// $this->addHook('smtp.after-connect', array($this, $oSmtpClient, $aCredentials));
$this->addHook('smtp.before-login', 'clientLogin');
// $this->addHook('smtp.after-login', array($this, $oSmtpClient, $aCredentials));
// $this->addHook('sieve.before-connect', array($this, $oSieveClient, &$aCredentials));
// $this->addHook('sieve.after-connect', array($this, $oSieveClient, $aCredentials));
$this->addHook('sieve.before-login', 'clientLogin');
// $this->addHook('sieve.after-login', array($this, $oSieveClient, $aCredentials));
$this->addHook('filter.account', 'filterAccount');
}
public function configMapping() : array
{
return [
\RainLoop\Plugins\Property::NewInstance('client_id')
->SetLabel('Client ID')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING),
\RainLoop\Plugins\Property::NewInstance('client_secret')
->SetLabel('Client Secret')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING),
];
}
public function clientLogin(\RainLoop\Model\Account $oAccount, \MailSo\Net\NetClient $oClient, array &$aCredentials) : void
{
$sPassword = $aCredentials['Password'];
$iGatLen = \strlen(static::GMAIL_TOKENS_PREFIX);
if ($sPassword && static::GMAIL_TOKENS_PREFIX === \substr($sPassword, 0, $iGatLen)) {
$aTokens = \json_decode(\substr($sPassword, $iGatLen));
$sAccessToken = !empty($aTokens[0]) ? $aTokens[0] : '';
$sRefreshToken = !empty($aTokens[1]) ? $aTokens[1] : '';
}
if ($sAccessToken && $sRefreshToken) {
$aCredentials['Password'] = $this->gmailRefreshToken($sAccessToken, $sRefreshToken);
\array_unshift($aCredentials['SASLMechanisms'], 'OAUTHBEARER', 'XOAUTH2');
}
}
public function filterAccount(\RainLoop\Model\Account $oAccount) : void
{
if ($oAccount instanceof \RainLoop\Model\MainAccount) {
/**
* TODO
* Because password rotates, so does the CryptKey.
* So we need to securely save a cryptkey.
* Encrypted using the old/new refresh token is an option:
* 1. decrypt cryptkey with the old refresh token
* 2. $oAccount->SetCryptKey('cryptkey')
* 3. encrypt cryptkey with the new refresh token
*/
}
}
protected function loginProcess(&$oAccount, $sEmail, $sPassword) : int
{
$oActions = \RainLoop::Actions();
$iErrorCode = \RainLoop\Notifications::UnknownError;
try
{
$oAccount = $oActions->LoginProcess($sEmail, $sPassword, '', '', false, true);
if ($oAccount instanceof \RainLoop\Model\Account) {
$oActions->AuthToken($oAccount);
$iErrorCode = 0;
} else {
$oAccount = null;
$iErrorCode = \RainLoop\Notifications::AuthError;
}
}
catch (\RainLoop\Exceptions\ClientException $oException)
{
$iErrorCode = $oException->getCode();
}
catch (\Exception $oException)
{
unset($oException);
$iErrorCode = \RainLoop\Notifications::UnknownError;
}
return $iErrorCode;
}
/**
* GMail
*/
protected static function gmailTokensPassword($sAccessToken, $sRefreshToken) : string
{
return static::GMAIL_TOKENS_PREFIX . \json_encode(array($sAccessToken, $sRefreshToken));
}
protected function gmailStoreTokens($oCache, $sAccessToken, $sRefreshToken) : void
{
$sCacheKey = 'tokens='.\sha1($sRefreshToken);
$oCache->Set($sCacheKey, $sAccessToken);
$oCache->SetTimer($sCacheKey);
}
protected function gmailRefreshToken($sAccessToken, $sRefreshToken) : string
{
$oActions = \RainLoop::Actions();
$oAccount = $oActions->getAccountFromToken(false);
$oDomain = $oAccount->Domain();
$oLogger = $oImapClient->Logger();
if ($oAccount && $oActions->GetIsJson()) {
$oCache = $oActions->Cacher($oAccount);
$sCacheKey = 'tokens='.\sha1($sRefreshToken);
$sCachedAccessToken = $oCache->Get($sCacheKey);
$iTime = $oCache->GetTimer($sCacheKey);
if (!$sCachedAccessToken || !$iTime) {
$this->gmailStoreTokens($oCache, $sAccessToken, $sRefreshToken);
} else if (\time() - 1200 > $iTime) { // 20min
$oGMail = $this->gmailConnector();
if ($oGMail) {
$aRefreshTokenResponse = $oGMail->getAccessToken(
static::TOKEN_URI,
'refresh_token',
array('refresh_token' => $sRefreshToken)
);
if (!empty($aRefreshTokenResponse['result']['access_token'])) {
$sCachedAccessToken = $aRefreshTokenResponse['result']['access_token'];
$this->gmailStoreTokens($oCache, $sCachedAccessToken, $sRefreshToken);
/*
$oAccount->SetPassword(static::gmailTokensPassword($sCachedAccessToken, $sRefreshToken));
$oActions->AuthToken($oAccount);
// $oActions->SetUpdateAuthToken($oActions->GetSpecAuthToken());
$oActions->sUpdateAuthToken = $oActions->GetSpecAuthToken();
$sUpdateToken = $oActions->GetUpdateAuthToken();
if ($sUpdateToken) {
$aResponseItem['UpdateToken'] = $sUpdateToken;
}
*/
}
}
}
if ($sCachedAccessToken) {
return $sCachedAccessToken;
}
}
return $sAccessToken;
}
protected function gmailConnector() : ?\OAuth2\Client
{
$oGMail = null;
$oActions = \RainLoop::Actions();
$client_id = \trim($this->Config()->Get('plugin', 'client_id', ''));
$client_secret = \trim($this->Config()->Get('plugin', 'client_secret', ''));
if ($client_id && $client_secret) {
include_once APP_VERSION_ROOT_PATH.'app/libraries/PHP-OAuth2/Client.php';
include_once APP_VERSION_ROOT_PATH.'app/libraries/PHP-OAuth2/GrantType/IGrantType.php';
include_once APP_VERSION_ROOT_PATH.'app/libraries/PHP-OAuth2/GrantType/AuthorizationCode.php';
include_once APP_VERSION_ROOT_PATH.'app/libraries/PHP-OAuth2/GrantType/RefreshToken.php';
try
{
$oGMail = new \OAuth2\Client($client_id, $client_secret);
$sProxy = $oActions->Config()->Get('labs', 'curl_proxy', '');
if (\strlen($sProxy)) {
$oGMail->setCurlOption(CURLOPT_PROXY, $sProxy);
$sProxyAuth = $oActions->Config()->Get('labs', 'curl_proxy_auth', '');
if (\strlen($sProxyAuth)) {
$oGMail->setCurlOption(CURLOPT_PROXYUSERPWD, $sProxyAuth);
}
}
}
catch (\Exception $oException)
{
$oActions->Logger()->WriteException($oException, \MailSo\Log\Enumerations\Type::ERROR);
}
}
return $oGMail;
}
protected function gmailPopupService() : string
{
$sLoginUrl = '';
$oAccount = null;
$oActions = \RainLoop::Actions();
$oHttp = $oActions->Http();
$bLogin = false;
$iErrorCode = \RainLoop\Notifications::UnknownError;
try
{
$oGMail = $this->gmailConnector();
if ($oHttp->HasQuery('error')) {
$iErrorCode = ('access_denied' === $oHttp->GetQuery('error')) ?
\RainLoop\Notifications::SocialGMailLoginAccessDisable : \RainLoop\Notifications::UnknownError;
} else if ($oGMail) {
$oAccount = $oActions->GetAccount();
$bLogin = !$oAccount;
$sCheckToken = '';
$sCheckAuth = '';
$sState = $oHttp->GetQuery('state');
if (!empty($sState)) {
$aParts = explode('|', $sState, 3);
$sCheckToken = !empty($aParts[1]) ? $aParts[1] : '';
$sCheckAuth = !empty($aParts[2]) ? $aParts[2] : '';
}
$sRedirectUrl = $oHttp->GetFullUrl().'?SocialGMail';
if (!$oHttp->HasQuery('code')) {
$aParams = array(
'scope' => \trim(\implode(' ', array(
'https://www.googleapis.com/auth/userinfo.email',
'https://www.googleapis.com/auth/userinfo.profile',
'https://mail.google.com/'
))),
'state' => '1|'.\RainLoop\Utils::GetConnectionToken().'|'.$oActions->GetSpecAuthToken(),
'response_type' => 'code'
);
$aParams['access_type'] = 'offline';
// $aParams['prompt'] = 'consent';
$sLoginUrl = $oGMail->getAuthenticationUrl(static::LOGIN_URI, $sRedirectUrl, $aParams);
} else if (!empty($sState) && $sCheckToken === \RainLoop\Utils::GetConnectionToken()) {
if (!empty($sCheckAuth)) {
$oActions->SetSpecAuthToken($sCheckAuth);
$oAccount = $oActions->GetAccount();
$bLogin = !$oAccount;
}
$aAuthorizationCodeResponse = $oGMail->getAccessToken(
static::TOKEN_URI,
'authorization_code',
array(
'code' => $oHttp->GetQuery('code'),
'redirect_uri' => $sRedirectUrl
)
);
$sAccessToken = !empty($aAuthorizationCodeResponse['result']['access_token']) ? $aAuthorizationCodeResponse['result']['access_token'] : '';
$sRefreshToken = !empty($aAuthorizationCodeResponse['result']['refresh_token']) ? $aAuthorizationCodeResponse['result']['refresh_token'] : '';
if (!empty($sAccessToken)) {
$oGMail->setAccessToken($sAccessToken);
$aUserInfoResponse = $oGMail->fetch('https://www.googleapis.com/oauth2/v2/userinfo');
if (!empty($aUserInfoResponse['result']['id'])) {
if ($bLogin) {
$aUserData = null;
if (!empty($aUserInfoResponse['result']['email'])) {
$aUserData = array(
'Email' => $aUserInfoResponse['result']['email'],
'Password' => static::gmailTokensPassword($sAccessToken, $sRefreshToken)
);
}
if ($aUserData && \is_array($aUserData) && !empty($aUserData['Email']) && isset($aUserData['Password'])) {
$iErrorCode = $this->loginProcess($oAccount, $aUserData['Email'], $aUserData['Password']);
} else {
$iErrorCode = \RainLoop\Notifications::SocialGMailLoginAccessDisable;
}
}
}
}
}
}
}
catch (\Exception $oException)
{
$oActions->Logger()->WriteException($oException, \MailSo\Log\Enumerations\Type::ERROR);
}
$oActions = \RainLoop::Actions();
$oActions->Http()->ServerNoCache();
\header('Content-Type: text/html; charset=utf-8');
$sHtml = \file_get_contents(APP_VERSION_ROOT_PATH.'app/templates/Social.html');
if ($sLoginUrl) {
$sHtml = \strtr($sHtml, array(
'{{RefreshMeta}}' => '<meta http-equiv="refresh" content="0; URL='.$sLoginUrl.'" />',
'{{Script}}' => ''
));
} else {
$sCallBackType = $bLogin ? '_login' : '';
$sConnectionFunc = 'rl_'.\md5(\RainLoop\Utils::GetConnectionToken()).'_gmail'.$sCallBackType.'_service';
$sHtml = \strtr($sHtml, array(
'{{RefreshMeta}}' => '',
'{{Script}}' => '<script data-cfasync="false">opener && opener.'.$sConnectionFunc.' && opener.'.
$sConnectionFunc.'('.$iErrorCode.'); self && self.close && self.close();</script>'
));
}
$bAppCssDebug = $oActions->Config()->Get('labs', 'use_app_debug_css', false);
return \strtr($sHtml, array(
'{{Stylesheet}}' => $oActions->StaticPath('css/social'.($bAppCssDebug ? '' : '.min').'.css'),
'{{Icon}}' => 'gmail'
));
}
}

View file

@ -0,0 +1,33 @@
(rl => {
if (rl) {
const
forgotUrl = rl.settings.get('forgotPasswordLinkUrl'),
registerUrl = rl.settings.get('registrationLinkUrl');
if (forgotUrl || registerUrl) {
addEventListener('rl-view-model', e => {
if (e.detail && 'Login' === e.detail.viewModelTemplateID) {
const container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'),
forgot = 'LOGIN/LABEL_FORGOT_PASSWORD',
register = 'LOGIN/LABEL_REGISTRATION';
if (container) {
let html = '';
if (forgotUrl) {
html = html + '<p class="forgot-link">'
+ '<a href="'+forgotUrl+'" target="_blank" class="g-ui-link" data-i18n="'+forgot+'">'+rl.i18n(forgot)+'</a>'
+ '</p>';
}
if (registerUrl) {
html = html + '<p class="registration-link">'
+ '<a href="'+registerUrl+'" target="_blank" class="g-ui-link" data-i18n="'+register+'">'+rl.i18n(register)+'</a>'
+ '</p>';
}
container.append(Element.fromHTML('<div class="controls clearfix">' + html + '</div>'));
}
}
});
}
}
})(window.rl);

View file

@ -0,0 +1,42 @@
<?php
class LoginRegisterPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Register and Forgot',
VERSION = '2.1',
RELEASE = '2021-10-04',
REQUIRED = '2.5.2',
CATEGORY = 'Login',
DESCRIPTION = 'Links on login screen for registration and forgotten password';
public function Init() : void
{
$this->UseLangs(true);
$this->addJs('LoginRegister.js');
$this->addHook('filter.app-data', 'FilterAppData');
}
public function configMapping() : array
{
return [
\RainLoop\Plugins\Property::NewInstance("forgot_password_link_url")
// ->SetLabel('TAB_LOGIN/LABEL_FORGOT_PASSWORD_LINK_URL')
->SetLabel('Forgot password url')
->SetType(\RainLoop\Enumerations\PluginPropertyType::URL),
\RainLoop\Plugins\Property::NewInstance("registration_link_url")
// ->SetLabel('TAB_LOGIN/LABEL_REGISTRATION_LINK_URL')
->SetLabel('Register url')
->SetType(\RainLoop\Enumerations\PluginPropertyType::URL),
];
}
public function FilterAppData($bAdmin, &$aResult)
{
if (!$bAdmin && \is_array($aResult) && empty($aResult['Auth'])) {
$aResult['forgotPasswordLinkUrl'] = \trim($this->Config()->Get('plugin', 'forgot_password_link_url', ''));
$aResult['registrationLinkUrl'] = \trim($this->Config()->Get('plugin', 'registration_link_url', ''));
}
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "نسيت كلمة السر",
"LABEL_REGISTRATION": "التسجيل"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Забравена парола",
"LABEL_REGISTRATION": "Регистрация"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Zapomenout heslo",
"LABEL_REGISTRATION": "Registrace"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Glemt adgangskode",
"LABEL_REGISTRATION": "Registrering"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Passwort vergessen",
"LABEL_REGISTRATION": "Registrieren"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Ξέχασα τον κωδικό μου",
"LABEL_REGISTRATION": "Εγγραφή"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Forgot password",
"LABEL_REGISTRATION": "Register"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Olvidé mi contraseña",
"LABEL_REGISTRATION": "Registrarse"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Unustasin salasõna",
"LABEL_REGISTRATION": "Registreerimine"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "فراموشی گذرواژه",
"LABEL_REGISTRATION": "ثبت‌نام"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Unohdin salasanani",
"LABEL_REGISTRATION": "Rekisteröidy"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Mot de passe oublié",
"LABEL_REGISTRATION": "S'enregistrer"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Elfelejtett jelszó",
"LABEL_REGISTRATION": "Regisztráció"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Lupa sandi",
"LABEL_REGISTRATION": "Registrasi"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Gleymdi lykilorði",
"LABEL_REGISTRATION": "Nýskráning"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Ho dimenticato la password",
"LABEL_REGISTRATION": "Registrazione"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "パスワードを忘れた",
"LABEL_REGISTRATION": "初回登録"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "비밀번호 분실",
"LABEL_REGISTRATION": "가입"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Pamiršau slaptažodį",
"LABEL_REGISTRATION": "Registracija"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Forgot password",
"LABEL_REGISTRATION": "Registration"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Glemt passord",
"LABEL_REGISTRATION": "Registrering"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Wachtwoord vergeten",
"LABEL_REGISTRATION": "Registreren"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Zapomniane hasło",
"LABEL_REGISTRATION": "Rejestracja"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Esqueceu a senha?",
"LABEL_REGISTRATION": "Registre-se"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Esqueci-me da palavra-passe",
"LABEL_REGISTRATION": "Registo"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Am uitat parola",
"LABEL_REGISTRATION": "Înregistrare"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Забытый пароль",
"LABEL_REGISTRATION": "Регистрация"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Zabudol som heslo",
"LABEL_REGISTRATION": "Registrácia"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Pozabljeno geslo",
"LABEL_REGISTRATION": "Registracija"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Glömt lösenord",
"LABEL_REGISTRATION": "Registrera"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Şifremi Unuttum",
"LABEL_REGISTRATION": "Registration"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Забули пароль?",
"LABEL_REGISTRATION": "Реєстрація"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "忘记密码",
"LABEL_REGISTRATION": "注册"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -0,0 +1,10 @@
{
"LOGIN": {
"LABEL_FORGOT_PASSWORD": "Forgot password",
"LABEL_REGISTRATION": "Registration"
},
"TAB_LOGIN": {
"LABEL_FORGOT_PASSWORD_LINK_URL": "Forgot password url",
"LABEL_REGISTRATION_LINK_URL": "Register url"
}
}

View file

@ -3,10 +3,12 @@
class OverrideSmtpCredentialsPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Override Smtp Credentials',
VERSION = '2.0',
NAME = 'Override SMTP Credentials',
VERSION = '2.1',
RELEASE = '2021-04-21',
REQUIRED = '2.5.0',
CATEGORY = 'Filters',
DESCRIPTION = 'Plugin which allows you to override smtp credentials specified users.';
DESCRIPTION = 'Override SMTP credentials for specific users.';
public function Init() : void
{

View file

@ -3,7 +3,9 @@
class ProxyauthLoginExamplePlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = '',
NAME = 'Proxy Auth Login Example',
VERSION = '2.1',
REQUIRED = '2.5.0',
CATEGORY = 'General',
DESCRIPTION = '';

153
plugins/recaptcha/index.php Normal file
View file

@ -0,0 +1,153 @@
<?php
class RecaptchaPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'reCaptcha',
AUTHOR = 'SnappyMail',
URL = 'https://snappymail.eu/',
VERSION = '2.12.1',
RELEASE = '2022-02-14',
REQUIRED = '2.12.1',
CATEGORY = 'General',
LICENSE = 'MIT',
DESCRIPTION = 'A CAPTCHA (v2) is a program that can generate and grade tests that humans can pass but current computer programs cannot. For example, humans can read distorted text as the one shown below, but current computer programs can\'t. More info at https://developers.google.com/recaptcha';
/**
* @return void
*/
public function Init() : void
{
$this->UseLangs(true);
$this->addJs('js/recaptcha.js');
$this->addHook('json.action-pre-call', 'AjaxActionPreCall');
$this->addHook('filter.json-response', 'FilterAjaxResponse');
$this->addHook('main.content-security-policy', 'ContentSecurityPolicy');
}
protected function configMapping() : array
{
return array(
\RainLoop\Plugins\Property::NewInstance('public_key')->SetLabel('Site key')
->SetAllowedInJs(true)
->SetDefaultValue(''),
\RainLoop\Plugins\Property::NewInstance('private_key')->SetLabel('Secret key')
->SetDefaultValue(''),
\RainLoop\Plugins\Property::NewInstance('theme')->SetLabel('Theme')
->SetAllowedInJs(true)
->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION)
->SetDefaultValue(array('light', 'dark')),
\RainLoop\Plugins\Property::NewInstance('error_limit')->SetLabel('Limit')
->SetType(\RainLoop\Enumerations\PluginPropertyType::SELECTION)
->SetDefaultValue(array('0', 1, 2, 3, 4, 5))
->SetDescription('')
);
}
/**
* @return string
*/
private function getCaptchaCacherKey()
{
return 'CaptchaNew/Login/'.\RainLoop\Utils::GetConnectionToken();
}
/**
* @return int
*/
private function getLimit()
{
$iConfigLimit = $this->Config()->Get('plugin', 'error_limit', 0);
if (0 < $iConfigLimit) {
$oCacher = $this->Manager()->Actions()->Cacher();
$sLimit = $oCacher && $oCacher->IsInited() ? $oCacher->Get($this->getCaptchaCacherKey()) : '0';
if (\strlen($sLimit) && \is_numeric($sLimit)) {
$iConfigLimit -= (int) $sLimit;
}
}
return $iConfigLimit;
}
/**
* @return void
*/
public function FilterAppDataPluginSection(bool $bAdmin, bool $bAuth, array &$aConfig) : void
{
if (!$bAdmin && !$bAuth) {
$aConfig['show_captcha_on_login'] = 1 > $this->getLimit();;
}
}
/**
* @param string $sAction
*/
public function AjaxActionPreCall(string $sAction)
{
if ('Login' === $sAction && 0 >= $this->getLimit()) {
$bResult = false;
$HTTP = \SnappyMail\HTTP\Request::factory();
$oResponse = $HTTP->doRequest('POST', 'https://www.google.com/recaptcha/api/siteverify', array(
'secret' => $this->Config()->Get('plugin', 'private_key', ''),
'response' => $this->Manager()->Actions()->GetActionParam('RecaptchaResponse', '')
));
if ($oResponse) {
$aResp = \json_decode($oResponse->body, true);
if (\is_array($aResp) && isset($aResp['success']) && $aResp['success']) {
$bResult = true;
}
}
if (!$bResult) {
$this->Manager()->Actions()->Logger()->Write('RecaptchaResponse:'.$sResult);
throw new \RainLoop\Exceptions\ClientException(105);
}
}
}
/**
* @param string $sAction
* @param array $aResponseItem
*/
public function FilterAjaxResponse(string $sAction, array &$aResponseItem)
{
if ('Login' === $sAction && $aResponseItem && isset($aResponseItem['Result'])) {
$oCacher = $this->Manager()->Actions()->Cacher();
$iConfigLimit = (int) $this->Config()->Get('plugin', 'error_limit', 0);
$sKey = $this->getCaptchaCacherKey();
if (0 < $iConfigLimit && $oCacher && $oCacher->IsInited()) {
if (false === $aResponseItem['Result']) {
$iLimit = 0;
$sLimut = $oCacher->Get($sKey);
if (\strlen($sLimut) && \is_numeric($sLimut)) {
$iLimit = (int) $sLimut;
}
$oCacher->Set($sKey, ++$iLimit);
if ($iConfigLimit <= $iLimit) {
$aResponseItem['Captcha'] = true;
}
} else {
$oCacher->Delete($sKey);
}
}
}
}
public function ContentSecurityPolicy(\SnappyMail\HTTP\CSP $CSP)
{
$CSP->script[] = 'https://www.google.com/recaptcha/';
$CSP->script[] = 'https://www.gstatic.com/recaptcha/';
$CSP->frame[] = 'https://www.google.com/recaptcha/';
$CSP->frame[] = 'https://recaptcha.google.com/recaptcha/';
}
}

View file

@ -0,0 +1,68 @@
(rl => {
rl && addEventListener('rl-view-model', e => {
const id = e.detail.viewModelTemplateID;
if (e.detail && ('AdminLogin' === id || 'Login' === id)
&& rl.pluginSettingsGet('recaptcha', 'show_captcha_on_login')) {
let
nId = null,
script;
const
mode = 'Login' === id ? 'user' : 'admin',
doc = document,
container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'),
ShowRecaptcha = () => {
if (window.grecaptcha && null === nId && container) {
const oEl = doc.createElement('div');
oEl.className = 'controls';
container.after(oEl);
nId = window.grecaptcha.render(oEl, {
'sitekey': rl.pluginSettingsGet('recaptcha', 'public_key'),
'theme': rl.pluginSettingsGet('recaptcha', 'theme')
});
}
},
StartRecaptcha = () => {
if (window.grecaptcha) {
ShowRecaptcha();
} else if (!script) {
script = doc.createElement('script');
// script.onload = ShowRecaptcha;
script.src = 'https://www.google.com/recaptcha/api.js?onload=ShowRecaptcha&render=explicit&hl=' + doc.documentElement.lang;
doc.head.append(script);
}
};
window.ShowRecaptcha = ShowRecaptcha;
StartRecaptcha();
addEventListener(`sm-${mode}-login`, e => {
if (null !== nId && window.grecaptcha) {
e.detail.set('RecaptchaResponse', window.grecaptcha.getResponse(nId));
} else {
e.preventDefault();
}
});
addEventListener(`sm-${mode}-login-response`, e => {
if (e.detail.error) {
if (null !== nId && window.grecaptcha) {
window.grecaptcha.reset(nId);
} else if (e.detail.data && e.detail.data.Captcha) {
StartRecaptcha();
}
}
});
}
});
})(window.rl);

View file

@ -6,7 +6,7 @@ class SnowfallOnLoginScreenPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Snowfall on login screen',
VERSION = '2.0',
CATEGORY = 'Fun',
DESCRIPTION = 'Snowfall on login screen plugin (just for fun).';
DESCRIPTION = 'Snowfall on login screen (just for fun).';
public function Init() : void
{

View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2021 SnappyMail Team
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,338 @@
<?php
use \RainLoop\Exceptions\ClientException;
use \RainLoop\Model\MainAccount;
class TwoFactorAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'Two Factor Authentication',
VERSION = '2.13.3',
RELEASE = '2022-03-04',
REQUIRED = '2.13.2',
CATEGORY = 'Login',
DESCRIPTION = 'Provides support for TOTP 2FA';
public function Init() : void
{
$this->UseLangs(true);
$this->addJs('js/TwoFactorAuthLogin.js');
$this->addJs('js/TwoFactorAuthSettings.js');
$this->addHook('login.success', 'DoLogin');
$this->addJsonHook('GetTwoFactorInfo', 'DoGetTwoFactorInfo');
$this->addJsonHook('CreateTwoFactorSecret', 'DoCreateTwoFactorSecret');
$this->addJsonHook('ShowTwoFactorSecret', 'DoShowTwoFactorSecret');
$this->addJsonHook('EnableTwoFactor', 'DoEnableTwoFactor');
$this->addJsonHook('VerifyTwoFactorCode', 'DoVerifyTwoFactorCode');
$this->addJsonHook('ClearTwoFactorInfo', 'DoClearTwoFactorInfo');
$this->addTemplate('templates/TwoFactorAuthSettings.html');
$this->addTemplate('templates/PopupsTwoFactorAuthTest.html');
}
public function DoLogin(MainAccount $oAccount)
{
if ($this->TwoFactorAuthProvider($oAccount)) {
$aData = $this->getTwoFactorInfo($oAccount);
if ($aData && isset($aData['IsSet'], $aData['Enable']) && !empty($aData['Secret']) && $aData['IsSet'] && $aData['Enable']) {
$sCode = \trim($this->jsonParam('totp_code', ''));
if (empty($sCode)) {
$this->Logger()->Write("TFA: Code required for {$oAccount->Email()}");
throw new ClientException(\RainLoop\Notifications::AuthError);
}
$bUseBackupCode = false;
if (6 < \strlen($sCode) && !empty($aData['BackupCodes'])) {
$aBackupCodes = \explode(' ', \trim(\preg_replace('/[^\d]+/', ' ', $aData['BackupCodes'])));
$bUseBackupCode = \in_array($sCode, $aBackupCodes);
if ($bUseBackupCode) {
$this->removeBackupCodeFromTwoFactorInfo($oAccount->Email(), $sCode);
}
}
if (!$bUseBackupCode && !$this->TwoFactorAuthProvider($oAccount)->VerifyCode($aData['Secret'], $sCode)) {
$this->Manager()->Actions()->LoggerAuthHelper($oAccount);
$this->Logger()->Write("TFA: Code failed for {$oAccount->Email()}");
throw new ClientException(\RainLoop\Notifications::AuthError);
}
$this->Logger()->Write("TFA: Code verified for {$oAccount->Email()}");
}
}
}
public function DoGetTwoFactorInfo() : array
{
$oAccount = $this->getMainAccountFromToken();
if (!$this->TwoFactorAuthProvider($oAccount)) {
return $this->jsonResponse(__FUNCTION__, false);
}
return $this->jsonResponse(__FUNCTION__, $this->getTwoFactorInfo($oAccount, true));
}
public function DoCreateTwoFactorSecret() : array
{
$oAccount = $this->getMainAccountFromToken();
if (!$this->TwoFactorAuthProvider($oAccount)) {
return $this->jsonResponse(__FUNCTION__, false);
}
$sEmail = $oAccount->Email();
$sSecret = $this->TwoFactorAuthProvider($oAccount)->CreateSecret();
$aCodes = array();
for ($iIndex = 9; $iIndex > 0; $iIndex--)
{
$aCodes[] = \rand(100000000, 900000000);
}
$this->StorageProvider()->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor',
\json_encode(array(
'User' => $sEmail,
'Enable' => false,
'Secret' => $sSecret,
'BackupCodes' => \implode(' ', $aCodes)
))
);
return $this->jsonResponse(__FUNCTION__, $this->getTwoFactorInfo($oAccount));
}
public function DoShowTwoFactorSecret() : array
{
$oAccount = $this->getMainAccountFromToken();
if (!$this->TwoFactorAuthProvider($oAccount)) {
return $this->jsonResponse(__FUNCTION__, false);
}
$aResult = $this->getTwoFactorInfo($oAccount);
unset($aResult['BackupCodes']);
$name = \rawurlencode($oAccount->Email());
// $issuer = \rawurlencode(\RainLoop\API::Config()->Get('webmail', 'title', 'SnappyMail'));
$QR = \SnappyMail\QRCode::getMinimumQRCode(
// "otpauth://totp/{$issuer}:{$name}?secret={$aResult['Secret']}&issuer={$issuer}",
"otpauth://totp/{$name}?secret={$aResult['Secret']}",
\SnappyMail\QRCode::ERROR_CORRECT_LEVEL_M
);
$aResult['QRCode'] = $QR->__toString();
return $this->jsonResponse(__FUNCTION__, $aResult);
}
public function DoEnableTwoFactor() : array
{
$oAccount = $this->getMainAccountFromToken();
if (!$this->TwoFactorAuthProvider($oAccount)) {
return $this->jsonResponse(__FUNCTION__, false);
}
$oSettings = $this->Manager()->Actions()->SettingsProvider()->Load($oAccount);
if ($this->Manager()->Actions()->HasActionParam('EnableTwoFactor')) {
$sValue = $this->GetActionParam('EnableTwoFactor', '');
$oSettings->SetConf('EnableTwoFactor', !empty($sValue));
}
$sEmail = $oAccount->Email();
$bResult = false;
$mData = $this->getTwoFactorInfo($oAccount);
if (isset($mData['Secret'], $mData['BackupCodes']))
{
$bResult = $this->StorageProvider()->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor',
\json_encode(array(
'User' => $sEmail,
'Enable' => '1' === \trim($this->jsonParam('Enable', '0')),
'Secret' => $mData['Secret'],
'BackupCodes' => $mData['BackupCodes']
))
);
}
return $this->jsonResponse(__FUNCTION__, $bResult);
}
public function DoVerifyTwoFactorCode() : array
{
$oAccount = $this->getMainAccountFromToken();
if (!$this->TwoFactorAuthProvider($oAccount)) {
return $this->jsonResponse(__FUNCTION__, false);
}
$sCode = \trim($this->jsonParam('Code', ''));
$aData = $this->getTwoFactorInfo($oAccount);
$sSecret = !empty($aData['Secret']) ? $aData['Secret'] : '';
return $this->jsonResponse(__FUNCTION__,
$this->TwoFactorAuthProvider($oAccount)->VerifyCode($sSecret, $sCode));
}
public function DoClearTwoFactorInfo() : array
{
$oAccount = $this->getMainAccountFromToken();
if (!$this->TwoFactorAuthProvider($oAccount)) {
return $this->jsonResponse(__FUNCTION__, false);
}
$this->StorageProvider()->Clear($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor'
);
return $this->jsonResponse(__FUNCTION__, $this->getTwoFactorInfo($oAccount, true));
}
protected function Logger() : \MailSo\Log\Logger
{
return $this->Manager()->Actions()->Logger();
}
protected function getMainAccountFromToken() : MainAccount
{
return $this->Manager()->Actions()->getMainAccountFromToken();
}
protected function StorageProvider() : \RainLoop\Providers\Storage
{
return $this->Manager()->Actions()->StorageProvider();
}
private $oTwoFactorAuthProvider;
protected function TwoFactorAuthProvider(MainAccount $oAccount) : ?TwoFactorAuthInterface
{
if (!$this->oTwoFactorAuthProvider) {
require __DIR__ . '/providers/interface.php';
require __DIR__ . '/providers/totp.php';
$this->oTwoFactorAuthProvider = new TwoFactorAuthTotp();
}
return $this->oTwoFactorAuthProvider;
}
protected function getTwoFactorInfo(MainAccount $oAccount, bool $bRemoveSecret = false) : array
{
$sEmail = $oAccount->Email();
$mData = null;
$aResult = array(
'User' => '',
'IsSet' => false,
'Enable' => false,
'Secret' => '',
'UrlTitle' => '',
'BackupCodes' => ''
);
if (!empty($sEmail))
{
$aResult['User'] = $sEmail;
$sData = $this->StorageProvider()->Get($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor'
);
if ($sData)
{
$mData = static::DecodeKeyValues($sData);
}
}
if (!empty($aResult['User']) &&
!empty($mData['User']) && !empty($mData['Secret']) &&
!empty($mData['BackupCodes']) && $sEmail === $mData['User'])
{
$aResult['IsSet'] = true;
$aResult['Enable'] = isset($mData['Enable']) ? !!$mData['Enable'] : false;
$aResult['Secret'] = $mData['Secret'];
$aResult['BackupCodes'] = $mData['BackupCodes'];
$aResult['UrlTitle'] = $this->Config()->Get('webmail', 'title', '');
}
if ($bRemoveSecret)
{
if (isset($aResult['Secret']))
{
unset($aResult['Secret']);
}
if (isset($aResult['UrlTitle']))
{
unset($aResult['UrlTitle']);
}
if (isset($aResult['BackupCodes']))
{
unset($aResult['BackupCodes']);
}
}
return $aResult;
}
protected function removeBackupCodeFromTwoFactorInfo(MainAccount $oAccount, string $sCode) : bool
{
if (!$oAccount || empty($sCode))
{
return false;
}
$sData = $this->StorageProvider()->Get($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor'
);
if ($sData)
{
$mData = static::DecodeKeyValues($sData);
if (!empty($mData['BackupCodes']))
{
$sBackupCodes = \preg_replace('/[^\d]+/', ' ', ' '.$mData['BackupCodes'].' ');
$sBackupCodes = \str_replace(' '.$sCode.' ', '', $sBackupCodes);
$mData['BackupCodes'] = \trim(\preg_replace('/[^\d]+/', ' ', $sBackupCodes));
return $this->StorageProvider()->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'two_factor',
\json_encode($mData)
);
}
}
return false;
}
private static function DecodeKeyValues(string $sData) : array
{
if (!\str_contains($sData, 'User')) {
$sData = \MailSo\Base\Utils::UrlSafeBase64Decode($sData);
if (!\strlen($sData)) {
return '';
}
$sKey = \md5(APP_SALT);
$sData = \is_callable('xxtea_decrypt')
? \xxtea_decrypt($sData, $sKey)
: \MailSo\Base\Xxtea::decrypt($sData, $sKey);
}
try {
return \json_decode($sData, true, 512, JSON_THROW_ON_ERROR) ?: array();
} catch (\Throwable $e) {
return \unserialize($sData) ?: array();
}
}
}

View file

@ -0,0 +1,21 @@
(rl => {
rl && addEventListener('rl-view-model', e => {
if (e.detail && 'Login' === e.detail.viewModelTemplateID) {
const container = e.detail.viewModelDom.querySelector('#plugin-Login-BottomControlGroup'),
placeholder = 'LOGIN/LABEL_TWO_FACTOR_CODE';
if (container) {
container.prepend(Element.fromHTML('<div class="controls">'
+ '<span class="fontastic">⏱</span>'
+ '<input name="totp_code" type="text" class="input-block-level"'
+ ' pattern="[0-9]*" inputmode="numeric"'
+ ' autocomplete="off" autocorrect="off" autocapitalize="none" spellcheck="false"'
+ ' data-bind="textInput: totp, disable: submitRequest" data-i18n="[placeholder]'+placeholder
+ '" placeholder="'+rl.i18n(placeholder)+'">'
+ '</div>'));
}
}
});
})(window.rl);

View file

@ -0,0 +1,209 @@
/*
import { trigger as translatorTrigger } from 'Common/Translator';
*/
(rl => { if (rl) {
const
pString = value => null != value ? '' + value : '',
Remote = new class {
/**
* @param {?Function} fCallback
* @param {string} sCode
*/
verifyCode(fCallback, sCode) {
rl.pluginRemoteRequest(fCallback, 'VerifyTwoFactorCode', {
Code: sCode
});
}
/**
* @param {?Function} fCallback
* @param {boolean} bEnable
*/
enableTwoFactor(fCallback, bEnable) {
rl.pluginRemoteRequest(fCallback, 'EnableTwoFactor', {
Enable: bEnable ? 1 : 0
});
}
};
class TwoFactorAuthSettings
{
constructor() {
this.processing = ko.observable(false);
this.clearing = ko.observable(false);
this.secreting = ko.observable(false);
this.viewUser = ko.observable('');
this.twoFactorStatus = ko.observable(false);
this.twoFactorTested = ko.observable(false);
this.viewSecret = ko.observable('');
this.viewQRCode = ko.observable('');
this.viewBackupCodes = ko.observable('');
this.viewEnable_ = ko.observable(false);
const fn = iError => iError && this.viewEnable_(false);
Object.entries({
viewEnable: {
read: this.viewEnable_,
write: (value) => {
value = !!value;
if (value && this.twoFactorTested()) {
this.viewEnable_(value);
Remote.enableTwoFactor(fn, value);
} else {
if (!value) {
this.viewEnable_(value);
}
Remote.enableTwoFactor(fn, false);
}
}
},
viewTwoFactorEnableTooltip: () => {
// translatorTrigger();
return this.twoFactorTested() || this.viewEnable_()
? ''
: rl.i18n('POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_TEST_BEFORE_DESC');
},
viewTwoFactorStatus: () => {
// translatorTrigger();
return rl.i18n('POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_'
+ (this.twoFactorStatus() ? '' : 'NOT_')
+ 'CONFIGURED_DESC'
);
},
twoFactorAllowedEnable: () => this.viewEnable() || this.twoFactorTested()
}).forEach(([key, fn]) => this[key] = ko.computed(fn));
this.onResult = this.onResult.bind(this);
this.onShowSecretResult = this.onShowSecretResult.bind(this);
}
showSecret() {
this.secreting(true);
rl.pluginRemoteRequest(this.onShowSecretResult, 'ShowTwoFactorSecret');
}
hideSecret() {
this.viewSecret('');
this.viewQRCode('');
this.viewBackupCodes('');
}
createTwoFactor() {
this.processing(true);
rl.pluginRemoteRequest(this.onResult, 'CreateTwoFactorSecret');
}
testTwoFactor() {
TwoFactorAuthTestPopupView.showModal([this.twoFactorTested]);
}
clearTwoFactor() {
this.hideSecret();
this.twoFactorTested(false);
this.clearing(true);
rl.pluginRemoteRequest(this.onResult, 'ClearTwoFactorInfo');
}
onShow() {
this.hideSecret('');
}
getQr() {
return 'otpauth://totp/' + encodeURIComponent(this.viewUser())
+ '?secret=' + encodeURIComponent(this.viewSecret())
+ '&issuer=' + encodeURIComponent('');
}
onResult(iError, oData) {
this.processing(false);
this.clearing(false);
if (iError) {
this.viewUser('');
this.viewEnable_(false);
this.twoFactorStatus(false);
this.twoFactorTested(false);
this.hideSecret('');
} else {
this.viewUser(pString(oData.Result.User));
this.viewEnable_(!!oData.Result.Enable);
this.twoFactorStatus(!!oData.Result.IsSet);
this.twoFactorTested(!!oData.Result.Tested);
this.viewSecret(pString(oData.Result.Secret));
this.viewQRCode(oData.Result.QRCode);
this.viewBackupCodes(pString(oData.Result.BackupCodes).replace(/[\s]+/g, ' '));
}
}
onShowSecretResult(iError, data) {
this.secreting(false);
if (iError) {
this.viewSecret('');
this.viewQRCode('');
} else {
this.viewSecret(pString(data.Result.Secret));
this.viewQRCode(pString(data.Result.QRCode));
}
}
onBuild() {
this.processing(true);
rl.pluginRemoteRequest(this.onResult, 'GetTwoFactorInfo');
}
}
class TwoFactorAuthTestPopupView extends rl.pluginPopupView {
constructor() {
super('TwoFactorAuthTest');
this.addObservables({
code: '',
codeStatus: null,
testing: false
});
ko.decorateCommands(this, {
testCodeCommand: self => self.code() && !self.testing()
});
}
testCodeCommand() {
this.testing(true);
Remote.verifyCode(iError => {
this.testing(false);
this.codeStatus(!iError);
this.twoFactorTested(!iError);
}, this.code());
}
onShow(twoFactorTested) {
this.code('');
this.codeStatus(null);
this.testing(false);
this.twoFactorTested = twoFactorTested;
}
}
rl.addSettingsViewModel(
TwoFactorAuthSettings,
'TwoFactorAuthSettings',
'POPUPS_TWO_FACTOR_CFG/LEGEND_TWO_FACTOR_AUTH',
'two-factor-auth'
);
}})(window.rl);

View file

@ -0,0 +1,22 @@
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "Zwei-Faktor-Authentifizierung"
LABEL_ENABLE_TWO_FACTOR = "Zwei-Faktor-Authentifizierung aktivieren"
LABEL_TWO_FACTOR_USER = "Benutzer"
LABEL_TWO_FACTOR_STATUS = "Status"
LABEL_TWO_FACTOR_SECRET = "Geheimnis"
LABEL_TWO_FACTOR_BACKUP_CODES = "Sicherungscodes"
BUTTON_CREATE = "Neues Geheimnis erstellen"
BUTTON_ACTIVATE = "Aktivieren"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Geheimnis einblenden"
BUTTON_HIDE_SECRET = "Geheminis ausblenden"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Konfiguriert"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nicht konfiguriert"
TWO_FACTOR_SECRET_DESC = "Importieren Sie diese Information in Ihre Google-Authenticator-Anwendung (oder andere TOTP-Anwendung), indem Sie den unten bereitgestellten QR-Code verwenden oder den Code manuell eingeben."
TWO_FACTOR_BACKUP_CODES_DESC = "Sollten Sie keine Codes über den Google Authenticator erhalten, können Sie einen Sicherungscode zur Anmeldung verwenden. Der Sicherungscode wird inaktiv, sobald Sie ihn zur Anmeldung verwendet haben."
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Sie können diese Einstellung nicht ohne vorherigen Test verändern."
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "Zwei-Faktor-Authentifizierung"
LABEL_CODE = "Code"
[LOGIN]
LABEL_TWO_FACTOR_CODE = "Verifizierungscode"

View file

@ -0,0 +1,22 @@
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "2-Step verification test"
LABEL_CODE = "Code"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "2-Step Verification (TOTP)"
LABEL_ENABLE_TWO_FACTOR = "Enable 2-Step verification"
LABEL_TWO_FACTOR_USER = "User"
LABEL_TWO_FACTOR_STATUS = "Status"
LABEL_TWO_FACTOR_SECRET = "Secret"
LABEL_TWO_FACTOR_BACKUP_CODES = "Backup codes"
BUTTON_CREATE = "Create a secret"
BUTTON_ACTIVATE = "Activate"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Show Secret"
BUTTON_HIDE_SECRET = "Hide Secret"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configured"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Not configured"
TWO_FACTOR_SECRET_DESC = "Import this info into your Google Authenticator client (or other TOTP client) using the provided QR code below or by entering the code manually.\n"
TWO_FACTOR_BACKUP_CODES_DESC = "If you can't receive codes via Google Authenticator (or other TOTP client), you can use backup codes to sign in. After youve used a backup code to sign in, it will become inactive.\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test."
[LOGIN]
LABEL_TWO_FACTOR_CODE = "Verification Code"

View file

@ -0,0 +1,22 @@
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "Prueba de verificación de 2 pasos"
LABEL_CODE = "Código"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "Verificación de 2 Pasos"
LABEL_ENABLE_TWO_FACTOR = "Activar la verificación de 2 pasos"
LABEL_TWO_FACTOR_USER = "Usuario"
LABEL_TWO_FACTOR_STATUS = "Estado"
LABEL_TWO_FACTOR_SECRET = "Clave secreta"
LABEL_TWO_FACTOR_BACKUP_CODES = "Códigos de copia de seguridad"
BUTTON_CREATE = "Crear nueva clave secreta"
BUTTON_ACTIVATE = "Activate"
LINK_TEST = "probar"
BUTTON_SHOW_SECRET = "Mostrar clave secreta"
BUTTON_HIDE_SECRET = "Ocultar clave secreta"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configurado"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "No configurado"
TWO_FACTOR_SECRET_DESC = "Importar esta información en su cliente Google Authenticator (u otro cliente TOTP) utilizando el código QR se indica debajo o introduciendo el código manualmente."
TWO_FACTOR_BACKUP_CODES_DESC = "Si usted no puede recibir los códigos a través de Google Authenticator, puede utilizar códigos de copia de seguridad para firmar pulg Después de que usted ha utilizado un código de copia de seguridad para iniciar sesión, se convertirá en inactiva."
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "You can't change this setting before test."
[LOGIN]
LABEL_TWO_FACTOR_CODE = "Código de verificación"

View file

@ -0,0 +1,22 @@
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "Authentification en deux étapes"
LABEL_ENABLE_TWO_FACTOR = "Activer l'authentification en deux étapes"
LABEL_TWO_FACTOR_USER = "Utilisateur"
LABEL_TWO_FACTOR_STATUS = "Statut"
LABEL_TWO_FACTOR_SECRET = "Secret"
LABEL_TWO_FACTOR_BACKUP_CODES = "Code de sauvegarde"
BUTTON_CREATE = "Créer une nouvelle clé secrète"
BUTTON_ACTIVATE = "Activer"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Montrer la clé secrète"
BUTTON_HIDE_SECRET = "Masquer la clé secrète"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configuré"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Non configuré"
TWO_FACTOR_SECRET_DESC = "Importez ces informations dans votre client Google Authenticator (ou un autre client TOTP) en utilisant le code QR fourni ci-dessous ou en entrant les valeurs manuellement."
TWO_FACTOR_BACKUP_CODES_DESC = "Si vous ne pouvez pas recevoir les codes par Google Authenticator, vous pouvez utiliser les codes de sauvegarde pour vous connecter. Après avoir fait cela, il deviendra inactif."
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Vous ne pouvez pas modifier ce paramètre avant de l'avoir essayé."
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "Test d'authentification en deux étapes"
LABEL_CODE = "Code"
[LOGIN]
LABEL_TWO_FACTOR_CODE = "Code de vérification"

View file

@ -0,0 +1,22 @@
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "2 lépcsős hitelesítés"
LABEL_ENABLE_TWO_FACTOR = "2 lépcsős hitelesítés engedélyezése"
LABEL_TWO_FACTOR_USER = "Felhasználó"
LABEL_TWO_FACTOR_STATUS = "Állapot"
LABEL_TWO_FACTOR_SECRET = "Titok"
LABEL_TWO_FACTOR_BACKUP_CODES = "Biztonsági kódok"
BUTTON_CREATE = "Új titok létrehozás"
BUTTON_ACTIVATE = "Aktivál"
LINK_TEST = "teszt"
BUTTON_SHOW_SECRET = "Titok megjelenítése"
BUTTON_HIDE_SECRET = "Titok elrejtése"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Beállítva"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Nincs beállítva"
TWO_FACTOR_SECRET_DESC = "Importáld ezt az infót a Google Authenticator kliensedbe (vagy más TOTP kliensbe) az alábbi QR kód használatával vagy a kód manuális megadatásával.\n"
TWO_FACTOR_BACKUP_CODES_DESC = "Ha nem kapod meg a kódokat a Google Authenticator kliensből (vagy más TOTP kliensből), akkor a bejelentkezéshez használhatod a biztonsági kódot. A biztonsági kód használata után inaktívvá válik.\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Tesztelés nélkül nem lehet megváltoztatni ezt a beállítást."
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "2-lépéses hitelesítés teszt"
LABEL_CODE = "Kód"
[LOGIN]
LABEL_TWO_FACTOR_CODE = "Megerősítő kód"

View file

@ -0,0 +1,22 @@
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "Test di verifica a 2 fattori"
LABEL_CODE = "Codice"
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "Verifica a 2 Fattori"
LABEL_ENABLE_TWO_FACTOR = "Abilita la verifica a 2 fattori"
LABEL_TWO_FACTOR_USER = "Utente"
LABEL_TWO_FACTOR_STATUS = "Stato"
LABEL_TWO_FACTOR_SECRET = "Chiave segreta"
LABEL_TWO_FACTOR_BACKUP_CODES = "Codici di backup"
BUTTON_CREATE = "Crea la chiave segreta"
BUTTON_ACTIVATE = "Attiva"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Mostra la chiave segreta"
BUTTON_HIDE_SECRET = "Nascondi la chiave segreta"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Configurato"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Non configurato"
TWO_FACTOR_SECRET_DESC = "Importa queste informazioni nella tua app Google Authenticator (o un altro programma per l'autenticazione a due fattori) utilizzando il codice QR fornito qui sotto o inserendo il codice manualmente.\n"
TWO_FACTOR_BACKUP_CODES_DESC = "Se non riesci a ricevere i codici tramite Google Authenticator (o un altro programma per l'autenticazione a due fattori), puoi utilizzare i codici di backup per accedere. Dopo che hai utilizzato un codice di backup per accedere, diventerà inattivo.\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Non puoi modificare questa impostazione prima del test."
[LOGIN]
LABEL_TWO_FACTOR_CODE = "Codice di Verifica"

View file

@ -0,0 +1,22 @@
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "2-Stap verificatie"
LABEL_ENABLE_TWO_FACTOR = "Gebruik 2-Stap verificatie"
LABEL_TWO_FACTOR_USER = "Gebruikersnaam"
LABEL_TWO_FACTOR_STATUS = "Status"
LABEL_TWO_FACTOR_SECRET = "Geheime sleutel"
LABEL_TWO_FACTOR_BACKUP_CODES = "Backup codes"
BUTTON_CREATE = "Nieuwe geheime sleutel aanmaken"
BUTTON_ACTIVATE = "Activate"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Bekijk geheime sleutel"
BUTTON_HIDE_SECRET = "Verberg geheime sleutel"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Geconfigureerd"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Niet geconfigureerd"
TWO_FACTOR_SECRET_DESC = "Importeer deze informatie in uw Google Authenticator-client (of andere TOTP-client) door gebruik te maken van de QR code hier beneden of door de code handmatig in te voeren."
TWO_FACTOR_BACKUP_CODES_DESC = "Als u geen codes ontvangt via de Google Authenticator kunt u de backup codes gebruiken om in te loggen. Na gebruik van de backup code wordt deze inactief."
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "U kunt 2-stap verificatie niet activeren voordat u het succesvol getest heeft."
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "2-Stap verificatie test"
LABEL_CODE = "Code"
[LOGIN]
LABEL_TWO_FACTOR_CODE = "Verificatie Code"

View file

@ -0,0 +1,22 @@
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "Tvåstegsverifiering (TOTP)"
LABEL_ENABLE_TWO_FACTOR = "Aktivera tvåstegsverifiering"
LABEL_TWO_FACTOR_USER = "Användare"
LABEL_TWO_FACTOR_STATUS = "Status"
LABEL_TWO_FACTOR_SECRET = "Hemlig kod"
LABEL_TWO_FACTOR_BACKUP_CODES = "Backupkoder"
BUTTON_CREATE = "Skapa ny hemlig kod"
BUTTON_ACTIVATE = "Aktivera"
LINK_TEST = "test"
BUTTON_SHOW_SECRET = "Visa hemlig kod"
BUTTON_HIDE_SECRET = "Göm hemlig kod"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "Konfigurerad"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "Inte konfigurarad"
TWO_FACTOR_SECRET_DESC = "Importera denna information till din Google Authenticator klient (eller annan TOTP klient) med denna QR kod eller manuellt med koden här nedan."
TWO_FACTOR_BACKUP_CODES_DESC = "Om du inte kan ta emot koderna med Google Authenticator, så använd backupkoderna för att logga in. När du använt backupkoderna för att logga in så blir dom inaktiva."
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "Du kan inte ändra denna inställning innan test."
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "Tvåstegsverifieringstest"
LABEL_CODE = "Kod"
[LOGIN]
LABEL_TWO_FACTOR_CODE = "Verifikationskod"

View file

@ -0,0 +1,22 @@
[POPUPS_TWO_FACTOR_CFG]
LEGEND_TWO_FACTOR_AUTH = "两步验证 (TOTP)"
LABEL_ENABLE_TWO_FACTOR = "启用两步验证"
LABEL_TWO_FACTOR_USER = "用户"
LABEL_TWO_FACTOR_STATUS = "状态"
LABEL_TWO_FACTOR_SECRET = "密钥"
LABEL_TWO_FACTOR_BACKUP_CODES = "备用代码"
BUTTON_CREATE = "创建新密钥"
BUTTON_ACTIVATE = "启用"
LINK_TEST = "测试"
BUTTON_SHOW_SECRET = "显示密钥"
BUTTON_HIDE_SECRET = "隐藏密钥"
TWO_FACTOR_SECRET_CONFIGURED_DESC = "已设置"
TWO_FACTOR_SECRET_NOT_CONFIGURED_DESC = "未设置"
TWO_FACTOR_SECRET_DESC = "将下面的信息导入 Google Authenticator (或其他 TOTP 客户端)。 扫描下面的二维码或手动输入密钥。\n"
TWO_FACTOR_BACKUP_CODES_DESC = "如果你无法从验证器获取验证码,你可以使用备用代码登录。 当你使用过某一个备用代码后,它将会失效。\n"
TWO_FACTOR_SECRET_TEST_BEFORE_DESC = "完成测试前你无法更改此设置。"
[POPUPS_TWO_FACTOR_TEST]
TITLE_TEST_CODE = "两步验证测试"
LABEL_CODE = "代码"
[LOGIN]
LABEL_TWO_FACTOR_CODE = "验证码"

View file

@ -0,0 +1,8 @@
<?php
interface TwoFactorAuthInterface
{
public function Label() : string;
public function VerifyCode(string $sSecret, string $sCode) : bool;
public function CreateSecret() : string;
}

View file

@ -0,0 +1,20 @@
<?php
class TwoFactorAuthTotp implements TwoFactorAuthInterface
{
public function Label() : string
{
return 'Two Factor Authenticator Code';
}
public function VerifyCode(string $sSecret, string $sCode) : bool
{
return \SnappyMail\TOTP::Verify($sSecret, $sCode);
}
public function CreateSecret() : string
{
return \SnappyMail\TOTP::CreateSecret();
}
}

View file

@ -0,0 +1,18 @@
<header>
<a class="close" href="#" data-bind="click: close">×</a>
<h3 data-i18n="POPUPS_TWO_FACTOR_TEST/TITLE_TEST_CODE"></h3>
</header>
<div class="modal-body form-horizontal">
<div class="control-group">
<label data-i18n="POPUPS_TWO_FACTOR_TEST/LABEL_CODE"></label>
<input type="text" class="uiInput inputName"
autofocus="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="textInput: code, onEnter: testCodeCommand" />
</div>
</div>
<footer>
<a class="btn" data-bind="command: testCodeCommand, css: { 'btn-success': true === codeStatus(), 'btn-danger': false === codeStatus() }">
<i data-bind="css: {'icon-ok': !testing(), 'icon-spinner': testing()}"></i>
<span data-i18n="GLOBAL/TEST"></span>
</a>
</footer>

View file

@ -0,0 +1,81 @@
<div class="b-settings-two-factor form-horizontal">
<div class="legend" data-i18n="POPUPS_TWO_FACTOR_CFG/LEGEND_TWO_FACTOR_AUTH"></div>
<div class="form-horizontal">
<div class="control-group" data-bind="visible: twoFactorStatus">
<div class="controls">
<div style="display: inline-block" data-bind="attr:{title: viewTwoFactorEnableTooltip}">
<div data-bind="component: {
name: 'Checkbox',
params: {
label: 'POPUPS_TWO_FACTOR_CFG/LABEL_ENABLE_TWO_FACTOR',
enable: twoFactorAllowedEnable,
value: viewEnable,
inline: true
}
}"></div>
</div>
&nbsp;&nbsp;&nbsp;
<span class="g-ui-link" data-bind="click: testTwoFactor, visible: twoFactorStatus"
data-i18n="POPUPS_TWO_FACTOR_CFG/LINK_TEST"></span>
</div>
</div>
<div class="control-group">
<label class="control-label">
<span data-i18n="POPUPS_TWO_FACTOR_CFG/LABEL_TWO_FACTOR_USER"></span>
</label>
<div class="controls" style="padding-top: 5px;">
<strong data-bind="text: viewUser"></strong>
</div>
</div>
<div class="control-group" data-bind="visible: '' === viewSecret() && twoFactorStatus() && !clearing()">
<div class="controls" style="padding-top: 5px;">
<strong data-bind="visible: secreting">...</strong>
<span class="g-ui-link" data-bind="click: showSecret, visible: !secreting()"
data-i18n="POPUPS_TWO_FACTOR_CFG/BUTTON_SHOW_SECRET"></span>
</div>
</div>
<div class="control-group" data-bind="visible: '' !== viewSecret()">
<label class="control-label">
<span data-i18n="POPUPS_TWO_FACTOR_CFG/LABEL_TWO_FACTOR_SECRET"></span>
</label>
<div class="controls" style="padding-top: 5px;">
<strong data-bind="text: viewSecret" style="user-select:text"></strong>
&nbsp;&nbsp;
<span class="g-ui-link" data-bind="click: hideSecret" data-i18n="POPUPS_TWO_FACTOR_CFG/BUTTON_HIDE_SECRET"></span>
<br />
<br />
<blockquote>
<p class="muted width100-on-mobile" style="width: 550px" data-i18n="POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_SECRET_DESC"></p>
</blockquote>
<pre data-bind="text: viewQRCode" style="line-height:1;letter-spacing:-1px"></pre>
</div>
</div>
<div class="control-group" data-bind="visible: '' !== viewBackupCodes()">
<label class="control-label">
<span data-i18n="POPUPS_TWO_FACTOR_CFG/LABEL_TWO_FACTOR_BACKUP_CODES"></span>
</label>
<div class="controls" style="padding-top: 5px;">
<pre data-bind="text: viewBackupCodes" style="user-select: text; width: 230px; word-break: break-word;"></pre>
<br />
<blockquote>
<p class="muted width100-on-mobile" style="width: 550px" data-i18n="POPUPS_TWO_FACTOR_CFG/TWO_FACTOR_BACKUP_CODES_DESC"></p>
</blockquote>
</div>
</div>
</div>
<a class="btn btn-danger" data-bind="click: clearTwoFactor, visible: twoFactorStatus">
<i class="fontastic" data-bind="css: {'icon-spinner': clearing()}"></i>
<span data-i18n="GLOBAL/CLEAR"></span>
</a>
<a class="btn" data-bind="click: createTwoFactor, visible: !twoFactorStatus()">
<i class="fontastic" data-bind="css: {'icon-spinner': processing()}"></i>
<span data-i18n="POPUPS_TWO_FACTOR_CFG/BUTTON_ACTIVATE"></span>
</a>
<!--
<a class="btn" data-bind="click: close, visible: viewEnable()">
<i class="icon-ok" ></i>
<span data-i18n="GLOBAL/DONE"></span>
</a>
-->
</div>

View file

@ -3,10 +3,12 @@
class WhiteListPlugin extends \RainLoop\Plugins\AbstractPlugin
{
const
NAME = 'White list',
VERSION = '2.0',
NAME = 'Whitelist',
VERSION = '2.1',
RELEASE = '2021-04-21',
REQUIRED = '2.5.0',
CATEGORY = 'Login',
DESCRIPTION = 'Simple white list plugin (with wildcard and exceptions functionality).';
DESCRIPTION = 'Simple login whitelist (with wildcard and exceptions functionality).';
public function Init() : void
{