Revamp the whole accounts system for better management and control.

This also solves RainLoop/#2134
This commit is contained in:
djmaze 2021-11-11 13:34:41 +01:00
parent ef4790d04e
commit a18d393f54
15 changed files with 441 additions and 401 deletions

View file

@ -1164,8 +1164,7 @@ END;
public static function UrlSafeBase64Decode(string $sValue) : string
{
$sValue = \rtrim(\strtr($sValue, '-_.', '+/='), '=');
return static::Base64Decode(\str_pad($sValue, \strlen($sValue) + (\strlen($sValue) % 4), '=', STR_PAD_RIGHT));
return \base64_decode(\strtr($sValue, '-_', '+/'), '=');
}
public static function ParseFetchSequence(string $sSequence) : array

View file

@ -31,6 +31,11 @@ class Actions
*/
const AUTH_SPEC_TOKEN_KEY = 'smaccount';
/**
* This session cookie optionally contains a \RainLoop\Model\AdditionalAccount
*/
const AUTH_ADDITIONAL_TOKEN_KEY = 'smadditional';
const AUTH_SPEC_LOGOUT_TOKEN_KEY = 'smspeclogout';
const AUTH_SPEC_LOGOUT_CUSTOM_MSG_KEY = 'smspeclogoutcmk';
@ -553,7 +558,7 @@ class Actions
{
$sKey = '';
if ($oAccount) {
$sKey = $oAccount->ParentEmailHelper();
$sKey = $this->GetMainEmail($oAccount);
}
$sIndexKey = empty($sKey) ? '_default_' : $sKey;
@ -918,7 +923,9 @@ class Actions
$aResult['StartupUrl'] = $this->compileLogParams($aResult['StartupUrl'], $oAccount, true);
}
$aResult['ParentEmail'] = $oAccount->ParentEmail();
if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) {
$aResult['ParentEmail'] = $oAccount->ParentEmail();
}
$oSettingsLocal = $this->SettingsProvider(true)->Load($oAccount);
@ -1128,32 +1135,6 @@ class Actions
return $bFirstOnEmpty && isset($aIdentities[0]) ? $aIdentities[0] : null;
}
/**
* @throws \MailSo\Base\Exceptions\Exception
*/
public function getAccountUnreadCountFromHash(string $sHash): int
{
$iResult = 0;
$oAccount = $this->GetAccountFromCustomToken($sHash);
if ($oAccount) {
try {
$oMailClient = new \MailSo\Mail\MailClient();
$oMailClient->SetLogger($this->Logger());
$oAccount->IncConnectAndLoginHelper($this->Plugins(), $oMailClient, $this->Config());
$iResult = $oMailClient->InboxUnreadCount();
$oMailClient->Disconnect();
} catch (\Throwable $oException) {
$this->Logger()->WriteException($oException);
}
}
return $iResult;
}
public function setConfigFromParams(Config\Application $oConfig, string $sParamName, string $sConfigSector, string $sConfigName, string $sType = 'string', ?callable $mStringCallback = null): void
{
$sValue = $this->GetActionParam($sParamName, '');
@ -1420,28 +1401,10 @@ class Actions
if (\count($aData)) {
$this->Logger()->Write('Import contacts from csv');
$iCount = $oAddressBookProvider->ImportCsvArray($oAccount->ParentEmailHelper(), $aData);
}
}
}
return $iCount;
}
private function importContactsFromVcfFile(Model\Account $oAccount, /*resource*/ $rFile): int
{
$iCount = 0;
if ($oAccount && \is_resource($rFile)) {
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) {
$sFile = \stream_get_contents($rFile);
if (\is_resource($rFile)) {
\fclose($rFile);
}
if (is_string($sFile) && 5 < \strlen($sFile)) {
$this->Logger()->Write('Import contacts from vcf');
$iCount = $oAddressBookProvider->ImportVcfFile($oAccount->ParentEmailHelper(), $sFile);
$iCount = $oAddressBookProvider->ImportCsvArray(
$this->GetMainEmail($oAccount),
$aData
);
}
}
}

View file

@ -8,67 +8,54 @@ use RainLoop\Model\Account;
use RainLoop\Model\Identity;
use RainLoop\Notifications;
use RainLoop\Providers\Storage\Enumerations\StorageType;
use RainLoop\Utils;
trait Accounts
{
protected function GetMainEmail(Account $oAccount)
{
return $oAccount instanceof \RainLoop\Model\AdditionalAccount ? $oAccount->ParentEmail() : $oAccount->Email();
}
public function GetAccounts(Account $oAccount): array
{
if (\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) {
throw new \LogicException('Only main account can have sub accounts');
}
if ($this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
$sAccounts = $this->StorageProvider()->Get($oAccount,
StorageType::CONFIG,
'accounts'
'additionalaccounts'
);
$aAccounts = $sAccounts ? \json_decode($sAccounts, true) : array();
if (\is_array($aAccounts) && \count($aAccounts)) {
if (1 === \count($aAccounts)) {
$this->SetAccounts($oAccount, array());
} else if (1 < \count($aAccounts)) {
$sOrder = $this->StorageProvider()->Get($oAccount,
StorageType::CONFIG,
'accounts_identities_order'
);
$aOrder = empty($sOrder) ? array() : \json_decode($sOrder, true);
if (isset($aOrder['Accounts']) && \is_array($aOrder['Accounts']) && 1 < \count($aOrder['Accounts'])) {
$aAccounts = \array_merge(\array_flip($aOrder['Accounts']), $aAccounts);
$aAccounts = \array_filter($aAccounts, function ($sHash) {
return 5 < \strlen($sHash);
});
}
}
if ($aAccounts && \is_array($aAccounts)) {
return $aAccounts;
}
}
$aAccounts = array();
if (!$oAccount->IsAdditionalAccount()) {
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken();
}
return $aAccounts;
return array();
}
protected function SetAccounts(Account $oAccount, array $aAccounts = array()): void
{
$sParentEmail = $oAccount->ParentEmailHelper();
if (!$aAccounts ||
(1 === \count($aAccounts) && !empty($aAccounts[$sParentEmail]))) {
if (\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) {
throw new \LogicException('Only main account can have sub accounts');
}
$sParentEmail = $oAccount->Email();
if (!$aAccounts) {
$this->StorageProvider()->Clear(
$oAccount,
StorageType::CONFIG,
'accounts'
'additionalaccounts'
);
} else {
$this->StorageProvider()->Put(
$oAccount,
StorageType::CONFIG,
'accounts',
'additionalaccounts',
\json_encode($aAccounts)
);
}
@ -79,36 +66,30 @@ trait Accounts
*/
public function DoAccountSetup(): array
{
$oAccount = $this->getAccountFromToken();
$oMainAccount = $this->getMainAccountFromToken();
if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oMainAccount)) {
return $this->FalseResponse(__FUNCTION__);
}
$sParentEmail = $oAccount->ParentEmailHelper();
$aAccounts = $this->GetAccounts($oAccount);
$aAccounts = $this->GetAccounts($oMainAccount);
$sEmail = \trim($this->GetActionParam('Email', ''));
$sPassword = $this->GetActionParam('Password', '');
$bNew = '1' === (string)$this->GetActionParam('New', '1');
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
if ($bNew && ($oAccount->Email() === $sEmail || $sParentEmail === $sEmail || isset($aAccounts[$sEmail]))) {
if ($bNew && ($oMainAccount->Email() === $sEmail || isset($aAccounts[$sEmail]))) {
throw new ClientException(Notifications::AccountAlreadyExists);
} else if (!$bNew && !isset($aAccounts[$sEmail])) {
throw new ClientException(Notifications::AccountDoesNotExist);
}
$oNewAccount = $this->LoginProcess($sEmail, $sPassword);
$oNewAccount->SetParentEmail($sParentEmail);
$oNewAccount = $this->LoginProcess($sEmail, $sPassword, false, $oMainAccount);
$aAccounts[$oNewAccount->Email()] = $oNewAccount->GetAuthToken();
if (!$oAccount->IsAdditionalAccount()) {
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken();
}
$aAccounts[$oNewAccount->Email()] = $oNewAccount->asTokenArray($oMainAccount);
$this->SetAccounts($oMainAccount, $aAccounts);
$this->SetAccounts($oAccount, $aAccounts);
return $this->TrueResponse(__FUNCTION__);
}
@ -117,31 +98,29 @@ trait Accounts
*/
public function DoAccountDelete(): array
{
$oAccount = $this->getAccountFromToken();
$oMainAccount = $this->getMainAccountFromToken();
if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
if (!$this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oMainAccount)) {
return $this->FalseResponse(__FUNCTION__);
}
$sParentEmail = $oAccount->ParentEmailHelper();
$sEmailToDelete = \trim($this->GetActionParam('EmailToDelete', ''));
$sEmailToDelete = \MailSo\Base\Utils::IdnToAscii($sEmailToDelete, true);
$aAccounts = $this->GetAccounts($oAccount);
$aAccounts = $this->GetAccounts($oMainAccount);
if (0 < \strlen($sEmailToDelete) && $sEmailToDelete !== $sParentEmail && isset($aAccounts[$sEmailToDelete])) {
unset($aAccounts[$sEmailToDelete]);
$oAccountToChange = null;
if ($oAccount->Email() === $sEmailToDelete && !empty($aAccounts[$sParentEmail])) {
$oAccountToChange = $this->GetAccountFromCustomToken($aAccounts[$sParentEmail]);
if ($oAccountToChange) {
$this->SetAuthToken($oAccountToChange);
}
if (\strlen($sEmailToDelete) && isset($aAccounts[$sEmailToDelete])) {
$bReload = false;
// $oAccount = $this->getAccountFromToken();
if ($oAccount->Email() === $sEmailToDelete) {
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
$bReload = true;
}
$this->SetAccounts($oAccount, $aAccounts);
return $this->TrueResponse(__FUNCTION__, array('Reload' => !!$oAccountToChange));
unset($aAccounts[$sEmailToDelete]);
$this->SetAccounts($oMainAccount, $aAccounts);
return $this->TrueResponse(__FUNCTION__, array('Reload' => $bReload));
}
return $this->FalseResponse(__FUNCTION__);
@ -188,8 +167,6 @@ trait Accounts
*/
public function DoAccountsAndIdentitiesSortOrder(): array
{
$oAccount = $this->getAccountFromToken();
$aAccounts = $this->GetActionParam('Accounts', null);
$aIdentities = $this->GetActionParam('Identities', null);
@ -197,10 +174,20 @@ trait Accounts
return $this->FalseResponse(__FUNCTION__);
}
return $this->DefaultResponse(__FUNCTION__, $this->StorageProvider()->Put($oAccount,
StorageType::CONFIG, 'accounts_identities_order',
$oAccount = $this->getMainAccountFromToken();
if (1 < \count($aAccounts)) {
$aAccounts = \array_filter(\array_merge(
\array_fill_keys($aOrder['Accounts'], null),
$this->GetAccounts($oAccount)
));
$this->SetAccounts($oAccount, $aAccounts);
}
return $this->DefaultResponse(__FUNCTION__, $this->StorageProvider()->Put(
$this->getMainAccountFromToken(),
StorageType::CONFIG,
'identities_order',
\json_encode(array(
'Accounts' => \is_array($aAccounts) ? $aAccounts : array(),
'Identities' => \is_array($aIdentities) ? $aIdentities : array()
))
));
@ -211,39 +198,39 @@ trait Accounts
*/
public function DoAccountsAndIdentities(): array
{
$oAccount = $this->getAccountFromToken();
$mAccounts = false;
$oAccount = $this->getMainAccountFromToken();
$aAccounts = false;
if ($this->GetCapa(false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
$mAccounts = $this->GetAccounts($oAccount);
$mAccounts = \array_keys($mAccounts);
foreach ($mAccounts as $iIndex => $sName) {
$mAccounts[$iIndex] = \MailSo\Base\Utils::IdnToUtf8($sName);
$aAccounts = \array_map(
'MailSo\\Base\\Utils::IdnToUtf8',
\array_keys($this->GetAccounts($oAccount))
);
if ($aAccounts) {
\array_unshift($aAccounts, \MailSo\Base\Utils::IdnToUtf8($oAccount->Email()));
}
}
return $this->DefaultResponse(__FUNCTION__, array(
'Accounts' => $mAccounts,
'Accounts' => $aAccounts,
'Identities' => $this->GetIdentities($oAccount)
));
}
/**
* @param Account $account
* @param Account $oAccount
* @return Identity[]
*/
public function GetIdentities(Account $account): array
public function GetIdentities(Account $oAccount): array
{
// A custom name for a single identity is also stored in this system
$allowMultipleIdentities = $this->GetCapa(false, Capa::IDENTITIES, $account);
$allowMultipleIdentities = $this->GetCapa(false, Capa::IDENTITIES, $oAccount);
// Get all identities
$identities = $this->IdentitiesProvider()->GetIdentities($account, $allowMultipleIdentities);
$identities = $this->IdentitiesProvider()->GetIdentities($oAccount, $allowMultipleIdentities);
// Sort identities
$orderString = $this->StorageProvider()->Get($account, StorageType::CONFIG, 'accounts_identities_order');
$orderString = $this->StorageProvider()->Get($oAccount, StorageType::CONFIG, 'identities_order');
$order = \json_decode($orderString, true) ?? [];
if (isset($order['Identities']) && \is_array($order['Identities']) && \count($order['Identities']) > 1) {
$list = \array_map(function ($item) {

View file

@ -5,6 +5,11 @@ namespace RainLoop\Actions;
trait Contacts
{
private function GetAccountParentEmail()
{
// $oAccount instanceof \RainLoop\Model\AdditionalAccount ? $oAccount->ParentEmail() : $oAccount->Email()
}
public function DoSaveContactsSyncData() : array
{
$oAccount = $this->getAccountFromToken();
@ -45,7 +50,7 @@ trait Contacts
if (isset($mData['Enable'], $mData['User'], $mData['Password'], $mData['Url']) && $mData['Enable'])
{
$bResult = $oAddressBookProvider->Sync(
$oAccount->ParentEmailHelper(),
$this->GetMainEmail($oAccount),
$mData['Url'], $mData['User'], $mData['Password']);
}
}
@ -75,7 +80,7 @@ trait Contacts
if ($oAbp->IsActive())
{
$iResultCount = 0;
$mResult = $oAbp->GetContacts($oAccount->ParentEmailHelper(),
$mResult = $oAbp->GetContacts($this->GetMainEmail($oAccount),
$iOffset, $iLimit, $sSearch, $iResultCount);
}
@ -98,7 +103,7 @@ trait Contacts
$bResult = false;
if (0 < \count($aFilteredUids) && $this->AddressBookProvider($oAccount)->IsActive())
{
$bResult = $this->AddressBookProvider($oAccount)->DeleteContacts($oAccount->ParentEmailHelper(), $aFilteredUids);
$bResult = $this->AddressBookProvider($oAccount)->DeleteContacts($this->GetMainEmail($oAccount), $aFilteredUids);
}
return $this->DefaultResponse(__FUNCTION__, $bResult);
@ -119,7 +124,7 @@ trait Contacts
$oContact = null;
if (0 < \strlen($sUid))
{
$oContact = $oAddressBookProvider->GetContactByID($oAccount->ParentEmailHelper(), $sUid);
$oContact = $oAddressBookProvider->GetContactByID($this->GetMainEmail($oAccount), $sUid);
}
if (!$oContact)
@ -159,7 +164,7 @@ trait Contacts
$oContact->PopulateDisplayAndFullNameValue(true);
$bResult = $oAddressBookProvider->ContactSave($oAccount->ParentEmailHelper(), $oContact);
$bResult = $oAddressBookProvider->ContactSave($this->GetMainEmail($oAccount), $oContact);
}
return $this->DefaultResponse(__FUNCTION__, array(
@ -300,7 +305,7 @@ trait Contacts
$this->oHttp->ServerNoCache();
return $this->AddressBookProvider($oAccount)->IsActive() ?
$this->AddressBookProvider($oAccount)->Export($oAccount->ParentEmailHelper(), 'vcf') : false;
$this->AddressBookProvider($oAccount)->Export($this->GetMainEmail($oAccount), 'vcf') : false;
}
public function RawContactsCsv() : bool
@ -315,7 +320,31 @@ trait Contacts
$this->oHttp->ServerNoCache();
return $this->AddressBookProvider($oAccount)->IsActive() ?
$this->AddressBookProvider($oAccount)->Export($oAccount->ParentEmailHelper(), 'csv') : false;
$this->AddressBookProvider($oAccount)->Export($this->GetMainEmail($oAccount), 'csv') : false;
}
private function importContactsFromVcfFile(Model\Account $oAccount, /*resource*/ $rFile): int
{
$iCount = 0;
if ($oAccount && \is_resource($rFile)) {
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if ($oAddressBookProvider && $oAddressBookProvider->IsActive()) {
$sFile = \stream_get_contents($rFile);
if (\is_resource($rFile)) {
\fclose($rFile);
}
if (is_string($sFile) && 5 < \strlen($sFile)) {
$this->Logger()->Write('Import contacts from vcf');
$iCount = $oAddressBookProvider->ImportVcfFile(
$this->GetMainEmail($oAccount),
$sFile
);
}
}
}
return $iCount;
}
}

View file

@ -324,7 +324,7 @@ trait Messages
$oSettings = $this->SettingsProvider()->Load($oAccount);
$this->AddressBookProvider($oAccount)->IncFrec(
$oAccount->ParentEmailHelper(), \array_values($aArrayToFrec),
$this->GetMainEmail($oAccount), \array_values($aArrayToFrec),
!!$oSettings->GetConf('ContactsAutosave',
!!$oConfig->Get('defaults', 'contacts_autosave', true)));
}

View file

@ -203,7 +203,7 @@ trait User
$oAccount = $this->getAccountFromToken(false);
if ($oAccount)
{
if (!$oAccount->IsAdditionalAccount())
if (!\is_subclass_of($oAccount, 'RainLoop\\Model\\Account'))
{
$this->ClearSignMeData();
Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
@ -393,7 +393,7 @@ trait User
$oAddressBookProvider = $this->AddressBookProvider($oAccount);
if ($oAddressBookProvider && $oAddressBookProvider->IsActive())
{
$aSuggestions = $oAddressBookProvider->GetSuggestions($oAccount->ParentEmailHelper(), $sQuery, $iLimit);
$aSuggestions = $oAddressBookProvider->GetSuggestions($this->GetMainEmail($oAccount), $sQuery, $iLimit);
if (!\count($aResult))
{
$aResult = $aSuggestions;

View file

@ -5,6 +5,7 @@ namespace RainLoop\Actions;
use RainLoop\Notifications;
use RainLoop\Utils;
use RainLoop\Model\Account;
use RainLoop\Model\AdditionalAccount;
use RainLoop\Providers\Storage\Enumerations\StorageType;
use RainLoop\Exceptions\ClientException;
@ -13,18 +14,13 @@ trait UserAuth
/**
* @var string
*/
private $sSpecAuthToken = null;
private $oSpecAuthAccount = null;
/**
* Or use 'aes-256-xts' ?
*/
private static $sCipher = 'aes-256-cbc-hmac-sha1';
private $oAdditionalAuthAccount = null;
private $oMainAuthAccount = null;
/**
* @throws \RainLoop\Exceptions\ClientException
*/
public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe = false): Account
public function LoginProcess(string &$sEmail, string &$sPassword, bool $bSignMe = false, Account $oMainAccount = null): Account
{
$sInputEmail = $sEmail;
@ -117,7 +113,14 @@ trait UserAuth
$oAccount = null;
$sClientCert = \trim($this->Config()->Get('ssl', 'client_cert', ''));
try {
$oAccount = Account::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true);
if ($oMainAccount) {
$oAccount = AdditionalAccount::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true);
if ($oAccount) {
$oAccount->SetParentEmail($oMainAccount->Email());
}
} else {
$oAccount = Account::NewInstanceByLogin($this, $sEmail, $sLogin, $sPassword, $sClientCert, true);
}
if (!$oAccount) {
throw new ClientException(Notifications::AuthError);
@ -141,86 +144,103 @@ trait UserAuth
}
/**
* @throws \RainLoop\Exceptions\ClientException
*/
public function GetAccountFromCustomToken(string $sToken): ?Account
{
return empty($sToken) ? null : Account::NewInstanceFromAuthToken($sToken);
}
/**
* Returns RainLoop\Model\AdditionalAccount when it exists,
* else returns RainLoop\Model\Account when it exists,
* else null
*
* @throws \RainLoop\Exceptions\ClientException
*/
public function getAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account
{
if (!$this->oSpecAuthAccount) {
if (!\is_string($this->sSpecAuthToken)) {
$this->sSpecAuthToken = '';
if (isset($_COOKIE[self::AUTH_SPEC_LOGOUT_TOKEN_KEY])) {
Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY);
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
// Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
} else {
$sAuthAccountHash = Utils::GetCookie(self::AUTH_SPEC_TOKEN_KEY);
if (empty($sAuthAccountHash)) {
$oAccount = $this->GetAccountFromSignMeToken();
if ($oAccount) {
$this->SetAuthToken($oAccount);
}
} else {
$this->sSpecAuthToken = $sAuthAccountHash;
}
}
}
$aData = \SnappyMail\Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode($this->sSpecAuthToken));
if (!empty($aData)) {
$this->oSpecAuthAccount = Account::NewInstanceFromTokenArray(
if (\is_null($this->oAdditionalAuthAccount) && isset($_COOKIE[self::AUTH_ADDITIONAL_TOKEN_KEY])) {
$aData = Utils::GetSecureCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
if ($aData) {
$this->oAdditionalAuthAccount = AdditionalAccount::NewInstanceFromTokenArray(
$this,
$aData,
$bThrowExceptionOnFalse
);
}
if (!$this->oAdditionalAuthAccount) {
$this->oAdditionalAuthAccount = false;
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
}
}
return $this->oAdditionalAuthAccount ?: $this->getMainAccountFromToken($bThrowExceptionOnFalse);
}
if ($bThrowExceptionOnFalse && !$this->oSpecAuthAccount) {
/**
* Returns RainLoop\Model\Account when it exists, else null
*
* @throws \RainLoop\Exceptions\ClientException
*/
public function getMainAccountFromToken(bool $bThrowExceptionOnFalse = true): ?Account
{
if (!$this->oMainAuthAccount) {
if (isset($_COOKIE[self::AUTH_SPEC_LOGOUT_TOKEN_KEY])) {
Utils::ClearCookie(self::AUTH_SPEC_LOGOUT_TOKEN_KEY);
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
// Utils::ClearCookie(self::AUTH_SPEC_TOKEN_KEY);
// Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
}
$aData = Utils::GetSecureCookie(self::AUTH_SPEC_TOKEN_KEY);
if ($aData) {
$this->oMainAuthAccount = Account::NewInstanceFromTokenArray(
$this,
$aData,
$bThrowExceptionOnFalse
);
} else {
$oAccount = $this->GetAccountFromSignMeToken();
if ($oAccount) {
$this->SetAuthToken($oAccount);
}
}
if ($bThrowExceptionOnFalse && !$this->oMainAuthAccount) {
throw new ClientException(\RainLoop\Notifications::AuthError);
}
}
return $this->oSpecAuthAccount;
return $this->oMainAuthAccount;
}
public function SetAuthToken(Account $oAccount): void
{
$sSpecAuthToken = \MailSo\Base\Utils::UrlSafeBase64Encode(\SnappyMail\Crypt::EncryptToJSON($oAccount));
if (\is_subclass_of($oAccount, 'RainLoop\\Model\\Account')) {
throw new \LogicException('Only main Account can be set as AuthToken');
}
$this->oAdditionalAuthAccount = false;
$this->oMainAuthAccount = $oAccount;
Utils::SetSecureCookie(self::AUTH_SPEC_TOKEN_KEY, $oAccount);
}
$this->sSpecAuthToken = $sSpecAuthToken;
$this->oSpecAuthAccount = null;
Utils::SetCookie(self::AUTH_SPEC_TOKEN_KEY, $sSpecAuthToken);
if (isset($aAccounts[$oAccount->Email()])) {
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken();
$this->SetAccounts($oAccount, $aAccounts);
public function SetAdditionalAuthToken(?AdditionalAccount $oAccount): void
{
$this->oAdditionalAuthAccount = $oAccount ?: false;
if ($oAccount) {
Utils::SetSecureCookie(self::AUTH_ADDITIONAL_TOKEN_KEY, $oAccount);
} else {
Utils::ClearCookie(self::AUTH_ADDITIONAL_TOKEN_KEY);
}
}
/**
* SignMe methods used for the "remember me" cookie
*/
private static function GetSignMeToken(): ?array
{
$sSignMeToken = Utils::GetCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
if (empty($sSignMeToken)) {
return null;
if ($sSignMeToken) {
$aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken);
if (isset($aResult['e'], $aResult['u']) && \SnappyMail\UUID::isValid($aResult['u'])) {
return $aResult;
}
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
}
$aResult = \SnappyMail\Crypt::DecryptUrlSafe($sSignMeToken);
return \is_array($aResult) ? $aResult : null;
}
private static function SetSignMeTokenCookie(array $aData): void
{
Utils::SetCookie(
self::AUTH_SIGN_ME_TOKEN_KEY,
\SnappyMail\Crypt::EncryptUrlSafe($aData),
\time() + 3600 * 24 * 30 // 30 days
);
return null;
}
private function SetSignMeToken(Account $oAccount): void
@ -230,25 +250,15 @@ trait UserAuth
$uuid = \SnappyMail\UUID::generate();
$data = \SnappyMail\Crypt::Encrypt($oAccount);
if ('xxtea' === $data[0]) {
static::SetSignMeTokenCookie(array(
Utils::SetCookie(
self::AUTH_SIGN_ME_TOKEN_KEY,
\SnappyMail\Crypt::EncryptUrlSafe([
'e' => $oAccount->Email(),
'u' => $uuid,
'x' => \base64_encode($data[1])
));
} else if ('sodium' === $data[0]) {
static::SetSignMeTokenCookie(array(
'e' => $oAccount->Email(),
'u' => $uuid,
's' => \base64_encode($data[1])
));
} else {
static::SetSignMeTokenCookie(array(
'e' => $oAccount->Email(),
'u' => $uuid,
'o' => \base64_encode($data[1])
));
}
$data[0] => \base64_encode($data[1])
]),
\time() + 3600 * 24 * 30 // 30 days
);
$this->StorageProvider()->Put(
$oAccount,
@ -261,58 +271,55 @@ trait UserAuth
public function GetAccountFromSignMeToken(): ?Account
{
$aTokenData = static::GetSignMeToken();
if (!empty($aTokenData)) {
$oAccount = null;
if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) {
if ($aTokenData) {
try
{
$sAuthToken = $this->StorageProvider()->Get(
$aTokenData['e'],
StorageType::SIGN_ME,
$aTokenData['u']
);
if (empty($sAuthToken)) {
return null;
}
if (!empty($aTokenData['x'])) {
$aAccountHash = \SnappyMail\Crypt::XxteaDecrypt($sAuthToken, \base64_decode($aTokenData['x']));
} else if (!empty($aTokenData['s'])) {
$aAccountHash = \SnappyMail\Crypt::SodiumDecrypt($sAuthToken, \base64_decode($aTokenData['s']));
} else if (!empty($aTokenData['o'])) {
$aAccountHash = \SnappyMail\Crypt::OpenSSLDecrypt($sAuthToken, \base64_decode($aTokenData['o']));
}
if (!empty($aAccountHash) && \is_array($aAccountHash)) {
$oAccount = Account::NewInstanceFromTokenArray($this, $aAccountHash);
if ($oAccount) {
try
{
$this->CheckMailConnection($oAccount);
// Update lifetime
$this->SetSignMeToken($oAccount);
if ($sAuthToken) {
$aAccountHash = \SnappyMail\Crypt::Decrypt([
\array_key_last($aTokenData),
\base64_decode(\end($aTokenData)),
$sAuthToken
]);
return $oAccount;
}
catch (\Throwable $oException)
{
}
$oAccount = \is_array($aAccountHash)
? Account::NewInstanceFromTokenArray($this, $aAccountHash) : null;
if ($oAccount) {
$this->CheckMailConnection($oAccount);
// Update lifetime
$this->SetSignMeToken($oAccount);
return $oAccount;
}
}
}
catch (\Throwable $oException)
{
}
$this->ClearSignMeData();
}
$this->ClearSignMeData();
return null;
}
protected function ClearSignMeData() : void
{
if (isset($_COOKIE[self::AUTH_SIGN_ME_TOKEN_KEY])) {
$aTokenData = static::GetSignMeToken();
if (!empty($aTokenData['e']) && !empty($aTokenData['u']) && \SnappyMail\UUID::isValid($aTokenData['u'])) {
$this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']);
}
$aTokenData = static::GetSignMeToken();
if ($aTokenData) {
$this->StorageProvider()->Clear($aTokenData['e'], StorageType::SIGN_ME, $aTokenData['u']);
Utils::ClearCookie(self::AUTH_SIGN_ME_TOKEN_KEY);
}
}
/**
* Logout methods
*/
public function SetAuthLogoutToken(): void
{
\header('X-RainLoop-Action: Logout');

View file

@ -42,21 +42,11 @@ class Account implements \JsonSerializable
*/
private $oDomain;
/**
* @var string
*/
private $sParentEmail = '';
public function Email() : string
{
return $this->sEmail;
}
public function ParentEmail() : string
{
return $this->sParentEmail;
}
public function ProxyAuthUser() : string
{
return $this->sProxyAuthUser;
@ -67,16 +57,6 @@ class Account implements \JsonSerializable
return $this->sProxyAuthPassword;
}
public function ParentEmailHelper() : string
{
return $this->sParentEmail ?: $this->sEmail;
}
public function IsAdditionalAccount() : bool
{
return !empty($this->sParentEmail);
}
public function IncLogin() : string
{
$sLogin = $this->sLogin;
@ -135,8 +115,7 @@ class Account implements \JsonSerializable
$this->sEmail,
$this->Domain()->IncHost(),
$this->Domain()->IncPort(),
$this->sPassword,
$this->sParentEmail
$this->sPassword
]));
}
@ -145,11 +124,6 @@ class Account implements \JsonSerializable
$this->sPassword = $sPassword;
}
public function SetParentEmail(string $sParentEmail) : void
{
$this->sParentEmail = \trim(\MailSo\Base\Utils::IdnToAscii($sParentEmail, true));
}
public function SetProxyAuthUser(string $sProxyAuthUser) : void
{
$this->sProxyAuthUser = $sProxyAuthUser;
@ -168,29 +142,15 @@ class Account implements \JsonSerializable
$this->sLogin, // 2
$this->sPassword, // 3
$this->sClientCert, // 4
$this->sParentEmail, // 5
$this->sProxyAuthUser, // 6
$this->sProxyAuthPassword // 7
$this->sProxyAuthUser, // 5
$this->sProxyAuthPassword // 6
);
}
public function GetAuthToken() : string
{
return Utils::EncodeKeyValues($this->jsonSerialize());
/*
return \MailSo\Base\Utils::UrlSafeBase64Encode(
\MailSo\Base\Crypt::Encrypt(
\json_encode($this),
\md5(APP_SALT.$sCustomKey)
)
);
*/
}
/**
* @throws \RainLoop\Exceptions\ClientException
*/
public static function NewInstanceFromAuthToken(\RainLoop\Actions $oActions, string $sToken): ?Account
public static function NewInstanceFromAuthToken(\RainLoop\Actions $oActions, string $sToken): ?self
{
return empty($sToken) ? null
: static::NewInstanceFromTokenArray(
@ -200,14 +160,16 @@ class Account implements \JsonSerializable
);
}
public static function NewInstanceByLogin(\RainLoop\Actions $oActions, string $sEmail, string $sLogin, string $sPassword, string $sClientCert = '', bool $bThrowException = false): ?self
public static function NewInstanceByLogin(\RainLoop\Actions $oActions,
string $sEmail, string $sLogin, string $sPassword, string $sClientCert = '',
bool $bThrowException = false): ?self
{
$oAccount = null;
if (\strlen($sEmail) && \strlen($sLogin) && \strlen($sPassword)) {
$oDomain = $oActions->DomainProvider()->Load(\MailSo\Base\Utils::GetDomainFromEmail($sEmail), true);
if ($oDomain) {
if ($oDomain->ValidateWhiteList($sEmail, $sLogin)) {
$oAccount = new self;
$oAccount = new static;
$oAccount->sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail, true);
$oAccount->sLogin = \MailSo\Base\Utils::IdnToAscii($sLogin);
@ -236,7 +198,7 @@ class Account implements \JsonSerializable
array $aAccountHash,
bool $bThrowExceptionOnFalse = false): ?self
{
if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 8 === \count($aAccountHash)) {
if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 7 === \count($aAccountHash)) {
$oAccount = static::NewInstanceByLogin(
$oActions,
$aAccountHash[1],
@ -248,15 +210,14 @@ class Account implements \JsonSerializable
if ($oAccount) {
// init proxy user/password
if (!empty($aAccountHash[6]) && !empty($aAccountHash[7])) {
$oAccount->SetProxyAuthUser($aAccountHash[6]);
$oAccount->SetProxyAuthPassword($aAccountHash[7]);
if (!empty($aAccountHash[5]) && !empty($aAccountHash[6])) {
$oAccount->SetProxyAuthUser($aAccountHash[5]);
$oAccount->SetProxyAuthPassword($aAccountHash[6]);
}
$oActions->Logger()->AddSecret($oAccount->Password());
$oActions->Logger()->AddSecret($oAccount->ProxyAuthPassword());
$oAccount->SetParentEmail($aAccountHash[5]);
return $oAccount;
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace RainLoop\Model;
use RainLoop\Utils;
use RainLoop\Exceptions\ClientException;
class AdditionalAccount extends Account
{
/**
* @var string
*/
private $sParentEmail = '';
public function ParentEmail() : string
{
return $this->sParentEmail;
}
public function SetParentEmail(string $sParentEmail) : void
{
$this->sParentEmail = \trim(\MailSo\Base\Utils::IdnToAscii($sParentEmail, true));
}
public function PasswordHash() : string
{
throw new \LogicException('Not allowed on AdditionalAccount');
}
public function Hash() : string
{
return \md5(parent::Hash() . $this->sParentEmail);
}
public function jsonSerialize()
{
$aData = parent::jsonSerialize();
$aData[] = $this->sParentEmail;
return $aData;
}
public function asTokenArray(Account $oMainAccount) : array
{
$sHash = $oMainAccount->PasswordHash();
$aData = $this->jsonSerialize();
$aData[3] = \SnappyMail\Crypt::EncryptUrlSafe($aData[3], $sHash);
$aData[] = \hash_hmac('sha1', $aData[3], $sHash);
return $aData;
}
public static function NewInstanceFromTokenArray(
\RainLoop\Actions $oActions,
array $aAccountHash,
bool $bThrowExceptionOnFalse = false) : ?Account /* PHP7.4: ?self*/
{
$iCount = \count($aAccountHash);
if (!empty($aAccountHash[0]) && 'account' === $aAccountHash[0] && 8 <= $iCount && 9 >= $iCount) {
$sHash = $oActions->getMainAccountFromToken()->PasswordHash();
$sPasswordHMAC = (8 < $iCount) ? \array_pop($aAccountHash) : null;
$sParentEmail = \array_pop($aAccountHash);
if ($sPasswordHMAC && $sPasswordHMAC === \hash_hmac('sha1', $aAccountHash[3], $sHash)) {
$aAccountHash[3] = \SnappyMail\Crypt::DecryptUrlSafe($aAccountHash[3], $sHash);
}
$oAccount = parent::NewInstanceFromTokenArray($oActions, $aAccountHash, $bThrowExceptionOnFalse);
if ($oAccount) {
$oAccount->SetParentEmail($sParentEmail);
return $oAccount;
}
}
return null;
}
}

View file

@ -142,8 +142,13 @@ class FileStorage implements \RainLoop\Providers\Files\IFiles
private function generateFullFileName(\RainLoop\Model\Account $oAccount, string $sKey, bool $bMkDir = false) : string
{
$sEmail = $oAccount->ParentEmailHelper() ?: 'nobody@unknown.tld';
$sSubEmail = $oAccount->IsAdditionalAccount() ? $oAccount->Email() : '';
if ($oAccount instanceof \RainLoop\Model\AdditionalAccount) {
$sEmail = $oAccount->ParentEmail();
$sSubEmail = $oAccount->Email();
} else {
$sEmail = $oAccount->Email() ?: 'nobody@unknown.tld';
$sSubEmail = '';
}
$aEmail = \explode('@', $sEmail ?: 'nobody@unknown.tld');
$sDomain = \trim(1 < \count($aEmail) ? \array_pop($aEmail) : '');

View file

@ -103,8 +103,8 @@ class FileStorage implements \RainLoop\Providers\Storage\IStorage
if (null === $mAccount) {
$iStorageType = StorageType::NOBODY;
} else if ($mAccount instanceof \RainLoop\Model\Account) {
$sEmail = $mAccount->ParentEmailHelper();
if ($this->bLocal && $mAccount->IsAdditionalAccount() && !$bForDeleteAction)
$sEmail = $mAccount instanceof \RainLoop\Model\AdditionalAccount ? $mAccount->ParentEmail() : $mAccount->Email();
if ($this->bLocal && $mAccount instanceof \RainLoop\Model\AdditionalAccount && !$bForDeleteAction)
{
$sSubEmail = $mAccount->Email();
}

View file

@ -811,14 +811,16 @@ class ServiceActions
return '';
}
/**
* Switch to AdditionalAccount
*/
public function ServiceChange() : string
{
$this->oHttp->ServerNoCache();
// $oAccount = $this->oActions->getAccountFromToken(false);
$oAccount = $this->oActions->GetAccount();
$oMainAccount = $this->oActions->getMainAccountFromToken(false);
if ($oAccount && $this->oActions->GetCapa(false, Enumerations\Capa::ADDITIONAL_ACCOUNTS, $oAccount))
if ($oMainAccount && $this->oActions->GetCapa(false, Enumerations\Capa::ADDITIONAL_ACCOUNTS, $oMainAccount))
{
$oAccountToLogin = null;
$sEmail = empty($this->aPaths[2]) ? '' : \urldecode(\trim($this->aPaths[2]));
@ -826,17 +828,16 @@ class ServiceActions
{
$sEmail = \MailSo\Base\Utils::IdnToAscii($sEmail);
$aAccounts = $this->oActions->GetAccounts($oAccount);
$aAccounts = $this->oActions->GetAccounts($oMainAccount);
if (isset($aAccounts[$sEmail]))
{
$oAccountToLogin = $this->oActions->GetAccountFromCustomToken($aAccounts[$sEmail]);
$oAccountToLogin = \RainLoop\Model\AdditionalAccount::NewInstanceFromTokenArray(
$this->oActions,
$aAccounts[$sEmail]
);
}
}
if ($oAccountToLogin)
{
$this->oActions->SetAuthToken($oAccountToLogin);
}
$this->oActions->SetAdditionalAuthToken($oAccountToLogin);
}
$this->oActions->Location('./');

View file

@ -147,6 +147,13 @@ class Utils
return isset($_COOKIE[$sName]) ? $_COOKIE[$sName] : $mDefault;
}
public static function GetSecureCookie(string $sName)
{
return isset($_COOKIE[$sName])
? \SnappyMail\Crypt::DecryptFromJSON(\MailSo\Base\Utils::UrlSafeBase64Decode($_COOKIE[$sName]))
: null;
}
public static function SetCookie(string $sName, string $sValue = '', int $iExpire = 0, bool $bHttpOnly = true)
{
$sPath = static::$CookieDefaultPath;
@ -161,6 +168,16 @@ class Utils
));
}
public static function SetSecureCookie(string $sName, $mValue, int $iExpire = 0, bool $bHttpOnly = true)
{
static::SetCookie(
$sName,
\MailSo\Base\Utils::UrlSafeBase64Encode(\SnappyMail\Crypt::EncryptToJSON($mValue)),
$iExpire,
true
);
}
public static function ClearCookie(string $sName)
{
if (isset($_COOKIE[$sName])) {

View file

@ -28,8 +28,8 @@ abstract class Crypt
public static function setCipher(string $cipher) : bool
{
if ($cipher) {
$aCiphers = static::listCiphers();
if (\in_array($cipher, $aCiphers)) {
$ciphers = static::listCiphers();
if (\in_array($cipher, $ciphers)) {
static::$cipher = $cipher;
return true;
}
@ -48,63 +48,60 @@ abstract class Crypt
);
}
public static function DecryptUrlSafe($sData, string $key = null) /* : mixed */
{
return \json_decode(
\zlib_decode(
static::XxteaDecrypt(
\MailSo\Base\Utils::UrlSafeBase64Decode($sData),
static::Passphrase($key)
)
),
true
);
}
public static function EncryptUrlSafe($mData, string $key = null) : string
{
return \MailSo\Base\Utils::UrlSafeBase64Encode(
static::XxteaEncrypt(
\zlib_encode(
\json_encode($mData),
ZLIB_ENCODING_RAW,
9
),
static::Passphrase($key)
)
);
}
public static function Decrypt(array $data, string $key = null) /* : mixed */
{
if (3 === \count($data) && isset($data[0], $data[1], $data[2])) {
$fn = "{$data[0]}Decrypt";
if (\method_exists(__CLASS__, $fn)) {
return \SnappyMail\Crypt::{$fn}($data[2], $data[1], $key);
try {
$fn = "{$data[0]}Decrypt";
if (\method_exists(__CLASS__, $fn)) {
$result = static::{$fn}($data[2], $data[1], $key);
return \json_decode($result, true);
}
} catch (\Throwable $e) {
\trigger_error(__CLASS__ . "::{$fn}(): " . $e->getMessage());
}
} else {
\trigger_error(__CLASS__ . '::Decrypt() invalid $data');
}
}
public static function DecryptFromJSON(string $data, string $key = null) /* : mixed */
{
$aData = \json_decode($data, true);
return \is_array($aData) ? static::Decrypt(\array_map('base64_decode', $aData), $key) : null;
$data = \json_decode($data, true);
if (!\is_array($data)) {
\trigger_error(__CLASS__ . '::DecryptFromJSON() invalid $data');
return null;
}
return static::Decrypt(\array_map('base64_decode', $data), $key);
}
public static function DecryptUrlSafe(string $data, string $key = null) /* : mixed */
{
$data = \explode('.', $data);
if (!\is_array($data)) {
\trigger_error(__CLASS__ . '::DecryptUrlSafe() invalid $data');
return null;
}
return static::Decrypt(\array_map('MailSo\\Base\\Utils::UrlSafeBase64Decode', $data), $key);
}
public static function Encrypt($data, string $key = null) : array
{
$data = \json_encode($data);
if (\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) {
$nonce = \random_bytes(24);
return ['sodium', $nonce, static::SodiumEncrypt($data, $nonce)];
}
if (static::$cipher && \is_callable('openssl_encrypt')) {
$nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES);
$result = ['sodium', $nonce, static::SodiumEncrypt($data, $nonce, $key)];
} else if (static::$cipher && \is_callable('openssl_encrypt')) {
$iv = \random_bytes(\openssl_cipher_iv_length(static::$cipher));
return ['openssl', $iv, static::OpenSSLEncrypt($data, $iv)];
$result = ['openssl', $iv, static::OpenSSLEncrypt($data, $iv, $key)];
} else {
$salt = \random_bytes(16);
$result = ['xxtea', $salt, static::XxteaEncrypt($data, $salt, $key)];
}
$salt = \random_bytes(16);
return ['xxtea', $salt, static::XxteaEncrypt($data, $salt)];
if (static::{"{$result[0]}Decrypt"}($result[2], $result[1], $key) !== $data) {
throw new \Exception('Encrypt/Decrypt mismatch');
}
return $result;
}
public static function EncryptToJSON($data, string $key = null) : string
@ -112,26 +109,31 @@ abstract class Crypt
return \json_encode(\array_map('base64_encode', static::Encrypt($data, $key)));
}
public static function EncryptUrlSafe($data, string $key = null) : string
{
return \implode('.', \array_map('MailSo\\Base\\Utils::UrlSafeBase64Encode', static::Encrypt($data, $key)));
}
public static function SodiumDecrypt(string $data, string $nonce, string $key = null) /* : mixed */
{
if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_decrypt')) {
return null;
}
return \json_decode(\zlib_decode(\sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
return \sodium_crypto_aead_xchacha20poly1305_ietf_decrypt(
$data,
APP_SALT,
$nonce,
\str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key))
)));
);
}
public static function SodiumEncrypt($data, string $nonce, string $key = null) : ?string
public static function SodiumEncrypt(string $data, string $nonce, string $key = null) : ?string
{
if (!\is_callable('sodium_crypto_aead_xchacha20poly1305_ietf_encrypt')) {
return null;
}
return \sodium_crypto_aead_xchacha20poly1305_ietf_encrypt(
\zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9),
$data,
APP_SALT,
$nonce,
\str_pad('', \SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_KEYBYTES, static::Passphrase($key))
@ -143,22 +145,22 @@ abstract class Crypt
if (!$data || !$iv || !static::$cipher || !\is_callable('openssl_decrypt')) {
return null;
}
return \json_decode(\zlib_decode(\openssl_decrypt(
return \openssl_decrypt(
$data,
static::$cipher,
static::Passphrase($key),
OPENSSL_RAW_DATA,
$iv
)), true);
);
}
public static function OpenSSLEncrypt($data, string $iv, string $key = null) : ?string
public static function OpenSSLEncrypt(string $data, string $iv, string $key = null) : ?string
{
if (!$data || !$iv || !static::$cipher || !\is_callable('openssl_encrypt')) {
return null;
}
return \openssl_encrypt(
\zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9),
$data,
static::$cipher,
static::Passphrase($key),
OPENSSL_RAW_DATA,
@ -172,18 +174,16 @@ abstract class Crypt
return null;
}
$key = $salt . static::Passphrase($key);
return \json_decode(\zlib_decode(\is_callable('xxtea_decrypt')
return \is_callable('xxtea_decrypt')
? \xxtea_decrypt($data, $key)
: \MailSo\Base\Xxtea::decrypt($data, $key)
), true);
: \MailSo\Base\Xxtea::decrypt($data, $key);
}
public static function XxteaEncrypt($data, string $salt, string $key = null) : ?string
public static function XxteaEncrypt(string $data, string $salt, string $key = null) : ?string
{
if (!$data || !$salt) {
return null;
}
$data = \zlib_encode(\json_encode($data), ZLIB_ENCODING_RAW, 9);
$key = $salt . static::Passphrase($key);
return \is_callable('xxtea_encrypt')
? \xxtea_encrypt($data, $key)

View file

@ -5,35 +5,33 @@
<span data-bind="visible: !isNew()" data-i18n="POPUPS_ADD_ACCOUNT/TITLE_UPDATE_ACCOUNT"></span>
</h3>
</header>
<div class="modal-body">
<div class="form-horizontal">
<div class="alert" data-bind="visible: '' !== submitError()">
<button type="button" class="close" data-bind="click: function () { submitError('') }">×</button>
<span data-bind="text: submitError"></span>
<div data-bind="visible: submitErrorAdditional">
<br />
<span data-bind="text: submitErrorAdditional"></span>
</div>
</div>
<br />
<div class="control-group" data-bind="css: {'error': emailError}">
<label class="control-label" data-i18n="GLOBAL/EMAIL"></label>
<div class="controls">
<label style="margin-top: 5px;" data-bind="visible: !isNew()"><strong data-bind="text: email"></strong></label>
<input type="email" class="input-xlarge"
autofocus="" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="visible: isNew, textInput: email, onEnter: addAccountCommand" />
</div>
</div>
<div class="control-group" data-bind="css: {'error': passwordError}">
<label class="control-label" data-i18n="GLOBAL/PASSWORD"></label>
<div class="controls">
<input type="password" class="inputPassword input-xlarge" autocomplete="current-password" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: password, onEnter: addAccountCommand" />
</div>
<form class="modal-body form-horizontal" autocomplete="off">
<div class="alert" data-bind="visible: '' !== submitError()">
<button type="button" class="close" data-bind="click: function () { submitError('') }">×</button>
<span data-bind="text: submitError"></span>
<div data-bind="visible: submitErrorAdditional">
<br />
<span data-bind="text: submitErrorAdditional"></span>
</div>
</div>
</div>
<br />
<div class="control-group" data-bind="css: {'error': emailError}">
<label class="control-label" data-i18n="GLOBAL/EMAIL"></label>
<div class="controls">
<label style="margin-top: 5px;" data-bind="visible: !isNew()"><strong data-bind="text: email"></strong></label>
<input type="email" class="input-xlarge"
autofocus="" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="visible: isNew, textInput: email, onEnter: addAccountCommand" />
</div>
</div>
<div class="control-group" data-bind="css: {'error': passwordError}">
<label class="control-label" data-i18n="GLOBAL/PASSWORD"></label>
<div class="controls">
<input type="password" class="inputPassword input-xlarge" autocomplete="new-password" autocorrect="off" autocapitalize="off" spellcheck="false"
data-bind="value: password, onEnter: addAccountCommand" />
</div>
</div>
</form>
<footer>
<a class="btn buttonAddAccount" data-bind="command: addAccountCommand">
<i data-bind="visible: isNew, css: {'icon-user-add': !submitRequest(), 'icon-spinner': submitRequest()}"></i>