mirror of
https://github.com/the-djmaze/snappymail.git
synced 2026-08-25 10:09:20 +03:00
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:
commit
827cfa5051
892 changed files with 110819 additions and 48675 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
104
plugins/change-password/drivers/ispconfig.php
Normal file
104
plugins/change-password/drivers/ispconfig.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
12
plugins/change-password/langs/it_IT.ini
Normal file
12
plugins/change-password/langs/it_IT.ini
Normal 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"
|
||||
5
plugins/change-password/style.css
Normal file
5
plugins/change-password/style.css
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
.form-horizontal.change-password .control-group {
|
||||
.control-label {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
3
plugins/ckeditor/ckeditor.js
vendored
3
plugins/ckeditor/ckeditor.js
vendored
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
207
plugins/ckeditor/style.css
Normal 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;
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
class CustomSystemFoldersPlugin extends \RainLoop\Plugins\AbstractPlugin
|
||||
{
|
||||
const
|
||||
NAME = '',
|
||||
NAME = 'Custom System Folders',
|
||||
CATEGORY = 'General',
|
||||
DESCRIPTION = 'Set custom sytem folders';
|
||||
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
58
plugins/demo-account/storage.php
Normal file
58
plugins/demo-account/storage.php
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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('*')
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
19
plugins/login-oauth2/LoginOAuth2.js
Normal file
19
plugins/login-oauth2/LoginOAuth2.js
Normal 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);
|
||||
323
plugins/login-oauth2/index.php
Normal file
323
plugins/login-oauth2/index.php
Normal 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'
|
||||
));
|
||||
}
|
||||
}
|
||||
33
plugins/login-register/LoginRegister.js
Normal file
33
plugins/login-register/LoginRegister.js
Normal 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);
|
||||
42
plugins/login-register/index.php
Normal file
42
plugins/login-register/index.php
Normal 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', ''));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
10
plugins/login-register/langs/ar-SA.json
Normal file
10
plugins/login-register/langs/ar-SA.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/bg-BG.json
Normal file
10
plugins/login-register/langs/bg-BG.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/cs-CZ.json
Normal file
10
plugins/login-register/langs/cs-CZ.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/da-DK.json
Normal file
10
plugins/login-register/langs/da-DK.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/de-DE.json
Normal file
10
plugins/login-register/langs/de-DE.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/el-GR.json
Normal file
10
plugins/login-register/langs/el-GR.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/en.json
Normal file
10
plugins/login-register/langs/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/es-ES.json
Normal file
10
plugins/login-register/langs/es-ES.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/et-EE.json
Normal file
10
plugins/login-register/langs/et-EE.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/fa-IR.json
Normal file
10
plugins/login-register/langs/fa-IR.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/fi-FI.json
Normal file
10
plugins/login-register/langs/fi-FI.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/fr-FR.json
Normal file
10
plugins/login-register/langs/fr-FR.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/hu-HU.json
Normal file
10
plugins/login-register/langs/hu-HU.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/id-ID.json
Normal file
10
plugins/login-register/langs/id-ID.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/is-IS.json
Normal file
10
plugins/login-register/langs/is-IS.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/it-IT.json
Normal file
10
plugins/login-register/langs/it-IT.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/ja-JP.json
Normal file
10
plugins/login-register/langs/ja-JP.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/ko-KR.json
Normal file
10
plugins/login-register/langs/ko-KR.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/lt-LT.json
Normal file
10
plugins/login-register/langs/lt-LT.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/lv-LV.json
Normal file
10
plugins/login-register/langs/lv-LV.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/nb-NO.json
Normal file
10
plugins/login-register/langs/nb-NO.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/nl-NL.json
Normal file
10
plugins/login-register/langs/nl-NL.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/pl-PL.json
Normal file
10
plugins/login-register/langs/pl-PL.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/pt-BR.json
Normal file
10
plugins/login-register/langs/pt-BR.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/pt-PT.json
Normal file
10
plugins/login-register/langs/pt-PT.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/ro-RO.json
Normal file
10
plugins/login-register/langs/ro-RO.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/ru-RU.json
Normal file
10
plugins/login-register/langs/ru-RU.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/sk-SK.json
Normal file
10
plugins/login-register/langs/sk-SK.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/sl-SI.json
Normal file
10
plugins/login-register/langs/sl-SI.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/sv-SE.json
Normal file
10
plugins/login-register/langs/sv-SE.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/tr-TR.json
Normal file
10
plugins/login-register/langs/tr-TR.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/uk-UA.json
Normal file
10
plugins/login-register/langs/uk-UA.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/zh-CN.json
Normal file
10
plugins/login-register/langs/zh-CN.json
Normal 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"
|
||||
}
|
||||
}
|
||||
10
plugins/login-register/langs/zh-TW.json
Normal file
10
plugins/login-register/langs/zh-TW.json
Normal 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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
153
plugins/recaptcha/index.php
Normal 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/';
|
||||
}
|
||||
|
||||
}
|
||||
68
plugins/recaptcha/js/recaptcha.js
Normal file
68
plugins/recaptcha/js/recaptcha.js
Normal 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);
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
20
plugins/two-factor-auth/LICENSE
Normal file
20
plugins/two-factor-auth/LICENSE
Normal 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.
|
||||
338
plugins/two-factor-auth/index.php
Normal file
338
plugins/two-factor-auth/index.php
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
21
plugins/two-factor-auth/js/TwoFactorAuthLogin.js
Normal file
21
plugins/two-factor-auth/js/TwoFactorAuthLogin.js
Normal 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);
|
||||
209
plugins/two-factor-auth/js/TwoFactorAuthSettings.js
Normal file
209
plugins/two-factor-auth/js/TwoFactorAuthSettings.js
Normal 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);
|
||||
22
plugins/two-factor-auth/langs/de_DE.ini
Normal file
22
plugins/two-factor-auth/langs/de_DE.ini
Normal 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"
|
||||
22
plugins/two-factor-auth/langs/en.ini
Normal file
22
plugins/two-factor-auth/langs/en.ini
Normal 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 you’ve 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"
|
||||
22
plugins/two-factor-auth/langs/es_ES.ini
Normal file
22
plugins/two-factor-auth/langs/es_ES.ini
Normal 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"
|
||||
22
plugins/two-factor-auth/langs/fr_FR.ini
Normal file
22
plugins/two-factor-auth/langs/fr_FR.ini
Normal 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"
|
||||
22
plugins/two-factor-auth/langs/hu-HU.ini
Normal file
22
plugins/two-factor-auth/langs/hu-HU.ini
Normal 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"
|
||||
22
plugins/two-factor-auth/langs/it_IT.ini
Normal file
22
plugins/two-factor-auth/langs/it_IT.ini
Normal 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"
|
||||
22
plugins/two-factor-auth/langs/nl_NL.ini
Normal file
22
plugins/two-factor-auth/langs/nl_NL.ini
Normal 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"
|
||||
22
plugins/two-factor-auth/langs/sv-SE.ini
Normal file
22
plugins/two-factor-auth/langs/sv-SE.ini
Normal 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"
|
||||
22
plugins/two-factor-auth/langs/zh_CN.ini
Normal file
22
plugins/two-factor-auth/langs/zh_CN.ini
Normal 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 = "验证码"
|
||||
8
plugins/two-factor-auth/providers/interface.php
Normal file
8
plugins/two-factor-auth/providers/interface.php
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
interface TwoFactorAuthInterface
|
||||
{
|
||||
public function Label() : string;
|
||||
public function VerifyCode(string $sSecret, string $sCode) : bool;
|
||||
public function CreateSecret() : string;
|
||||
}
|
||||
20
plugins/two-factor-auth/providers/totp.php
Normal file
20
plugins/two-factor-auth/providers/totp.php
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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>
|
||||
81
plugins/two-factor-auth/templates/TwoFactorAuthSettings.html
Normal file
81
plugins/two-factor-auth/templates/TwoFactorAuthSettings.html
Normal 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>
|
||||
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue