Merge pull request #39 from FWest98/fw/identities

Add a pluggable identity system
This commit is contained in:
the-djmaze 2020-11-12 23:29:00 +01:00 committed by GitHub
commit 222c51d61a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 985 additions and 1088 deletions

View file

@ -8,14 +8,15 @@ RUN apt-get install -y \
build-essential chrpath libssl-dev \
libxft-dev libfreetype6 libfreetype6-dev \
libpng-dev libjpeg62-turbo-dev \
libfontconfig1 libfontconfig1-dev libzip-dev
libfontconfig1 libfontconfig1-dev libzip-dev libldap2-dev
RUN pecl install mcrypt-1.0.2 && \
docker-php-ext-enable mcrypt
RUN docker-php-ext-configure intl && \
docker-php-ext-configure ldap && \
docker-php-ext-configure gd --with-freetype-dir=/usr/include/ --with-jpeg-dir=/usr/include/ && \
docker-php-ext-install opcache pdo_mysql zip intl gd
docker-php-ext-install opcache pdo_mysql zip intl gd ldap
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

2
.editorconfig Normal file
View file

@ -0,0 +1,2 @@
[*.php]
indent_style = tab

1
.gitignore vendored
View file

@ -15,3 +15,4 @@
/data
/MULTIPLY
/include.php
.idea/

File diff suppressed because it is too large Load diff

View file

@ -2,9 +2,13 @@
namespace RainLoop\Actions;
use \RainLoop\Enumerations\Capa;
use \RainLoop\Exceptions\ClientException;
use \RainLoop\Notifications;
use RainLoop\Enumerations\Capa;
use RainLoop\Exceptions\ClientException;
use RainLoop\Model\Account;
use RainLoop\Model\Identity;
use RainLoop\Notifications;
use RainLoop\Providers\Storage\Enumerations\StorageType;
use function trim;
trait Accounts
{
@ -12,12 +16,11 @@ trait Accounts
/**
* @throws \MailSo\Base\Exceptions\Exception
*/
public function DoAccountSetup() : array
public function DoAccountSetup(): array
{
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(false, false, Capa::ADDITIONAL_ACCOUNTS, $oAccount))
{
if (!$this->GetCapa(false, false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
return $this->FalseResponse(__FUNCTION__);
}
@ -25,17 +28,14 @@ trait Accounts
$aAccounts = $this->GetAccounts($oAccount);
$sEmail = \trim($this->GetActionParam('Email', ''));
$sEmail = trim($this->GetActionParam('Email', ''));
$sPassword = $this->GetActionParam('Password', '');
$bNew = '1' === (string) $this->GetActionParam('New', '1');
$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 && ($oAccount->Email() === $sEmail || $sParentEmail === $sEmail || isset($aAccounts[$sEmail]))) {
throw new ClientException(Notifications::AccountAlreadyExists);
}
else if (!$bNew && !isset($aAccounts[$sEmail]))
{
} else if (!$bNew && !isset($aAccounts[$sEmail])) {
throw new ClientException(Notifications::AccountDoesNotExist);
}
@ -43,8 +43,7 @@ trait Accounts
$oNewAccount->SetParentEmail($sParentEmail);
$aAccounts[$oNewAccount->Email()] = $oNewAccount->GetAuthToken();
if (!$oAccount->IsAdditionalAccount())
{
if (!$oAccount->IsAdditionalAccount()) {
$aAccounts[$oAccount->Email()] = $oAccount->GetAuthToken();
}
@ -55,31 +54,27 @@ trait Accounts
/**
* @throws \MailSo\Base\Exceptions\Exception
*/
public function DoAccountDelete() : array
public function DoAccountDelete(): array
{
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(false, false, Capa::ADDITIONAL_ACCOUNTS, $oAccount))
{
if (!$this->GetCapa(false, false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
return $this->FalseResponse(__FUNCTION__);
}
$sParentEmail = $oAccount->ParentEmailHelper();
$sEmailToDelete = \trim($this->GetActionParam('EmailToDelete', ''));
$sEmailToDelete = trim($this->GetActionParam('EmailToDelete', ''));
$sEmailToDelete = \MailSo\Base\Utils::IdnToAscii($sEmailToDelete, true);
$aAccounts = $this->GetAccounts($oAccount);
if (0 < \strlen($sEmailToDelete) && $sEmailToDelete !== $sParentEmail && isset($aAccounts[$sEmailToDelete]))
{
if (0 < \strlen($sEmailToDelete) && $sEmailToDelete !== $sParentEmail && isset($aAccounts[$sEmailToDelete])) {
unset($aAccounts[$sEmailToDelete]);
$oAccountToChange = null;
if ($oAccount->Email() === $sEmailToDelete && !empty($aAccounts[$sParentEmail]))
{
if ($oAccount->Email() === $sEmailToDelete && !empty($aAccounts[$sParentEmail])) {
$oAccountToChange = $this->GetAccountFromCustomToken($aAccounts[$sParentEmail], false, false);
if ($oAccountToChange)
{
if ($oAccountToChange) {
$this->AuthToken($oAccountToChange);
}
}
@ -94,88 +89,50 @@ trait Accounts
/**
* @throws \MailSo\Base\Exceptions\Exception
*/
public function DoIdentityUpdate() : array
public function DoIdentityUpdate(): array
{
$oAccount = $this->getAccountFromToken();
$oIdentity = new \RainLoop\Model\Identity();
if (!$oIdentity->FromJSON($this->GetActionParams(), true))
{
if (!$oIdentity->FromJSON($this->GetActionParams(), true)) {
throw new ClientException(Notifications::InvalidInputArgument);
}
$aIdentities = $this->GetIdentities($oAccount);
$bAdded = false;
$aIdentitiesForSave = array();
foreach ($aIdentities as $oItem)
{
if ($oItem)
{
if ($oItem->Id() === $oIdentity->Id())
{
$aIdentitiesForSave[] = $oIdentity;
$bAdded = true;
}
else
{
$aIdentitiesForSave[] = $oItem;
}
}
}
if (!$bAdded)
{
$aIdentitiesForSave[] = $oIdentity;
}
return $this->DefaultResponse(__FUNCTION__, $this->SetIdentities($oAccount, $aIdentitiesForSave));
$this->IdentitiesProvider()->UpdateIdentity($oAccount, $oIdentity);
return $this->DefaultResponse(__FUNCTION__, true);
}
/**
* @throws \MailSo\Base\Exceptions\Exception
*/
public function DoIdentityDelete() : array
public function DoIdentityDelete(): array
{
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(false, false, Capa::IDENTITIES, $oAccount))
{
if (!$this->GetCapa(false, false, Capa::IDENTITIES, $oAccount)) {
return $this->FalseResponse(__FUNCTION__);
}
$sId = \trim($this->GetActionParam('IdToDelete', ''));
if (empty($sId))
{
$sId = trim($this->GetActionParam('IdToDelete', ''));
if (empty($sId)) {
throw new ClientException(Notifications::UnknownError);
}
$aNew = array();
$aIdentities = $this->GetIdentities($oAccount);
foreach ($aIdentities as $oItem)
{
if ($oItem && $sId !== $oItem->Id())
{
$aNew[] = $oItem;
}
}
return $this->DefaultResponse(__FUNCTION__, $this->SetIdentities($oAccount, $aNew));
$this->IdentitiesProvider()->DeleteIdentity($oAccount, $sId);
return $this->DefaultResponse(__FUNCTION__, true);
}
/**
* @throws \MailSo\Base\Exceptions\Exception
*/
public function DoAccountsAndIdentitiesSortOrder() : array
public function DoAccountsAndIdentitiesSortOrder(): array
{
$oAccount = $this->getAccountFromToken();
$aAccounts = $this->GetActionParam('Accounts', null);
$aIdentities = $this->GetActionParam('Identities', null);
if (!\is_array($aAccounts) && !\is_array($aIdentities))
{
if (!\is_array($aAccounts) && !\is_array($aIdentities)) {
return $this->FalseResponse(__FUNCTION__);
}
@ -191,19 +148,17 @@ trait Accounts
/**
* @throws \MailSo\Base\Exceptions\Exception
*/
public function DoAccountsAndIdentities() : array
public function DoAccountsAndIdentities(): array
{
$oAccount = $this->getAccountFromToken();
$mAccounts = false;
if ($this->GetCapa(false, false, Capa::ADDITIONAL_ACCOUNTS, $oAccount))
{
if ($this->GetCapa(false, false, Capa::ADDITIONAL_ACCOUNTS, $oAccount)) {
$mAccounts = $this->GetAccounts($oAccount);
$mAccounts = \array_keys($mAccounts);
foreach ($mAccounts as $iIndex => $sName)
{
foreach ($mAccounts as $iIndex => $sName) {
$mAccounts[$iIndex] = \MailSo\Base\Utils::IdnToUtf8($sName);
}
}
@ -214,26 +169,35 @@ trait Accounts
));
}
private function SetIdentities(\RainLoop\Model\Account $oAccount, array $aIdentities = array()) : bool
/**
* @param Account $account
* @return Identity[]
*/
public function GetIdentities(Account $account): array
{
$bAllowIdentities = $this->GetCapa(false, false, Capa::IDENTITIES, $oAccount);
if (!$account) return [];
$aResult = array();
foreach ($aIdentities as $oItem)
{
if (!$bAllowIdentities && $oItem && !$oItem->IsAccountIdentities())
{
continue;
}
// A custom name for a single identity is also stored in this system
$allowMultipleIdentities = $this->GetCapa(false, false, Capa::IDENTITIES, $account);
$aResult[] = $oItem->ToSimpleJSON();
// Get all identities
$identities = $this->IdentitiesProvider()->GetIdentities($account, $allowMultipleIdentities);
// Sort identities
$orderString = $this->StorageProvider()->Get($account, StorageType::CONFIG, 'accounts_identities_order');
$order = json_decode($orderString, true) ?? [];
if (isset($order['Identities']) && is_array($order['Identities']) && count($order['Identities']) > 1) {
$list = array_map(function ($item) {
if ('' === $item) $item = '---';
return $item;
}, $order['Identities']);
usort($identities, function ($a, $b) use ($list) {
return array_search($a->Id(true), $list) < array_search($b->Id(true), $list) ? -1 : 1;
});
}
return $this->StorageProvider(true)->Put($oAccount,
\RainLoop\Providers\Storage\Enumerations\StorageType::CONFIG,
'identities',
\json_encode($aResult)
);
return $identities;
}
}

View file

@ -2,7 +2,10 @@
namespace RainLoop\Model;
class Identity implements \JsonSerializable
use JsonSerializable;
use MailSo\Base\Utils;
class Identity implements JsonSerializable
{
/**
* @var string
@ -50,54 +53,89 @@ class Identity implements \JsonSerializable
$this->bSignatureInsertBefore = false;
}
public function Id(bool $bFillOnEmpty = false) : string
public function Id(bool $bFillOnEmpty = false): string
{
return $bFillOnEmpty ? ('' === $this->sId ? '---' : $this->sId) : $this->sId;
}
public function Email() : string
public function Email(): string
{
return $this->sEmail;
}
public function SetEmail(string $sEmail) : self
public function SetEmail(string $sEmail): self
{
$this->sEmail = $sEmail;
return $this;
}
public function Name() : string
public function Name(): string
{
return $this->sName;
}
public function ReplyTo() : string
public function ReplyTo(): string
{
return $this->sReplyTo;
}
public function Bcc() : string
public function Bcc(): string
{
return $this->sBcc;
}
public function Signature() : string
public function Signature(): string
{
return $this->sSignature;
}
public function SignatureInsertBefore() : bool
public function SignatureInsertBefore(): bool
{
return $this->bSignatureInsertBefore;
}
public function FromJSON(array $aData, bool $bAjax = false) : bool
public function SetId(string $sId): Identity
{
if (!empty($aData['Email']))
{
$this->sId = $sId;
return $this;
}
public function SetName(string $sName): Identity
{
$this->sName = $sName;
return $this;
}
public function SetReplyTo(string $sReplyTo): Identity
{
$this->sReplyTo = $sReplyTo;
return $this;
}
public function SetBcc(string $sBcc): Identity
{
$this->sBcc = $sBcc;
return $this;
}
public function SetSignature(string $sSignature): Identity
{
$this->sSignature = $sSignature;
return $this;
}
public function SetSignatureInsertBefore(bool $bSignatureInsertBefore): Identity
{
$this->bSignatureInsertBefore = $bSignatureInsertBefore;
return $this;
}
public function FromJSON(array $aData, bool $bAjax = false): bool
{
if (!empty($aData['Email'])) {
$this->sId = !empty($aData['Id']) ? $aData['Id'] : '';
$this->sEmail = $bAjax ? \MailSo\Base\Utils::IdnToAscii($aData['Email'], true) : $aData['Email'];
$this->sEmail = $bAjax ? Utils::IdnToAscii($aData['Email'], true) : $aData['Email'];
$this->sName = isset($aData['Name']) ? $aData['Name'] : '';
$this->sReplyTo = !empty($aData['ReplyTo']) ? $aData['ReplyTo'] : '';
$this->sBcc = !empty($aData['Bcc']) ? $aData['Bcc'] : '';
@ -111,7 +149,7 @@ class Identity implements \JsonSerializable
return false;
}
public function ToSimpleJSON() : array
public function ToSimpleJSON(): array
{
return array(
'Id' => $this->Id(),
@ -127,9 +165,8 @@ class Identity implements \JsonSerializable
public function jsonSerialize()
{
return array(
// '@Object' => 'Object/Identity',
'Id' => $this->Id(),
'Email' => \MailSo\Base\Utils::IdnToUtf8($this->Email()),
'Email' => Utils::IdnToUtf8($this->Email()),
'Name' => $this->Name(),
'ReplyTo' => $this->ReplyTo(),
'Bcc' => $this->Bcc(),
@ -138,12 +175,12 @@ class Identity implements \JsonSerializable
);
}
public function Validate() : bool
public function Validate(): bool
{
return !empty($this->sEmail);
}
public function IsAccountIdentities() : bool
public function IsAccountIdentities(): bool
{
return '' === $this->Id();
}

View file

@ -0,0 +1,139 @@
<?php
namespace RainLoop\Providers;
use RainLoop\Model\Account;
use RainLoop\Model\Identity;
use RainLoop\Providers\Identities\IIdentities;
class Identities extends AbstractProvider
{
/** @var IIdentities[] */
private $drivers;
/** @var Identity[][][] */
private $identitiesPerDriverPerAccount;
/**
* Identities constructor.
* @param IIdentities[] $drivers
*/
public function __construct(?array $drivers = null)
{
if ($drivers === null) $drivers = [];
$this->drivers = array_filter($drivers, function ($driver) {
return $driver instanceof IIdentities;
});
}
/**
* @param Account $account
* @param bool $allowMultipleIdentities
* @return Identity[]
*/
public function GetIdentities(Account $account, bool $allowMultipleIdentities): array
{
// Find all identities stored in the system
$identities = $this->MergeIdentitiesPerDriver($this->GetIdentiesPerDriver($account));
// Find the primary identity
$primaryIdentity = current(array_filter($identities, function ($identity) {
return $identity->IsAccountIdentities();
}));
// If no primary identity is found, generate default one from account info
if ($primaryIdentity === null || $primaryIdentity === false)
$identities[] = $primaryIdentity = new Identity('', $account->Email());
// Return only primary identity or all identities
return $allowMultipleIdentities ? $identities : [$primaryIdentity];
}
public function UpdateIdentity(Account $account, Identity $identity)
{
// Find all identities in the system
$identities = &$this->GetIdentiesPerDriver($account);
$isNew = true;
foreach ($this->drivers as $driver) {
if (!$driver->SupportsStore()) continue;
$driverIdentities = &$identities[$driver->Name()];
if (!isset($driverIdentities[$identity->Id(true)]))
continue;
// We update the identity in all writeable stores
$driverIdentities[$identity->Id(true)] = $identity;
$isNew = false;
$driver->SetIdentities($account, $driverIdentities);
}
// If it is a new identity we add it to any storage driver
if ($isNew) {
// Pick any storage driver to store the result, typically only file storage
$storageDriver = current(array_filter($this->drivers, function ($driver) {
return $driver->SupportsStore();
}));
$identities[$storageDriver->Name()][$identity->Id(true)] = $identity;
$storageDriver->SetIdentities($account, $identities[$storageDriver->Name()]);
}
}
public function DeleteIdentity(Account $account, string $identityId)
{
// On deletion, we remove the identity from all drivers if they are writeable.
$identities = &$this->GetIdentiesPerDriver($account);
foreach ($this->drivers as $driver) {
if (!$driver->SupportsStore()) continue;
$driverIdentities = &$identities[$driver->Name()];
if (!isset($driverIdentities[$identityId]))
continue;
// We found it, so remove and update storage if relevant
$identity = $driverIdentities[$identityId];
if ($identity->IsAccountIdentities()) continue; // never remove the primary identity
unset($driverIdentities[$identityId]);
$driver->SetIdentities($account, $driverIdentities);
}
}
private function &GetIdentiesPerDriver(Account $account): array
{
if (isset($this->identitiesPerDriverPerAccount[$account->Email()]))
return $this->identitiesPerDriverPerAccount[$account->Email()];
$identitiesPerDriver = [];
foreach ($this->drivers as $driver) {
$driverIdentities = $driver->GetIdentities($account);
foreach ($driverIdentities as $identity)
$identitiesPerDriver[$driver->Name()][$identity->Id(true)] = $identity;
}
$this->identitiesPerDriverPerAccount[$account->Email()] = $identitiesPerDriver;
return $this->identitiesPerDriverPerAccount[$account->Email()];
}
/**
* @param Identity[][] $identitiesPerDriver
* @return Identity[]
*/
private function MergeIdentitiesPerDriver(array $identitiesPerDriver): array
{
// Merge logic for the identities
$identities = [];
foreach ($this->drivers as $driver) {
// Merge and replace by key
foreach ($identitiesPerDriver[$driver->Name()] as $identity)
$identities[$identity->Id(true)] = $identity;
}
return array_values($identities);
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace RainLoop\Providers\Identities;
use RainLoop\Model\Account;
use RainLoop\Model\Identity;
use RainLoop\Providers\Storage;
class FileIdentities implements IIdentities
{
/**
* @var Storage
*/
private $localStorageProvider;
/**
* FileIdentities constructor.
* @param Storage $localStorageProvider
*/
public function __construct(Storage $localStorageProvider)
{
$this->localStorageProvider = $localStorageProvider;
}
/**
* @inheritDoc
*/
public function GetIdentities(Account $account): array
{
if (!$account) return [];
$data = $this->localStorageProvider->Get($account, Storage\Enumerations\StorageType::CONFIG, 'identities');
$subIdentities = json_decode($data, true) ?? [];
$result = [];
foreach ($subIdentities as $subIdentity) {
$identity = new Identity();
$identity->FromJSON($subIdentity);
if (!$identity->Validate()) continue;
if ($identity->IsAccountIdentities()) $identity->SetEmail($account->Email());
$result[] = $identity;
}
return $result;
}
/**
* @inheritDoc
*/
public function SetIdentities(Account $account, array $identities)
{
$jsons = array_map(function ($identity) {
return $identity->ToSimpleJSON();
}, $identities);
$this->localStorageProvider->Put($account, Storage\Enumerations\StorageType::CONFIG, 'identities', json_encode($jsons));
}
/**
* @inheritDoc
*/
public function SupportsStore(): bool
{
return true;
}
public function Name(): string
{
return "File";
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace RainLoop\Providers\Identities;
use RainLoop\Model\Account;
use RainLoop\Model\Identity;
interface IIdentities
{
/**
* @param Account $account
*
* @return Identity[]
*/
public function GetIdentities(Account $account): array;
/**
* @param Account $account
* @param Identity[] $identities
*
* @return void
*/
public function SetIdentities(Account $account, array $identities);
/**
* @return bool
*/
public function SupportsStore(): bool;
/**
* @return string
*/
public function Name(): string;
}

View file

@ -0,0 +1,47 @@
<?php
namespace RainLoop\Providers\Identities;
use RainLoop\Exceptions\Exception;
use RainLoop\Model\Account;
use RainLoop\Model\Identity;
class TestIdentities implements IIdentities
{
/**
* @param Account $account
* @return Identity[]
*/
public function GetIdentities(Account $account): array
{
$oIdentity = new Identity('', $account->Email());
$oIdentity->SetName("Test Name");
return [$oIdentity];
}
/**
* @param Account $account
* @param Identity[] $identities
*
* @return void
* @throws Exception
*/
public function SetIdentities(Account $account, array $identities)
{
throw new Exception("Not implemented");
}
/**
* @return bool
*/
public function SupportsStore(): bool
{
return false;
}
public function Name(): string
{
return "Test";
}
}

View file

@ -2,9 +2,11 @@
namespace RainLoop\Providers\Suggestions;
class TestSuggestions implements \RainLoop\Providers\Suggestions\ISuggestions
use RainLoop\Model\Account;
class TestSuggestions implements ISuggestions
{
public function Process(\RainLoop\Model\Account $oAccount, string $sQuery, int $iLimit = 20) : array
public function Process(Account $oAccount, string $sQuery, int $iLimit = 20) : array
{
return array(
array($oAccount->Email(), ''),

View file

@ -3972,11 +3972,6 @@ istextorbinary@2.2.1:
editions "^1.3.3"
textextensions "2"
jquery@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.5.1.tgz#d7b4d08e1bfdb86ad2f1a3d039ea17304717abb5"
integrity sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"